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.wip/yesman
parent
13cb8de4b6
commit
5d9cb68ba2
@ -0,0 +1,8 @@ |
||||
[package] |
||||
name = "cassiopeia" |
||||
version = "0.1.0" |
||||
authors = ["Mx Kookie <kookie@spacekookie.de>"] |
||||
edition = "2018" |
||||
|
||||
[dependencies] |
||||
chrono = "*" |
@ -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")]); |
||||
} |
@ -0,0 +1,3 @@ |
||||
fn main() { |
||||
println!("Hello, world!"); |
||||
} |
Loading…
Reference in new issue