use std::borrow::Cow;
use std::sync::{Mutex, OnceLock, PoisonError};
use serde::de::{DeserializeOwned, IgnoredAny};
use serde_saphyr::{
Budget, DefaultMessageFormatter, DuplicateKeyPolicy, Error, Localizer, Location,
MergeKeyPolicy, MessageFormatter, NonFiniteFloatPolicy, Options,
};
use super::limits::{Exceeded, Limits, Raise};
use super::{At, FieldPath, Input, InputError, Problem, Step};
pub(super) fn parse<T: DeserializeOwned>(
input: &Input,
budget: &Budget,
raise: Raise,
) -> Result<T, InputError> {
read(input, options(budget, Reading::Config), budget, raise)
}
pub(super) fn parse_types<T: DeserializeOwned>(
input: &Input,
budget: &Budget,
raise: Raise,
) -> Result<T, InputError> {
read(input, options(budget, Reading::Listing), budget, raise)
}
fn read<T: DeserializeOwned>(
input: &Input,
options: Options,
budget: &Budget,
raise: Raise,
) -> Result<T, InputError> {
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),
}],
)
})
}
#[derive(Clone, Copy)]
enum Reading {
Config,
Listing,
}
fn options(budget: &Budget, reading: Reading) -> Options {
let (duplicates, merges, floats) = match reading {
Reading::Config => (
DuplicateKeyPolicy::Error,
MergeKeyPolicy::Error,
NonFiniteFloatPolicy::Default,
),
Reading::Listing => (
DuplicateKeyPolicy::PassThrough,
MergeKeyPolicy::AsOrdinary,
NonFiniteFloatPolicy::PassThrough,
),
};
serde_saphyr::options! {
budget: Some(budget.clone()),
strict_booleans: true,
duplicate_keys: duplicates,
merge_keys: merges,
non_finite_float_policy: floats,
with_snippet: false,
}
}
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()
}
}
#[derive(Debug, Default)]
pub(super) struct Located {
verdict: Mutex<Option<(Limits, bool)>>,
document: OnceLock<Option<yamled::Document>>,
}
impl Clone for Located {
fn clone(&self) -> Self {
Self {
verdict: Mutex::new(self.verdict()),
document: self.document.clone(),
}
}
}
impl PartialEq for Located {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl Eq for Located {}
impl Located {
fn verdict(&self) -> Option<(Limits, bool)> {
self.verdict
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
fn passes(&self, input: &Input, limits: &Limits) -> bool {
let mut verdict = self.verdict.lock().unwrap_or_else(PoisonError::into_inner);
if let Some((seen, passed)) = verdict.as_ref()
&& seen == limits
{
return *passed;
}
let passed = within(input, &limits.budget());
*verdict = Some((limits.clone(), passed));
passed
}
}
pub(super) fn places(
input: &Input,
limits: &Limits,
paths: &[FieldPath],
at: At,
) -> Vec<Option<(usize, usize)>> {
let document = document(input, limits);
paths
.iter()
.map(|path| {
document
.and_then(|document| match at {
At::Value => document.locate_nearest(&yamled_path(path.steps())),
At::Key => nearest_key(document, path.steps()),
})
.map(|place| (place.line, place.column))
})
.collect()
}
pub(super) fn document<'a>(input: &'a Input, limits: &Limits) -> Option<&'a yamled::Document> {
let located = &input.yaml_document;
located
.passes(input, limits)
.then(|| {
located
.document
.get_or_init(|| yamled::Document::parse(&input.text).ok())
.as_ref()
})
.flatten()
}
fn within(input: &Input, budget: &Budget) -> bool {
let options = options(budget, Reading::Listing);
serde_saphyr::from_str_with_options::<IgnoredAny>(&input.text, options).is_ok()
}
fn nearest_key(document: &yamled::Document, mut steps: &[Step]) -> Option<yamled::Location> {
loop {
let path = yamled_path(steps);
if let Some(node) = document.node(&path) {
return Some(node.key().unwrap_or_else(|| node.value()));
}
match steps.split_last() {
Some((_, rest)) => steps = rest,
None => return document.locate_nearest(&path),
}
}
}
fn yamled_path(steps: &[Step]) -> yamled::Path {
steps
.iter()
.fold(yamled::Path::root(), |path, step| match step {
Step::Key(key) => path.key(key.as_str()),
Step::Index(index) => path.index(*index),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_second_placing_reuses_the_document() {
let input = Input::new("a.yaml", "files:\n 'a.b':\n label: ''\n");
let path = FieldPath::root().key("files").key("a.b").key("label");
assert!(input.yaml_document.document.get().is_none());
assert_eq!(input.place(&path).map(|at| at.line), Some(3));
let first = input.yaml_document.document.get().unwrap();
assert!(first.is_some());
assert_eq!(input.place(&path).map(|at| at.line), Some(3));
let second = input.yaml_document.document.get().unwrap();
assert!(std::ptr::eq(first, second));
}
fn placed_with(text: &str, limits: Limits) -> (Input, Option<(usize, usize)>) {
let input = Input::new("a.yaml", text).limits(limits);
let at = input
.place(&FieldPath::root().key("a"))
.map(|at| (at.line, at.column));
(input, at)
}
#[test]
fn listing_the_top_level_builds_the_document_that_placing_reuses() {
let input = Input::new("a.yaml", "a: 1\n");
assert!(input.yaml_document.document.get().is_none());
input.top_level().unwrap();
let first = input.yaml_document.document.get().unwrap();
assert!(input.place(&FieldPath::root().key("a")).is_some());
let second = input.yaml_document.document.get().unwrap();
assert!(std::ptr::eq(first, second));
}
#[test]
fn a_text_over_the_size_limit_builds_no_document() {
let limits = Limits {
max_reader_input_bytes: Some(4),
..Limits::NONE
};
let (input, at) = placed_with("a: 1\nb: 2\n", limits);
assert_eq!(at, None);
assert_eq!(input.place_key(&FieldPath::root().key("a")), None);
assert!(input.yaml_document.document.get().is_none());
}
#[test]
fn a_text_over_the_depth_or_node_limit_builds_no_document() {
let text = "a: {b: {c: {d: 1}}}\n";
assert_eq!(placed_with(text, Limits::NONE).1, Some((1, 4)));
for limits in [
Limits {
max_depth: Some(2),
..Limits::NONE
},
Limits {
max_nodes: Some(3),
..Limits::NONE
},
] {
let (input, at) = placed_with(text, limits);
assert_eq!(at, None);
assert!(input.yaml_document.document.get().is_none());
}
}
fn tight() -> Limits {
Limits {
max_depth: Some(2),
..Limits::NONE
}
}
#[test]
fn a_clone_tightened_after_the_document_was_built_is_refused() {
let text = "a: {b: {c: {d: 1}}}\n";
let first = FieldPath::root().key("a");
let loose = Input::new("a.yaml", text);
assert!(loose.place(&first).is_some());
let tightened = loose.clone().limits(tight());
assert!(tightened.place(&first).is_none());
assert!(loose.place(&first).is_some());
}
#[test]
fn a_refusal_ends_when_the_limits_that_gave_it_do() {
let text = "a: {b: {c: {d: 1}}}\n";
let first = FieldPath::root().key("a");
let refused = Input::new("a.yaml", text).limits(tight());
assert!(refused.place(&first).is_none());
let freed = refused.clone().limits(Limits::NONE);
assert!(freed.place(&first).is_some());
assert!(refused.place(&first).is_none());
}
#[test]
fn an_input_equals_its_clone_whether_or_not_it_was_placed_on() {
let input = Input::new("a.yaml", "a: 1\n");
let fresh = input.clone();
assert!(input.place(&FieldPath::root().key("a")).is_some());
assert_eq!(input, fresh);
}
}