//! Parse the cassiopeia file format //! //! Each file is associated with a single project. This way there is //! no need to associate session enries with multiple customers and //! projcets. Currently there's also no way to cross-relate sessions //! between projects or clients, although the metadata in the header //! is available to do so in the future //! //! ## Structure //! //! `cassiopeia` files should use the `.cass` extension, although this //! implementation is not opinionated on that. //! //! A line starting with `;` is a comment and can be ignored. A line //! can have a comment anywhere, which means that everything after it //! gets ignored. There are no block comments. //! //! A regular statements has two parts: a key, and a value. Available //! keys are: //! //! - HEADER //! - START //! - STOP //! - FINISH //! //! A file has to have at least one `HEADER` key, containing a certain //! number of fields to be considered valid. The required number of //! fields may vary between versions. //! //! ### HEADER //! //! `cassiopeia` in princpile only needs a single value to parse a //! file, which is `version`. It is however recommended to add //! additional metadata to allow future processing into clients and //! cross-referencing projects. Importantly: header keys that are not //! expected will be ignored. //! //! The general header format is a comma-separated list with a key //! value pair, separated by an equals sign. You can use spaces in //! both keys and values without having to escape them or use special //! quotes. Leading and trailing spaces will be removed. //! //! ``` //! HEADER version=0.0.0,location=Berlin //! HEADER work schedule=mon tue wed //! ``` //! //! When re-writing the file format, known/ accepted keys should go //! first. All other unknown keys will be printed alphabetically at //! the end. This way it's possible for an outdated implementation to //! pass through unknown keys, or users to add their own keys. use chrono::{DateTime, Utc}; use std::{fs::File, io::Read, path::Path}; /// A cassiopeia file that has been successfully parsed pub struct TimeFile { path: PathBuf, content: Vec, } impl TimeFile { /// Open an existing `.cass` file on disk. Panics! pub fn open(p: impl Into) -> Self { let mut f = File::open(p).unwrap(); let mut cont = String::new(); f.read_to_string(&mut cont).unwrap(); } } /// A statement in a `.cass` line /// /// While the whole file get's re-written on every run to update /// version numbers and header values, the structure of the file is /// preserved. pub enum Statement { /// A blank line Blank, /// A comment line that is echo-ed back out Comment(String), /// Header value Header(Vec), /// A session start value Start(DateTime), /// A session stop value Stop(DateTime), /// A project finish value Finish(DateTime), } /// A set of header value pub struct HeaderVal { /// Header key key: String, /// Header value val: String, } impl HeaderVal { fn new>(key: S, val: S) -> Self { Self { key: key.into(), val: val.into(), } } /// Test if a header value is known to this implementation fn known(&self) -> bool { match self.key { "version" => true, _ => false, } } } /// A builder for cass files #[cfg(tests)] struct FileBuilder { acc: Vec, } impl FileBuilder { fn new() -> Self { Self { acc: vec![] } } fn header(mut self, data: Vec<(&str, &str)>) -> Self { self.acc.push(Statement::Header( data.into_iter() .map(|(key, val)| HeaderVal::new(key, val)) .collect(), )); self } fn build(self) -> String { format!(";; This file was generated by cassiopeia (reference)\n{}", self.acc.into_iter().map(|s| s.render()).collect::().join("\n")) } } #[test] fn empty_file() { let fb = FileBuilder::new().header(vec![("version", "0.3.0"), ("project", "testing")]); }