aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia/src/file.rs
blob: 94da234e866183e2cb5f81abce0a0553f42b4e28 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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")]);
}