cassiopeia: changing parser output to more generic IR structure

This allows a few things: a, it's a persistant format that we can
mirror to disk again, AND can adapt because all type information is
known, and it allows for new entries to be added to the IR more
easily, without having to worry about exact formatting, or
re-inferring order from the TimeFile abstraction.
wip/yesman
Katharina Fey 3 years ago
parent 4c97f3208a
commit 236cf191b9
  1. 159
      apps/cassiopeia/src/data.rs
  2. 26
      apps/cassiopeia/src/date.rs
  3. 63
      apps/cassiopeia/src/format/ir.rs
  4. 32
      apps/cassiopeia/src/format/mod.rs
  5. 3
      apps/cassiopeia/src/format/parser.rs
  6. 30
      apps/cassiopeia/src/lib.rs
  7. 29
      apps/cassiopeia/src/time.rs

@ -4,14 +4,15 @@
//! used to generate new files, and perform various lookups and
//! analysis tasks.
use crate::format::LineCfg;
use chrono::{DateTime, Duration, Local, FixedOffset as Offset, NaiveDate};
use crate::{
format::{IrItem, IrType, MakeIr},
Date, Time,
};
use chrono::{DateTime, Duration, FixedOffset as Offset, Local, NaiveDate};
use std::collections::BTreeMap;
#[derive(Debug, Default)]
pub struct TimeFile {
/// Raw line buffers to echo back into the file
lines: Vec<LineCfg>,
/// A parsed header structure
header: BTreeMap<String, String>,
/// A parsed session structure
@ -21,70 +22,158 @@ pub struct TimeFile {
}
impl TimeFile {
pub(crate) fn append(mut self, line: LineCfg) -> Self {
let lo = self.lines.len();
pub(crate) fn append(&mut self, line: IrItem) {
match line {
LineCfg::Header(ref header) => self.header = header.clone(),
LineCfg::Start(Some(time)) => self.sessions.push(Session::start(time, lo)),
LineCfg::Stop(Some(time)) => self.get_last_session().stop(time, lo),
LineCfg::Invoice(Some(date)) => self.invoices.push(Invoice::new(date, lo)),
IrItem {
tt: IrType::Header(ref header),
..
} => self.header = header.clone(),
IrItem {
tt: IrType::Start(time),
lo,
} => self.sessions.push(Session::start(time.into())),
IrItem {
tt: IrType::Stop(time),
lo,
} => self.get_last_session().unwrap().stop(time.into()),
IrItem {
tt: IrType::Invoice(date),
lo,
} => self.invoices.push(Invoice::new(date.into())),
_ => {}
}
}
self.lines.push(line);
self
fn get_last_session(&mut self) -> Option<&mut Session> {
self.sessions.last_mut()
}
fn get_last_session(&mut self) -> &mut Session {
self.sessions.last_mut().unwrap()
fn get_last_invoice(&mut self) -> Option<&mut Invoice> {
self.invoices.last_mut()
}
/// Start a new session (optionally 15-minute rounded)
pub fn start(&mut self, round: bool) -> Option<()> {
let now = Local::now();
///
/// This function returns the new session object that will have to
/// be turned into an IR line to be written back into the file
pub(crate) fn start(&mut self, round: bool) -> Option<Session> {
// Check if the last session was closed
match self.get_last_session() {
Some(s) if !s.finished() => return None,
_ => {}
}
// Create a new time
let now = if round {
Time::now().round()
} else {
Time::now()
};
Some(Session::start(now))
}
/// Stop the last session that was started, returning a completed
/// session
pub(crate) fn stop(&mut self, round: bool) -> Option<Session> {
match self.get_last_session() {
Some(s) if s.finished() => return None,
None => return None,
_ => {}
}
// Create a new time
let now = if round {
Time::now().round()
} else {
Time::now()
};
self.get_last_session().cloned().map(|mut s| {
s.stop(now);
s
})
}
/// Add a new invoice block to the time file
pub(crate) fn invoice(&mut self) -> Option<()> {
let today = Date::today();
let last_sess = self.get_last_session().cloned();
match self.get_last_invoice() {
// Check if _today_ there has been already an invoice
Some(i) if i.date == today => return None,
// Check if since the last invoice there has been at least
// _one_ terminated session.
Some(i)
if !last_sess
.map(|s| !s.stop.map(|s| s.after(&i.date)).unwrap_or(false))
.unwrap_or(false) =>
{
return None
}
// Otherwise, we create an invoice
_ => {}
}
self.invoices.push(Invoice::new(today));
Some(())
}
}
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct Session {
start: DateTime<Offset>,
stop: Option<DateTime<Offset>>,
/// Track the lines this session took place in
lines: (usize, usize),
start: Time,
stop: Option<Time>,
}
impl Session {
/// Create a new session with a start time
fn start(start: DateTime<Offset>, line: usize) -> Self {
Self {
start,
stop: None,
lines: (line, 0),
}
fn start(start: Time) -> Self {
Self { start, stop: None }
}
/// Finalise a session with a stop time
fn stop(&mut self, stop: DateTime<Offset>, line: usize) {
fn stop(&mut self, stop: Time) {
self.stop = Some(stop);
self.lines.1 = line;
}
/// Check whether this session was already finished
pub fn finished(&self) -> bool {
self.stop.is_some()
}
/// Get the length of the session, if it was already finished
pub fn length(&self) -> Option<Duration> {
self.stop.map(|stop| stop - self.start)
self.stop.as_ref().map(|stop| stop - &self.start)
}
}
impl MakeIr for Session {
fn make_ir(&self) -> IrType {
match self.stop {
Some(ref time) => IrType::Stop(time.clone()),
None => IrType::Start(self.start.clone()),
}
}
}
#[derive(Debug)]
pub struct Invoice {
date: NaiveDate,
line: usize,
date: Date,
}
impl Invoice {
fn new(date: NaiveDate, line: usize) -> Self {
Self { date, line }
fn new(date: Date) -> Self {
Self { date }
}
}
impl MakeIr for Invoice {
fn make_ir(&self) -> IrType {
IrType::Invoice(self.date.clone())
}
}

@ -0,0 +1,26 @@
use crate::Time;
use chrono::{FixedOffset as Offset, NaiveDate};
/// A convenienc wrapper around [chrono::NaiveDate](chrono::NaiveDate)
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date {
inner: NaiveDate,
}
impl Date {
pub fn today() -> Self {
Self::from(Time::now().date())
}
pub(crate) fn from(d: chrono::Date<Offset>) -> Self {
Self {
inner: d.naive_local(),
}
}
}
impl From<NaiveDate> for Date {
fn from(inner: NaiveDate) -> Self {
Self { inner }
}
}

@ -0,0 +1,63 @@
use crate::{format::LineCfg, Date, Time, TimeFile};
use std::collections::BTreeMap;
/// A set of IR parsed items that makes up a whole cass file
pub(crate) type IrStream = Vec<IrItem>;
/// Intermediate representation for parsing and generating files
///
/// The CASS IR is largely based on the output of the parser's
/// [`LineCfg`](crate::format::LineCfg), but with concrete types used
/// in the data layer (namely [`Date`][date] and [`Time`][time]),
/// while also keeping track of the line numbers to allow idempotent
/// file changes.
///
/// Something not yet implemented is comment pass-through (this needs
/// to happen in the parser first), but will likely be implemented in
/// a future version.
///
/// [date]: crate::Date
/// [time]: crate::Time
#[derive(Debug, Clone)]
pub(crate) struct IrItem {
pub(crate) tt: IrType,
pub(crate) lo: usize,
}
/// Disambiguate between different IR line types with their payload
#[derive(Debug, Clone)]
pub(crate) enum IrType {
/// A line with parsed header information
Header(BTreeMap<String, String>),
/// Start a session at a given timestapm
Start(Time),
/// Stop a session at a given timestamp
Stop(Time),
/// Invoice a block of previous work
Invoice(Date),
/// An item that gets ignored
Ignore,
}
/// Generate a stream of IR items from the raw parser output
pub(crate) fn generate_ir(buf: impl Iterator<Item = LineCfg>) -> IrStream {
buf.enumerate().fold(vec![], |mut buf, (lo, item)| {
#[cfg_attr(rustfmt, rustfmt_skip)]
buf.push(match item {
LineCfg::Header(map) => IrItem { tt: IrType::Header(map), lo },
LineCfg::Start(Some(time)) => IrItem { tt: IrType::Start(time.into()), lo },
LineCfg::Stop(Some(time)) => IrItem { tt: IrType::Stop(time.into()), lo },
LineCfg::Invoice(Some(date)) => IrItem { tt: IrType::Invoice(date.into()), lo },
LineCfg::Ignore => IrItem { tt: IrType::Ignore, lo },
_ => IrItem { tt: IrType::Ignore, lo },
});
buf
})
}
pub(crate) trait MakeIr {
/// Make a new IR line from an object
fn make_ir(&self) -> IrType;
}

@ -1,17 +1,33 @@
//! cassiopeia file format
mod ir;
mod lexer;
mod parser;
pub(crate) use ir::{IrItem, IrStream, IrType, MakeIr};
pub(crate) use lexer::{LineLexer, LineToken, Token};
pub(crate) use parser::LineCfg;
use crate::TimeFile;
use std::{fs::File, io::Read};
#[derive(Default)]
pub struct ParseOutput {
pub(crate) ir: IrStream,
pub(crate) tf: TimeFile,
}
impl ParseOutput {
fn append(mut self, ir: IrItem) -> Self {
self.tf.append(ir.clone());
self.ir.push(ir);
self
}
}
/// Load a file from disk and parse it into a
/// [`TimeFile`](crate::TimeFile)
pub fn load_file(path: &str) -> Option<TimeFile> {
pub fn load_file(path: &str) -> Option<ParseOutput> {
let mut f = File::open(path).ok()?;
let mut content = String::new();
f.read_to_string(&mut content).ok()?;
@ -19,11 +35,13 @@ pub fn load_file(path: &str) -> Option<TimeFile> {
let mut lines: Vec<String> = content.split("\n").map(|l| l.to_owned()).collect();
Some(
lines
.iter_mut()
.map(|line| lexer::lex(line))
.map(|lex| parser::parse(lex))
.filter(|line| line.valid())
.fold(TimeFile::default(), |file, line| file.append(line)),
ir::generate_ir(
lines
.iter_mut()
.map(|line| lexer::lex(line))
.map(|lex| parser::parse(lex)),
)
.into_iter()
.fold(ParseOutput::default(), |output, ir| output.append(ir)),
)
}

@ -53,6 +53,9 @@ pub(crate) fn parse<'l>(lex: LineLexer<'l>) -> LineCfg {
(Stop(_), LineToken { tt: T::Date, slice }) => Stop(parse_datetime(slice)),
(Invoice(_), LineToken { tt: T::Date, slice }) => Invoice(parse_date(slice)),
// Pass empty lines through,
(Empty, _) => Empty,
// Ignore everything else (which will be filtered)
_ => Ignore,
})

@ -9,28 +9,32 @@
//! https://git.spacekookie.de/kookienomicon/tree/apps/cassiopeia
mod data;
mod date;
mod format;
pub mod meta;
mod time;
pub use data::{Session, TimeFile};
pub use date::Date;
pub use format::load_file;
pub use time::Time;
/// A state handler for all cass interactions
use data::{Invoice, Session, TimeFile};
use format::{ir, IrStream, ParseOutput};
/// A state handler and primary API for all cass interactions
///
///
/// This could be a stateless API, but I like being able to refer to
/// fields that need to be saved for later here. This API wraps
/// around [`TimeFile`](crate::TimeFile), so that you don't have to! ✨
pub struct Cassiopeia {
path: String,
tf: TimeFile,
ir: IrStream,
}
impl Cassiopeia {
/// Load a cass file from disk, parsing it into a [`TimeFile`](crate::TimeFile)
pub fn load(path: &str) -> Option<Self> {
let path = path.to_owned();
load_file(path.as_str()).map(|tf| Self { path, tf })
load_file(path.as_str()).map(|ParseOutput { tf, ir }| Self { path, tf, ir })
}
/// Store the modified time file back to disk
@ -40,11 +44,15 @@ impl Cassiopeia {
/// Start a new work session (with optional 15 minute rounding)
pub fn start(&mut self, round: bool) -> Option<()> {
self.tf.start(round)
self.tf.start(round)?;
Some(())
}
/// Stop the existing work session (with optional 15 minute rounding)
pub fn stop(&mut self, round: bool) -> Option<()> {
self.tf.stop(round)?;
Some(())
}
@ -96,7 +104,7 @@ impl<'cass> Invoicer<'cass> {
}
}
/// S
/// Enable the invoice generation feature
pub fn generate(self) -> Self {
Self {
generate: true,
@ -120,10 +128,12 @@ impl<'cass> Invoicer<'cass> {
pub fn run(self) -> Option<()> {
if self.generate {
eprintln!("Integration with invoice(1) is currently not implemented. Sorry :()");
eprintln!("Integration with invoice(1) is currently not implemented. Sorry :(");
return None;
}
None
self.tf.tf.invoice()?;
Some(())
}
}

@ -1,14 +1,32 @@
use crate::Date;
use chrono::{
DateTime, FixedOffset as Offset, Local, NaiveDateTime, NaiveTime, TimeZone, Timelike, Utc,
DateTime, Duration, FixedOffset as Offset, Local, NaiveDateTime, NaiveTime, TimeZone, Timelike,
Utc,
};
use std::{cmp::Ordering, ops::Sub};
/// A convenience wrapper around [DateTime][t] with fixed timezone
///
/// [t]: chrono::DateTime
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Time {
inner: DateTime<Offset>,
}
impl From<DateTime<Offset>> for Time {
fn from(inner: DateTime<Offset>) -> Self {
Self { inner }
}
}
impl<'t> Sub for &'t Time {
type Output = Duration;
fn sub(self, o: &'t Time) -> Self::Output {
self.inner - o.inner
}
}
impl Time {
/// Get the current local time and pin it to a fixed Tz offset
pub fn now() -> Self {
@ -18,6 +36,15 @@ impl Time {
}
}
pub(crate) fn date(&self) -> chrono::Date<Offset> {
self.inner.date()
}
/// Check if a time stamp happened _after_ a date
pub fn after(&self, date: &Date) -> bool {
&Date::from(self.date()) > date
}
#[cfg(test)]
pub(crate) fn fixed(hour: u32, min: u32, sec: u32) -> Self {
Self {

Loading…
Cancel
Save