#![deny(future_incompatible, nonstandard_style, unsafe_code, private_in_public)]
#![warn(rust_2018_idioms, clippy::pedantic, missing_docs)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::{collections::HashSet, path::PathBuf};
use nom::{
branch::alt,
bytes::complete::{tag, take_till},
character::complete::{char, line_ending, not_line_ending, space0, space1},
combinator::not,
combinator::{all_consuming, cut, eof, iterator, map, opt},
sequence::{delimited, preceded, terminated, tuple},
Finish, Parser,
};
use nom_locate::position;
pub use crate::{
account::{Account, Balance, Close, Open, Pad},
amount::{Amount, Currency, Decimal, Price},
date::Date,
error::Error,
event::Event,
transaction::{Cost, Link, Posting, PostingPrice, Tag, Transaction},
};
#[deprecated(note = "use `metadata::Value` instead", since = "1.0.0-beta.3")]
#[doc(hidden)]
pub type MetadataValue<D> = metadata::Value<D>;
mod account;
mod amount;
mod date;
mod error;
mod event;
pub mod metadata;
mod transaction;
pub fn parse<D: Decimal>(input: &str) -> Result<BeancountFile<D>, Error> {
match all_consuming(beancount_file)(Span::new(input)).finish() {
Ok((_, content)) => Ok(content),
Err(nom::error::Error { input, .. }) => Err(Error::new(input)),
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct BeancountFile<D> {
pub options: Vec<BeanOption>,
pub includes: Vec<PathBuf>,
pub directives: Vec<Directive<D>>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct BeanOption {
pub name: String,
pub value: String,
}
impl<D> BeancountFile<D> {
#[must_use]
pub fn option(&self, key: &str) -> Option<&str> {
self.options
.iter()
.find(|opt| opt.name == key)
.map(|opt| &opt.value[..])
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Directive<D> {
pub date: Date,
pub content: DirectiveContent<D>,
pub metadata: metadata::Map<D>,
pub line_number: u32,
}
#[allow(missing_docs)]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DirectiveContent<D> {
Transaction(Transaction<D>),
Price(Price<D>),
Balance(Balance<D>),
Open(Open),
Close(Close),
Pad(Pad),
Commodity(Currency),
Event(Event),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ConversionError;
type Span<'a> = nom_locate::LocatedSpan<&'a str>;
type IResult<'a, O> = nom::IResult<Span<'a>, O>;
fn beancount_file<D: Decimal>(input: Span<'_>) -> IResult<'_, BeancountFile<D>> {
let mut iter = iterator(input, entry);
let mut options = Vec::new();
let mut includes = Vec::new();
let mut tag_stack = HashSet::new();
let mut directives = Vec::new();
for entry in &mut iter {
match entry {
Entry::Directive(mut d) => {
if let DirectiveContent::Transaction(trx) = &mut d.content {
trx.tags.extend(tag_stack.iter().cloned());
}
directives.push(d);
}
Entry::Option(option) => {
options.push(option);
}
Entry::Include(path) => {
includes.push(path);
}
Entry::PushTag(tag) => {
tag_stack.insert(tag);
}
Entry::PopTag(tag) => {
tag_stack.remove(&tag);
}
Entry::Comment => (),
}
}
let (input, _) = iter.finish()?;
Ok((
input,
BeancountFile {
options,
includes,
directives,
},
))
}
enum Entry<D> {
Directive(Directive<D>),
Option(BeanOption),
Include(PathBuf),
PushTag(Tag),
PopTag(Tag),
Comment,
}
fn entry<D: Decimal>(input: Span<'_>) -> IResult<'_, Entry<D>> {
alt((
directive.map(Entry::Directive),
option.map(|(name, value)| {
Entry::Option(BeanOption {
name: name.into(),
value: value.into(),
})
}),
include.map(|p| Entry::Include(p)),
tag_stack_operation,
line.map(|_| Entry::Comment),
))(input)
}
fn directive<D: Decimal>(input: Span<'_>) -> IResult<'_, Directive<D>> {
let (input, position) = position(input)?;
let (input, date) = date::parse(input)?;
let (input, _) = cut(space1)(input)?;
let (input, (content, metadata)) = alt((
map(transaction::parse, |(t, m)| {
(DirectiveContent::Transaction(t), m)
}),
tuple((
terminated(
alt((
map(
preceded(tag("price"), cut(preceded(space1, amount::price))),
DirectiveContent::Price,
),
map(
preceded(tag("balance"), cut(preceded(space1, account::balance))),
DirectiveContent::Balance,
),
map(
preceded(tag("open"), cut(preceded(space1, account::open))),
DirectiveContent::Open,
),
map(
preceded(tag("close"), cut(preceded(space1, account::close))),
DirectiveContent::Close,
),
map(
preceded(tag("pad"), cut(preceded(space1, account::pad))),
DirectiveContent::Pad,
),
map(
preceded(tag("commodity"), cut(preceded(space1, amount::currency))),
DirectiveContent::Commodity,
),
map(
preceded(tag("event"), cut(preceded(space1, event::parse))),
DirectiveContent::Event,
),
)),
end_of_line,
),
metadata::parse,
)),
))(input)?;
Ok((
input,
Directive {
date,
content,
metadata,
line_number: position.location_line(),
},
))
}
fn option(input: Span<'_>) -> IResult<'_, (&str, &str)> {
let (input, _) = tag("option")(input)?;
let (input, key) = preceded(space1, string)(input)?;
let (input, value) = preceded(space1, string)(input)?;
let (input, _) = end_of_line(input)?;
Ok((input, (key, value)))
}
fn include(input: Span<'_>) -> IResult<'_, PathBuf> {
let (input, _) = tag("include")(input)?;
let (input, path) = cut(delimited(space1, string, end_of_line))(input)?;
Ok((input, path.into()))
}
fn tag_stack_operation<D>(input: Span<'_>) -> IResult<'_, Entry<D>> {
alt((
preceded(tuple((tag("pushtag"), space1)), transaction::parse_tag).map(Entry::PushTag),
preceded(tuple((tag("poptag"), space1)), transaction::parse_tag).map(Entry::PopTag),
))(input)
}
fn end_of_line(input: Span<'_>) -> IResult<'_, ()> {
let (input, _) = space0(input)?;
let (input, _) = opt(comment)(input)?;
let (input, _) = alt((line_ending, eof))(input)?;
Ok((input, ()))
}
fn comment(input: Span<'_>) -> IResult<'_, ()> {
let (input, _) = char(';')(input)?;
let (input, _) = not_line_ending(input)?;
Ok((input, ()))
}
fn line(input: Span<'_>) -> IResult<'_, ()> {
let (input, _) = not_line_ending(input)?;
let (input, _) = line_ending(input)?;
Ok((input, ()))
}
fn empty_line(input: Span<'_>) -> IResult<'_, ()> {
let (input, _) = not(eof)(input)?;
end_of_line(input)
}
fn string(input: Span<'_>) -> IResult<'_, &str> {
map(
delimited(char('"'), take_till(|c: char| c == '"'), char('"')),
|s: Span<'_>| *s.fragment(),
)(input)
}