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
146
147
148
149
150
//! Cassiopeia plain text time tracking tool
//!
//! Versions `0.1` and `0.2` were written in Ruby and are thus
//! deprecated.  Most likely you are interested in `cass(1)`, the
//! simple plain text time tracking utility, part of the kookie-office
//! suite of commandline tools!  This is the library powering it.
//!
//! For more documentation, check out:
//! https://git.spacekookie.de/kookienomicon/tree/apps/cassiopeia

mod data;
mod date;
mod format;
pub mod meta;
mod time;

pub use date::Date;
pub use time::Time;

use data::{Invoice, Session, TimeFile};
use format::{
    ir::{append_ir, clean_ir, IrStream, MakeIr},
    ParseOutput,
};

/// A state handler and primary API for all cass interactions
///
///
pub struct Cassiopeia {
    path: String,
    tf: TimeFile,
    ir: IrStream,
}

impl Cassiopeia {
    /// Load a cass file from disk, parsing it into a [`TimeFile`](crate::TimeFile)
    pub fn load(path: &str) -> Option<Self> {
        let path = path.to_owned();
        format::load_file(path.as_str()).map(|ParseOutput { tf, ir }| Self { path, tf, ir })
    }

    /// Store the modified time file back to disk
    pub fn store(&self) -> Option<()> {
        Some(())
    }

    /// Start a new work session (with optional 15 minute rounding)
    pub fn start(&mut self, round: bool) -> Option<()> {
        let s = self.tf.start(round)?;
        clean_ir(&mut self.ir);
        append_ir(&mut self.ir, s.make_ir());
        format::write_file(self.path.as_str(), &mut self.ir)
    }

    /// Stop the existing work session (with optional 15 minute rounding)
    pub fn stop(&mut self, round: bool) -> Option<()> {
        let s = self.tf.stop(round)?;
        clean_ir(&mut self.ir);
        append_ir(&mut self.ir, s.make_ir());
        format::write_file(self.path.as_str(), &mut self.ir)
    }

    /// Add an invoice block to the time file
    pub fn invoice<'slf>(&'slf mut self) -> Invoicer<'slf> {
        Invoicer::new(self)
    }

    /// Write out the file IR as is, updating only the header version
    pub fn update(&mut self) -> Option<()> {
        clean_ir(&mut self.ir);
        format::write_file(self.path.as_str(), &mut self.ir)
    }
}

/// An invoice generator builder
///
/// The most simple use-case of this type is to provide no parameters
/// and simply add an `INVOICE` line to the cass file.  Adittionally
/// you may provide the client and project name, which will then
/// require the `client_db` path to be set as well.
///
/// ```rust,no_run
/// # let mut cass = cassiopeia::Cassiopeia::load("").unwrap();
/// cass.invoice().run();
/// ```
///
/// Additional errors can be thrown if the client or project are not
/// known in the client db.
///
/// ```rust,no_run
/// # let mut cass = cassiopeia::Cassiopeia::load("").unwrap();
/// cass.invoice()
///     .generate()
///     .db("/home/office/clients.yml".into())
///     .client("ACME".into())
///     .run();
/// ```
pub struct Invoicer<'cass> {
    tf: &'cass mut Cassiopeia,
    generate: bool,
    client_db: String,
    client: String,
    project: String,
}

impl<'cass> Invoicer<'cass> {
    pub fn new(tf: &'cass mut Cassiopeia) -> Self {
        Self {
            tf,
            generate: false,
            client_db: String::new(),
            client: String::new(),
            project: String::new(),
        }
    }

    /// Enable the invoice generation feature
    pub fn generate(self) -> Self {
        Self {
            generate: true,
            ..self
        }
    }

    /// Provide the client database file (.yml format)
    pub fn db(self, client_db: String) -> Self {
        Self { client_db, ..self }
    }

    /// Provide the client to invoice
    pub fn client(self, client: String) -> Self {
        Self { client, ..self }
    }

    pub fn project(self, project: String) -> Self {
        Self { project, ..self }
    }

    pub fn run(mut self) -> Option<()> {
        if self.generate {
            eprintln!("Integration with invoice(1) is currently not implemented.  Sorry :(");
            return None;
        }

        let inv = self.tf.tf.invoice()?;
        clean_ir(&mut self.tf.ir);
        append_ir(&mut self.tf.ir, inv.make_ir());
        format::write_file(self.tf.path.as_str(), &mut self.tf.ir)
    }
}