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}