W3MWSSJ7LUAUUAQJH47BNNGLM2O6CEMJ2MA5PY72EAA6GIW67ANAC
---
source: common/beancount-types/tests/account_template.rs
assertion_line: 30
expression: template
---
Template {
base: Acc {
name: "Assets:Banking",
},
segments: [],
}
---
source: common/beancount-types/tests/account_template.rs
expression: template
---
Parsing {
inner: [
SelectorSegment {
source: SelectorError {
selector: "institution",
},
span: SourceSpan {
offset: SourceOffset(
15,
),
length: SourceOffset(
13,
),
},
},
SelectorSegment {
source: SelectorError {
selector: "account",
},
span: SourceSpan {
offset: SourceOffset(
29,
),
length: SourceOffset(
9,
),
},
},
SelectorSegment {
source: SelectorError {
selector: "position",
},
span: SourceSpan {
offset: SourceOffset(
46,
),
length: SourceOffset(
10,
),
},
},
],
template: "Assets:Banking:{institution}:{account}:Stocks:{position}",
}
---
source: common/beancount-types/tests/account_template.rs
expression: template
---
Parsing {
inner: [
LiteralSegment {
source: SegmentError {
segment: "Main Account",
backtrace: Backtrace(
(),
),
},
span: SourceSpan {
offset: SourceOffset(
29,
),
length: SourceOffset(
12,
),
},
},
LiteralSegment {
source: SegmentError {
segment: "stocks",
backtrace: Backtrace(
(),
),
},
span: SourceSpan {
offset: SourceOffset(
42,
),
length: SourceOffset(
6,
),
},
},
],
template: "Assets:Banking:{institution}:Main Account:stocks:{position}",
}
---
source: common/beancount-types/tests/account_template.rs
expression: template
---
Parsing {
inner: [
BaseAccount {
source: AccountError {
name: "Assets:Banking:Bank of America",
backtrace: Backtrace(
(),
),
},
span: SourceSpan {
offset: SourceOffset(
0,
),
length: SourceOffset(
30,
),
},
},
],
template: "Assets:Banking:Bank of America:{account}",
}
---
source: common/beancount-types/tests/account_template.rs
assertion_line: 7
expression: template
---
EmptyTemplate
---
source: common/beancount-types/tests/account_template.rs
expression: template
---
Template {
base: Acc {
name: "Assets:Banking",
},
segments: [
Selector(
AnySelector(
"institution",
),
),
],
}
---
source: common/beancount-types/tests/account_template.rs
expression: template
---
Template {
base: Acc {
name: "Assets:Banking",
},
segments: [
Selector(
AnySelector(
"institution",
),
),
Selector(
AnySelector(
"account",
),
),
Selector(
AnySelector(
"position",
),
),
],
}
---
source: common/beancount-types/tests/account_template.rs
expression: template
---
Template {
base: Acc {
name: "Assets:Banking",
},
segments: [
Selector(
AnySelector(
"institution",
),
),
Selector(
AnySelector(
"account",
),
),
Literal(
Seg {
inner: "Stocks",
},
),
Selector(
AnySelector(
"position",
),
),
],
}
---
source: common/beancount-types/tests/account_template.rs
expression: template
---
Parsing {
inner: [
BaseAccount {
source: AccountError {
name: "Assets:Banking:Bank of America",
backtrace: Backtrace(
(),
),
},
span: SourceSpan {
offset: SourceOffset(
0,
),
length: SourceOffset(
30,
),
},
},
LiteralSegment {
source: SegmentError {
segment: "cash",
backtrace: Backtrace(
(),
),
},
span: SourceSpan {
offset: SourceOffset(
41,
),
length: SourceOffset(
4,
),
},
},
LiteralSegment {
source: SegmentError {
segment: "{po",
backtrace: Backtrace(
(),
),
},
span: SourceSpan {
offset: SourceOffset(
46,
),
length: SourceOffset(
3,
),
},
},
],
template: "Assets:Banking:Bank of America:{account}:cash:{po",
}
use beancount_types::AccountTemplate;
use miette::Diagnostic;
use snafu::Snafu;
#[derive(Clone, Copy, Debug)]
struct AnySelector<'t>(&'t str);
impl<'t> TryFrom<&'t str> for AnySelector<'t> {
type Error = NeverSelectorError;
fn try_from(value: &'t str) -> Result<Self, Self::Error> {
Ok(Self(value.try_into().unwrap()))
}
}
#[derive(Debug, Diagnostic, Snafu)]
enum NeverSelectorError {}
#[derive(Clone, Copy, Debug)]
struct NoSelector();
impl<'t> TryFrom<&'t str> for NoSelector {
type Error = SelectorError<'t>;
fn try_from(selector: &'t str) -> Result<Self, Self::Error> {
Err(SelectorError { selector })
}
}
#[derive(Debug, Diagnostic, Snafu)]
struct SelectorError<'t> {
selector: &'t str,
}
#[test]
fn parsing_empty_template_fails() {
let template = AccountTemplate::<AnySelector>::parse("").unwrap_err();
insta::assert_debug_snapshot!(template);
}
#[test]
fn parsing_only_base_account_works() {
let template = AccountTemplate::<AnySelector>::parse("Assets:Banking").unwrap();
insta::assert_debug_snapshot!(template);
}
#[test]
fn parsing_base_account_and_trailing_selector_works() {
let template = AccountTemplate::<AnySelector>::parse("Assets:Banking:{institution}").unwrap();
insta::assert_debug_snapshot!(template);
}
#[test]
fn parsing_base_account_and_multiple_trailing_selectors_works() {
let template =
AccountTemplate::<AnySelector>::parse("Assets:Banking:{institution}:{account}:{position}")
.unwrap();
insta::assert_debug_snapshot!(template);
}
#[test]
fn parsing_base_account_and_mixed_trailing_selectors_and_literals_works() {
let template = AccountTemplate::<AnySelector>::parse(
"Assets:Banking:{institution}:{account}:Stocks:{position}",
)
.unwrap();
insta::assert_debug_snapshot!(template);
}
#[test]
fn parsing_fails_for_invalid_literals() {
let template = AccountTemplate::<AnySelector>::parse(
"Assets:Banking:{institution}:Main Account:stocks:{position}",
)
.unwrap_err();
insta::assert_debug_snapshot!(template);
}
#[test]
fn parsing_fails_for_invalid_selectors() {
let template = AccountTemplate::<NoSelector>::parse(
"Assets:Banking:{institution}:{account}:Stocks:{position}",
)
.unwrap_err();
insta::assert_debug_snapshot!(template);
}
#[test]
fn parsing_fails_for_invalid_base_account() {
let template =
AccountTemplate::<AnySelector>::parse("Assets:Banking:Bank of America:{account}")
.unwrap_err();
insta::assert_debug_snapshot!(template);
}
#[test]
fn multiple_parsing_errors_are_reported_in_bulk() {
let template =
AccountTemplate::<AnySelector>::parse("Assets:Banking:Bank of America:{account}:cash:{po")
.unwrap_err();
insta::assert_debug_snapshot!(template);
}
use core::fmt::Debug;
use core::ops::Index;
use miette::Diagnostic;
use miette::SourceSpan;
use snafu::ResultExt as _;
use snafu::Snafu;
use crate::account::AccountError;
use crate::account::SegmentError;
use crate::Acc;
use crate::Account;
use crate::Seg;
#[derive(Debug, Diagnostic, Snafu)]
pub enum Error<'t, E>
where
E: 'static + Diagnostic,
{
#[snafu(display("empty templates are not supported"))]
EmptyTemplate {},
#[snafu(display("errors while parsing template"))]
Parsing {
#[related]
inner: Vec<ParsingError<E>>,
#[source_code]
template: &'t str,
},
}
#[derive(Debug, Diagnostic, Snafu)]
pub enum ParsingError<E>
where
E: 'static + Diagnostic,
{
#[snafu(display("could not parse base account"))]
BaseAccount {
source: AccountError,
#[label]
span: SourceSpan,
},
#[snafu(display("could not parse literal segment"))]
LiteralSegment {
#[diagnostic_source]
source: SegmentError,
#[label]
span: SourceSpan,
},
#[snafu(display("could not parse selector segment"))]
SelectorSegment {
source: E,
#[label]
span: SourceSpan,
},
}
#[derive(Debug)]
pub struct Template<'t, Selector> {
base: &'t Acc,
segments: Vec<Segment<'t, Selector>>,
}
impl<'t, Selector> Template<'t, Selector>
where
Selector: TryFrom<&'t str>,
Selector::Error: Diagnostic,
{
pub fn parse(template: &'t str) -> Result<Self, Error<'t, Selector::Error>> {
snafu::ensure!(!template.is_empty(), EmptyTemplateSnafu {});
let (base, segments) = template.find(":{").map_or((template, ""), |mid| {
(&template[..mid], &template[mid + 1..])
});
let initial_offset = base.len();
let base = base.try_into().context(BaseAccountSnafu {
span: 0..base.len(),
});
let segments = parse_trailing_segments(segments, initial_offset);
let errors = match (base, segments) {
(Ok(base), Ok(segments)) => return Ok(Self { base, segments }),
(Ok(_), Err(errors)) => errors,
(Err(error), Ok(_)) => vec![error],
(Err(error), Err(mut errors)) => {
errors.insert(0, error);
errors
}
};
ParsingSnafu {
inner: errors,
template,
}
.fail()
}
}
impl<'t, Selector> Template<'t, Selector> {
pub fn render<'c, Context>(&'t self, context: &'c Context) -> Account
where
Context: Index<&'t Selector, Output = Seg>,
Selector: 't,
{
let fields = self.segments.iter().map(|field| match field {
Segment::Literal(seg) => *seg,
Segment::Selector(selector) => context.index(selector),
});
fields.fold(self.base.to_owned(), |account, segment| {
account.join(&segment)
})
}
}
impl<'t, Selector> TryFrom<&'t str> for Template<'t, Selector>
where
Selector: TryFrom<&'t str>,
Selector::Error: 'static + Diagnostic,
{
type Error = Error<'t, Selector::Error>;
fn try_from(template: &'t str) -> Result<Self, Self::Error> {
Self::parse(template)
}
}
#[derive(Clone, Debug)]
pub(crate) enum Segment<'t, Selector> {
Literal(&'t Seg),
Selector(Selector),
}
impl<'t, Selector> Segment<'t, Selector>
where
Selector: TryFrom<&'t str>,
Selector::Error: 'static + Diagnostic,
{
fn parse(segment: &'t str, span: SourceSpan) -> Result<Self, ParsingError<Selector::Error>> {
if segment.starts_with('{') && segment.ends_with('}') {
Ok(Self::Selector(
segment[1..segment.len() - 1]
.trim()
.try_into()
.context(SelectorSegmentSnafu { span })?,
))
} else {
<&Seg>::try_from(segment)
.map(Self::from)
.context(LiteralSegmentSnafu { span })
}
}
}
impl<'t, Selector> From<&'t Seg> for Segment<'t, Selector> {
fn from(seg: &'t Seg) -> Self {
Self::Literal(seg)
}
}
fn parse_trailing_segments<'t, Selector>(
segments: &'t str,
initial_offset: usize,
) -> Result<Vec<Segment<Selector>>, Vec<ParsingError<Selector::Error>>>
where
Selector: TryFrom<&'t str>,
Selector::Error: Diagnostic,
{
if segments.is_empty() {
return Ok(vec![]);
}
let segments = segments.split(':').scan(initial_offset, {
|offset, segment| {
let start = *offset + 1;
*offset += segment.len() + 1;
Some(Segment::parse(segment, (start..*offset).into()))
}
});
let (length, _) = segments.size_hint();
segments.fold(Ok(Vec::with_capacity(length)), |result, segment| {
match (result, segment) {
(Ok(mut segments), Ok(segment)) => {
segments.push(segment);
Ok(segments)
}
(Ok(_), Err(error)) => Err(vec![error]),
(Err(errors), Ok(_)) => Err(errors),
(Err(mut errors), Err(error)) => {
errors.push(error);
Err(errors)
}
}
})
}