use std::{
collections::HashSet,
fs::File,
io::Read,
path::{Path, PathBuf},
str::FromStr,
};
use nom::{
branch::alt,
bytes::complete::{tag, take_while},
character::complete::{char, line_ending, not_line_ending, space0, space1},
combinator::{all_consuming, cut, eof, iterator, map, not, opt, value},
sequence::{delimited, preceded, terminated},
Finish, Parser,
};
use nom_locate::position;
pub use crate::{
account::{Account, Balance, Close, Open, Pad},
amount::{Amount, Currency, Decimal, Price},
date::Date,
error::{ConversionError, Error},
event::Event,
transaction::{Cost, Link, Posting, PostingPrice, Tag, Transaction},
};
use crate::{
error::{ReadFileErrorContent, ReadFileErrorV2},
iterator::Iter,
};
#[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;
mod iterator;
pub mod metadata;
mod transaction;
pub fn parse<D: Decimal>(input: &str) -> Result<BeancountFile<D>, Error> {
input.parse()
}
pub fn parse_iter<'a, D: Decimal + 'a>(
input: &'a str,
) -> impl Iterator<Item = Result<Entry<D>, Error>> + 'a {
Iter::new(input, iterator(Span::new(input), entry::<D>))
}
impl<D: Decimal> FromStr for BeancountFile<D> {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
parse_iter(input).collect()
}
}
#[allow(deprecated)]
#[deprecated(
since = "2.4.0",
note = "use `read_files_v2` or `read_files_to_vec` instead"
)]
pub fn read_files<D: Decimal, F: FnMut(Entry<D>)>(
files: impl IntoIterator<Item = PathBuf>,
on_entry: F,
) -> Result<(), error::ReadFileError> {
read_files_v2(files, on_entry).map_err(|err| match err.error {
ReadFileErrorContent::Io(err) => error::ReadFileError::Io(err),
ReadFileErrorContent::Syntax(err) => error::ReadFileError::Syntax(err),
})
}
pub fn read_files_to_vec<D: Decimal>(
files: impl IntoIterator<Item = PathBuf>,
) -> Result<Vec<Entry<D>>, ReadFileErrorV2> {
let mut vec = Vec::new();
read_files_v2(files, |entry| vec.push(entry))?;
Ok(vec)
}
pub fn read_files_v2<D: Decimal, F: FnMut(Entry<D>)>(
files: impl IntoIterator<Item = PathBuf>,
mut on_entry: F,
) -> Result<(), ReadFileErrorV2> {
let mut loaded: HashSet<PathBuf> = HashSet::new();
let mut pending: Vec<PathBuf> = files
.into_iter()
.map(|p| {
p.canonicalize()
.map_err(|err| ReadFileErrorV2::from_io(p, err))
})
.collect::<Result<_, _>>()?;
let mut buffer = String::new();
while let Some(path) = pending.pop() {
if loaded.contains(&path) {
continue;
}
loaded.insert(path.clone());
buffer.clear();
File::open(&path)
.and_then(|mut f| f.read_to_string(&mut buffer))
.map_err(|err| ReadFileErrorV2::from_io(path.clone(), err))?;
for result in parse_iter::<D>(&buffer) {
let entry = match result {
Ok(entry) => entry,
Err(err) => return Err(ReadFileErrorV2::from_syntax(path, err)),
};
match entry {
Entry::Include(include) => {
let path = if include.is_relative() {
let Some(parent) = path.parent() else {
unreachable!("there must be a parent if the file was valid")
};
parent.join(&include)
} else {
include
};
let path = path
.canonicalize()
.map_err(|err| ReadFileErrorV2::from_io(path, err))?;
if !loaded.contains(&path) {
pending.push(path.clone());
}
on_entry(Entry::Include(path));
}
entry => on_entry(entry),
}
}
}
Ok(())
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct BeancountFile<D> {
pub options: Vec<BeanOption>,
pub includes: Vec<PathBuf>,
pub directives: Vec<Directive<D>>,
}
impl<D> Default for BeancountFile<D> {
fn default() -> Self {
Self {
options: Vec::new(),
includes: Vec::new(),
directives: Vec::new(),
}
}
}
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[..])
}
}
impl<D: Decimal> BeancountFile<D> {
pub fn read_files(files: impl IntoIterator<Item = PathBuf>) -> Result<Self, ReadFileErrorV2> {
let mut file = BeancountFile::default();
read_files_v2(files, |entry| {
file.extend(std::iter::once(entry));
})?;
Ok(file)
}
}
impl<D> Extend<Entry<D>> for BeancountFile<D> {
fn extend<T: IntoIterator<Item = Entry<D>>>(&mut self, iter: T) {
for entry in iter {
match entry {
Entry::Directive(d) => self.directives.push(d),
Entry::Option(o) => self.options.push(o),
Entry::Include(p) => self.includes.push(p),
}
}
}
}
impl<D> FromIterator<Entry<D>> for BeancountFile<D> {
fn from_iter<T: IntoIterator<Item = Entry<D>>>(iter: T) -> Self {
let mut file = BeancountFile::default();
file.extend(iter);
file
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Directive<D> {
pub date: Date,
pub content: DirectiveContent<D>,
pub metadata: metadata::Map<D>,
pub line_number: u32,
}
impl<D: Decimal> FromStr for Directive<D> {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match all_consuming(directive).parse(Span::new(s)).finish() {
Ok((_, d)) => Ok(d),
Err(err) => Err(Error::new(s, err.input)),
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq)]
#[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),
}
impl<D> DirectiveContent<D> {
pub fn as_transaction(&self) -> Option<&Transaction<D>> {
match self {
DirectiveContent::Transaction(transaction) => Some(transaction),
_ => None,
}
}
pub fn as_price(&self) -> Option<&Price<D>> {
match self {
DirectiveContent::Price(price) => Some(price),
_ => None,
}
}
pub fn as_balance(&self) -> Option<&Balance<D>> {
match self {
DirectiveContent::Balance(balance) => Some(balance),
_ => None,
}
}
pub fn as_open(&self) -> Option<&Open> {
match self {
DirectiveContent::Open(open) => Some(open),
_ => None,
}
}
pub fn as_close(&self) -> Option<&Close> {
match self {
DirectiveContent::Close(close) => Some(close),
_ => None,
}
}
pub fn as_pad(&self) -> Option<&Pad> {
match self {
DirectiveContent::Pad(pad) => Some(pad),
_ => None,
}
}
pub fn as_commodity(&self) -> Option<&Currency> {
match self {
DirectiveContent::Commodity(currency) => Some(currency),
_ => None,
}
}
pub fn as_event(&self) -> Option<&Event> {
match self {
DirectiveContent::Event(event) => Some(event),
_ => None,
}
}
}
type Span<'a> = nom_locate::LocatedSpan<&'a str>;
type IResult<'a, O> = nom::IResult<Span<'a>, O>;
#[allow(missing_docs)]
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum Entry<D> {
Directive(Directive<D>),
Option(BeanOption),
Include(PathBuf),
}
impl<D> Entry<D> {
pub fn as_directive(&self) -> Option<&Directive<D>> {
match self {
Entry::Directive(directive) => Some(directive),
_ => None,
}
}
pub fn as_option(&self) -> Option<&BeanOption> {
match self {
Entry::Option(option) => Some(option),
_ => None,
}
}
pub fn as_include(&self) -> Option<&Path> {
match self {
Entry::Include(include) => Some(include),
_ => None,
}
}
}
enum RawEntry<D> {
Directive(Directive<D>),
Option(BeanOption),
Include(PathBuf),
PushTag(Tag),
PopTag(Tag),
Comment,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct BeanOption {
pub name: String,
pub value: String,
}
fn entry<D: Decimal>(input: Span<'_>) -> IResult<'_, RawEntry<D>> {
alt((
directive.map(RawEntry::Directive),
option.map(|(name, value)| RawEntry::Option(BeanOption { name, value })),
include.map(|p| RawEntry::Include(p)),
tag_stack_operation,
line.map(|()| RawEntry::Comment),
))
.parse(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).parse(input)?;
let (input, (content, metadata)) = alt((
map(transaction::parse, |(t, m)| {
(DirectiveContent::Transaction(t), m)
}),
(
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,
),
))
.parse(input)?;
Ok((
input,
Directive {
date,
content,
metadata,
line_number: position.location_line(),
},
))
}
fn option(input: Span<'_>) -> IResult<'_, (String, String)> {
let (input, _) = tag("option")(input)?;
let (input, key) = preceded(space1, string).parse(input)?;
let (input, value) = preceded(space1, string).parse(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)).parse(input)?;
Ok((input, path.into()))
}
fn tag_stack_operation<D>(input: Span<'_>) -> IResult<'_, RawEntry<D>> {
alt((
preceded((tag("pushtag"), space1), transaction::parse_tag).map(RawEntry::PushTag),
preceded((tag("poptag"), space1), transaction::parse_tag).map(RawEntry::PopTag),
))
.parse(input)
}
fn end_of_line(input: Span<'_>) -> IResult<'_, ()> {
let (input, _) = space0(input)?;
let (input, _) = opt(comment).parse(input)?;
let (input, _) = alt((line_ending, eof)).parse(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).parse(input)?;
end_of_line(input)
}
fn string(input: Span<'_>) -> IResult<'_, String> {
let (input, _) = char('"')(input)?;
let mut string = String::new();
let mut take_data = take_while(|c: char| c != '"' && c != '\\');
let (mut input, mut part) = take_data.parse(input)?;
while !part.fragment().is_empty() {
string.push_str(part.fragment());
let (new_input, escaped) =
opt(alt((value('"', tag("\\\"")), value('\\', tag("\\\\"))))).parse_complete(input)?;
let Some(escaped) = escaped else { break };
string.push(escaped);
let (new_input, new_part) = take_data.parse(new_input)?;
input = new_input;
part = new_part;
}
let (input, _) = char('"')(input)?;
Ok((input, string))
}