From 4fd6c87c43af132a2871982282d2f9ecd244b892 Mon Sep 17 00:00:00 2001 From: Mx Kookie Date: Thu, 7 Jan 2021 23:48:07 +0100 Subject: cassiopeia: update CLI handling and add new commands --- apps/cassiopeia/.envrc | 1 - apps/cassiopeia/shell.nix | 9 +---- apps/cassiopeia/src/bin/cass.rs | 77 +++++++++++++++--------------------- apps/cassiopeia/src/error.rs | 21 +++++++++- apps/cassiopeia/src/format/mod.rs | 59 +++++++++++++++------------ apps/cassiopeia/src/format/parser.rs | 11 +----- apps/cassiopeia/src/lib.rs | 34 ++++++++-------- apps/cassiopeia/src/time.rs | 2 +- 8 files changed, 104 insertions(+), 110 deletions(-) delete mode 100644 apps/cassiopeia/.envrc (limited to 'apps/cassiopeia') diff --git a/apps/cassiopeia/.envrc b/apps/cassiopeia/.envrc deleted file mode 100644 index 051d09d292a8..000000000000 --- a/apps/cassiopeia/.envrc +++ /dev/null @@ -1 +0,0 @@ -eval "$(lorri direnv)" diff --git a/apps/cassiopeia/shell.nix b/apps/cassiopeia/shell.nix index a365404a622b..ca6afad60654 100644 --- a/apps/cassiopeia/shell.nix +++ b/apps/cassiopeia/shell.nix @@ -1,8 +1 @@ -with import {}; - -stdenv.mkDerivation { - name = "cassiopeia"; - buildInputs = with pkgs; [ - rustracer rustup clangStdenv - ]; -} +import diff --git a/apps/cassiopeia/src/bin/cass.rs b/apps/cassiopeia/src/bin/cass.rs index 8bceddc911a4..3fea64380623 100644 --- a/apps/cassiopeia/src/bin/cass.rs +++ b/apps/cassiopeia/src/bin/cass.rs @@ -1,4 +1,4 @@ -use cassiopeia::{meta, Cassiopeia}; +use cassiopeia::{error::ParseResult, meta, Cassiopeia}; use clap::{App, Arg, SubCommand}; fn main() { @@ -69,64 +69,49 @@ If you want to report a bug, please do so on my mailing list: lists.sr.ht/~space let cass_file = cli.value_of(meta::ARG_FILE).unwrap(); let mut cass = match Cassiopeia::load(cass_file) { - Some(cf) => cf, - None => { - eprintln!( - "Invalid CASS file '{}'; file not found, or unparsable.", - cass_file - ); - std::process::exit(2); + Ok(cf) => cf, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); } }; // Parse the matches generated by clap match cli.subcommand() { ("start", Some(ops)) => { - // This parameter turns rounding OFF let round = ops.is_present(meta::ARG_ROUNDING); - match cass.start(!round) { - Some(()) => println!("Started session!"), - None => { - eprintln!("Failed to start session..."); - std::process::exit(1); - } - } + run_command(|| cass.start(!round)); } ("stop", Some(ops)) => { - // This parameter turns rounding OFF let round = ops.is_present(meta::ARG_ROUNDING); - match cass.stop(!round) { - Some(()) => println!("Stopped session!"), - None => { - eprintln!("Failed to stop session..."); - std::process::exit(1); - } - } + run_command(|| cass.stop(!round)); } ("invoice", _) => { - println!("Invoice command only partially implemented! No generation is supported"); - match cass.invoice().run() { - Some(()) => println!("Added INVOICE block"), - None => { - eprintln!("Failed to add INVOICE block..."); - std::process::exit(1); - } - } + eprintln!("Invoice command only partially implemented! No generation is supported"); + run_command(|| cass.invoice().run()); } - ("update", _) => match cass.update() { - Some(()) => println!("Updated file to new version: {}", meta::VERSION), - None => { - eprintln!("Failed to update file..."); - std::process::exit(1); - } - }, - (meta::CMD_STAT, _) => match cass.stat() { - Some(s) => println!("{}", s), - None => { - eprintln!("Failed to collect time statistics..."); - std::process::exit(1); - } - }, + ("update", _) => run_command(|| cass.update()), + (meta::CMD_STAT, _) => run_command(|| { + let stats = cass.stat()?; + println!("{}", stats); + Ok(()) + }), (_, _) => todo!(), } } + +/// Run a closure and print the associated error message +/// +/// Set the exit code for the program. +fn run_command(mut cmd: F) +where + F: FnMut() -> ParseResult<()>, +{ + match cmd() { + Ok(_) => std::process::exit(0), + Err(e) => { + eprintln!("{}", e); + std::process::exit(2); + } + } +} diff --git a/apps/cassiopeia/src/error.rs b/apps/cassiopeia/src/error.rs index 31f5414c4f86..24bbb4965494 100644 --- a/apps/cassiopeia/src/error.rs +++ b/apps/cassiopeia/src/error.rs @@ -1,7 +1,7 @@ //! A set of error types for cassiopeia -use std::error::Error; use std::fmt::{self, Display, Formatter}; +use std::{error::Error, io}; /// User errors that can occur when using cassiopeia /// @@ -45,6 +45,14 @@ pub enum ParseError { /// This error means that the structure of the parsed file is /// wrong, with an invalid sequence of events expressed User(UserError), + /// The requested file did not exist + NoSuchFile, + /// The file could not be read + BadPermissions, + /// The file could not be written to + FileNotWritable, + /// Other file related errors + FileUnknown(String), /// An invalid keyword was found BadKeyword { line: usize, tokn: String }, /// A bad timestamp was found @@ -70,3 +78,14 @@ impl From for ParseError { ParseError::User(user) } } + +impl From for ParseError { + fn from(e: io::Error) -> Self { + use io::ErrorKind::*; + match e.kind() { + NotFound => Self::NoSuchFile, + PermissionDenied => Self::BadPermissions, + _ => Self::FileUnknown(format!("{}", e)), + } + } +} diff --git a/apps/cassiopeia/src/format/mod.rs b/apps/cassiopeia/src/format/mod.rs index 89f3a6ccb466..65c2264d6829 100644 --- a/apps/cassiopeia/src/format/mod.rs +++ b/apps/cassiopeia/src/format/mod.rs @@ -8,13 +8,17 @@ mod parser; pub(crate) use lexer::{LineLexer, LineToken, Token}; pub(crate) use parser::LineCfg; -use crate::TimeFile; +use crate::{ + error::{ParseError, ParseResult}, + TimeFile, +}; use ir::{IrItem, IrStream}; use std::{ fs::{File, OpenOptions}, io::{Read, Write}, }; +/// A crate internal representation of the IR stream and timefile #[derive(Default)] pub(crate) struct ParseOutput { pub(crate) ir: IrStream, @@ -22,46 +26,51 @@ pub(crate) struct ParseOutput { } impl ParseOutput { - fn append(mut self, ir: IrItem) -> Self { - self.tf.append(ir.clone()); + fn append(mut self, ir: IrItem) -> ParseResult { + self.tf.append(ir.clone())?; self.ir.push(ir); - self + Ok(self) } } /// Load a file from disk and parse it into a /// [`TimeFile`](crate::TimeFile) -pub(crate) fn load_file(path: &str) -> Option { - let mut f = File::open(path).ok()?; +pub(crate) fn load_file(path: &str) -> ParseResult { + // Load the raw file contents + let mut f = File::open(path)?; let mut content = String::new(); - f.read_to_string(&mut content).ok()?; + f.read_to_string(&mut content)?; + // Split the file by lines - .cass is a line based format let mut lines: Vec = content.split("\n").map(|l| l.to_owned()).collect(); - Some( - ir::generate_ir( - lines - .iter_mut() - .map(|line| lexer::lex(line)) - .map(|lex| parser::parse(lex)), - ) + // Build an iterator over parsed lines + let parsed = lines + .iter_mut() + .map(|line| lexer::lex(line)) + .map(|lex| parser::parse(lex)); + + // Generate the IR from parse output, then build the timefile + ir::generate_ir(parsed) .into_iter() - .fold(ParseOutput::default(), |output, ir| output.append(ir)), - ) + .fold(Ok(ParseOutput::default()), |out, ir| match out { + Ok(mut out) => out.append(ir), + e @ Err(_) => e, + }) } /// Write a file with the updated IR stream -pub(crate) fn write_file(path: &str, ir: &mut IrStream) -> Option<()> { +pub(crate) fn write_file(path: &str, ir: &mut IrStream) -> ParseResult<()> { ir::update_header(ir); let mut lines = ir.into_iter().map(|ir| gen::line(ir)).collect::>(); lines.insert(0, gen::head_comment()); - let mut f = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(path) - .ok()?; - f.write_all(lines.join("\n").as_bytes()).ok()?; - Some(()) + // let mut f = OpenOptions::new() + // .write(true) + // .create(true) + // .truncate(true) + // .open(path) + // .ok()?; + // f.write_all(lines.join("\n").as_bytes()).ok()?; + Ok(()) } diff --git a/apps/cassiopeia/src/format/parser.rs b/apps/cassiopeia/src/format/parser.rs index d1f0bcdebc68..78433c0f5cec 100644 --- a/apps/cassiopeia/src/format/parser.rs +++ b/apps/cassiopeia/src/format/parser.rs @@ -24,15 +24,6 @@ pub enum LineCfg { Ignore, } -impl LineCfg { - pub(crate) fn valid(&self) -> bool { - match self { - LineCfg::Ignore => false, - _ => true, - } - } -} - pub(crate) fn parse<'l>(lex: LineLexer<'l>) -> LineCfg { use LineCfg::*; use Token as T; @@ -54,7 +45,7 @@ pub(crate) fn parse<'l>(lex: LineLexer<'l>) -> LineCfg { (Invoice(_), LineToken { tt: T::Date, slice }) => Invoice(parse_date(slice)), // Pass empty lines through, - (Empty, _) => Empty, + (empty, _) => empty, // Ignore everything else (which will be filtered) _ => Ignore, diff --git a/apps/cassiopeia/src/lib.rs b/apps/cassiopeia/src/lib.rs index 895110fe218d..7b4409c9689f 100644 --- a/apps/cassiopeia/src/lib.rs +++ b/apps/cassiopeia/src/lib.rs @@ -10,16 +10,18 @@ mod data; mod date; -mod error; mod format; -pub mod meta; mod time; mod timeline; +pub mod error; +pub mod meta; + pub use date::Date; pub use time::Time; use data::{Invoice, Session, TimeFile}; +use error::{ParseError, ParseResult}; use format::{ ir::{append_ir, clean_ir, IrStream, MakeIr}, ParseOutput, @@ -36,27 +38,22 @@ pub struct Cassiopeia { impl Cassiopeia { /// Load a cass file from disk, parsing it into a [`TimeFile`](crate::TimeFile) - pub fn load(path: &str) -> Option { + pub fn load(path: &str) -> ParseResult { 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 delta = self.tf.timeline.start(Time::rounded(round)).ok()?; + pub fn start(&mut self, round: bool) -> ParseResult<()> { + let delta = self.tf.timeline.start(Time::rounded(round))?; clean_ir(&mut self.ir); append_ir(&mut self.ir, delta.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 delta = self.tf.timeline.stop(Time::rounded(round)).ok()?; + pub fn stop(&mut self, round: bool) -> ParseResult<()> { + let delta = self.tf.timeline.stop(Time::rounded(round))?; clean_ir(&mut self.ir); append_ir(&mut self.ir, delta.make_ir()); format::write_file(self.path.as_str(), &mut self.ir) @@ -68,14 +65,14 @@ impl Cassiopeia { } /// Write out the file IR as is, updating only the header version - pub fn update(&mut self) -> Option<()> { + pub fn update(&mut self) -> ParseResult<()> { clean_ir(&mut self.ir); format::write_file(self.path.as_str(), &mut self.ir) } /// Collect statistics on previous work sessions - pub fn stat(&self) -> Option { - None + pub fn stat(&self) -> ParseResult { + todo!() } } @@ -102,6 +99,7 @@ impl Cassiopeia { /// .client("ACME".into()) /// .run(); /// ``` +#[allow(unused)] pub struct Invoicer<'cass> { tf: &'cass mut Cassiopeia, generate: bool, @@ -143,13 +141,13 @@ impl<'cass> Invoicer<'cass> { Self { project, ..self } } - pub fn run(mut self) -> Option<()> { + pub fn run(mut self) -> ParseResult<()> { if self.generate { eprintln!("Integration with invoice(1) is currently not implemented. Sorry :("); - return None; + return Err(ParseError::Unknown); } - let delta = self.tf.tf.timeline.invoice(Date::today()).ok()?; + let delta = self.tf.tf.timeline.invoice(Date::today())?; clean_ir(&mut self.tf.ir); append_ir(&mut self.tf.ir, delta.make_ir()); format::write_file(self.tf.path.as_str(), &mut self.tf.ir) diff --git a/apps/cassiopeia/src/time.rs b/apps/cassiopeia/src/time.rs index a5147b93aaac..4cda0718d922 100644 --- a/apps/cassiopeia/src/time.rs +++ b/apps/cassiopeia/src/time.rs @@ -81,7 +81,7 @@ impl Time { let naive = self.inner.time(); let (new_min, incr_hour) = match naive.minute() { // 0-7 => (0, false) - m if m >= 0 && m < 7 => (0, false), + m if m < 7 => (0, false), // 7-22 => (15, false) m if m >= 7 && m < 22 => (15, false), // 22-37 => (30, false) -- cgit v1.2.3