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