You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
145 lines
4.1 KiB
145 lines
4.1 KiB
//! 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")]);
|
|
}
|
|
|