aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia/src/format
diff options
context:
space:
mode:
authorMx Kookie <kookie@spacekookie.de>2020-12-11 13:38:58 +0000
committerMx Kookie <kookie@spacekookie.de>2020-12-21 05:19:49 +0100
commit5502c6d320f05f43239fc8e2b2839eb3fd5d742a (patch)
tree76f5d1d5685f6281789b4559db1bd257855b774b /apps/cassiopeia/src/format
parentafd8a74e43fc57662381b16d418a559f471c80f5 (diff)
cassiopeia: implementing basic file parser
Diffstat (limited to 'apps/cassiopeia/src/format')
-rw-r--r--apps/cassiopeia/src/format/lexer.rs91
-rw-r--r--apps/cassiopeia/src/format/mod.rs26
-rw-r--r--apps/cassiopeia/src/format/parser.rs78
3 files changed, 165 insertions, 30 deletions
diff --git a/apps/cassiopeia/src/format/lexer.rs b/apps/cassiopeia/src/format/lexer.rs
index f062ca4238c1..bdb89f5180e5 100644
--- a/apps/cassiopeia/src/format/lexer.rs
+++ b/apps/cassiopeia/src/format/lexer.rs
@@ -1,6 +1,7 @@
//! Cassiopeia file lexer
-use logos::Logos;
+use logos::{Lexer, Logos};
+use std::iter::Iterator;
/// A basic line lexer type
///
@@ -8,8 +9,7 @@ use logos::Logos;
/// does not attempt to parse the line specifics. This is what the
/// content lexer is for.
#[derive(Logos, Debug, PartialEq)]
-enum Line {
-
+pub(crate) enum Token {
#[token("HEADER")]
Header,
@@ -21,7 +21,7 @@ enum Line {
#[token("INVOICE")]
Invoice,
-
+
#[regex(r"\w+=[^,$]+[,$]")]
HeaderData,
@@ -32,85 +32,120 @@ enum Line {
#[token(" ", logos::skip)]
Space,
-
+
+ #[regex(";;.*")]
+ Comment,
+
#[error]
Error,
}
+/// A single token type on a line
+#[derive(Debug)]
+pub(crate) struct LineToken<'l> {
+ pub(crate) tt: Token,
+ pub(crate) slice: &'l str,
+}
-// pub fn test_this() {
-// // let mut lex = Line::lexer("HEADER version=0.0.0,location=Berlin,");
-// let mut lex = Line::lexer("START 2020-11-11 13:00:00+01:00");
+/// A lexer wrapped for a single line
+pub(crate) struct LineLexer<'l> {
+ lexer: Lexer<'l, Token>,
+}
+
+impl<'l> LineLexer<'l> {
+ pub(crate) fn get_all(self) -> Vec<LineToken<'l>> {
+ let mut acc = vec![];
+ for l in self {
+ acc.push(l);
+ }
+ acc
+ }
+}
-// while let Some(t) = lex.next() {
-// println!("{:?}: {}", t, lex.slice());
-// }
-// }
+impl<'l> Iterator for LineLexer<'l> {
+ type Item = LineToken<'l>;
+ fn next(&mut self) -> Option<Self::Item> {
+ self.lexer.next().map(|tt| Self::Item {
+ tt,
+ slice: self.lexer.slice(),
+ })
+ }
+}
+
+/// Take a line of input and lex it into a stream of tokens
+pub(crate) fn lex<'l>(line: &'l mut String) -> LineLexer<'l> {
+ LineLexer {
+ lexer: Token::lexer(line),
+ }
+}
#[test]
fn basic_header() {
- let mut lex = Line::lexer("HEADER version=0.0.0,location=Berlin Lichtenberg,");
+ let mut lex = Token::lexer("HEADER version=0.0.0,location=Berlin Lichtenberg,");
- assert_eq!(lex.next(), Some(Line::Header));
+ assert_eq!(lex.next(), Some(Token::Header));
assert_eq!(lex.span(), 0..6);
assert_eq!(lex.slice(), "HEADER");
- assert_eq!(lex.next(), Some(Line::HeaderData));
+ assert_eq!(lex.next(), Some(Token::HeaderData));
assert_eq!(lex.span(), 7..21);
assert_eq!(lex.slice(), "version=0.0.0,");
- assert_eq!(lex.next(), Some(Line::HeaderData));
+ assert_eq!(lex.next(), Some(Token::HeaderData));
assert_eq!(lex.span(), 21..49);
assert_eq!(lex.slice(), "location=Berlin Lichtenberg,");
assert_eq!(lex.next(), None);
}
-
#[test]
fn basic_start() {
- let mut lex = Line::lexer("START 2020-11-11 13:00:00+01:00");
+ let mut lex = Token::lexer("START 2020-11-11 13:00:00+01:00");
- assert_eq!(lex.next(), Some(Line::Start));
+ assert_eq!(lex.next(), Some(Token::Start));
assert_eq!(lex.span(), 0..5);
assert_eq!(lex.slice(), "START");
- assert_eq!(lex.next(), Some(Line::Date));
+ assert_eq!(lex.next(), Some(Token::Date));
assert_eq!(lex.span(), 5..31);
assert_eq!(lex.slice(), " 2020-11-11 13:00:00+01:00");
assert_eq!(lex.next(), None);
}
-
#[test]
fn basic_stop() {
- let mut lex = Line::lexer("STOP 2020-11-11 13:00:00+01:00");
+ let mut lex = Token::lexer("STOP 2020-11-11 13:00:00+01:00");
- assert_eq!(lex.next(), Some(Line::Stop));
+ assert_eq!(lex.next(), Some(Token::Stop));
assert_eq!(lex.span(), 0..4);
assert_eq!(lex.slice(), "STOP");
- assert_eq!(lex.next(), Some(Line::Date));
+ assert_eq!(lex.next(), Some(Token::Date));
assert_eq!(lex.span(), 4..30);
assert_eq!(lex.slice(), " 2020-11-11 13:00:00+01:00");
assert_eq!(lex.next(), None);
}
-
#[test]
fn basic_invoice() {
- let mut lex = Line::lexer("INVOICE 2020-11-11 13:00:00+01:00");
+ let mut lex = Token::lexer("INVOICE 2020-11-11 13:00:00+01:00");
- assert_eq!(lex.next(), Some(Line::Invoice));
+ assert_eq!(lex.next(), Some(Token::Invoice));
assert_eq!(lex.span(), 0..7);
assert_eq!(lex.slice(), "INVOICE");
- assert_eq!(lex.next(), Some(Line::Date));
+ assert_eq!(lex.next(), Some(Token::Date));
assert_eq!(lex.span(), 7..33);
assert_eq!(lex.slice(), " 2020-11-11 13:00:00+01:00");
assert_eq!(lex.next(), None);
}
+
+#[test]
+fn basic_comment() {
+ let mut lex = Token::lexer(";; This file is auto generated!");
+ assert_eq!(lex.next(), Some(Token::Comment));
+}
diff --git a/apps/cassiopeia/src/format/mod.rs b/apps/cassiopeia/src/format/mod.rs
index 766bb22be13e..beab2f7aac66 100644
--- a/apps/cassiopeia/src/format/mod.rs
+++ b/apps/cassiopeia/src/format/mod.rs
@@ -1,3 +1,25 @@
-//! cassiopeia file format handling
+//! cassiopeia file format
-pub(crate) mod lexer;
+mod lexer;
+mod parser;
+
+pub(crate) use lexer::{LineLexer, LineToken, Token};
+pub(crate) use parser::LineCfg;
+
+use crate::TimeFile;
+use std::{fs::File, io::Read};
+
+pub(crate) fn load_file(path: &str) {
+ let mut f = File::open(path).unwrap();
+ let mut content = String::new();
+ f.read_to_string(&mut content).unwrap();
+
+ let mut lines: Vec<String> = content.split("\n").map(|l| l.to_owned()).collect();
+
+ lines
+ .iter_mut()
+ .map(|line| lexer::lex(line))
+ .map(|lex| parser::parse(lex))
+ .filter(|line| line.valid())
+ .fold(TimeFile::default(), |file, line| file.append(line));
+}
diff --git a/apps/cassiopeia/src/format/parser.rs b/apps/cassiopeia/src/format/parser.rs
new file mode 100644
index 000000000000..cc4b1b7c77df
--- /dev/null
+++ b/apps/cassiopeia/src/format/parser.rs
@@ -0,0 +1,78 @@
+//! cassiopeia parser
+//!
+//! Takes a lexer's token stream as an input, and outputs a fully
+//! parsed time file.
+
+use crate::format::{LineLexer, LineToken, Token};
+use chrono::{DateTime, FixedOffset as Offset};
+use logos::Lexer;
+use std::collections::BTreeMap;
+use std::iter::Iterator;
+
+/// A type-parsed line in a time file
+#[derive(Debug)]
+pub enum LineCfg {
+ /// A header line with a set of keys and values
+ Header(BTreeMap<String, String>),
+ /// A session start line with a date and time
+ Start(Option<DateTime<Offset>>),
+ /// A session stop line with a date and time
+ Stop(Option<DateTime<Offset>>),
+ /// An invoice line with a date
+ Invoice(Option<DateTime<Offset>>),
+ /// An empty line
+ Empty,
+ /// A temporary value that is invalid
+ #[doc(hidden)]
+ 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;
+
+ #[cfg_attr(rustfmt, rustfmt_skip)]
+ lex.get_all().into_iter().fold(Ignore, |cfg, tok| match (cfg, tok) {
+ // If the first token is a comment, we ignore it
+ (Ignore, LineToken { tt: T::Comment, .. }, ) => Ignore,
+ // If the first token is a keyword, we wait for more data
+ (Ignore, LineToken { tt: T::Header, .. }) => Header(Default::default()),
+ (Ignore, LineToken { tt: T::Start, .. }) => Start(None),
+ (Ignore, LineToken { tt: T::Stop, .. }) => Stop(None),
+ (Ignore, LineToken { tt: T::Invoice, .. }) => Invoice(None),
+
+ // If the first token _was_ a keyword, fill in the data
+ (Header(map), LineToken { tt: T::HeaderData, slice }) => Header(append_data(map, slice)),
+ (Start(_), LineToken { tt: T::Date, slice }) => Start(parse_date(slice)),
+ (Stop(_), LineToken { tt: T::Date, slice }) => Stop(parse_date(slice)),
+ (Invoice(_), LineToken { tt: T::Date, slice }) => Invoice(parse_date(slice)),
+
+ // Pass empty lines through,
+ (Empty, _) => Empty,
+
+ // Ignore everything else (which will be filtered)
+ _ => Ignore,
+ })
+}
+
+fn append_data(mut map: BTreeMap<String, String>, slice: &str) -> BTreeMap<String, String> {
+ let split = slice.split("=").collect::<Vec<_>>();
+ map.insert(split[0].into(), split[1].into());
+ map
+}
+
+fn parse_date(slice: &str) -> Option<DateTime<Offset>> {
+ Some(
+ DateTime::parse_from_str(slice, "%Y-%m-%d %H:%M:%S%:z")
+ .expect("Failed to parse date; invalid format!"),
+ )
+}