aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia
diff options
context:
space:
mode:
authorMx Kookie <kookie@spacekookie.de>2020-11-12 19:34:24 +0100
committerMx Kookie <kookie@spacekookie.de>2020-12-21 05:19:47 +0100
commit5d9cb68ba21f338b4c4618ed2eafae76680d79dc (patch)
tree6c951d2e156aa2c4d7a991016c7e17fa1fb7e79b /apps/cassiopeia
parent13cb8de4b6904679c9cab4d8396db6aa0d593a5b (diff)
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.
Diffstat (limited to 'apps/cassiopeia')
-rw-r--r--apps/cassiopeia/Cargo.toml8
-rw-r--r--apps/cassiopeia/src/file.rs145
-rw-r--r--apps/cassiopeia/src/main.rs3
3 files changed, 156 insertions, 0 deletions
diff --git a/apps/cassiopeia/Cargo.toml b/apps/cassiopeia/Cargo.toml
new file mode 100644
index 000000000000..db7acbd1ba77
--- /dev/null
+++ b/apps/cassiopeia/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "cassiopeia"
+version = "0.1.0"
+authors = ["Mx Kookie <kookie@spacekookie.de>"]
+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 000000000000..94da234e8661
--- /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<Statement>,
+}
+
+impl TimeFile {
+ /// Open an existing `.cass` file on disk. Panics!
+ pub fn open(p: impl Into<Path>) -> 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<HeaderVal>),
+ /// A session start value
+ Start(DateTime<Utc>),
+ /// A session stop value
+ Stop(DateTime<Utc>),
+ /// A project finish value
+ Finish(DateTime<Utc>),
+}
+
+/// A set of header value
+pub struct HeaderVal {
+ /// Header key
+ key: String,
+ /// Header value
+ val: String,
+}
+
+impl HeaderVal {
+ fn new<S: Into<String>>(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<Statement>,
+}
+
+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::<Vec<_>().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 000000000000..e7a11a969c03
--- /dev/null
+++ b/apps/cassiopeia/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+ println!("Hello, world!");
+}