Skip to content

Commit

Permalink
improve error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
carderne committed Jan 20, 2024
1 parent d7fddea commit 67fbefb
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 45 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Generated by Cargo
# Coverage files
*.profraw

# will have compiled files and executables
debug/
target/
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@ test:
.PHONY: lint
lint:
cargo clippy

.PHONY: coverage
coverage:
RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='test.profraw' cargo test
grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/tests.lcov
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Using [pest](https://pest.rs/) for parsing. Two useful links:
- [pest bootstrap parsing](https://github.com/pest-parser/pest/tree/master/meta/src)
- [playground](https://pest.rs/#editor)

Planned featuers:
Planned features:
- [x] Parse beancount files
- [x] Stricter transaction keywords
- [x] Propagate line numbers for debugging
Expand Down
1 change: 1 addition & 0 deletions example.bean
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
* Example beancount file

fo
** Random metadata stuff
option "operating_currency" "GBP"
2000-01-01 custom "fava-option" "language" "en"
Expand Down
2 changes: 2 additions & 0 deletions grammar.pest
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ entry = _{
| transaction
| COMMENT
| space+
| badline
}

heading = _{ "*" ~ anyline }
Expand Down Expand Up @@ -64,4 +65,5 @@ tag = @{ "#" ~ ASCII_ALPHA_LOWER+ }
link = @{ "^" ~ ASCII_ALPHA_LOWER+ }

COMMENT = _{ NEWLINE? ~ space* ~ ";" ~ anyline }
badline = { (!NEWLINE ~ ANY)+ }
anyline = _{ (!NEWLINE ~ ANY)* }
21 changes: 19 additions & 2 deletions src/directives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,23 @@ impl fmt::Display for Transaction {
}
}

#[derive(Debug)]
pub struct Badline {
line: usize,
}

impl Badline {
pub fn new(line: usize) -> Self {
Self { line }
}
}

impl fmt::Display for Badline {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Badline: L{line}", line=self.line)
}
}

pub enum Directive {
Eoi(Eoi),
ConfigCustom(ConfigCustom),
Expand Down Expand Up @@ -655,8 +672,8 @@ mod tests {
#[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 entries = parser::parse(&text).unwrap();
let (dirs, _) = parser::consume(entries);
let date = NaiveDate::parse_from_str("2023-01-01", DATE_FMT).unwrap();
let a = &Open {
date,
Expand Down
23 changes: 15 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,25 @@ fn main() {
}
}

fn load(text: String) -> Vec<directives::Directive> {
let entries = parser::parse(&text);
let mut directives = parser::consume(entries);
parser::sort(&mut directives);
book::balance_transactions(&mut directives);
utils::print_directives(&directives);
directives
fn load(text: String) -> Result<Vec<directives::Directive>, parser::ParseError> {
let entries = parser::parse(&text)?;
let (dirs, bad) = parser::consume(entries);
if bad.len() > 0 {
utils::print_badlines(bad)
}
let mut dirs = dirs;
parser::sort(&mut dirs);
book::balance_transactions(&mut dirs);
utils::print_directives(&dirs);
Ok(dirs)
}

fn balance(path: &String) {
let text = std::fs::read_to_string(path).expect("cannot read file");
let directives = load(text);
let directives = load(text).unwrap_or_else(|e| {
println!("Error: something went wrong: {e}");
std::process::exit(1);
});
let bals = balance::get_balances(directives);
utils::print_bals(bals);
// println!("{bals:?}");
Expand Down
166 changes: 133 additions & 33 deletions src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,120 @@
use std::cmp::Ordering;
use std::fmt;

use pest::error::LineColLocation;
use pest::iterators::Pairs;
use pest::Parser;

use crate::directives;
use crate::directives::Directive;
use crate::directives::{self, Badline};
use crate::grammar::{BeanParser, Rule};
use crate::utils;

pub fn parse(data: &str) -> Pairs<'_, Rule> {
BeanParser::parse(Rule::root, data)
.expect("parse failed")
.next()
.unwrap()
.into_inner() // go inside the root element
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum ParseErrorType {
Parse,
Into,
}

pub fn consume(entries: Pairs<'_, Rule>) -> Vec<Directive> {
entries
.map(|entry| {
eprintln!("debug:\t{:?}\t{:?}", entry.as_rule(), entry.as_span(),);
match entry.as_rule() {
Rule::option => {
Directive::ConfigOption(directives::ConfigOption::from_entry(entry))
}
Rule::custom => {
Directive::ConfigCustom(directives::ConfigCustom::from_entry(entry))
}
Rule::commodity => Directive::Commodity(directives::Commodity::from_entry(entry)),
Rule::open => Directive::Open(directives::Open::from_entry(entry)),
Rule::close => Directive::Close(directives::Close::from_entry(entry)),
Rule::balance => Directive::Balance(directives::Balance::from_entry(entry)),
Rule::pad => Directive::Pad(directives::Pad::from_entry(entry)),
Rule::price => Directive::Price(directives::Price::from_entry(entry)),
Rule::document => Directive::Document(directives::Document::from_entry(entry)),
Rule::transaction => {
Directive::Transaction(directives::Transaction::from_entry(entry))
}
Rule::EOI => Directive::Eoi(directives::Eoi::from_entry(entry)),
_ => unreachable!("no rule for this entry!"),
}
})
.collect()
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ParseError {
ty: ParseErrorType,
line: usize,
col: usize,
}

impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"ParseError: ({ty:?}) L{line} C{col}",
ty = self.ty,
line = self.line,
col = self.col,
)
}
}

pub fn parse(data: &str) -> Result<Pairs<'_, Rule>, ParseError> {
let mut entries = match BeanParser::parse(Rule::root, data) {
Ok(pairs) => Ok(pairs),
Err(error) => {
let (line, col) = match error.line_col {
LineColLocation::Pos(pos) => pos,
LineColLocation::Span(pos, _) => pos,
};
let ty = ParseErrorType::Parse;
Err(ParseError { ty, line, col })
}
}?;
match entries.next() {
Some(entry) => {
utils::print_pair(&entry, 0);
Ok(entry.into_inner())
}
None => Err(ParseError {
ty: ParseErrorType::Into,
line: 0,
col: 0,
}),
}
}

pub fn consume(entries: Pairs<'_, Rule>) -> (Vec<Directive>, Vec<Badline>) {
let mut bad: Vec<Badline> = Vec::with_capacity(entries.len());
let mut dirs: Vec<Directive> = Vec::new();
for entry in entries {
eprintln!("debug:\t{:?}\t{:?}", entry.as_rule(), entry.as_span(),);
match entry.as_rule() {
Rule::option => {
dirs.push(Directive::ConfigOption(
directives::ConfigOption::from_entry(entry),
));
}
Rule::custom => {
dirs.push(Directive::ConfigCustom(
directives::ConfigCustom::from_entry(entry),
));
}
Rule::commodity => {
dirs.push(Directive::Commodity(directives::Commodity::from_entry(
entry,
)));
}
Rule::open => {
dirs.push(Directive::Open(directives::Open::from_entry(entry)));
}
Rule::close => {
dirs.push(Directive::Close(directives::Close::from_entry(entry)));
}
Rule::balance => {
dirs.push(Directive::Balance(directives::Balance::from_entry(entry)));
}
Rule::pad => {
dirs.push(Directive::Pad(directives::Pad::from_entry(entry)));
}
Rule::price => {
dirs.push(Directive::Price(directives::Price::from_entry(entry)));
}
Rule::document => {
dirs.push(Directive::Document(directives::Document::from_entry(entry)));
}
Rule::transaction => {
dirs.push(Directive::Transaction(directives::Transaction::from_entry(
entry,
)));
}
Rule::EOI => {
dirs.push(Directive::Eoi(directives::Eoi::from_entry(entry)));
}
Rule::badline => {
let (line, _) = entry.line_col();
bad.push(directives::Badline::new(line));
}
_ => unreachable!("no rule for this entry!"),
};
}
(dirs, bad)
}

pub fn sort(directives: &mut [Directive]) {
Expand All @@ -49,3 +123,29 @@ pub fn sort(directives: &mut [Directive]) {
other => other,
});
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse() {
let text = r#"2023-01-01 open Assets:Bank GBP"#;
let entries = parse(&text).unwrap();
let (dirs, _) = consume(entries);
let got = &dirs[0];
match got {
Directive::Open(_) => (),
_ => panic!("Found wrong directive type"),
}
}

#[test]
fn test_bad() {
let text = r#"
2023-01-01 foo
"#;
let entries = parse(&text).unwrap();
let (_, bad) = consume(entries);
assert!(bad.len() == 1);
}
}
33 changes: 33 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use crate::{grammar::Rule, directives::Badline};
use pest::iterators::Pair;

use crate::directives::{AccBal, Directive};

pub fn print_directives(directives: &Vec<Directive>) {
Expand All @@ -15,6 +18,36 @@ pub fn print_bals(bals: AccBal) {
}
}

pub fn print_badlines(bad: Vec<Badline>) {
for b in bad {
println!("{b}");
}
}

pub fn print_pair(pair: &Pair<Rule>, depth: usize) {
if depth == 0 {
println!(" -- Debug full parse output");
}

let indent = " ".repeat(depth);
let inner_pairs: Vec<Pair<Rule>> = pair.clone().into_inner().collect();

if inner_pairs.is_empty() {
// It's a leaf node
println!("{}{:?}: {}", indent, pair.as_rule(), pair.as_str());
} else {
// Not a leaf node, just print the rule
println!("{}{:?}:", indent, pair.as_rule());
// Recursively print inner pairs
for inner_pair in inner_pairs {
print_pair(&inner_pair, depth + 1);
}
}
if depth == 0 {
println!(" -- END Debug full parse output");
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down

0 comments on commit 67fbefb

Please sign in to comment.