Skip to main content

ocpi_tariffs/
fix.rs

1//! Applying [`Edit`]s to the source of a parsed [`json::Document`].
2//!
3//! Producing the edits and agreeing to apply them are separate steps, which is what lets edits
4//! drawn from different kinds of warning be resolved together. A `warning::Set<schema::Warning>`
5//! and a `warning::Set<lint::tariff::Warning>` are different types, so no single call can take
6//! both; [`Edit`] is the common form each reduces to, and a caller concatenates them before one
7//! [`apply`].
8//!
9//! Applying each set in turn instead would resolve the second against a re-parsed document, where
10//! the ids of the first no longer mean the same elements, so two edits landing on one element
11//! could not be resolved against each other at all.
12//!
13//! [`apply`] resolves a whole set of edits against one document in a single pass. Resolving
14//! them one at a time would be wrong for the same reason within a set: which span removes an
15//! element depends on which of its siblings survive, so removals have to be resolved together.
16//! See [`json::Document::removal_spans`].
17
18#[cfg(test)]
19mod test_apply;
20
21#[cfg(test)]
22mod test_edits;
23
24#[cfg(test)]
25mod test_json;
26
27#[cfg(test)]
28mod test_lint_warning;
29
30#[cfg(test)]
31mod test_price_invariant;
32
33#[cfg(test)]
34mod test_schema_warning;
35
36use std::{
37    collections::{BTreeMap, BTreeSet},
38    fmt,
39};
40
41use crate::{json, lint, schema, string, warning, weekday};
42
43/// The edits that resolve `warnings` against the document they were raised on.
44///
45/// A [`Fixable`] warning says what edit would resolve it; it does not decide whether that edit
46/// is wanted. Filter the [`warning::Set`] before calling this to leave a fix out, with
47/// `Set::remove_unexpected_fields` and its siblings.
48///
49/// The edits come back in document order, since a [`warning::Set`] is keyed by
50/// [`json::ElemId`]. Pass them to [`apply`], which resolves how they interact.
51pub fn edits<W: Fixable>(
52    doc: &json::Document<'_>,
53    warnings: &warning::Set<W>,
54) -> Result<Vec<Edit>, Error> {
55    let mut out: Vec<Edit> = Vec::new();
56
57    for group in warnings {
58        let (element, raised) = group.to_parts();
59        let live = doc
60            .element(element.id)
61            .ok_or(Error::UnknownElement(element.id))?;
62
63        for warning in raised {
64            if let Some(edit) = warning.fix(live) {
65                out.push(edit);
66            }
67        }
68    }
69
70    Ok(out)
71}
72
73/// A [`Warning`](crate::Warning) that a mechanical edit can resolve.
74///
75/// An implementation reports what edit would resolve the warning, not whether the edit should
76/// be made. Whether a given fix is wanted is the caller's decision, so a caller that does not
77/// want one filters the warning out of the set before calling [`edits`].
78///
79/// Sealed, because only this crate can build an [`Edit`]: an implementation outside it could
80/// return nothing but `None`.
81#[expect(
82    private_bounds,
83    reason = "`Sealed` is crate-private on purpose; that is what seals the trait"
84)]
85pub trait Fixable: crate::Warning + sealed::Sealed {
86    /// The edit that resolves this warning at `element`, or `None` if nothing mechanical can
87    /// resolve it.
88    fn fix(&self, element: &json::Element<'_>) -> Option<Edit>;
89}
90
91/// Seals [`Fixable`] against implementations outside this crate.
92pub(crate) mod sealed {
93    /// The supertrait of [`Fixable`](super::Fixable) that only this crate can name, and so the
94    /// only crate that can implement `Fixable`.
95    pub(crate) trait Sealed {}
96}
97
98impl sealed::Sealed for schema::Warning {}
99
100impl Fixable for schema::Warning {
101    #[expect(
102        clippy::match_same_arms,
103        reason = "one arm per variant, so a new variant has to state whether it can be fixed"
104    )]
105    fn fix(&self, element: &json::Element<'_>) -> Option<Edit> {
106        match self {
107            // A `null` value has no semantic meaning in OCPI. The item can be removed.
108            Self::NullField => Some(Edit::remove(element.id())),
109
110            // The OCPI spec does not define this field so the fix feature can remove it.
111            Self::UnexpectedField => Some(Edit::remove(element.id())),
112
113            // A non-spec field is off-spec but used because we can infer what it should have been.
114            // Removing it would change how the document prices, so there is no fix action.
115            Self::NonSpecField => None,
116
117            // Write the variant with the correct case.
118            Self::IncorrectCase {
119                expected,
120                actual: _,
121            } => Some(Edit::replace(element.id(), Json::string(expected))),
122
123            // All the below issues require intervention from the author.
124            Self::MissingField { name: _ }
125            | Self::InvalidType {
126                expected: _,
127                actual: _,
128            }
129            | Self::StringTooLong { max: _, len: _ }
130            | Self::InvalidValue {
131                expected: _,
132                actual: _,
133            }
134            | Self::Cardinality => None,
135        }
136    }
137}
138
139impl sealed::Sealed for lint::tariff::Warning {}
140
141impl Fixable for lint::tariff::Warning {
142    #[expect(
143        clippy::match_same_arms,
144        reason = "one arm per variant, so a new variant has to state whether it can be fixed"
145    )]
146    fn fix(&self, element: &json::Element<'_>) -> Option<Edit> {
147        match self {
148            // A list of all seven days matches every day, which is what leaving the list out
149            // means, so the element matches the same days without it.
150            Self::ContainsEntireWeek => Some(Edit::remove(element.id())),
151
152            // Which days a list matches is a question of membership, so neither the order of
153            // the days nor a repeat of one changes it.
154            Self::DayOfWeekDuplicates | Self::DayOfWeekUnsorted => sorted_day_of_week(element),
155
156            // An empty list matches no day at all and removing it would match every day, so
157            // there is no edit that keeps the meaning. The author has to say which was meant.
158            Self::DayOfWeekEmpty => None,
159
160            // `23:59` and the `00:00` the spec asks for do not cover the same day, so
161            // rewriting one to the other moves a boundary by a minute.
162            Self::EndTimeIsNearEndOfDay => None,
163
164            // Nothing mechanical resolves these. Two fields disagree and only the author knows
165            // which of them is wrong, or the value is unusable and only the author knows what
166            // it was meant to be. `Duration` is the one wrapped enum with no fixable variant:
167            // a duration that does not parse, is not an int, or overflows is all it reports.
168            Self::Duration(_)
169            | Self::MinPriceIsGreaterThanMax
170            | Self::StartDateTimeIsAfterEndDateTime => None,
171
172            // TODO #406 Fixable as a removal of this element, but only when its value is exactly
173            // `00:00`: a bound that is not there restricts nothing, so dropping a `00:00`
174            // leaves the same window. `is_day_end` also accepts `23:59`, and dropping that
175            // widens the window by a minute, which is `EndTimeIsNearEndOfDay`'s objection.
176            // When both bounds are present only the one the warning sits on can go in a pass;
177            // the other is raised again on the next, so the pair takes two fixing passes.
178            Self::ContainsEntireDay => None,
179
180            // TODO #405 Fixable at this element, not yet written. The warning is raised only
181            // once the value has parsed as a valid alpha-3, so the `country::Code` behind it
182            // already holds the alpha-2 to write in its place, and the lowering normalizes the
183            // two strings to that same `Code` regardless. Needs a crate-visible alpha-3
184            // lookup, because `Code::from_alpha_3` is reachable from `country` alone.
185            Self::CpoCountryCodeShouldBeAlpha2 => None,
186
187            // TODO #376 Fixable, but not by editing the element the warning sits on: an element
188            // that can never match is discarded whole, which is the canonicalizer's job rather
189            // than a local edit to one restriction.
190            Self::MaxZeroNeverMatch | Self::NeverValid => None,
191
192            // TODO #375 Fixable as a removal of this element: an object holding no
193            // restriction fields restricts nothing. This is the same semantics as no object.
194            // A document is usually left with an empty object by an earlier fix rather than
195            // written that way, so resolving it takes a second pass over the document.
196            Self::RestrictionsEmpty => None,
197
198            // TODO #407 The lowering's own warnings, each wrapping an enum that mixes the two
199            // cases. A lower-case country, currency or string is `IncorrectCase` and a
200            // rewrite; so is a value carrying escape codes that decode. An invalid code, a
201            // malformed escape or a number out of range is not. Resolving them means `Fixable`
202            // reaching into the wrapped enum, since the wrapper is the wrong altitude for the
203            // decision.
204            Self::Country(_)
205            | Self::Currency(_)
206            | Self::DateTime(_)
207            | Self::Money(_)
208            | Self::Number(_)
209            | Self::String(_) => None,
210        }
211    }
212}
213
214/// The edit that puts a `day_of_week` list in spec order and drops its repeats.
215///
216/// Returns `None` unless every item is a day this can name, so a list holding anything else is
217/// left exactly as the author wrote it. The schema accepts a day in any case, and the match
218/// here is exact, so a lower-case list is one of the lists that keeps its warning.
219fn sorted_day_of_week(element: &json::Element<'_>) -> Option<Edit> {
220    let items = element.value().as_array()?;
221    let mut days: Vec<weekday::Weekday> = Vec::with_capacity(items.len());
222
223    for item in items {
224        let json::Value::String(raw) = item.value() else {
225            return None;
226        };
227
228        days.push(weekday::Weekday::from_canonical(raw.as_unescaped_str())?);
229    }
230
231    days.sort_unstable();
232    days.dedup();
233
234    let names: Vec<&str> = days.iter().map(|day| day.canonical()).collect();
235
236    Some(Edit::replace(element.id(), Json::string_array(&names)))
237}
238
239/// Apply `edits` to the source of `doc` and return the edited JSON.
240///
241/// The output is re-parsed before it is returned. Since every replacement payload is one JSON
242/// value by construction, and a value is legal wherever another one was, this should not be able
243/// to fail; it is kept as a backstop on the splicing, and on the payload builders in a release
244/// build where their own assertion is compiled out.
245///
246/// Edits interact, and the rules are not symmetric:
247///
248/// | Case | Outcome |
249/// |---|---|
250/// | Two removals of the same element | Removed once |
251/// | A removal of an element inside another removed element | The ancestor's span covers it |
252/// | A replacement of an element that is also removed | The removal wins |
253/// | A replacement of an element inside a removed element | The removal wins |
254/// | Two replacements of the same element, same text | Replaced once |
255/// | Two replacements of the same element, differing text | [`Error::Duplicate`] |
256/// | A replacement of an element inside another replaced element | [`Error::Conflict`] |
257/// | A removal of an element inside a replaced element | [`Error::Conflict`] |
258///
259/// A removal absorbs any edit inside it because the bytes go away either way, so nothing an
260/// inner edit intended is lost. A replacement cannot absorb one: its payload was written
261/// without knowledge of the inner edit, so applying it would discard that edit silently.
262///
263/// Two replacements asking for the same text are one edit proposed twice, not a disagreement:
264/// a `day_of_week` list can be both unsorted and repeating, and the same sorted list resolves
265/// either warning. Only replacements that disagree about the text are an error.
266pub fn apply(doc: &json::Document<'_>, edits: &[Edit]) -> Result<String, Error> {
267    let splices = resolve(doc, edits)?;
268    let edited = splice(doc.source(), &splices)?;
269
270    check_parses(&edited)?;
271
272    Ok(edited)
273}
274
275/// A single change to one element of a document.
276///
277/// Opaque, and produced only by [`edits`]. Which edit resolves a warning is this crate's
278/// decision; a caller's decision is *which warnings to resolve*, made by filtering the
279/// [`warning::Set`] before calling [`edits`]. That is also the only layer at which the choice can
280/// be expressed, since an `Edit` records what to change and not the warning that asked for it.
281#[derive(Clone, Debug, Eq, PartialEq)]
282pub struct Edit(Change);
283
284impl Edit {
285    /// Remove the element from its parent, along with the separator that joined it to its
286    /// siblings.
287    pub(crate) fn remove(elem: json::ElemId) -> Self {
288        Self(Change::Remove(elem))
289    }
290
291    /// Replace the element's value with `json`, leaving an object field's key in place.
292    pub(crate) fn replace(elem: json::ElemId, json: Json) -> Self {
293        Self(Change::Replace { elem, json })
294    }
295}
296
297/// What an [`Edit`] does to its element.
298#[derive(Clone, Debug, Eq, PartialEq)]
299enum Change {
300    /// Remove the element from its parent, along with the separator that joined it to its
301    /// siblings.
302    Remove(json::ElemId),
303
304    /// Replace the element's value with `json`, leaving an object field's key in place.
305    Replace {
306        /// The element whose value is rewritten.
307        elem: json::ElemId,
308
309        /// The value taking its place.
310        json: Json,
311    },
312}
313
314/// One JSON value represented as a `String`.
315///
316/// The text is spliced in place of an element's own span, so it stands in for exactly one value:
317/// it carries no field key, no comma, and no second value. Which value it is does not matter, as
318/// any JSON value is a valid replacement from a JSON spec point-of-view.
319///
320/// There is no constructor that takes JSON source. Each one takes the content to write and builds
321/// the text itself, escaping what it writes, so a `Json` is one valid JSON value by construction.
322/// What that does *not* promise is that it is the right value for where it lands: it is not
323/// checked against the OCPI schema for that position, so [`apply`] will happily write a number
324/// where an array was.
325#[derive(Clone, Debug, Eq, PartialEq)]
326pub(crate) struct Json(String);
327
328impl Json {
329    /// A JSON array holding each of `values` as a JSON string.
330    ///
331    /// The quoting and escaping happen here, so a caller passes the string content it wants
332    /// written rather than JSON source: a value holding a quote or a backslash arrives in the
333    /// document as that same one character.
334    pub(crate) fn string_array(values: &[&str]) -> Self {
335        let mut json = String::from("[");
336
337        for (index, value) in values.iter().enumerate() {
338            if index > 0 {
339                json.push_str(", ");
340            }
341
342            push_json_string(&mut json, value);
343        }
344
345        json.push(']');
346
347        Self::built(json)
348    }
349
350    /// A JSON string.
351    ///
352    /// `value` is quoted and escaped, so a caller passes the string content it wants
353    /// written rather than JSON source.
354    pub(crate) fn string(value: &str) -> Self {
355        let mut json = String::new();
356
357        push_json_string(&mut json, value);
358
359        Self::built(json)
360    }
361
362    /// The text this value will be written as, exactly as it was built.
363    pub fn as_str(&self) -> &str {
364        &self.0
365    }
366
367    /// Take text built by a constructor above, holding it to being a single JSON value.
368    ///
369    /// Every constructor ends here, so the check covers all of them. It is a `debug_assert`
370    /// rather than a returned error because the producer is always this crate. Text that is not
371    /// one JSON value is a bug in the constructor that built it, not something a caller passed in
372    /// or can act on. A release build still cannot write a corrupt document, since [`apply`]
373    /// re-parses the whole result and reports [`Error::Internal`]; what the assertion adds is the
374    /// producer's own stack rather than that after-the-fact report.
375    fn built(text: String) -> Self {
376        debug_assert!(
377            is_one_json_value(&text),
378            "`fix` built text that is not a single JSON value: `{text}`"
379        );
380
381        Self(text)
382    }
383
384    /// Take `text` verbatim, for tests needing a payload shape no constructor above builds.
385    #[cfg(test)]
386    fn raw(text: &str) -> Self {
387        Self::built(text.to_owned())
388    }
389}
390
391impl fmt::Display for Json {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        f.write_str(&self.0)
394    }
395}
396
397/// Write `value` into `json` as a quoted JSON string.
398fn push_json_string(json: &mut String, value: &str) {
399    json.push('"');
400
401    for c in value.chars() {
402        match c {
403            '"' => json.push_str(r#"\""#),
404            '\\' => json.push_str(r"\\"),
405            '\n' => json.push_str(r"\n"),
406            '\r' => json.push_str(r"\r"),
407            '\t' => json.push_str(r"\t"),
408            '\u{8}' => json.push_str(r"\b"),
409            '\u{c}' => json.push_str(r"\f"),
410
411            // JSON forbids a literal control character inside a string, and the ones above are
412            // the only control characters with a shorthand, so the rest need the `\u` form.
413            c if c < ' ' => {
414                let code = u32::from(c);
415
416                json.push_str(r"\u00");
417                json.push(hex_digit(code >> 4));
418                json.push(hex_digit(code & 0xf));
419            }
420
421            c => json.push(c),
422        }
423    }
424
425    json.push('"');
426}
427
428/// The lower-case hex digit for `nibble`, which every caller here has already masked to one.
429fn hex_digit(nibble: u32) -> char {
430    char::from_digit(nibble, 16).unwrap_or('0')
431}
432
433/// Whether `text` is exactly one JSON value, which is what a replacement has room for.
434fn is_one_json_value(text: &str) -> bool {
435    let Ok(checked) = string::ReasonableLen::new(text) else {
436        return false;
437    };
438
439    json::parse(checked).is_ok()
440}
441
442/// Why a set of [`Edit`]s could not be applied.
443///
444/// Most of these say the edits asked for something contradictory, or named an element that is
445/// not there, and the caller can act on them. [`Error::Internal`] and [`Error::OverlappingSpans`]
446/// are different in kind: no set of edits can ask for either, so reaching one is a bug in this
447/// module rather than anything the caller did. They are returned rather than panicked on because
448/// the caller is about to write the result over someone's tariff, and refusing to is better than
449/// crashing in their process or, worse, handing back JSON that has been corrupted.
450#[derive(Debug, Eq, PartialEq)]
451pub enum Error {
452    /// More than one replacement targets this element, and they disagree about the text.
453    Duplicate(json::ElemId),
454
455    /// Applying `outer` would silently discard the edit to `inner`, which sits inside it.
456    Conflict {
457        /// The replaced element.
458        outer: json::ElemId,
459
460        /// The element edited inside it.
461        inner: json::ElemId,
462    },
463
464    /// The document has no element with this id.
465    UnknownElement(json::ElemId),
466
467    /// The removals could not be resolved to spans.
468    Removal(json::RemovalError),
469
470    /// Splicing produced source that is no longer valid JSON, which is a bug in this module.
471    ///
472    /// A replacement carries a payload that is one JSON value by construction, and a value is
473    /// legal wherever another value was, so no payload should cause this. What is left is the
474    /// removal spans and the splice itself, so reaching this means one of those is wrong - or, in
475    /// a release build, that a payload builder is, since its own assertion is compiled out.
476    Internal(json::Error),
477
478    /// The spliced source exceeds the maximum size deemed reasonable.
479    ///
480    /// A caller can reach this: replacements longer than what they stand in for can take a
481    /// document that was within the limit past it. This is the only size bound in the module, and
482    /// it is on the whole output, since a bound on one payload would not catch that.
483    OutputTooLarge,
484
485    /// Two resolved spans overlap, so erasing both would corrupt the output. No set of edits
486    /// should reach this; it means resolution itself is wrong.
487    OverlappingSpans {
488        /// The span that starts first.
489        first: json::Span,
490
491        /// The span that starts inside it.
492        second: json::Span,
493    },
494}
495
496impl fmt::Display for Error {
497    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
498        match self {
499            Self::Duplicate(id) => {
500                write!(f, "More than one edit replaces the element with id `{id}`.")
501            }
502            Self::Conflict { outer, inner } => write!(
503                f,
504                "Replacing the element with id `{outer}` would discard the edit to the element with id `{inner}` inside it."
505            ),
506            Self::UnknownElement(id) => write!(f, "The document has no element with id `{id}`."),
507            Self::Removal(error) => write!(f, "{error}"),
508            Self::Internal(error) => write!(
509                f,
510                "The edits spliced into JSON that does not parse, which is a bug in `fix`: {error}"
511            ),
512            Self::OutputTooLarge => write!(
513                f,
514                "The edited JSON exceeds the reasonable maximum `{} MB`.",
515                string::ReasonableLen::FACTOR
516            ),
517            Self::OverlappingSpans { first, second } => write!(
518                f,
519                "The spans `{first:?}` and `{second:?}` overlap, so the edits can not both be applied."
520            ),
521        }
522    }
523}
524
525impl std::error::Error for Error {
526    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
527        match self {
528            Self::Removal(error) => Some(error),
529            Self::Internal(error) => Some(error),
530            Self::Duplicate(_)
531            | Self::Conflict { outer: _, inner: _ }
532            | Self::UnknownElement(_)
533            | Self::OutputTooLarge
534            | Self::OverlappingSpans {
535                first: _,
536                second: _,
537            } => None,
538        }
539    }
540}
541
542/// One resolved edit: the span of source it covers and the text that takes its place.
543#[derive(Clone, Copy, Debug, Eq, PartialEq)]
544struct Splice<'edit> {
545    span: json::Span,
546
547    /// Empty for a removal.
548    text: &'edit str,
549}
550
551/// A replacement that has been resolved to the span it rewrites.
552#[derive(Clone, Copy, Debug)]
553struct Replacement<'edit> {
554    elem: json::ElemId,
555    span: json::Span,
556    text: &'edit str,
557}
558
559/// Resolve `edits` into the spans to rewrite, sorted by start offset.
560fn resolve<'edit>(
561    doc: &json::Document<'_>,
562    edits: &'edit [Edit],
563) -> Result<Vec<Splice<'edit>>, Error> {
564    let mut removals: BTreeSet<json::ElemId> = BTreeSet::new();
565    let mut replacements: BTreeMap<json::ElemId, &'edit str> = BTreeMap::new();
566
567    for edit in edits {
568        match &edit.0 {
569            Change::Remove(id) => {
570                removals.insert(*id);
571            }
572            Change::Replace { elem, json } => {
573                let earlier = replacements.insert(*elem, json.as_str());
574
575                if earlier.is_some_and(|earlier| earlier != json.as_str()) {
576                    return Err(Error::Duplicate(*elem));
577                }
578            }
579        }
580    }
581
582    let removed = doc.removal_spans(&removals).map_err(Error::Removal)?;
583    let replaced = resolve_replacements(doc, &replacements, &removals, &removed)?;
584
585    check_no_removal_inside_a_replacement(doc, &removals, &replaced)?;
586
587    let mut splices: Vec<Splice<'edit>> = Vec::new();
588
589    for span in removed {
590        splices.push(Splice { span, text: "" });
591    }
592
593    for replacement in replaced {
594        splices.push(Splice {
595            span: replacement.span,
596            text: replacement.text,
597        });
598    }
599
600    splices.sort_by_key(|splice| splice.span);
601
602    Ok(splices)
603}
604
605/// Resolve each replacement to its span, dropping the ones a removal absorbs.
606fn resolve_replacements<'edit>(
607    doc: &json::Document<'_>,
608    replacements: &BTreeMap<json::ElemId, &'edit str>,
609    removals: &BTreeSet<json::ElemId>,
610    removed: &[json::Span],
611) -> Result<Vec<Replacement<'edit>>, Error> {
612    let mut resolved: Vec<Replacement<'edit>> = Vec::new();
613
614    for (elem, text) in replacements {
615        if removals.contains(elem) {
616            continue;
617        }
618
619        let element = doc.element(*elem).ok_or(Error::UnknownElement(*elem))?;
620        let span = element.span();
621
622        if removed.iter().any(|erased| contains(*erased, span)) {
623            continue;
624        }
625
626        resolved.push(Replacement {
627            elem: *elem,
628            span,
629            text,
630        });
631    }
632
633    resolved.sort_by_key(|replacement| replacement.span);
634
635    check_no_nested_replacement(&resolved)?;
636
637    Ok(resolved)
638}
639
640/// Reject a replacement that sits inside another replacement.
641///
642/// Element spans nest or are disjoint, and no two elements share a start offset, so sorting
643/// by span puts an enclosing replacement before the one it encloses.
644fn check_no_nested_replacement(replaced: &[Replacement<'_>]) -> Result<(), Error> {
645    let mut outer: Option<Replacement<'_>> = None;
646
647    for replacement in replaced {
648        if let Some(open) = outer {
649            if replacement.span.start < open.span.end {
650                return Err(Error::Conflict {
651                    outer: open.elem,
652                    inner: replacement.elem,
653                });
654            }
655        }
656
657        outer = Some(*replacement);
658    }
659
660    Ok(())
661}
662
663/// Reject a removal that sits inside a replacement.
664fn check_no_removal_inside_a_replacement(
665    doc: &json::Document<'_>,
666    removals: &BTreeSet<json::ElemId>,
667    replaced: &[Replacement<'_>],
668) -> Result<(), Error> {
669    for id in removals {
670        let element = doc.element(*id).ok_or(Error::UnknownElement(*id))?;
671        let span = element.span();
672        let enclosing = replaced
673            .iter()
674            .find(|replacement| contains(replacement.span, span));
675
676        if let Some(replacement) = enclosing {
677            return Err(Error::Conflict {
678                outer: replacement.elem,
679                inner: *id,
680            });
681        }
682    }
683
684    Ok(())
685}
686
687/// Rewrite `source` by replacing each span with its text.
688fn splice(source: &str, splices: &[Splice<'_>]) -> Result<String, Error> {
689    let mut out = String::with_capacity(source.len());
690    let mut pos: u32 = 0;
691
692    for splice in splices {
693        if splice.span.start < pos {
694            let previous = splices
695                .iter()
696                .find(|earlier| earlier.span.end == pos)
697                .map(|earlier| earlier.span)
698                .unwrap_or_default();
699
700            return Err(Error::OverlappingSpans {
701                first: previous,
702                second: splice.span,
703            });
704        }
705
706        out.push_str(slice(source, pos, splice.span.start));
707        out.push_str(splice.text);
708        pos = splice.span.end;
709    }
710
711    out.push_str(slice(source, pos, source_len(source)));
712
713    Ok(out)
714}
715
716/// Return true if `outer` covers every byte of `inner`.
717fn contains(outer: json::Span, inner: json::Span) -> bool {
718    inner.start >= outer.start && inner.end <= outer.end
719}
720
721/// Re-parse edited JSON, so a corrupted splice is reported rather than written.
722fn check_parses(json: &str) -> Result<(), Error> {
723    let json = string::ReasonableLen::new(json).map_err(|_e| Error::OutputTooLarge)?;
724
725    json::parse(json).map_err(Error::Internal)?;
726
727    Ok(())
728}
729
730/// The byte range `start..end` of `source`.
731///
732/// Every span comes from the parser that read `source`, so it always lands on a character
733/// boundary; an out of range span yields an empty `&str` rather than a panic.
734fn slice(source: &str, start: u32, end: u32) -> &str {
735    let Ok(start) = usize::try_from(start) else {
736        return "";
737    };
738    let Ok(end) = usize::try_from(end) else {
739        return "";
740    };
741
742    source.get(start..end).unwrap_or("")
743}
744
745/// The length of `source` as a span offset.
746fn source_len(source: &str) -> u32 {
747    u32::try_from(source.len()).unwrap_or(u32::MAX)
748}