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; /// 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), /// 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) -> 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.into_iter().map(|(k, v)| (k, v.replace(",", ""))).collect()), 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; } pub(crate) fn clean_ir(ir: &mut IrStream) { ir.remove(0); // FIXME: this is required to remove the leading // comment, which will be manually re-generated at // the moment, but which would just add more blank // lines between the new comment, and the first line // in this current format. This is very bad, yikes // yikes yikes, but what can I do, I have a deadline // (not really) lol // FIXME: this hack gets rid of a trailing empty line if it exists // to make sure we don't have any gaps between work sessions. if match ir.last() { Some(IrItem { tt: IrType::Ignore, .. }) => true, _ => false, } { ir.pop(); } } /// Taken an IrType and append it to an existing IR stream pub(crate) fn append_ir(ir: &mut IrStream, tt: IrType) { let lo = ir.last().unwrap().lo; ir.push(IrItem { tt, lo }); } /// Search for the header that contains the version string and update it pub(crate) fn update_header(ir: &mut IrStream) { ir.iter_mut().for_each(|item| match item.tt { IrType::Header(ref mut map) if map.contains_key("version") => { map.insert("version".into(), crate::meta::VERSION.into()); } _ => {} }); }