use core::fmt::Debug;
use core::ops::Index;
use core::str::FromStr;
use beancount_account::AccountError;
use beancount_account::SegmentError;
use miette::Diagnostic;
use miette::SourceSpan;
use serde::Deserialize;
use snafu::ResultExt as _;
use snafu::Snafu;
use crate::Acc;
use crate::Account;
use crate::Seg;
#[derive(Debug, Diagnostic, Snafu)]
pub enum Error<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: String,
},
}
#[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(Clone, Debug, Deserialize)]
#[serde(
bound = "Selector: FromStr, Selector::Err: 'static + Diagnostic",
try_from = "String"
)]
pub struct Template<Selector> {
base: Account,
segments: Vec<Segment<Selector>>,
}
impl<Selector> Template<Selector>
where
Selector: FromStr,
Selector::Err: Diagnostic,
{
pub fn parse(template: &str) -> Result<Self, Error<Selector::Err>> {
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<Selector> {
#[must_use]
pub fn base(&self) -> &Acc {
&self.base
}
pub fn render<'c, Context>(&'t self, context: &'c Context) -> Account
where
Context: Index<&'t Selector, Output = Seg>,
{
let fields = self.segments.iter().map(|field| match field {
Segment::Literal(seg) => seg,
Segment::Selector(selector) => context.index(selector),
});
fields.fold(self.base.clone(), Account::join)
}
}
impl<Selector> TryFrom<&str> for Template<Selector>
where
Selector: FromStr,
Selector::Err: 'static + Diagnostic,
{
type Error = Error<Selector::Err>;
fn try_from(template: &str) -> Result<Self, Self::Error> {
Self::parse(template)
}
}
impl<Selector> TryFrom<String> for Template<Selector>
where
Selector: FromStr,
Selector::Err: 'static + Diagnostic,
{
type Error = Error<Selector::Err>;
fn try_from(template: String) -> Result<Self, Self::Error> {
Self::parse(&template)
}
}
#[derive(Clone, Debug)]
enum Segment<Selector> {
Literal(crate::Segment),
Selector(Selector),
}
impl<Selector> Segment<Selector>
where
Selector: FromStr,
Selector::Err: Diagnostic,
{
fn parse(segment: &str, span: SourceSpan) -> Result<Self, ParsingError<Selector::Err>> {
if segment.starts_with('{') && segment.ends_with('}') {
Ok(Self::Selector(
segment[1..segment.len() - 1]
.trim()
.parse()
.context(SelectorSegmentSnafu { span })?,
))
} else {
crate::Segment::try_from(segment)
.map(Self::from)
.context(LiteralSegmentSnafu { span })
}
}
}
impl<Selector> From<crate::Segment> for Segment<Selector> {
fn from(seg: crate::Segment) -> Self {
Self::Literal(seg)
}
}
fn parse_trailing_segments<Selector>(
segments: &str,
initial_offset: usize,
) -> Result<Vec<Segment<Selector>>, Vec<ParsingError<Selector::Err>>>
where
Selector: FromStr,
Selector::Err: 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();
#[allow(clippy::manual_try_fold, /* reason = "we want to collect multiple errors here" */)]
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)
}
}
})
}