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}