From 5d9cb68ba21f338b4c4618ed2eafae76680d79dc Mon Sep 17 00:00:00 2001 From: Mx Kookie Date: Thu, 12 Nov 2020 19:34:24 +0100 Subject: [PATCH] cassiopeia: init project This is the beginning of a Rust re-implementation of the original ruby scripts. cassiopeia is a simple time tracking tool, that integrates into the larger ecosystem of project management tools that I use to organise my business. --- apps/cassiopeia/Cargo.toml | 8 ++ apps/cassiopeia/src/file.rs | 145 ++++++++++++++++++++++++++++++++++++ apps/cassiopeia/src/main.rs | 3 + 3 files changed, 156 insertions(+) create mode 100644 apps/cassiopeia/Cargo.toml create mode 100644 apps/cassiopeia/src/file.rs create mode 100644 apps/cassiopeia/src/main.rs diff --git a/apps/cassiopeia/Cargo.toml b/apps/cassiopeia/Cargo.toml new file mode 100644 index 00000000000..db7acbd1ba7 --- /dev/null +++ b/apps/cassiopeia/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cassiopeia" +version = "0.1.0" +authors = ["Mx Kookie "] +edition = "2018" + +[dependencies] +chrono = "*" \ No newline at end of file diff --git a/apps/cassiopeia/src/file.rs b/apps/cassiopeia/src/file.rs new file mode 100644 index 00000000000..94da234e866 --- /dev/null +++ b/apps/cassiopeia/src/file.rs @@ -0,0 +1,145 @@ +//! 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")]); +} diff --git a/apps/cassiopeia/src/main.rs b/apps/cassiopeia/src/main.rs new file mode 100644 index 00000000000..e7a11a969c0 --- /dev/null +++ b/apps/cassiopeia/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}