use std::borrow::Cow;
use serde::de::DeserializeOwned;
use serde_saphyr::{
Budget, DefaultMessageFormatter, DuplicateKeyPolicy, Error, Localizer, Location,
MergeKeyPolicy, MessageFormatter,
};
use super::limits::{Exceeded, Raise};
use super::{Input, InputError, Problem};
#[cfg(feature = "validate")]
use super::{Step, field_steps};
pub(super) fn parse<T: DeserializeOwned>(
input: &Input,
budget: &Budget,
raise: Raise,
) -> Result<T, InputError> {
let options = serde_saphyr::options! {
budget: Some(budget.clone()),
strict_booleans: true,
duplicate_keys: DuplicateKeyPolicy::Error,
merge_keys: MergeKeyPolicy::Error,
with_snippet: false,
};
serde_saphyr::from_str_with_options(&input.text, options).map_err(|error| {
let location = error.location();
InputError::of(
input.name.clone(),
vec![Problem {
line: location
.and_then(|at| usize::try_from(at.line()).ok())
.map(|line| line + input.lines_before),
column: location.and_then(|at| usize::try_from(at.column()).ok()),
field: String::new(),
message: message(input, error.without_snippet(), budget, raise),
}],
)
})
}
fn message(input: &Input, error: &Error, budget: &Budget, raise: Raise) -> String {
if let Error::Budget { breach, .. } = error
&& let Some(exceeded) = Exceeded::breach(breach, budget, raise)
{
return exceeded.to_string();
}
let formatter = DefaultMessageFormatter.with_localizer(&Unplaced);
let Error::AliasError {
error: inner,
locations,
..
} = error
else {
return plain(&formatter, error);
};
let mut failing = inner.as_ref();
while let Error::AliasError { error, .. } | Error::WithSnippet { error, .. } = failing {
failing = error;
}
let at = failing
.location()
.filter(|at| *at != Location::UNKNOWN)
.unwrap_or(locations.defined_location);
let line = usize::try_from(at.line()).unwrap_or_default() + input.lines_before;
let column = at.column();
let text = plain(&formatter, failing);
format!("{text} (defined at line {line}, column {column})")
}
fn plain(formatter: &dyn MessageFormatter, mut error: &Error) -> String {
while let Error::AliasError { error: inner, .. } | Error::WithSnippet { error: inner, .. } =
error
{
error = inner;
}
let mut text = String::new();
for character in formatter.format_message(error).chars() {
if character.is_control() || matches!(character, '\u{2028}' | '\u{2029}') {
text.extend(character.escape_debug());
} else {
text.push(character);
}
}
text
}
struct Unplaced;
impl Localizer for Unplaced {
fn attach_location<'a>(&self, base: Cow<'a, str>, _: Location) -> Cow<'a, str> {
base
}
fn alias_used_at(&self, _: Location) -> String {
String::new()
}
}
#[cfg(feature = "validate")]
pub(super) fn places(text: &str, fields: &[&str]) -> Vec<Option<(usize, usize)>> {
let document = yamled::Document::parse(text).ok();
fields
.iter()
.map(|field| {
document
.as_ref()
.and_then(|document| document.locate_nearest(&path(&field_steps(field))))
.map(|at| (at.line, at.column))
})
.collect()
}
#[cfg(feature = "validate")]
fn path(steps: &[Step<'_>]) -> yamled::Path {
steps
.iter()
.fold(yamled::Path::root(), |path, step| match step {
Step::Key(key) => path.key(*key),
Step::Index(index) => path.index(*index),
})
}