Skip to main content

ocpi_tariffs/
json.rs

1//! JSON parsing and typed decoding for the OCPI CDR pricing/generating and CDR/Tariff linting pipeline.
2//!
3//! # Parsing vs decoding
4//!
5//! Parsing and decoding are intentionally separated so the linter can emit
6//! actionable warnings rather than hard parse errors.
7//!
8//! **Parsing** ([`parse_object`]) converts a raw JSON `&str` into an
9//! [`Element`] tree. The parser is deliberately lenient about string content:
10//! it only verifies structural correctness (balanced delimiters, valid
11//! top-level values) and leaves escape sequences and control characters
12//! untouched inside [`RawStr`].
13//!
14//! **Decoding** ([`decode`]) interprets the raw JSON String as a `&str`.
15//! Calling [`RawStr::decode_escapes`] validates escape sequences and
16//! rejects control characters, returning [`decode::Warning`]
17//! values instead of hard errors. This lets the linter pinpoint the exact
18//! field, report what is wrong, and suggest a corrected encoding.
19//! The `price` and `generate` mods can choose to hard fail on specific `Warning`s.
20//!
21pub mod decode;
22mod parser;
23pub mod write;
24
25#[cfg(test)]
26pub(crate) mod test;
27
28#[cfg(test)]
29mod test_line_col;
30
31#[cfg(test)]
32mod test_path;
33
34#[cfg(test)]
35mod test_path_matches_glob;
36
37#[cfg(test)]
38mod test_removal;
39
40#[cfg(test)]
41mod test_source_json;
42
43use std::{
44    borrow::{Borrow, Cow},
45    collections::{btree_set, BTreeMap, BTreeSet},
46    fmt::{self, Write as _},
47    rc::Rc,
48};
49
50use crate::{
51    string,
52    warning::{Caveat, CaveatDeferred},
53};
54
55pub(crate) use parser::parse;
56pub use parser::{Error, ErrorKind as ParseErrorKind};
57
58/// Parse a raw JSON `&str` into a [`Document`] and require the root value to be a JSON object.
59///
60/// The input size is gated: 5 MB (5,000,000 bytes) or more returns
61/// [`ParseError::SizeExceedsMax`].
62pub fn parse_object(json: &str) -> Result<Document<'_>, ParseError> {
63    let json = string::ReasonableLen::new(json).map_err(|_e| ParseError::SizeExceedsMax)?;
64    let doc = parse(json).map_err(ParseError::Json)?;
65
66    if !doc.root().is_object() {
67        return Err(ParseError::ShouldBeAnObject);
68    }
69
70    Ok(doc)
71}
72
73#[derive(Debug)]
74/// Why a raw JSON `&str` could not be turned into a [`Document`].
75pub enum ParseError {
76    /// The JSON parser was unable to parse the JSON str.
77    Json(Error),
78
79    /// The OCPI object should be a JSON object.
80    ShouldBeAnObject,
81
82    /// The size of the input `str` exceeds the maximum deemed reasonable.
83    SizeExceedsMax,
84}
85
86impl fmt::Display for ParseError {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            Self::Json(error) => write!(f, "{error}"),
90            Self::ShouldBeAnObject => f.write_str("The CDR should be an object."),
91            Self::SizeExceedsMax => write!(
92                f,
93                "The input `&str` exceeds the reasonable maximum `{} MB`.",
94                string::ReasonableLen::FACTOR
95            ),
96        }
97    }
98}
99
100impl std::error::Error for ParseError {
101    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
102        match &self {
103            ParseError::Json(err) => Some(err),
104            ParseError::ShouldBeAnObject | ParseError::SizeExceedsMax => None,
105        }
106    }
107}
108
109/// The output of [`parse_object`]: the element tree with path resolution embedded in each
110/// element.
111#[derive(Clone, Debug)]
112pub struct Document<'buf> {
113    /// Shared inner state; also held by every element in the tree.
114    inner: Rc<DocumentInner<'buf>>,
115    /// Root element of the parsed tree.
116    root: Element<'buf>,
117}
118
119impl<'buf> Document<'buf> {
120    /// Returns the source JSON string this document was parsed from.
121    pub fn source(&self) -> &'buf str {
122        self.inner.source
123    }
124
125    /// Returns the root element of this document.
126    pub fn root(&self) -> &Element<'buf> {
127        &self.root
128    }
129
130    /// Returns the element with `id`, or `None` if this document has no such element.
131    ///
132    /// Walks the parent chain up from `id` and then descends back down the tree, so the
133    /// cost is O(depth) plus a scan of each ancestor's children. Meant for looking up
134    /// the occasional edit site, not for bulk traversal.
135    ///
136    /// NOTE: an [`ElemId`] produced by a different parse of the same source `&str`
137    /// cannot be told apart from one of this document's own ids.
138    pub fn element(&self, id: ElemId) -> Option<&Element<'buf>> {
139        let ancestry = self.inner.paths.ancestry(id)?;
140        let (root_id, descent) = ancestry.split_first()?;
141
142        if *root_id != self.root.id {
143            return None;
144        }
145
146        let mut element = &self.root;
147
148        for child_id in descent {
149            element = child_of(element, *child_id)?;
150        }
151
152        Some(element)
153    }
154
155    /// Resolves the source spans to erase in order to remove every element in `ids`.
156    ///
157    /// The returned spans are sorted by start offset and never overlap, so erasing them
158    /// from [`Document::source`] leaves JSON that still parses.
159    ///
160    /// Siblings are resolved as a group, because which span removes a child depends on
161    /// which of its siblings survive:
162    ///
163    /// | Case | Span erased |
164    /// |---|---|
165    /// | Run of children followed by a surviving sibling | the run's first key or value, up to the start of that sibling |
166    /// | Run of children reaching the closing delimiter | the end of the last surviving sibling's value, up to the end of the run |
167    /// | Every child of an object or array | everything between the `{}` or `[]` delimiters |
168    ///
169    /// A run that reaches the closing delimiter carries no trailing comma of its own, so
170    /// it has to swallow the comma of the sibling before it. Resolving each id on its own
171    /// and merging the spans afterwards gets that case wrong: it leaves the earlier
172    /// sibling's comma dangling.
173    ///
174    /// An id nested inside another id in the same set needs nothing beyond being dropped,
175    /// since the ancestor's span already covers it.
176    pub fn removal_spans(&self, ids: &BTreeSet<ElemId>) -> Result<Vec<Span>, RemovalError> {
177        let mut by_parent: BTreeMap<ElemId, BTreeSet<ElemId>> = BTreeMap::new();
178
179        for id in ids {
180            let parent = self.parent_of(*id)?;
181            by_parent.entry(parent).or_default().insert(*id);
182        }
183
184        let mut spans: Vec<Span> = Vec::new();
185
186        for (parent_id, removed) in by_parent {
187            let parent = self
188                .element(parent_id)
189                .ok_or(RemovalError::UnknownElement(parent_id))?;
190
191            spans.extend(sibling_removal_spans(parent, &removed)?);
192        }
193
194        spans.sort_unstable();
195
196        Ok(without_nested(spans))
197    }
198
199    /// Return the id of the parent of the [`Element`] with the given `id`.
200    fn parent_of(&self, id: ElemId) -> Result<ElemId, RemovalError> {
201        match self.inner.paths.entries.get(id.0) {
202            None => Err(RemovalError::UnknownElement(id)),
203            Some(PathEntry::Root) => Err(RemovalError::Root),
204            Some(PathEntry::Field { parent, key: _ } | PathEntry::Item { parent, index: _ }) => {
205                Ok(*parent)
206            }
207        }
208    }
209}
210
211/// Why a set of elements could not be resolved to removal spans.
212#[derive(Copy, Clone, Debug, Eq, PartialEq)]
213pub enum RemovalError {
214    /// The root element has no parent, so removing it would leave no document behind.
215    Root,
216
217    /// No element in this document has this id.
218    UnknownElement(ElemId),
219}
220
221impl fmt::Display for RemovalError {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        match self {
224            Self::Root => f.write_str("The root element can not be removed."),
225            Self::UnknownElement(id) => write!(f, "The document has no element with id `{id}`."),
226        }
227    }
228}
229
230impl std::error::Error for RemovalError {}
231
232/// Returns the direct child of `parent` that has `id`.
233fn child_of<'a, 'buf>(parent: &'a Element<'buf>, id: ElemId) -> Option<&'a Element<'buf>> {
234    match parent.value() {
235        Value::Object(fields) => fields
236            .iter()
237            .map(Field::element)
238            .find(|element| element.id == id),
239        Value::Array(items) => items.iter().find(|element| element.id == id),
240        Value::Null | Value::True | Value::False | Value::String(_) | Value::Number(_) => None,
241    }
242}
243
244/// Resolves the spans to erase to remove `removed` from `parent`, in source order.
245fn sibling_removal_spans(
246    parent: &Element<'_>,
247    removed: &BTreeSet<ElemId>,
248) -> Result<Vec<Span>, RemovalError> {
249    let children = child_spans(parent);
250
251    for id in removed {
252        if !children.iter().any(|child| child.id == *id) {
253            return Err(RemovalError::UnknownElement(*id));
254        }
255    }
256
257    let mut spans: Vec<Span> = Vec::new();
258    let mut kept_end: Option<u32> = None;
259    let mut run: Option<Run> = None;
260
261    for child in &children {
262        if removed.contains(&child.id) {
263            let start = run.map(|open| open.start).unwrap_or(child.start);
264            run = Some(Run {
265                start,
266                end: child.end,
267                next_sibling_start: child.next_sibling_start,
268            });
269            continue;
270        }
271
272        // A surviving sibling closes the open run: erase up to where that sibling starts,
273        // which takes the run's own trailing comma with it.
274        if let Some(open) = run.take() {
275            spans.push(Span::new(open.start, open.next_sibling_start));
276        }
277
278        kept_end = Some(child.end);
279    }
280
281    let Some(open) = run else {
282        return Ok(spans);
283    };
284
285    // A run still open here runs up to the closing delimiter, so it has no trailing comma
286    // of its own and has to swallow the comma of the sibling before it. With no surviving
287    // sibling before it, every child was removed and the whole interior goes.
288    let span = match kept_end {
289        Some(end) => Span::new(end, open.end),
290        None => interior_span(parent),
291    };
292
293    spans.push(span);
294
295    Ok(spans)
296}
297
298/// A maximal run of adjacent children being removed from the same parent.
299#[derive(Copy, Clone, Debug)]
300struct Run {
301    /// First byte to erase; the `start` of the run's first child.
302    start: u32,
303    /// One past the last byte of the run's last value.
304    end: u32,
305    /// The `next_sibling_start` of the run's last child.
306    next_sibling_start: u32,
307}
308
309/// Where a child of an object or array sits in the source, as far as removal is concerned.
310#[derive(Copy, Clone, Debug)]
311struct ChildSpan {
312    id: ElemId,
313    /// First byte to erase: the key of an object field, the value of an array item.
314    start: u32,
315    /// One past the last byte of the child's value.
316    end: u32,
317    /// Where the next sibling starts: one past this child's trailing comma and the
318    /// whitespace after it. Equal to `end` for the last child, which has no comma.
319    next_sibling_start: u32,
320}
321
322/// The removal spans of every child of `parent`, in source order.
323fn child_spans(parent: &Element<'_>) -> Vec<ChildSpan> {
324    match parent.value() {
325        Value::Object(fields) => fields
326            .iter()
327            .map(|field| ChildSpan {
328                id: field.element.id,
329                start: field.key_span.start,
330                end: field.element.span.end,
331                next_sibling_start: field.element.full_span_end,
332            })
333            .collect(),
334        Value::Array(items) => items
335            .iter()
336            .map(|item| ChildSpan {
337                id: item.id,
338                start: item.span.start,
339                end: item.span.end,
340                next_sibling_start: item.full_span_end,
341            })
342            .collect(),
343        Value::Null | Value::True | Value::False | Value::String(_) | Value::Number(_) => {
344            Vec::new()
345        }
346    }
347}
348
349/// The span between an object's or array's delimiters, each of which is one byte wide.
350fn interior_span(element: &Element<'_>) -> Span {
351    let end = element.span.end.saturating_sub(1);
352    let start = element.span.start.saturating_add(1).min(end);
353
354    Span::new(start, end)
355}
356
357/// Drops the spans that sit inside an earlier span.
358///
359/// A removed element nested inside another removed element is already covered by its
360/// ancestor's span. JSON is a tree, so two removal spans either nest or are disjoint;
361/// they never partially overlap.
362fn without_nested(spans: Vec<Span>) -> Vec<Span> {
363    let mut kept: Vec<Span> = Vec::new();
364
365    for span in spans {
366        let Some(last) = kept.last_mut() else {
367            kept.push(span);
368            continue;
369        };
370
371        if span.start >= last.end {
372            kept.push(span);
373            continue;
374        }
375
376        // Nesting means `last` already reaches past `span`, so this leaves it as it is.
377        // Extending rather than ignoring keeps a partial overlap from under-erasing.
378        last.end = last.end.max(span.end);
379    }
380
381    kept
382}
383
384/// A JSON [`Element`] with identity, source span, and value.
385///
386/// Each element carries a shared reference to the document it was parsed from,
387/// so [`Element::path()`] can resolve its path.
388#[derive(Clone, Debug)]
389pub struct Element<'buf> {
390    /// Shared document state.
391    doc: Rc<DocumentInner<'buf>>,
392    /// Unique identifier within the document; sequentially assigned depth-first.
393    id: ElemId,
394    /// Byte range of the value only; use for replacement edits.
395    span: Span,
396    /// End of the value plus any trailing comma and whitespace; use for removal edits.
397    /// Equal to `span.end` when there is no trailing comma (root element, or last sibling).
398    full_span_end: u32,
399    /// Parsed value, borrowing from the source `&str`.
400    value: Value<'buf>,
401}
402
403impl PartialEq for Element<'_> {
404    fn eq(&self, other: &Self) -> bool {
405        self.id == other.id
406            && self.span == other.span
407            && self.full_span_end == other.full_span_end
408            && self.value == other.value
409    }
410}
411
412impl Eq for Element<'_> {}
413
414impl<'buf> Element<'buf> {
415    /// Returns this element's identity within its [`Document`].
416    pub fn id(&self) -> ElemId {
417        self.id
418    }
419
420    /// Returns the span covering this element's value alone, without any trailing comma.
421    ///
422    /// See [`Element::full_span`] for the span that reaches the next sibling.
423    pub fn span(&self) -> Span {
424        self.span
425    }
426
427    /// Returns the span covering the value plus any trailing comma and the whitespace
428    /// after it, so the span ends where the next sibling begins.
429    ///
430    /// When there is no trailing comma (the root element, or the last child of its
431    /// parent), `full_span() == span()`.
432    ///
433    /// Use [`Element::span`] to replace a value. Removal is not simply a matter of
434    /// erasing `full_span`: which span removes a child depends on which of its siblings
435    /// survive, and erasing the `full_span` of a last child leaves a dangling comma on
436    /// the sibling before it. Use [`Document::removal_spans`], which resolves a whole set
437    /// of removals against the surviving siblings.
438    pub fn full_span(&self) -> Span {
439        Span {
440            start: self.span.start,
441            end: self.full_span_end,
442        }
443    }
444
445    /// Returns this element's JSON value.
446    pub fn value(&self) -> &Value<'buf> {
447        &self.value
448    }
449
450    /// Returns the RFC 9535 path to this element.
451    ///
452    /// NOTE: The `Path` is constructed anew every time this functions is called.
453    pub fn path(&self) -> Path {
454        self.doc.paths.path_of(self)
455    }
456
457    /// Returns the slice of the source JSON that this element spans.
458    #[expect(
459        clippy::string_slice,
460        reason = "spans are produced by the parser from the same source, so slices are always valid"
461    )]
462    #[expect(
463        clippy::as_conversions,
464        reason = "The index is guaranteed within bounds by the parser"
465    )]
466    pub fn source_json_value(&self) -> &'buf str {
467        &self.doc.source[self.span.start as usize..self.span.end as usize]
468    }
469
470    /// The full source string; all element spans are relative to this.
471    pub fn source(&self) -> &'buf str {
472        self.doc.source
473    }
474
475    /// Return the location that this element begins at.
476    #[expect(
477        clippy::string_slice,
478        reason = "spans are produced by the parser from the same source, so slices are always valid"
479    )]
480    #[expect(
481        clippy::as_conversions,
482        reason = "The index is guaranteed within bounds by the parser"
483    )]
484    pub fn location(&self) -> Location {
485        let source = self.doc.source;
486
487        // Slice up to the start of the span to calculate line and col numbers.
488        let lead_in = &source[..self.span.start as usize];
489        line_col(lead_in)
490    }
491
492    /// Return the inner `Value` by ref.
493    pub fn as_value(&self) -> &Value<'buf> {
494        &self.value
495    }
496
497    /// Return `Some(&str)` if the `Value` is a `String`.
498    pub fn to_raw_str(&self) -> Option<RawStr<'buf>> {
499        self.value.to_raw_str()
500    }
501
502    /// Return `Some(&[Field])` if the `Value` is a `Object`.
503    pub fn as_object_fields(&self) -> Option<&[Field<'buf>]> {
504        self.value.as_object_fields()
505    }
506
507    /// Returns the items if this element is a JSON array, `None` otherwise.
508    pub fn as_array(&self) -> Option<&[Element<'buf>]> {
509        self.value.as_array()
510    }
511
512    /// Returns the number as it was written in the source if this element is a JSON number,
513    /// `None` otherwise. The text is not parsed, so it keeps the precision the document had.
514    pub fn as_number_str(&self) -> Option<&str> {
515        self.value.as_number()
516    }
517
518    /// Return true if the `Element`s `Value` is null.
519    pub fn is_null(&self) -> bool {
520        self.value.is_null()
521    }
522
523    /// Return true if the `Element`s `Value` is an object.
524    pub fn is_object(&self) -> bool {
525        self.value.is_object()
526    }
527
528    /// Return true if the `Element`s `Value` is an array.
529    pub fn is_array(&self) -> bool {
530        self.value.is_array()
531    }
532}
533
534/// A JSON value that borrows its content from the source JSON `&str`.
535#[derive(Clone, Debug, Eq, PartialEq)]
536pub enum Value<'buf> {
537    /// JSON `null` literal.
538    Null,
539    /// JSON `true` literal.
540    True,
541    /// JSON `false` literal.
542    False,
543    /// String content with quotes removed; escape sequences are not decoded.
544    String(RawStr<'buf>),
545    /// Raw number text; not guaranteed to fit any specific numeric type.
546    Number(&'buf str),
547    /// Ordered list of child elements.
548    Array(Vec<Element<'buf>>),
549    /// Ordered list of key-value fields.
550    Object(Vec<Field<'buf>>),
551}
552
553impl fmt::Display for Value<'_> {
554    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
555        match self {
556            Self::Null => write!(f, "null"),
557            Self::True => write!(f, "true"),
558            Self::False => write!(f, "false"),
559            Self::String(s) => write!(f, "{}", s.as_unescaped_str()),
560            Self::Number(s) => write!(f, "{s}"),
561            Self::Array(..) => f.write_str("[...]"),
562            Self::Object(..) => f.write_str("{...}"),
563        }
564    }
565}
566
567/// Byte range of a JSON token within the source string.
568#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
569pub struct Span {
570    /// Byte offset of the first byte of the token.
571    pub start: u32,
572    /// Byte offset one past the last byte of the token.
573    pub end: u32,
574}
575
576impl Span {
577    fn new(start: u32, end: u32) -> Self {
578        Self { start, end }
579    }
580}
581
582/// A file location expressed as line and column.
583#[derive(Clone, Copy, Debug, PartialEq, Eq)]
584pub struct Location {
585    /// The line index is 0 based.
586    pub line: u32,
587
588    /// The col index is 0 based.
589    pub col: u32,
590}
591
592impl From<(u32, u32)> for Location {
593    fn from(value: (u32, u32)) -> Self {
594        Self {
595            line: value.0,
596            col: value.1,
597        }
598    }
599}
600
601impl From<Location> for (u32, u32) {
602    fn from(value: Location) -> Self {
603        (value.line, value.col)
604    }
605}
606
607impl PartialEq<(u32, u32)> for Location {
608    fn eq(&self, other: &(u32, u32)) -> bool {
609        self.line == other.0 && self.col == other.1
610    }
611}
612
613impl fmt::Display for Location {
614    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
615        write!(f, "{}:{}", self.line, self.col)
616    }
617}
618
619/// Return the line and column indices of the end of the slice.
620///
621/// The line and column indices are zero based.
622pub fn line_col(s: &str) -> Location {
623    let mut chars = s.chars().rev();
624    let mut line = 0_u32;
625    let mut col = 0_u32;
626
627    // The col only needs to be calculated on the final line so we iterate from the last char
628    // back to the start of the line and then only continue to count the lines after that.
629    //
630    // This is less work than continuously counting chars from the front of the slice.
631    for c in chars.by_ref() {
632        // If the `&str` is multiline, we count the line and stop accumulating the col count too.
633        if c == '\n' {
634            let Some(n) = line.checked_add(1) else {
635                break;
636            };
637            line = n;
638            break;
639        }
640        let Some(n) = col.checked_add(1) else {
641            break;
642        };
643        col = n;
644    }
645
646    // The col is now known, continue to the start of the str counting newlines as we go.
647    for c in chars {
648        if c == '\n' {
649            let Some(n) = line.checked_add(1) else {
650                break;
651            };
652            line = n;
653        }
654    }
655
656    Location { line, col }
657}
658
659/// Unique sequential index of a JSON [`Element`] within a document.
660///
661/// Assigned depth-first by the parser. `Parser::alloc_id` uses `checked_add`
662/// so the counter never wraps silently — overflow becomes `ParseError::TooLarge`.
663#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
664pub struct ElemId(usize);
665
666impl fmt::Display for ElemId {
667    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
668        fmt::Display::fmt(&self.0, f)
669    }
670}
671
672/// Records how one element was reached from its parent.
673#[derive(Debug)]
674enum PathEntry<'buf> {
675    /// The root element; has no parent.
676    Root,
677    /// An object field; the element's path ends with a key segment.
678    Field {
679        /// Id of the parent object element.
680        parent: ElemId,
681        /// Key text borrowed from the source JSON, without surrounding quotes.
682        key: RawStr<'buf>,
683    },
684    /// An array item; the element's path ends with an index segment.
685    Item {
686        /// Id of the parent array element.
687        parent: ElemId,
688        /// Zero-based position within the parent array.
689        index: u32,
690    },
691}
692
693/// Shared state carried by every [`Element`] produced from the same parse.
694///
695/// Wrapped in [`Rc`] so that each element can resolve its own path without
696/// holding a live reference to the original [`Document`].
697#[derive(Debug)]
698struct DocumentInner<'buf> {
699    /// The full source string; all element spans are relative to this.
700    source: &'buf str,
701    /// Parent-pointer table used to reconstruct element paths.
702    paths: PathTable<'buf>,
703}
704
705/// A table recording the parentage of every [`Element`] produced by a parse.
706#[derive(Debug, Default)]
707struct PathTable<'buf> {
708    /// The `entries` `Vec` is indexed using `ElemId`.
709    entries: Vec<PathEntry<'buf>>,
710}
711
712impl<'buf> PathTable<'buf> {
713    fn push(&mut self, entry: PathEntry<'buf>) {
714        self.entries.push(entry);
715    }
716
717    /// Returns the chain of ids from the root down to `id`, both included.
718    ///
719    /// Cost is O(depth). Returns `None` if `id` is not in this table. The walk always
720    /// terminates because a parent is allocated before its children, so every parent id
721    /// is smaller than the id it was reached from.
722    fn ancestry(&self, id: ElemId) -> Option<Vec<ElemId>> {
723        let mut chain = vec![id];
724        let mut current = id;
725
726        loop {
727            match self.entries.get(current.0)? {
728                PathEntry::Root => break,
729                PathEntry::Field { parent, key: _ } | PathEntry::Item { parent, index: _ } => {
730                    chain.push(*parent);
731                    current = *parent;
732                }
733            }
734        }
735
736        chain.reverse();
737
738        Some(chain)
739    }
740
741    /// Build the full RFC 9535 path to `element` by walking the parent-pointer chain.
742    ///
743    /// Cost is O(depth) per path construction.
744    ///
745    /// NOTE: The `'buf` lifetime shared by `element` and `self` prevents cross-document
746    /// misuse at compile time for documents with distinct source lifetimes.
747    ///
748    /// # Panics
749    ///
750    /// Panics if `element` was not produced by the same parse that created this table.
751    fn path_of(&self, element: &Element<'buf>) -> Path {
752        let mut entries: Vec<&PathEntry<'buf>> = Vec::new();
753        let mut elem_id = element.id;
754
755        // Walk back up the path chain.
756        loop {
757            let entry = self
758                .entries
759                .get(elem_id.0)
760                .expect("ElemId always refers to a valid PathEntry");
761
762            match entry {
763                PathEntry::Root => {
764                    entries.push(entry);
765                    break;
766                }
767                PathEntry::Field { parent, key: _ } | PathEntry::Item { parent, index: _ } => {
768                    entries.push(entry);
769                    elem_id = *parent;
770                }
771            }
772        }
773
774        // Reverse the elements so we can walk forward along the chain.
775        entries.reverse();
776
777        let mut out = String::with_capacity(30);
778
779        for entry in entries {
780            let res = match entry {
781                PathEntry::Root => write!(out, "$"),
782                PathEntry::Field { parent: _, key } => {
783                    write!(out, ".{}", key.as_unescaped_str())
784                }
785                PathEntry::Item { parent: _, index } => {
786                    // Array indices use bracket notation per RFC 9535, e.g.
787                    // `$.elements[0]`, rather than a dotted `.0` segment.
788                    write!(out, "[{index}]")
789                }
790            };
791
792            res.expect("Writing to a String can only fail if the system runs out of heap memory");
793        }
794
795        Path(out)
796    }
797}
798
799#[derive(Clone, PartialOrd, Ord, PartialEq, Eq)]
800/// The RFC 9535 path locating an [`Element`] in its [`Document`], such as
801/// `$.charging_periods[0].dimensions`.
802///
803/// A warning is reported against a path, so this is what tells a caller which field a
804/// warning is about.
805pub struct Path(String);
806
807impl Path {
808    /// Consume the `Path` and return the underlying `String`.
809    pub fn into_string(self) -> String {
810        self.0
811    }
812
813    /// Return the path as a `str`.
814    pub fn as_str(&self) -> &str {
815        &self.0
816    }
817
818    /// Iterate the [`Component`]s of this path in order, skipping the `$` root.
819    ///
820    /// For example, `$.elements[0].id` yields `Member("elements")`,
821    /// `Index("0")`, `Member("id")`. The root path `$` yields nothing.
822    pub fn components(&self) -> Components<'_> {
823        Components::over(&self.0)
824    }
825}
826
827/// A single [`Path`] component: an object member or an array index.
828#[derive(Clone, Copy, Debug, PartialEq, Eq)]
829pub enum Component<'a> {
830    /// An object member, the `name` in a `.name` segment.
831    Member(&'a str),
832    /// An array index, the decimal digits in a `[n]` segment.
833    Index(&'a str),
834}
835
836/// Iterator over the [`Component`]s of a [`Path`]; see [`Path::components`].
837#[derive(Clone, Debug)]
838pub struct Components<'a> {
839    rest: &'a str,
840}
841
842impl<'a> Components<'a> {
843    /// Iterate the components of a raw `JSONPath` string, skipping a leading `$`.
844    pub(crate) fn over(path: &'a str) -> Self {
845        Self {
846            rest: path.strip_prefix('$').unwrap_or(path),
847        }
848    }
849}
850
851impl<'a> Iterator for Components<'a> {
852    type Item = Component<'a>;
853
854    fn next(&mut self) -> Option<Self::Item> {
855        if let Some(after) = self.rest.strip_prefix('.') {
856            // `.name`: read up to the next segment delimiter.
857            let end = after.find(['.', '[']).unwrap_or(after.len());
858            let (name, tail) = after.split_at(end);
859            self.rest = tail;
860            Some(Component::Member(name))
861        } else if let Some(after) = self.rest.strip_prefix('[') {
862            // `[index]`: read up to the closing bracket.
863            let end = after.find(']').unwrap_or(after.len());
864            let (index, tail) = after.split_at(end);
865            self.rest = tail.strip_prefix(']').unwrap_or(tail);
866            Some(Component::Index(index))
867        } else {
868            // Empty (root) or malformed input: stop iterating.
869            None
870        }
871    }
872}
873
874impl PartialEq<str> for Path {
875    fn eq(&self, other: &str) -> bool {
876        self.0 == other
877    }
878}
879
880impl PartialEq<&str> for Path {
881    fn eq(&self, other: &&str) -> bool {
882        self.0 == *other
883    }
884}
885
886impl fmt::Debug for Path {
887    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
888        f.write_str(&self.0)
889    }
890}
891
892impl fmt::Display for Path {
893    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
894        fmt::Display::fmt(&self.0, f)
895    }
896}
897
898/// Set of path with a common issue.
899#[derive(Debug)]
900pub struct PathSet<'set>(BTreeSet<&'set Path>);
901
902impl<'set> PathSet<'set> {
903    pub(crate) fn new(paths: BTreeSet<&'set Path>) -> Self {
904        Self(paths)
905    }
906
907    /// Return the field paths as a `Vec` of `String`s.
908    pub fn to_strings(&self) -> Vec<String> {
909        self.0.iter().map(ToString::to_string).collect()
910    }
911
912    /// Return the field paths as a `Vec` of `String`s.
913    pub fn into_strings(self) -> Vec<String> {
914        self.0.into_iter().map(ToString::to_string).collect()
915    }
916
917    /// Return true if the list of unexpected fields is empty.
918    pub fn is_empty(&self) -> bool {
919        self.0.is_empty()
920    }
921
922    /// Return the number of unexpected fields.
923    pub fn len(&self) -> usize {
924        self.0.len()
925    }
926
927    /// Return an Iterator over the unexpected fields.
928    pub fn iter(&self) -> btree_set::Iter<'_, &Path> {
929        self.0.iter()
930    }
931}
932
933impl<'set> IntoIterator for PathSet<'set> {
934    type Item = &'set Path;
935
936    type IntoIter = btree_set::IntoIter<&'set Path>;
937
938    fn into_iter(self) -> Self::IntoIter {
939        self.0.into_iter()
940    }
941}
942
943impl<'a, 'set> IntoIterator for &'a PathSet<'set> {
944    type Item = &'a &'set Path;
945
946    type IntoIter = btree_set::Iter<'a, &'set Path>;
947
948    fn into_iter(self) -> Self::IntoIter {
949        self.0.iter()
950    }
951}
952
953#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
954/// Which of the six JSON kinds a [`Value`] is, without its content.
955///
956/// A schema mismatch is reported in terms of these, saying which kind was expected and
957/// which was found.
958pub enum ValueKind {
959    /// The JSON `null` literal.
960    Null,
961    /// The JSON `true` or `false` literal.
962    Bool,
963    /// A JSON number.
964    Number,
965    /// A JSON string.
966    String,
967    /// A JSON array.
968    Array,
969    /// A JSON object.
970    Object,
971}
972
973impl fmt::Display for ValueKind {
974    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
975        match self {
976            ValueKind::Null => write!(f, "null"),
977            ValueKind::Bool => write!(f, "bool"),
978            ValueKind::Number => write!(f, "number"),
979            ValueKind::String => write!(f, "string"),
980            ValueKind::Array => write!(f, "array"),
981            ValueKind::Object => write!(f, "object"),
982        }
983    }
984}
985
986impl<'buf> Value<'buf> {
987    /// Returns which JSON kind this value is, discarding its content.
988    pub fn kind(&self) -> ValueKind {
989        match self {
990            Value::Null => ValueKind::Null,
991            Value::True | Value::False => ValueKind::Bool,
992            Value::String(_) => ValueKind::String,
993            Value::Number(_) => ValueKind::Number,
994            Value::Array(_) => ValueKind::Array,
995            Value::Object(_) => ValueKind::Object,
996        }
997    }
998
999    /// Return true if the `Value` is null.
1000    pub fn is_null(&self) -> bool {
1001        matches!(self, Value::Null)
1002    }
1003
1004    /// Return true if the `Value` is an array.
1005    pub fn is_array(&self) -> bool {
1006        matches!(self, Value::Array(..))
1007    }
1008
1009    /// Return true if the `Value` is an object.
1010    pub fn is_object(&self) -> bool {
1011        matches!(self, Value::Object(..))
1012    }
1013
1014    /// Return true if the `Value` can't contain child elements.
1015    pub fn is_scalar(&self) -> bool {
1016        matches!(
1017            self,
1018            Value::Null | Value::True | Value::False | Value::String(_) | Value::Number(_)
1019        )
1020    }
1021
1022    /// Returns the items if this is a JSON array, `None` otherwise.
1023    pub fn as_array(&self) -> Option<&[Element<'buf>]> {
1024        if let Value::Array(elems) = self {
1025            Some(elems)
1026        } else {
1027            None
1028        }
1029    }
1030
1031    /// Returns the number as it was written in the source if this is a JSON number,
1032    /// `None` otherwise.
1033    pub fn as_number(&self) -> Option<&str> {
1034        if let Value::Number(s) = self {
1035            Some(s)
1036        } else {
1037            None
1038        }
1039    }
1040
1041    /// Return `Some(&str)` if the `Value` is a `String`.
1042    pub fn to_raw_str(&self) -> Option<RawStr<'buf>> {
1043        if let Value::String(s) = self {
1044            Some(*s)
1045        } else {
1046            None
1047        }
1048    }
1049
1050    /// Return `Some(&[Field])` if the `Value` is a `Object`.
1051    pub fn as_object_fields(&self) -> Option<&[Field<'buf>]> {
1052        if let Value::Object(fields) = self {
1053            Some(fields)
1054        } else {
1055            None
1056        }
1057    }
1058}
1059
1060/// An object field; upholds the invariant that the inner [`Element`]'s path ends with a key.
1061#[derive(Clone, Debug, Eq, PartialEq)]
1062pub struct Field<'buf> {
1063    /// Span of the key token, including surrounding `"` delimiters.
1064    key_span: Span,
1065    /// The value element; its path ends with the key from `key_span`.
1066    element: Element<'buf>,
1067}
1068
1069impl<'buf> Field<'buf> {
1070    /// Consume the `Field` and return the inner `Element`.
1071    pub fn into_element(self) -> Element<'buf> {
1072        self.element
1073    }
1074
1075    /// Return the inner `Element`.
1076    pub fn element(&self) -> &Element<'buf> {
1077        &self.element
1078    }
1079
1080    /// Returns the span covering the field's quoted key alone.
1081    pub fn key_span(&self) -> Span {
1082        self.key_span
1083    }
1084
1085    /// Returns the span covering `"key": value` plus any trailing comma and the whitespace
1086    /// after it, so the span ends where the next field begins.
1087    ///
1088    /// When there is no trailing comma (the last field), `full_span()` covers
1089    /// `"key": value` only.
1090    ///
1091    /// As with [`Element::full_span`], removing a field is not simply a matter of erasing
1092    /// this span; use [`Document::removal_spans`]. Erasing the `full_span` of a last field
1093    /// leaves a dangling comma on the field before it, and a field's value cannot be
1094    /// erased on its own without leaving a key with nothing after the colon.
1095    pub fn full_span(&self) -> Span {
1096        Span {
1097            start: self.key_span.start,
1098            end: self.element.full_span_end,
1099        }
1100    }
1101
1102    /// Returns the key text without surrounding `"` delimiters.
1103    #[expect(
1104        clippy::arithmetic_side_effects,
1105        reason = "key_span always spans a quoted string, so +1/-1 to strip the surrounding quote bytes is safe"
1106    )]
1107    #[expect(
1108        clippy::string_slice,
1109        reason = "key_span is produced by the parser from the same source; +1/-1 strips the ASCII quote bytes"
1110    )]
1111    #[expect(
1112        clippy::as_conversions,
1113        reason = "The index is guaranteed within bounds by the parser"
1114    )]
1115    pub fn key(&self) -> RawStr<'buf> {
1116        let src = self.element.source();
1117        let s = &src[self.key_span.start as usize + 1..self.key_span.end as usize - 1];
1118        RawStr::from_str(s)
1119    }
1120
1121    /// Returns the slice of the source JSON spanning `"key": value`.
1122    #[expect(
1123        clippy::string_slice,
1124        reason = "spans are produced by the parser from the same source, so slices are always valid"
1125    )]
1126    #[expect(
1127        clippy::as_conversions,
1128        reason = "The index is guaranteed within bounds by the parser"
1129    )]
1130    pub fn source_json(&self) -> &'buf str {
1131        let src = self.element.source();
1132        &src[self.key_span.start as usize..self.element.span.end as usize]
1133    }
1134}
1135
1136/// An object's fields keyed by their undecoded name, for looking a field up by key.
1137pub type RawMap<'buf> = BTreeMap<RawStr<'buf>, Element<'buf>>;
1138/// A [`RawMap`] that borrows its elements instead of owning them.
1139pub type RawRefMap<'a, 'buf> = BTreeMap<RawStr<'buf>, &'a Element<'buf>>;
1140
1141#[expect(dead_code, reason = "pending use in `tariff::lint`")]
1142pub(crate) trait FieldsIntoExt<'buf> {
1143    fn into_map(self) -> RawMap<'buf>;
1144}
1145
1146impl<'buf> FieldsIntoExt<'buf> for Vec<Field<'buf>> {
1147    fn into_map(self) -> RawMap<'buf> {
1148        self.into_iter()
1149            .map(|field| (field.key(), field.into_element()))
1150            .collect()
1151    }
1152}
1153
1154/// A `&str` with surrounding quotes removed; escape sequences are not decoded.
1155#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
1156pub struct RawStr<'buf>(&'buf str);
1157
1158/// Impl `Borrow` so `RawStr` plays well with hashed collections.
1159impl Borrow<str> for RawStr<'_> {
1160    fn borrow(&self) -> &str {
1161        self.0
1162    }
1163}
1164
1165/// Impl `Borrow` so `RawStr` plays well with hashed collections.
1166impl Borrow<str> for &RawStr<'_> {
1167    fn borrow(&self) -> &str {
1168        self.0
1169    }
1170}
1171
1172impl<'buf> RawStr<'buf> {
1173    fn from_str(source: &'buf str) -> Self {
1174        Self(source)
1175    }
1176
1177    /// Compare `other` against this raw `&str`, decoding any JSON escape
1178    /// sequences in the `&str` on the fly without allocating.
1179    ///
1180    /// Returns `Ok(true)`/`Ok(false)` for the comparison, or `Err` if the key
1181    /// contains a decoding problem (an invalid escape or a control character) at
1182    /// or before the first differing character.
1183    pub fn eq_escape_aware(&self, other: &str) -> Result<bool, decode::Warning> {
1184        decode::eq(self.0, other)
1185    }
1186
1187    /// Compare this raw `&str` against a list of `&str`s, decoding any JSON escape
1188    /// sequences in the `&str` on the fly without allocating.
1189    ///
1190    /// Returns true if any of the `other` `&str`s match self.
1191    pub fn eq_any_escape_aware(&self, other: &[&str]) -> bool {
1192        other
1193            .iter()
1194            .any(|s| decode::eq(self.0, s).ok().unwrap_or(false))
1195    }
1196
1197    /// Like [`RawStr::eq_any_escape_aware`], but compares ASCII letters case-insensitively.
1198    pub fn eq_any_escape_aware_ignore_ascii_case(&self, other: &[&str]) -> bool {
1199        other.iter().any(|s| {
1200            decode::eq_ignore_ascii_case(self.0, s)
1201                .ok()
1202                .unwrap_or(false)
1203        })
1204    }
1205
1206    /// Return the raw unescaped `&str`.
1207    pub fn as_unescaped_str(&self) -> &'buf str {
1208        self.0
1209    }
1210
1211    /// Return the `&str` with all escapes decoded.
1212    pub fn decode_escapes(&self) -> CaveatDeferred<Cow<'_, str>, decode::Warning> {
1213        decode::from_raw(self.0)
1214    }
1215
1216    /// Return a `&str` marked as either having escapes or not.
1217    pub fn has_escapes(&self, elem: &Element<'buf>) -> Caveat<PendingStr<'buf>, decode::Warning> {
1218        decode::analyze(self.0, elem)
1219    }
1220
1221    /// Report whether the string contains escape sequences and whether its decoded form
1222    /// contains non-printable ASCII, in a single pass without allocating.
1223    ///
1224    /// This combines the escape test of [`RawStr::has_escapes`] with the printability
1225    /// check a caller would otherwise run on a string returned from [`RawStr::decode_escapes`].
1226    pub fn lexical_issues(&self) -> LexicalIssues {
1227        decode::lexical_issues(self.0)
1228    }
1229}
1230
1231/// The lexical issues a [`RawStr`] may contain, discovered in a single pass by
1232/// [`RawStr::lexical_issues`].
1233#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1234pub struct LexicalIssues {
1235    /// The raw string contains one or more JSON escape sequences.
1236    pub escapes: bool,
1237
1238    /// The decoded string contains non-printable ASCII: an ASCII control character or
1239    /// ASCII whitespace.
1240    pub non_printable_ascii: bool,
1241}
1242
1243/// Marks a `&str` as having escapes or not.
1244pub enum PendingStr<'buf> {
1245    /// The `&str` has no escapes and can be used as is.
1246    NoEscapes(&'buf str),
1247
1248    /// The `&str` has escape chars and needs to be unescaped before trying to parse into another form.
1249    HasEscapes(EscapeStr<'buf>),
1250}
1251
1252/// A `&str` with escape chars.
1253pub struct EscapeStr<'buf>(&'buf str);
1254
1255impl<'buf> EscapeStr<'buf> {
1256    /// Decode the escape sequences, reporting each one that is invalid as a
1257    /// [`decode::Warning`] rather than failing.
1258    pub fn decode_escapes(&self) -> CaveatDeferred<Cow<'buf, str>, decode::Warning> {
1259        decode::from_raw(self.0)
1260    }
1261
1262    /// Consume the `EscapeStr` and return the raw bytes as a str.
1263    pub fn into_raw(self) -> &'buf str {
1264        self.0
1265    }
1266}