#[cfg(test)]
mod test_apply;
#[cfg(test)]
mod test_edits;
#[cfg(test)]
mod test_json;
#[cfg(test)]
mod test_lint_warning;
#[cfg(test)]
mod test_price_invariant;
#[cfg(test)]
mod test_schema_warning;
use std::{
collections::{BTreeMap, BTreeSet},
fmt,
};
use crate::{json, lint, schema, string, warning, weekday};
pub fn edits<W: Fixable>(
doc: &json::Document<'_>,
warnings: &warning::Set<W>,
) -> Result<Vec<Edit>, Error> {
let mut out: Vec<Edit> = Vec::new();
for group in warnings {
let (element, raised) = group.to_parts();
let live = doc
.element(element.id)
.ok_or(Error::UnknownElement(element.id))?;
for warning in raised {
if let Some(edit) = warning.fix(live) {
out.push(edit);
}
}
}
Ok(out)
}
#[expect(
private_bounds,
reason = "`Sealed` is crate-private on purpose; that is what seals the trait"
)]
pub trait Fixable: crate::Warning + sealed::Sealed {
fn fix(&self, element: &json::Element<'_>) -> Option<Edit>;
}
pub(crate) mod sealed {
pub(crate) trait Sealed {}
}
impl sealed::Sealed for schema::Warning {}
impl Fixable for schema::Warning {
#[expect(
clippy::match_same_arms,
reason = "one arm per variant, so a new variant has to state whether it can be fixed"
)]
fn fix(&self, element: &json::Element<'_>) -> Option<Edit> {
match self {
Self::NullField => Some(Edit::remove(element.id())),
Self::UnexpectedField => Some(Edit::remove(element.id())),
Self::NonSpecField => None,
Self::IncorrectCase {
expected,
actual: _,
} => Some(Edit::replace(element.id(), Json::string(expected))),
Self::MissingField { name: _ }
| Self::InvalidType {
expected: _,
actual: _,
}
| Self::StringTooLong { max: _, len: _ }
| Self::InvalidValue {
expected: _,
actual: _,
}
| Self::Cardinality => None,
}
}
}
impl sealed::Sealed for lint::tariff::Warning {}
impl Fixable for lint::tariff::Warning {
#[expect(
clippy::match_same_arms,
reason = "one arm per variant, so a new variant has to state whether it can be fixed"
)]
fn fix(&self, element: &json::Element<'_>) -> Option<Edit> {
match self {
Self::ContainsEntireWeek => Some(Edit::remove(element.id())),
Self::DayOfWeekDuplicates | Self::DayOfWeekUnsorted => sorted_day_of_week(element),
Self::DayOfWeekEmpty => None,
Self::EndTimeIsNearEndOfDay => None,
Self::Duration(_)
| Self::MinPriceIsGreaterThanMax
| Self::StartDateTimeIsAfterEndDateTime => None,
Self::ContainsEntireDay => None,
Self::CpoCountryCodeShouldBeAlpha2 => None,
Self::MaxZeroNeverMatch | Self::NeverValid => None,
Self::RestrictionsEmpty => None,
Self::Country(_)
| Self::Currency(_)
| Self::DateTime(_)
| Self::Money(_)
| Self::Number(_)
| Self::String(_) => None,
}
}
}
fn sorted_day_of_week(element: &json::Element<'_>) -> Option<Edit> {
let items = element.value().as_array()?;
let mut days: Vec<weekday::Weekday> = Vec::with_capacity(items.len());
for item in items {
let json::Value::String(raw) = item.value() else {
return None;
};
days.push(weekday::Weekday::from_canonical(raw.as_unescaped_str())?);
}
days.sort_unstable();
days.dedup();
let names: Vec<&str> = days.iter().map(|day| day.canonical()).collect();
Some(Edit::replace(element.id(), Json::string_array(&names)))
}
pub fn apply(doc: &json::Document<'_>, edits: &[Edit]) -> Result<String, Error> {
let splices = resolve(doc, edits)?;
let edited = splice(doc.source(), &splices)?;
check_parses(&edited)?;
Ok(edited)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Edit(Change);
impl Edit {
pub(crate) fn remove(elem: json::ElemId) -> Self {
Self(Change::Remove(elem))
}
pub(crate) fn replace(elem: json::ElemId, json: Json) -> Self {
Self(Change::Replace { elem, json })
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Change {
Remove(json::ElemId),
Replace {
elem: json::ElemId,
json: Json,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Json(String);
impl Json {
pub(crate) fn string_array(values: &[&str]) -> Self {
let mut json = String::from("[");
for (index, value) in values.iter().enumerate() {
if index > 0 {
json.push_str(", ");
}
push_json_string(&mut json, value);
}
json.push(']');
Self::built(json)
}
pub(crate) fn string(value: &str) -> Self {
let mut json = String::new();
push_json_string(&mut json, value);
Self::built(json)
}
pub fn as_str(&self) -> &str {
&self.0
}
fn built(text: String) -> Self {
debug_assert!(
is_one_json_value(&text),
"`fix` built text that is not a single JSON value: `{text}`"
);
Self(text)
}
#[cfg(test)]
fn raw(text: &str) -> Self {
Self::built(text.to_owned())
}
}
impl fmt::Display for Json {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
fn push_json_string(json: &mut String, value: &str) {
json.push('"');
for c in value.chars() {
match c {
'"' => json.push_str(r#"\""#),
'\\' => json.push_str(r"\\"),
'\n' => json.push_str(r"\n"),
'\r' => json.push_str(r"\r"),
'\t' => json.push_str(r"\t"),
'\u{8}' => json.push_str(r"\b"),
'\u{c}' => json.push_str(r"\f"),
c if c < ' ' => {
let code = u32::from(c);
json.push_str(r"\u00");
json.push(hex_digit(code >> 4));
json.push(hex_digit(code & 0xf));
}
c => json.push(c),
}
}
json.push('"');
}
fn hex_digit(nibble: u32) -> char {
char::from_digit(nibble, 16).unwrap_or('0')
}
fn is_one_json_value(text: &str) -> bool {
let Ok(checked) = string::ReasonableLen::new(text) else {
return false;
};
json::parse(checked).is_ok()
}
#[derive(Debug, Eq, PartialEq)]
pub enum Error {
Duplicate(json::ElemId),
Conflict {
outer: json::ElemId,
inner: json::ElemId,
},
UnknownElement(json::ElemId),
Removal(json::RemovalError),
Internal(json::Error),
OutputTooLarge,
OverlappingSpans {
first: json::Span,
second: json::Span,
},
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Duplicate(id) => {
write!(f, "More than one edit replaces the element with id `{id}`.")
}
Self::Conflict { outer, inner } => write!(
f,
"Replacing the element with id `{outer}` would discard the edit to the element with id `{inner}` inside it."
),
Self::UnknownElement(id) => write!(f, "The document has no element with id `{id}`."),
Self::Removal(error) => write!(f, "{error}"),
Self::Internal(error) => write!(
f,
"The edits spliced into JSON that does not parse, which is a bug in `fix`: {error}"
),
Self::OutputTooLarge => write!(
f,
"The edited JSON exceeds the reasonable maximum `{} MB`.",
string::ReasonableLen::FACTOR
),
Self::OverlappingSpans { first, second } => write!(
f,
"The spans `{first:?}` and `{second:?}` overlap, so the edits can not both be applied."
),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Removal(error) => Some(error),
Self::Internal(error) => Some(error),
Self::Duplicate(_)
| Self::Conflict { outer: _, inner: _ }
| Self::UnknownElement(_)
| Self::OutputTooLarge
| Self::OverlappingSpans {
first: _,
second: _,
} => None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Splice<'edit> {
span: json::Span,
text: &'edit str,
}
#[derive(Clone, Copy, Debug)]
struct Replacement<'edit> {
elem: json::ElemId,
span: json::Span,
text: &'edit str,
}
fn resolve<'edit>(
doc: &json::Document<'_>,
edits: &'edit [Edit],
) -> Result<Vec<Splice<'edit>>, Error> {
let mut removals: BTreeSet<json::ElemId> = BTreeSet::new();
let mut replacements: BTreeMap<json::ElemId, &'edit str> = BTreeMap::new();
for edit in edits {
match &edit.0 {
Change::Remove(id) => {
removals.insert(*id);
}
Change::Replace { elem, json } => {
let earlier = replacements.insert(*elem, json.as_str());
if earlier.is_some_and(|earlier| earlier != json.as_str()) {
return Err(Error::Duplicate(*elem));
}
}
}
}
let removed = doc.removal_spans(&removals).map_err(Error::Removal)?;
let replaced = resolve_replacements(doc, &replacements, &removals, &removed)?;
check_no_removal_inside_a_replacement(doc, &removals, &replaced)?;
let mut splices: Vec<Splice<'edit>> = Vec::new();
for span in removed {
splices.push(Splice { span, text: "" });
}
for replacement in replaced {
splices.push(Splice {
span: replacement.span,
text: replacement.text,
});
}
splices.sort_by_key(|splice| splice.span);
Ok(splices)
}
fn resolve_replacements<'edit>(
doc: &json::Document<'_>,
replacements: &BTreeMap<json::ElemId, &'edit str>,
removals: &BTreeSet<json::ElemId>,
removed: &[json::Span],
) -> Result<Vec<Replacement<'edit>>, Error> {
let mut resolved: Vec<Replacement<'edit>> = Vec::new();
for (elem, text) in replacements {
if removals.contains(elem) {
continue;
}
let element = doc.element(*elem).ok_or(Error::UnknownElement(*elem))?;
let span = element.span();
if removed.iter().any(|erased| contains(*erased, span)) {
continue;
}
resolved.push(Replacement {
elem: *elem,
span,
text,
});
}
resolved.sort_by_key(|replacement| replacement.span);
check_no_nested_replacement(&resolved)?;
Ok(resolved)
}
fn check_no_nested_replacement(replaced: &[Replacement<'_>]) -> Result<(), Error> {
let mut outer: Option<Replacement<'_>> = None;
for replacement in replaced {
if let Some(open) = outer {
if replacement.span.start < open.span.end {
return Err(Error::Conflict {
outer: open.elem,
inner: replacement.elem,
});
}
}
outer = Some(*replacement);
}
Ok(())
}
fn check_no_removal_inside_a_replacement(
doc: &json::Document<'_>,
removals: &BTreeSet<json::ElemId>,
replaced: &[Replacement<'_>],
) -> Result<(), Error> {
for id in removals {
let element = doc.element(*id).ok_or(Error::UnknownElement(*id))?;
let span = element.span();
let enclosing = replaced
.iter()
.find(|replacement| contains(replacement.span, span));
if let Some(replacement) = enclosing {
return Err(Error::Conflict {
outer: replacement.elem,
inner: *id,
});
}
}
Ok(())
}
fn splice(source: &str, splices: &[Splice<'_>]) -> Result<String, Error> {
let mut out = String::with_capacity(source.len());
let mut pos: u32 = 0;
for splice in splices {
if splice.span.start < pos {
let previous = splices
.iter()
.find(|earlier| earlier.span.end == pos)
.map(|earlier| earlier.span)
.unwrap_or_default();
return Err(Error::OverlappingSpans {
first: previous,
second: splice.span,
});
}
out.push_str(slice(source, pos, splice.span.start));
out.push_str(splice.text);
pos = splice.span.end;
}
out.push_str(slice(source, pos, source_len(source)));
Ok(out)
}
fn contains(outer: json::Span, inner: json::Span) -> bool {
inner.start >= outer.start && inner.end <= outer.end
}
fn check_parses(json: &str) -> Result<(), Error> {
let json = string::ReasonableLen::new(json).map_err(|_e| Error::OutputTooLarge)?;
json::parse(json).map_err(Error::Internal)?;
Ok(())
}
fn slice(source: &str, start: u32, end: u32) -> &str {
let Ok(start) = usize::try_from(start) else {
return "";
};
let Ok(end) = usize::try_from(end) else {
return "";
};
source.get(start..end).unwrap_or("")
}
fn source_len(source: &str) -> u32 {
u32::try_from(source.len()).unwrap_or(u32::MAX)
}