diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..84fd878 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,14 @@ +name: test +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test --all-features diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bd3169a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "bean-rs" +license = "MIT" +authors = ["Chris Arderne bool { + true + } +} + +impl fmt::Display for Debug { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "L{line:0>4} ", line = self.line) + } +} + +#[derive(Debug, PartialEq)] +pub struct Amount { + pub number: f64, + pub ccy: Ccy, +} + +impl fmt::Display for Amount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{number} {ccy}", number = self.number, ccy = self.ccy,) + } +} + +impl Amount { + pub fn new(entry: Pair) -> Self { + let mut pairs = entry.clone().into_inner(); + let number: f64 = pairs.next().unwrap().as_str().parse().unwrap(); + let ccy = pairs.next().unwrap().as_str().to_string(); + Self { number, ccy } + } +} + +#[derive(Debug, PartialEq)] +pub struct ConfigCustom { + debug: Debug, +} + +impl ConfigCustom { + pub fn new(entry: Pair) -> Self { + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { debug } + } +} + +impl fmt::Display for ConfigCustom { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{debug}-- ignore custom", debug = self.debug) + } +} + +#[derive(Debug, PartialEq)] +pub struct EOI { + debug: Debug, +} + +impl EOI { + pub fn new(entry: Pair) -> Self { + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { debug } + } +} + +impl fmt::Display for EOI { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{debug}-- EOI", debug = self.debug) + } +} + +#[derive(Debug, PartialEq)] +pub struct ConfigOption { + key: String, + val: String, + debug: Debug, +} + +impl ConfigOption { + pub fn new(entry: Pair) -> Self { + let mut pairs = entry.clone().into_inner(); + let key = pairs.next().unwrap().as_str().to_string(); + let val = pairs.next().unwrap().as_str().to_string(); + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { key, val, debug } + } +} + +impl fmt::Display for ConfigOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{debug}{key} {val}", + debug = self.debug, + key = self.key, + val = self.val, + ) + } +} + +#[derive(Debug, PartialEq)] +pub struct Metadata { + key: String, + val: String, + debug: Debug, +} + +impl Metadata { + pub fn new(entry: Pair) -> Self { + let mut pairs = entry.clone().into_inner(); + let key = pairs.next().unwrap().as_str().to_string(); + let val = pairs.next().unwrap().as_str().to_string(); + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { key, val, debug } + } +} + +impl fmt::Display for Metadata { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{debug} {key}:{val}", + debug = self.debug, + key = self.key, + val = self.val, + ) + } +} + +#[derive(Debug, PartialEq)] +pub struct Commodity { + date: String, + ccy: String, + meta: Vec, + debug: Debug, +} + +impl Commodity { + pub fn new(entry: Pair) -> Self { + let mut pairs = entry.clone().into_inner(); + let date = pairs.next().unwrap().as_str().to_string(); + let ccy = pairs.next().unwrap().as_str().to_string(); + let mut meta: Vec = Vec::new(); + while let Some(pair) = pairs.next() { + if pair.as_rule() == Rule::metadata { + let p = Metadata::new(pair); + meta.push(p) + } + } + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { + date, + ccy, + meta, + debug, + } + } +} + +impl fmt::Display for Commodity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut meta_string: String = "".to_owned(); + let m_slice = &self.meta[..]; + for m in m_slice { + let line: &str = &format!("\n{m}"); + meta_string.push_str(line); + } + write!( + f, + "{debug}{date} {ccy}{meta}", + debug = self.debug, + date = self.date, + ccy = self.ccy, + meta = meta_string, + ) + } +} + +#[derive(Debug, PartialEq)] +pub struct Open { + date: String, + account: Account, + debug: Debug, +} + +impl Open { + pub fn new(entry: Pair) -> Self { + let mut pairs = entry.clone().into_inner(); + let date = pairs.next().unwrap().as_str().to_string(); + let account = pairs.next().unwrap().as_str().to_string(); + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { + date, + account, + debug, + } + } +} + +impl fmt::Display for Open { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{debug}{date} {account}", + debug = self.debug, + date = self.date, + account = self.account, + ) + } +} + +#[derive(Debug, PartialEq)] +pub struct Close { + date: String, + account: Account, + debug: Debug, +} + +impl Close { + pub fn new(entry: Pair) -> Self { + let mut pairs = entry.clone().into_inner(); + let date = pairs.next().unwrap().as_str().to_string(); + let account = pairs.next().unwrap().as_str().to_string(); + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { + date, + account, + debug, + } + } +} + +impl fmt::Display for Close { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{debug}{date} {account}", + debug = self.debug, + date = self.date, + account = self.account, + ) + } +} + +#[derive(Debug, PartialEq)] +pub struct Balance { + date: String, + account: Account, + amount: Amount, + debug: Debug, +} + +impl Balance { + pub fn new(entry: Pair) -> Self { + let mut pairs = entry.clone().into_inner(); + let date = pairs.next().unwrap().as_str().to_string(); + let account = pairs.next().unwrap().as_str().to_string(); + let amount_entry = pairs.next().unwrap(); + let amount = Amount::new(amount_entry); + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { + date, + account, + amount, + debug, + } + } +} + +impl fmt::Display for Balance { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{debug}{date} {account} {amount}", + debug = self.debug, + date = self.date, + account = self.account, + amount = self.amount, + ) + } +} + +#[derive(Debug, PartialEq)] +pub struct Pad { + date: String, + account_to: Account, + account_from: Account, + debug: Debug, +} + +impl Pad { + pub fn new(entry: Pair) -> Self { + let mut pairs = entry.clone().into_inner(); + let date = pairs.next().unwrap().as_str().to_string(); + let account_to = pairs.next().unwrap().as_str().to_string(); + let account_from = pairs.next().unwrap().as_str().to_string(); + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { + date, + account_to, + account_from, + debug, + } + } +} + +impl fmt::Display for Pad { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{debug}{date} {account_to} {account_from}", + debug = self.debug, + date = self.date, + account_to = self.account_to, + account_from = self.account_from, + ) + } +} + +#[derive(Debug, PartialEq)] +pub struct Price { + date: String, + commodity: String, + amount: Amount, + debug: Debug, +} + +impl Price { + pub fn new(entry: Pair) -> Self { + let mut pairs = entry.clone().into_inner(); + let date = pairs.next().unwrap().as_str().to_string(); + let commodity = pairs.next().unwrap().as_str().to_string(); + let amount_entry = pairs.next().unwrap(); + let amount = Amount::new(amount_entry); + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { + date, + commodity, + amount, + debug, + } + } +} + +impl fmt::Display for Price { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{debug}{date} {commodity} {amount}", + debug = self.debug, + date = self.date, + commodity = self.commodity, + amount = self.amount, + ) + } +} + +#[derive(Debug, PartialEq)] +pub struct Posting { + account: Account, + amount: Option, + debug: Debug, +} + +impl Posting { + pub fn new(entry: Pair) -> Self { + let mut pairs = entry.clone().into_inner(); + let account = pairs.next().unwrap().as_str().to_string(); + let amount = if let Some(_) = pairs.peek() { + Some(Amount::new(pairs.next().unwrap())) + } else { + None + }; + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { + account, + amount, + debug, + } + } +} + +impl fmt::Display for Posting { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let amount_str = match &self.amount { + Some(amount) => amount.to_string(), + None => String::from(""), + }; + + write!( + f, + "{debug} {account} {amount}", + debug = self.debug, + account = self.account, + amount = amount_str, + ) + } +} + +#[derive(Debug, PartialEq)] +pub struct Transaction { + date: String, + ty: String, + payee: Option, + narration: String, + postings: Vec, + meta: Vec, + debug: Debug, +} + +pub fn get_payee_narration(pairs: &mut Pairs) -> (Option, String) { + let first_val = pairs.next().unwrap().as_str().to_string(); + if pairs.peek().unwrap().as_rule() == Rule::narration { + let narration = pairs.next().unwrap().as_str().to_string(); + return (Some(first_val), narration); + } else { + return (None, first_val); + } +} + +impl Transaction { + pub fn new(entry: Pair) -> Self { + let mut pairs = entry.clone().into_inner(); + let date = pairs.next().unwrap().as_str().to_string(); + let ty = pairs.next().unwrap().as_str().to_string(); + let (payee, narration) = get_payee_narration(&mut pairs); + let mut postings: Vec = Vec::new(); + let mut meta: Vec = Vec::new(); + while let Some(pair) = pairs.next() { + if pair.as_rule() == Rule::posting { + postings.push(Posting::new(pair)); + } else if pair.as_rule() == Rule::metadata { + meta.push(Metadata::new(pair)); + } + } + let (line, _) = entry.line_col(); + let debug = Debug { line }; + Self { + date, + ty, + payee, + narration, + postings, + meta, + debug, + } + } +} + +impl fmt::Display for Transaction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let payee_str = match &self.payee { + Some(payee) => payee.as_str(), + None => "", + }; + + let mut posting_string: String = "".to_owned(); + let slice = &self.postings[..]; + for p in slice { + let line: &str = &format!("\n{p}"); + posting_string.push_str(line); + } + + let mut meta_string: String = "".to_owned(); + let m_slice = &self.meta[..]; + for m in m_slice { + let line: &str = &format!("\n{m}"); + meta_string.push_str(line); + } + + write!( + f, + "{debug}{date} {ty} {payee} {narration}{meta}{postings}", + debug = self.debug, + date = self.date, + ty = self.ty, + payee = payee_str, + narration = self.narration, + meta = meta_string, + postings = posting_string, + ) + } +} + +pub enum Directive { + EOI(EOI), + ConfigCustom(ConfigCustom), + ConfigOption(ConfigOption), + Commodity(Commodity), + Open(Open), + Close(Close), + Balance(Balance), + Pad(Pad), + Price(Price), + Transaction(Transaction), +} + +impl fmt::Display for Directive { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Directive::EOI(d) => write!(f, "{d}"), + Directive::ConfigCustom(d) => write!(f, "{d}"), + Directive::ConfigOption(d) => write!(f, "{d}"), + Directive::Commodity(d) => write!(f, "{d}"), + Directive::Open(d) => write!(f, "{d}"), + Directive::Close(d) => write!(f, "{d}"), + Directive::Balance(d) => write!(f, "{d}"), + Directive::Pad(d) => write!(f, "{d}"), + Directive::Price(d) => write!(f, "{d}"), + Directive::Transaction(d) => write!(f, "{d}"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser; + #[test] + fn test_open() { + let text = r#"2023-01-01 open Assets:Bank GBP"#; + let entries = parser::parse(&text); + let dirs = parser::consume(entries); + let a = &Open { + date: String::from("2023-01-01"), + account: String::from("Assets:Bank"), + debug: Debug { line: 2 }, + }; + let got = &dirs[0]; + match got { + Directive::Open(i) => { + assert!(i == a); + } + _ => panic!("Found wrong directive type"), + } + } +} diff --git a/src/grammar.rs b/src/grammar.rs new file mode 100644 index 0000000..8760271 --- /dev/null +++ b/src/grammar.rs @@ -0,0 +1,5 @@ +use pest_derive::Parser; + +#[derive(Parser)] +#[grammar = "grammar.pest"] +pub struct BeanParser; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2171811 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,37 @@ +mod directives; +mod grammar; +mod parser; +mod utils; + +use clap::{Parser, Subcommand}; + +use crate::utils::print_directives; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Balance { path: String }, +} + +fn main() { + let cli = Cli::parse(); + match &cli.command { + Commands::Balance { path } => { + balance(path); + } + } +} + +fn balance(path: &String) { + let text = std::fs::read_to_string(path).expect("cannot read file"); + let entries = parser::parse(&text); + let directives = parser::consume(entries); + print_directives(directives); +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..3e8ba89 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,35 @@ +use pest::iterators::Pairs; +use pest::Parser; + +use crate::directives; +use crate::directives::Directive; +use crate::grammar::{BeanParser, Rule}; + +pub fn parse(data: &str) -> Pairs<'_, Rule> { + BeanParser::parse(Rule::root, data) + .expect("parse failed") + .next() + .unwrap() + .into_inner() // go inside the root element +} + +pub fn consume(entries: Pairs<'_, Rule>) -> Vec { + entries + .map(|entry| { + eprintln!("debug:\t{:?}\t{:?}", entry.as_rule(), entry.as_span(),); + match entry.as_rule() { + Rule::option => Directive::ConfigOption(directives::ConfigOption::new(entry)), + Rule::custom => Directive::ConfigCustom(directives::ConfigCustom::new(entry)), + Rule::commodity => Directive::Commodity(directives::Commodity::new(entry)), + Rule::open => Directive::Open(directives::Open::new(entry)), + Rule::close => Directive::Close(directives::Close::new(entry)), + Rule::balance => Directive::Balance(directives::Balance::new(entry)), + Rule::pad => Directive::Pad(directives::Pad::new(entry)), + Rule::price => Directive::Price(directives::Price::new(entry)), + Rule::transaction => Directive::Transaction(directives::Transaction::new(entry)), + Rule::EOI => Directive::EOI(directives::EOI::new(entry)), + _ => unreachable!("no rule for this entry!"), + } + }) + .collect() +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..75d25c7 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,17 @@ +use crate::directives::Directive; + +pub fn print_directives(directives: Vec) { + for d in directives { + println!("{d}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn useless_print_directives() { + let vec = Vec::new(); + print_directives(vec) + } +}