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** ([`parser`], [`parse`]) 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_source_json;
39
40use std::{
41    borrow::{Borrow, Cow},
42    collections::{btree_set, BTreeMap, BTreeSet},
43    fmt::{self, Write as _},
44    rc::Rc,
45};
46
47use crate::{
48    string,
49    warning::{Caveat, CaveatDeferred},
50};
51
52pub(crate) use parser::parse;
53pub use parser::{Error, ErrorKind as ParseErrorKind};
54
55/// Parse a raw JSON `&str` into a [`Document`] and require the root value to be a JSON object.
56///
57/// The input size is gated by [`string::ReasonableLen`]; oversized input returns
58/// [`ParseError::SizeExceedsMax`].
59pub fn parse_object(json: &str) -> Result<Document<'_>, ParseError> {
60    let json = string::ReasonableLen::new(json).map_err(|_e| ParseError::SizeExceedsMax)?;
61    let doc = parse(json).map_err(ParseError::Json)?;
62
63    if !doc.root().is_object() {
64        return Err(ParseError::ShouldBeAnObject);
65    }
66
67    Ok(doc)
68}
69
70#[derive(Debug)]
71pub enum ParseError {
72    /// The JSON parser was unable to parse the JSON str.
73    Json(Error),
74
75    /// The OCPI object should be a JSON object.
76    ShouldBeAnObject,
77
78    /// The size of the input `str` exceeds the maximum deemed reasonable.
79    SizeExceedsMax,
80}
81
82impl fmt::Display for ParseError {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::Json(error) => write!(f, "{error}"),
86            Self::ShouldBeAnObject => f.write_str("The CDR should be an object."),
87            Self::SizeExceedsMax => write!(
88                f,
89                "The input `&str` exceeds the reasonable maximum `{} MB`.",
90                string::ReasonableLen::FACTOR
91            ),
92        }
93    }
94}
95
96impl std::error::Error for ParseError {
97    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
98        match &self {
99            ParseError::Json(err) => Some(err),
100            ParseError::ShouldBeAnObject | ParseError::SizeExceedsMax => None,
101        }
102    }
103}
104
105/// The output of [`parse`]: the element tree with path resolution embedded in each element.
106#[derive(Clone, Debug)]
107pub struct Document<'buf> {
108    /// Shared inner state; also held by every element in the tree.
109    inner: Rc<DocumentInner<'buf>>,
110    /// Root element of the parsed tree.
111    root: Element<'buf>,
112}
113
114impl<'buf> Document<'buf> {
115    /// Returns the source JSON string this document was parsed from.
116    pub fn source(&self) -> &'buf str {
117        self.inner.source
118    }
119
120    /// Returns the root element of this document.
121    pub fn root(&self) -> &Element<'buf> {
122        &self.root
123    }
124}
125
126/// A JSON [`Element`] with identity, source span, and value.
127///
128/// Each element carries a shared reference to the document it was parsed from,
129/// so [`Element::path()`] can resolve its path.
130#[derive(Clone, Debug)]
131pub struct Element<'buf> {
132    /// Shared document state.
133    doc: Rc<DocumentInner<'buf>>,
134    /// Unique identifier within the document; sequentially assigned depth-first.
135    id: ElemId,
136    /// Byte range of the value only; use for replacement edits.
137    span: Span,
138    /// End of the value plus any trailing comma and whitespace; use for removal edits.
139    /// Equal to `span.end` when there is no trailing comma (root element, or last sibling).
140    full_span_end: u32,
141    /// Parsed value, borrowing from the source `&str`.
142    value: Value<'buf>,
143}
144
145impl PartialEq for Element<'_> {
146    fn eq(&self, other: &Self) -> bool {
147        self.id == other.id
148            && self.span == other.span
149            && self.full_span_end == other.full_span_end
150            && self.value == other.value
151    }
152}
153
154impl Eq for Element<'_> {}
155
156impl<'buf> Element<'buf> {
157    pub fn id(&self) -> ElemId {
158        self.id
159    }
160
161    pub fn span(&self) -> Span {
162        self.span
163    }
164
165    /// Returns the span covering the value plus any trailing comma and whitespace.
166    ///
167    /// Choose the removal span based on the element's position in its parent:
168    ///
169    /// | Case | Span to erase |
170    /// |---|---|
171    /// | Replace value | [`Element::span`] |
172    /// | Remove non-last item | `element.full_span()` |
173    /// | Remove last item (siblings exist) | `siblings[i-1].span().end .. element.span().end` |
174    /// | Remove only item | `element.span()` |
175    ///
176    /// When there is no trailing comma (root element or last sibling),
177    /// `full_span() == span()`. Erasing `full_span` of a last item leaves a
178    /// dangling comma on the previous sibling; use the predecessor's `span().end`
179    /// as the start of the removal range instead.
180    pub fn full_span(&self) -> Span {
181        Span {
182            start: self.span.start,
183            end: self.full_span_end,
184        }
185    }
186
187    pub fn value(&self) -> &Value<'buf> {
188        &self.value
189    }
190
191    /// Returns the RFC 9535 path to this element.
192    ///
193    /// NOTE: The `Path` is constructed anew every time this functions is called.
194    pub fn path(&self) -> Path {
195        self.doc.paths.path_of(self)
196    }
197
198    /// Returns the slice of the source JSON that this element spans.
199    #[expect(
200        clippy::string_slice,
201        reason = "spans are produced by the parser from the same source, so slices are always valid"
202    )]
203    #[expect(
204        clippy::as_conversions,
205        reason = "The index is guaranteed within bounds by the parser"
206    )]
207    pub fn source_json_value(&self) -> &'buf str {
208        &self.doc.source[self.span.start as usize..self.span.end as usize]
209    }
210
211    /// The full source string; all element spans are relative to this.
212    pub fn source(&self) -> &'buf str {
213        self.doc.source
214    }
215
216    /// Return the location that this element begins at.
217    #[expect(
218        clippy::string_slice,
219        reason = "spans are produced by the parser from the same source, so slices are always valid"
220    )]
221    #[expect(
222        clippy::as_conversions,
223        reason = "The index is guaranteed within bounds by the parser"
224    )]
225    pub fn location(&self) -> Location {
226        let source = self.doc.source;
227
228        // Slice up to the start of the span to calculate line and col numbers.
229        let lead_in = &source[..self.span.start as usize];
230        line_col(lead_in)
231    }
232
233    /// Return the inner `Value` by ref.
234    pub fn as_value(&self) -> &Value<'buf> {
235        &self.value
236    }
237
238    /// Return `Some(&str)` if the `Value` is a `String`.
239    pub fn to_raw_str(&self) -> Option<RawStr<'buf>> {
240        self.value.to_raw_str()
241    }
242
243    /// Return `Some(&[Field])` if the `Value` is a `Object`.
244    pub fn as_object_fields(&self) -> Option<&[Field<'buf>]> {
245        self.value.as_object_fields()
246    }
247
248    pub fn as_array(&self) -> Option<&[Element<'buf>]> {
249        self.value.as_array()
250    }
251
252    pub fn as_number_str(&self) -> Option<&str> {
253        self.value.as_number()
254    }
255
256    /// Return true if the `Element`s `Value` is null.
257    pub fn is_null(&self) -> bool {
258        self.value.is_null()
259    }
260
261    /// Return true if the `Element`s `Value` is an object.
262    pub fn is_object(&self) -> bool {
263        self.value.is_object()
264    }
265
266    /// Return true if the `Element`s `Value` is an array.
267    pub fn is_array(&self) -> bool {
268        self.value.is_array()
269    }
270}
271
272/// A JSON value that borrows its content from the source JSON `&str`.
273#[derive(Clone, Debug, Eq, PartialEq)]
274pub enum Value<'buf> {
275    /// JSON `null` literal.
276    Null,
277    /// JSON `true` literal.
278    True,
279    /// JSON `false` literal.
280    False,
281    /// String content with quotes removed; escape sequences are not decoded.
282    String(RawStr<'buf>),
283    /// Raw number text; not guaranteed to fit any specific numeric type.
284    Number(&'buf str),
285    /// Ordered list of child elements.
286    Array(Vec<Element<'buf>>),
287    /// Ordered list of key-value fields.
288    Object(Vec<Field<'buf>>),
289}
290
291impl fmt::Display for Value<'_> {
292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        match self {
294            Self::Null => write!(f, "null"),
295            Self::True => write!(f, "true"),
296            Self::False => write!(f, "false"),
297            Self::String(s) => write!(f, "{}", s.as_unescaped_str()),
298            Self::Number(s) => write!(f, "{s}"),
299            Self::Array(..) => f.write_str("[...]"),
300            Self::Object(..) => f.write_str("{...}"),
301        }
302    }
303}
304
305/// Byte range of a JSON token within the source string.
306#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
307pub struct Span {
308    /// Byte offset of the first byte of the token.
309    pub start: u32,
310    /// Byte offset one past the last byte of the token.
311    pub end: u32,
312}
313
314impl Span {
315    fn new(start: u32, end: u32) -> Self {
316        Self { start, end }
317    }
318}
319
320/// A file location expressed as line and column.
321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub struct Location {
323    /// The line index is 0 based.
324    pub line: u32,
325
326    /// The col index is 0 based.
327    pub col: u32,
328}
329
330impl From<(u32, u32)> for Location {
331    fn from(value: (u32, u32)) -> Self {
332        Self {
333            line: value.0,
334            col: value.1,
335        }
336    }
337}
338
339impl From<Location> for (u32, u32) {
340    fn from(value: Location) -> Self {
341        (value.line, value.col)
342    }
343}
344
345impl PartialEq<(u32, u32)> for Location {
346    fn eq(&self, other: &(u32, u32)) -> bool {
347        self.line == other.0 && self.col == other.1
348    }
349}
350
351impl fmt::Display for Location {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        write!(f, "{}:{}", self.line, self.col)
354    }
355}
356
357/// Return the line and column indices of the end of the slice.
358///
359/// The line and column indices are zero based.
360pub fn line_col(s: &str) -> Location {
361    let mut chars = s.chars().rev();
362    let mut line = 0_u32;
363    let mut col = 0_u32;
364
365    // The col only needs to be calculated on the final line so we iterate from the last char
366    // back to the start of the line and then only continue to count the lines after that.
367    //
368    // This is less work than continuously counting chars from the front of the slice.
369    for c in chars.by_ref() {
370        // If the `&str` is multiline, we count the line and stop accumulating the col count too.
371        if c == '\n' {
372            let Some(n) = line.checked_add(1) else {
373                break;
374            };
375            line = n;
376            break;
377        }
378        let Some(n) = col.checked_add(1) else {
379            break;
380        };
381        col = n;
382    }
383
384    // The col is now known, continue to the start of the str counting newlines as we go.
385    for c in chars {
386        if c == '\n' {
387            let Some(n) = line.checked_add(1) else {
388                break;
389            };
390            line = n;
391        }
392    }
393
394    Location { line, col }
395}
396
397/// Unique sequential index of a JSON [`Element`] within a document.
398///
399/// Assigned depth-first by the parser. `Parser::alloc_id` uses `checked_add`
400/// so the counter never wraps silently — overflow becomes `ParseError::TooLarge`.
401#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd)]
402pub struct ElemId(usize);
403
404/// Records how one element was reached from its parent.
405#[derive(Debug)]
406enum PathEntry<'buf> {
407    /// The root element; has no parent.
408    Root,
409    /// An object field; the element's path ends with a key segment.
410    Field {
411        /// Id of the parent object element.
412        parent: ElemId,
413        /// Key text borrowed from the source JSON, without surrounding quotes.
414        key: RawStr<'buf>,
415    },
416    /// An array item; the element's path ends with an index segment.
417    Item {
418        /// Id of the parent array element.
419        parent: ElemId,
420        /// Zero-based position within the parent array.
421        index: u32,
422    },
423}
424
425/// Shared state carried by every [`Element`] produced from the same parse.
426///
427/// Wrapped in [`Rc`] so that each element can resolve its own path without
428/// holding a live reference to the original [`Document`].
429#[derive(Debug)]
430struct DocumentInner<'buf> {
431    /// The full source string; all element spans are relative to this.
432    source: &'buf str,
433    /// Parent-pointer table used to reconstruct element paths.
434    paths: PathTable<'buf>,
435}
436
437/// A table recording the parentage of every [`Element`] produced by a parse.
438#[derive(Debug, Default)]
439struct PathTable<'buf> {
440    /// The `entries` `Vec` is indexed using `ElemId`.
441    entries: Vec<PathEntry<'buf>>,
442}
443
444impl<'buf> PathTable<'buf> {
445    fn push(&mut self, entry: PathEntry<'buf>) {
446        self.entries.push(entry);
447    }
448
449    /// Build the full RFC 9535 path to `element` by walking the parent-pointer chain.
450    ///
451    /// Cost is O(depth) per path construction.
452    ///
453    /// NOTE: The `'buf` lifetime shared by `element` and `self` prevents cross-document
454    /// misuse at compile time for documents with distinct source lifetimes.
455    ///
456    /// # Panics
457    ///
458    /// Panics if `element` was not produced by the same parse that created this table.
459    fn path_of(&self, element: &Element<'buf>) -> Path {
460        let mut entries: Vec<&PathEntry<'buf>> = Vec::new();
461        let mut elem_id = element.id;
462
463        // Walk back up the path chain.
464        loop {
465            let entry = self
466                .entries
467                .get(elem_id.0)
468                .expect("ElemId always refers to a valid PathEntry");
469
470            match entry {
471                PathEntry::Root => {
472                    entries.push(entry);
473                    break;
474                }
475                PathEntry::Field { parent, key: _ } | PathEntry::Item { parent, index: _ } => {
476                    entries.push(entry);
477                    elem_id = *parent;
478                }
479            }
480        }
481
482        // Reverse the elements so we can walk forward along the chain.
483        entries.reverse();
484
485        let mut out = String::with_capacity(30);
486
487        for entry in entries {
488            let res = match entry {
489                PathEntry::Root => write!(out, "$"),
490                PathEntry::Field { parent: _, key } => {
491                    write!(out, ".{}", key.as_unescaped_str())
492                }
493                PathEntry::Item { parent: _, index } => {
494                    // Array indices use bracket notation per RFC 9535, e.g.
495                    // `$.elements[0]`, rather than a dotted `.0` segment.
496                    write!(out, "[{index}]")
497                }
498            };
499
500            res.expect("Writing to a String can only fail if the system runs out of heap memory");
501        }
502
503        Path(out)
504    }
505}
506
507#[derive(Clone, PartialOrd, Ord, PartialEq, Eq)]
508pub struct Path(String);
509
510impl Path {
511    pub fn into_string(self) -> String {
512        self.0
513    }
514
515    pub fn as_str(&self) -> &str {
516        &self.0
517    }
518
519    /// Iterate the [`Component`]s of this path in order, skipping the `$` root.
520    ///
521    /// For example, `$.elements[0].id` yields `Member("elements")`,
522    /// `Index("0")`, `Member("id")`. The root path `$` yields nothing.
523    pub fn components(&self) -> Components<'_> {
524        Components::over(&self.0)
525    }
526}
527
528/// A single [`Path`] component: an object member or an array index.
529#[derive(Clone, Copy, Debug, PartialEq, Eq)]
530pub enum Component<'a> {
531    /// An object member, the `name` in a `.name` segment.
532    Member(&'a str),
533    /// An array index, the decimal digits in a `[n]` segment.
534    Index(&'a str),
535}
536
537/// Iterator over the [`Component`]s of a [`Path`]; see [`Path::components`].
538#[derive(Clone, Debug)]
539pub struct Components<'a> {
540    rest: &'a str,
541}
542
543impl<'a> Components<'a> {
544    /// Iterate the components of a raw `JSONPath` string, skipping a leading `$`.
545    pub(crate) fn over(path: &'a str) -> Self {
546        Self {
547            rest: path.strip_prefix('$').unwrap_or(path),
548        }
549    }
550}
551
552impl<'a> Iterator for Components<'a> {
553    type Item = Component<'a>;
554
555    fn next(&mut self) -> Option<Self::Item> {
556        if let Some(after) = self.rest.strip_prefix('.') {
557            // `.name`: read up to the next segment delimiter.
558            let end = after.find(['.', '[']).unwrap_or(after.len());
559            let (name, tail) = after.split_at(end);
560            self.rest = tail;
561            Some(Component::Member(name))
562        } else if let Some(after) = self.rest.strip_prefix('[') {
563            // `[index]`: read up to the closing bracket.
564            let end = after.find(']').unwrap_or(after.len());
565            let (index, tail) = after.split_at(end);
566            self.rest = tail.strip_prefix(']').unwrap_or(tail);
567            Some(Component::Index(index))
568        } else {
569            // Empty (root) or malformed input: stop iterating.
570            None
571        }
572    }
573}
574
575impl PartialEq<str> for Path {
576    fn eq(&self, other: &str) -> bool {
577        self.0 == other
578    }
579}
580
581impl PartialEq<&str> for Path {
582    fn eq(&self, other: &&str) -> bool {
583        self.0 == *other
584    }
585}
586
587impl fmt::Debug for Path {
588    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
589        f.write_str(&self.0)
590    }
591}
592
593impl fmt::Display for Path {
594    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
595        fmt::Display::fmt(&self.0, f)
596    }
597}
598
599/// Set of path with a common issue.
600#[derive(Debug)]
601pub struct PathSet<'set>(BTreeSet<&'set Path>);
602
603impl<'set> PathSet<'set> {
604    pub(crate) fn new(paths: BTreeSet<&'set Path>) -> Self {
605        Self(paths)
606    }
607
608    /// Return the field paths as a `Vec` of `String`s.
609    pub fn to_strings(&self) -> Vec<String> {
610        self.0.iter().map(ToString::to_string).collect()
611    }
612
613    /// Return the field paths as a `Vec` of `String`s.
614    pub fn into_strings(self) -> Vec<String> {
615        self.0.into_iter().map(ToString::to_string).collect()
616    }
617
618    /// Return true if the list of unexpected fields is empty.
619    pub fn is_empty(&self) -> bool {
620        self.0.is_empty()
621    }
622
623    /// Return the number of unexpected fields.
624    pub fn len(&self) -> usize {
625        self.0.len()
626    }
627
628    /// Return an Iterator over the unexpected fields.
629    pub fn iter(&self) -> btree_set::Iter<'_, &Path> {
630        self.0.iter()
631    }
632}
633
634impl<'set> IntoIterator for PathSet<'set> {
635    type Item = &'set Path;
636
637    type IntoIter = btree_set::IntoIter<&'set Path>;
638
639    fn into_iter(self) -> Self::IntoIter {
640        self.0.into_iter()
641    }
642}
643
644impl<'a, 'set> IntoIterator for &'a PathSet<'set> {
645    type Item = &'a &'set Path;
646
647    type IntoIter = btree_set::Iter<'a, &'set Path>;
648
649    fn into_iter(self) -> Self::IntoIter {
650        self.0.iter()
651    }
652}
653
654#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
655pub enum ValueKind {
656    Null,
657    Bool,
658    Number,
659    String,
660    Array,
661    Object,
662}
663
664impl fmt::Display for ValueKind {
665    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
666        match self {
667            ValueKind::Null => write!(f, "null"),
668            ValueKind::Bool => write!(f, "bool"),
669            ValueKind::Number => write!(f, "number"),
670            ValueKind::String => write!(f, "string"),
671            ValueKind::Array => write!(f, "array"),
672            ValueKind::Object => write!(f, "object"),
673        }
674    }
675}
676
677impl<'buf> Value<'buf> {
678    pub fn kind(&self) -> ValueKind {
679        match self {
680            Value::Null => ValueKind::Null,
681            Value::True | Value::False => ValueKind::Bool,
682            Value::String(_) => ValueKind::String,
683            Value::Number(_) => ValueKind::Number,
684            Value::Array(_) => ValueKind::Array,
685            Value::Object(_) => ValueKind::Object,
686        }
687    }
688
689    pub fn is_null(&self) -> bool {
690        matches!(self, Value::Null)
691    }
692
693    /// Return true if the `Value` is an array.
694    pub fn is_array(&self) -> bool {
695        matches!(self, Value::Array(..))
696    }
697
698    /// Return true if the `Value` is an object.
699    pub fn is_object(&self) -> bool {
700        matches!(self, Value::Object(..))
701    }
702
703    /// Return true if the `Value` can't contain child elements.
704    pub fn is_scalar(&self) -> bool {
705        matches!(
706            self,
707            Value::Null | Value::True | Value::False | Value::String(_) | Value::Number(_)
708        )
709    }
710
711    pub fn as_array(&self) -> Option<&[Element<'buf>]> {
712        if let Value::Array(elems) = self {
713            Some(elems)
714        } else {
715            None
716        }
717    }
718
719    pub fn as_number(&self) -> Option<&str> {
720        if let Value::Number(s) = self {
721            Some(s)
722        } else {
723            None
724        }
725    }
726
727    /// Return `Some(&str)` if the `Value` is a `String`.
728    pub fn to_raw_str(&self) -> Option<RawStr<'buf>> {
729        if let Value::String(s) = self {
730            Some(*s)
731        } else {
732            None
733        }
734    }
735
736    /// Return `Some(&[Field])` if the `Value` is a `Object`.
737    pub fn as_object_fields(&self) -> Option<&[Field<'buf>]> {
738        if let Value::Object(fields) = self {
739            Some(fields)
740        } else {
741            None
742        }
743    }
744}
745
746/// An object field; upholds the invariant that the inner [`Element`]'s path ends with a key.
747#[derive(Clone, Debug, Eq, PartialEq)]
748pub struct Field<'buf> {
749    /// Span of the key token, including surrounding `"` delimiters.
750    key_span: Span,
751    /// The value element; its path ends with the key from `key_span`.
752    element: Element<'buf>,
753}
754
755impl<'buf> Field<'buf> {
756    /// Consume the `Field` and return the inner `Element`.
757    pub fn into_element(self) -> Element<'buf> {
758        self.element
759    }
760
761    /// Return the inner `Element`.
762    pub fn element(&self) -> &Element<'buf> {
763        &self.element
764    }
765
766    pub fn key_span(&self) -> Span {
767        self.key_span
768    }
769
770    /// Returns the span covering `"key": value` plus any trailing comma and whitespace.
771    ///
772    /// Choose the removal span based on the field's position in its parent:
773    ///
774    /// | Case | Span to erase |
775    /// |---|---|
776    /// | Remove non-last field | `field.full_span()` |
777    /// | Remove last field (siblings exist) | `fields[i-1].element().span().end .. field.element().span().end` |
778    /// | Remove only field | `field.element().span()` |
779    ///
780    /// When there is no trailing comma (last field), `full_span()` covers
781    /// `"key": value` only. Erasing it leaves a dangling comma on the previous
782    /// field; use the predecessor's `element().span().end` as the start instead.
783    pub fn full_span(&self) -> Span {
784        Span {
785            start: self.key_span.start,
786            end: self.element.full_span_end,
787        }
788    }
789
790    /// Returns the key text without surrounding `"` delimiters.
791    #[expect(
792        clippy::arithmetic_side_effects,
793        reason = "key_span always spans a quoted string, so +1/-1 to strip the surrounding quote bytes is safe"
794    )]
795    #[expect(
796        clippy::string_slice,
797        reason = "key_span is produced by the parser from the same source; +1/-1 strips the ASCII quote bytes"
798    )]
799    #[expect(
800        clippy::as_conversions,
801        reason = "The index is guaranteed within bounds by the parser"
802    )]
803    pub fn key(&self) -> RawStr<'buf> {
804        let src = self.element.source();
805        let s = &src[self.key_span.start as usize + 1..self.key_span.end as usize - 1];
806        RawStr::from_str(s)
807    }
808
809    /// Returns the slice of the source JSON spanning `"key": value`.
810    #[expect(
811        clippy::string_slice,
812        reason = "spans are produced by the parser from the same source, so slices are always valid"
813    )]
814    #[expect(
815        clippy::as_conversions,
816        reason = "The index is guaranteed within bounds by the parser"
817    )]
818    pub fn source_json(&self) -> &'buf str {
819        let src = self.element.source();
820        &src[self.key_span.start as usize..self.element.span.end as usize]
821    }
822}
823
824pub type RawMap<'buf> = BTreeMap<RawStr<'buf>, Element<'buf>>;
825pub type RawRefMap<'a, 'buf> = BTreeMap<RawStr<'buf>, &'a Element<'buf>>;
826
827#[expect(dead_code, reason = "pending use in `tariff::lint`")]
828pub(crate) trait FieldsIntoExt<'buf> {
829    fn into_map(self) -> RawMap<'buf>;
830}
831
832impl<'buf> FieldsIntoExt<'buf> for Vec<Field<'buf>> {
833    fn into_map(self) -> RawMap<'buf> {
834        self.into_iter()
835            .map(|field| (field.key(), field.into_element()))
836            .collect()
837    }
838}
839
840/// A `&str` with surrounding quotes removed; escape sequences are not decoded.
841#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
842pub struct RawStr<'buf>(&'buf str);
843
844/// Impl `Borrow` so `RawStr` plays well with hashed collections.
845impl Borrow<str> for RawStr<'_> {
846    fn borrow(&self) -> &str {
847        self.0
848    }
849}
850
851/// Impl `Borrow` so `RawStr` plays well with hashed collections.
852impl Borrow<str> for &RawStr<'_> {
853    fn borrow(&self) -> &str {
854        self.0
855    }
856}
857
858impl<'buf> RawStr<'buf> {
859    fn from_str(source: &'buf str) -> Self {
860        Self(source)
861    }
862
863    /// Compare `other` against this raw `&str`, decoding any JSON escape
864    /// sequences in the `&str` on the fly without allocating.
865    ///
866    /// Returns `Ok(true)`/`Ok(false)` for the comparison, or `Err` if the key
867    /// contains a decoding problem (an invalid escape or a control character) at
868    /// or before the first differing character.
869    pub fn eq_escape_aware(&self, other: &str) -> Result<bool, decode::Warning> {
870        decode::eq(self.0, other)
871    }
872
873    /// Compare this raw `&str` against a list of `&str`s, decoding any JSON escape
874    /// sequences in the `&str` on the fly without allocating.
875    ///
876    /// Returns true if any of the `other` `&str`s match self.
877    pub fn eq_any_escape_aware(&self, other: &[&str]) -> bool {
878        other
879            .iter()
880            .any(|s| decode::eq(self.0, s).ok().unwrap_or(false))
881    }
882
883    /// Like [`RawStr::eq_any_escape_aware`], but compares ASCII letters case-insensitively.
884    pub fn eq_any_escape_aware_ignore_ascii_case(&self, other: &[&str]) -> bool {
885        other.iter().any(|s| {
886            decode::eq_ignore_ascii_case(self.0, s)
887                .ok()
888                .unwrap_or(false)
889        })
890    }
891
892    /// Return the raw unescaped `&str`.
893    pub fn as_unescaped_str(&self) -> &'buf str {
894        self.0
895    }
896
897    /// Return the `&str` with all escapes decoded.
898    pub fn decode_escapes(&self) -> CaveatDeferred<Cow<'_, str>, decode::Warning> {
899        decode::from_raw(self.0)
900    }
901
902    /// Return a `&str` marked as either having escapes or not.
903    pub fn has_escapes(&self, elem: &Element<'buf>) -> Caveat<PendingStr<'buf>, decode::Warning> {
904        decode::analyze(self.0, elem)
905    }
906
907    /// Report whether the string contains escape sequences and whether its decoded form
908    /// contains non-printable ASCII, in a single pass without allocating.
909    ///
910    /// This combines the escape test of [`RawStr::has_escapes`] with the printability
911    /// check a caller would otherwise run on a string returned from [`RawStr::decode_escapes`].
912    pub fn lexical_issues(&self) -> LexicalIssues {
913        decode::lexical_issues(self.0)
914    }
915}
916
917/// The lexical issues a [`RawStr`] may contain, discovered in a single pass by
918/// [`RawStr::lexical_issues`].
919#[derive(Clone, Copy, Debug, Eq, PartialEq)]
920pub struct LexicalIssues {
921    /// The raw string contains one or more JSON escape sequences.
922    pub escapes: bool,
923
924    /// The decoded string contains non-printable ASCII: an ASCII control character or
925    /// ASCII whitespace.
926    pub non_printable_ascii: bool,
927}
928
929/// Marks a `&str` as having escapes or not.
930pub enum PendingStr<'buf> {
931    /// The `&str` has no escapes and can be used as is.
932    NoEscapes(&'buf str),
933
934    /// The `&str` has escape chars and needs to be unescaped before trying to parse into another form.
935    HasEscapes(EscapeStr<'buf>),
936}
937
938/// A `&str` with escape chars.
939pub struct EscapeStr<'buf>(&'buf str);
940
941impl<'buf> EscapeStr<'buf> {
942    pub fn decode_escapes(&self) -> CaveatDeferred<Cow<'buf, str>, decode::Warning> {
943        decode::from_raw(self.0)
944    }
945
946    /// Consume the `EscapeStr` and return the raw bytes as a str.
947    pub fn into_raw(self) -> &'buf str {
948        self.0
949    }
950}