shape/lib.rs
1mod accepts;
2mod case_enum;
3mod child_shape;
4mod display;
5mod from_json;
6mod hashing;
7mod helpers;
8mod merge;
9mod meta;
10pub mod name;
11
12pub mod graphql;
13pub mod location;
14#[cfg(test)]
15mod tests;
16mod visitor;
17
18use std::hash::Hash;
19use std::hash::Hasher;
20use std::iter::empty;
21
22pub use accepts::ShapeMismatch;
23pub use case_enum::Error;
24pub use case_enum::ShapeCase;
25pub use helpers::OffsetRange;
26use helpers::Ref;
27use indexmap::IndexMap;
28use indexmap::IndexSet;
29use meta::ShapeMeta;
30pub use visitor::ShapeVisitor;
31
32use crate::case_enum::all::all;
33use crate::case_enum::one::one;
34use crate::location::Location;
35use crate::merge::MergeSet;
36use crate::name::Name;
37use crate::name::WeakScope;
38
39/// The `shape::Shape` struct provides a recursive, immutable, reference-counted
40/// tree/DAG format for representing and enforcing common structures and usage
41/// patterns of JSON-like data.
42///
43/// The `Shape` system is not bound to any particular programming language, so
44/// it does not inherit a data model that it must represent and defend, yet it
45/// must adopt/assume _some_ concrete data model, since a type system without a
46/// data model to enforce is as useful as a straitjacket on a coat rack. JSON
47/// was chosen for its relative simplicity, its ubiquity as a data interchange
48/// format used across programming languages, and because JSON is often used in
49/// scenarios without a static type system to help catch errors before runtime.
50///
51/// The `Shape` system has no source syntax for denoting shapes directly, but
52/// you can use the `Shape::*` helper functions to create shapes
53/// programmatically, in Rust. `Shape::pretty_print()` provides a human-readable
54/// representation of a `Shape` for debugging and testing purposes.
55///
56/// All in all, this _Static `Shape` System_ (SSS) supports the following
57/// type-theoretic features:
58///
59/// - [x] Primitive shapes: `Bool`, `String`, `Int`, `Float`, `Null`
60/// - [x] Singleton primitive shapes: `true`, `false`, `"hello"`, `42`, `null`
61/// - [x] `Array` shapes, supporting both static tuples and dynamic lists
62/// - [x] `Object` shapes, supporting both static fields and dynamic string keys
63/// - [x] `One<S1, S2, ...>` union shapes, representing a set of shape
64/// alternatives
65/// - [x] `All<S1, S2, ...>` intersection shapes, representing a set
66/// simultaneous requirements
67/// - [x] `shape.field(name)` and `shape.item(index)` methods for accessing the
68/// shape of a subproperty of a shape
69/// - [x] `Name` shape references, with support for symbolic subproperty shape
70/// access
71/// - [x] `Error` shapes, representing a failure of shape processing, with
72/// support for chains of errors and partial shape data
73/// - [x] `None` shapes, representing the absence of a value (helpful for
74/// representing optionality of shapes)
75/// - [x] `subshape.satisfies(supershape)` and `supershape.accepts(subshape)`
76/// methods for testing shape relationships
77/// - [x] `shape.accepts_json(json)` method for testing whether concrete JSON
78/// data satisfies some expected shape
79/// - [x] `shape.pretty_print()` method for debugging and testing
80
81#[derive(Clone, Eq)]
82// [`Shape`] enforces the simplification of [`ShapeCase`] variants, because
83// there is no way to create a [`Shape`] without simplifying the input
84// [`ShapeCase`]. This is a very useful invariant because it allows each
85// [`ShapeCase`] to assume its immediate [`Shape`] children have already been
86// simplified.
87//
88// In addition simplification, [`Shape`] supports testing shape-shape acceptance
89// (or the equivalent inverse, satisfaction) with `super.accepts(sub)` and/or
90// `sub.satisfies(super)`. See also `shape.accepts_json(json)` for testing
91// whether concrete JSON data satisfies some expected `shape`.
92//
93// In the future, we may internalize/canonize shapes to reduce memory usage,
94// especially for well-known shapes like `Bool` and `Int` and `String`. This
95// would require either thread safety (is `type Ref<T> = std::sync::Arc<T>`
96// enough?) or maintaining per-thread canonical shape tables.
97pub struct Shape {
98 // This field is private, but if you want to match against an immutable
99 // reference to the `ShapeCase` variant, use `match shape.case() { ... }`.
100 case: Ref<ShapeCase>,
101
102 /// The combination of locations which, combined, produce this shape.
103 ///
104 /// Many cases will only have a single location, but when shapes are
105 /// simplified, their locations are all retained in the result.
106 ///
107 /// Currently [`ShapeMeta::Loc(Location)`] is the only variant here, but we
108 /// can add other kinds of metadata in the future.
109 meta: Ref<ShapeMeta>,
110}
111
112impl PartialEq for Shape {
113 fn eq(&self, other: &Self) -> bool {
114 self.case == other.case
115 }
116}
117
118impl Hash for Shape {
119 fn hash<H: Hasher>(&self, state: &mut H) {
120 // Since the PartialEq implementation ignores self.locations, so must
121 // the Hash implementation.
122 self.case.hash(state);
123 }
124}
125
126impl Shape {
127 /// Create a `Shape` from a [`ShapeCase`] variant.
128 ///
129 /// This method is crate-private to help enforce some invariants.
130 pub(crate) fn new(case: ShapeCase, locations: impl IntoIterator<Item = Location>) -> Shape {
131 let meta = ShapeMeta::new(&case, locations, []);
132 Shape {
133 case: Ref::new(case),
134 meta: Ref::new(meta),
135 }
136 }
137
138 /// Create a `Shape` from a [`ShapeCase`] variant with errors attached.
139 pub(crate) fn new_with_errors(
140 case: ShapeCase,
141 locations: impl IntoIterator<Item = Location>,
142 errors: impl IntoIterator<Item = Error>,
143 ) -> Shape {
144 let meta = ShapeMeta::new_with_errors(&case, locations, [], errors);
145 Shape {
146 case: Ref::new(case),
147 meta: Ref::new(meta),
148 }
149 }
150
151 /// When boolean helper methods like `.is_none()` and `.is_null()` are not
152 /// enough, you can match against the underlying [`ShapeCase`] by obtaining an
153 /// immutable `&ShapeCase` reference using the `shape.case()` method.
154 #[must_use]
155 pub fn case(&self) -> &ShapeCase {
156 self.case.as_ref()
157 }
158
159 /// Returns an iterator over all [`Location`]s associated with this shape.
160 pub fn locations(&self) -> impl Iterator<Item = &Location> {
161 let self_locs = self.meta.locations();
162
163 let unique_locs: IndexSet<&Location> = match self.case() {
164 ShapeCase::One(shapes) => self_locs
165 .chain(shapes.iter().flat_map(|s| s.meta.locations()))
166 .collect(),
167 ShapeCase::All(shapes) => self_locs
168 .chain(shapes.iter().flat_map(|s| s.meta.locations()))
169 .collect(),
170 _ => self_locs.collect(),
171 };
172
173 unique_locs.into_iter()
174 }
175
176 /// Returns an iterator over all [`Name`]s associated with this shape.
177 pub fn names(&self) -> impl Iterator<Item = &Name> {
178 self.meta.names()
179 }
180
181 pub fn nested_base_names(&self) -> impl Iterator<Item = &str> {
182 self.meta.nested_base_names()
183 }
184
185 /// Returns a [`Shape`] that accepts any boolean value, `true` or `false`.
186 #[must_use]
187 pub fn bool(locations: impl IntoIterator<Item = Location>) -> Self {
188 Self::new(ShapeCase::Bool(None), locations)
189 }
190
191 /// Returns a [`Shape`] that accepts only the specified boolean value.
192 #[must_use]
193 pub fn bool_value(value: bool, locations: impl IntoIterator<Item = Location>) -> Self {
194 Self::new(ShapeCase::Bool(Some(value)), locations)
195 }
196
197 /// Returns a [`Shape`] that accepts any string value.
198 #[must_use]
199 pub fn string(locations: impl IntoIterator<Item = Location>) -> Self {
200 Self::new(ShapeCase::String(None), locations)
201 }
202
203 /// Returns a [`Shape`] that accepts only the specified string value.
204 #[must_use]
205 pub fn string_value(value: &str, locations: impl IntoIterator<Item = Location>) -> Self {
206 Self::new(ShapeCase::String(Some(value.to_string())), locations)
207 }
208
209 /// Returns a [`Shape`] that accepts any integer value.
210 #[must_use]
211 pub fn int(locations: impl IntoIterator<Item = Location>) -> Self {
212 Self::new(ShapeCase::Int(None), locations)
213 }
214
215 /// Returns a [`Shape`] that accepts only the specified integer value.
216 #[must_use]
217 pub fn int_value(value: i64, locations: impl IntoIterator<Item = Location>) -> Self {
218 Self::new(ShapeCase::Int(Some(value)), locations)
219 }
220
221 /// Returns a [`Shape`] that accepts any floating point value.
222 #[must_use]
223 pub fn float(locations: impl IntoIterator<Item = Location>) -> Self {
224 Self::new(ShapeCase::Float, locations)
225 }
226
227 /// Returns a [`Shape`] that accepts only the JSON `null` value.
228 #[must_use]
229 pub fn null(locations: impl IntoIterator<Item = Location>) -> Self {
230 Self::new(ShapeCase::Null, locations)
231 }
232
233 #[must_use]
234 pub fn is_null(&self) -> bool {
235 self.case.is_null()
236 }
237
238 /// Returns a symbolic reference to a named shape, potentially not yet
239 /// defined.
240 ///
241 /// In order to add items to the subpath of this named shape, call the
242 /// `.field(name)` and/or `.item(index)` methods.
243 ///
244 /// Note that variable shapes are represented by [`ShapeCase::Name`] where the
245 /// name string includes the initial `$` character.
246 #[must_use]
247 pub fn name(name: &str, locations: impl IntoIterator<Item = Location>) -> Self {
248 let locations = locations.into_iter().collect::<Vec<_>>();
249 Self::new(
250 ShapeCase::Name(
251 name::Name::base(name.to_string(), locations.clone()),
252 WeakScope::none(),
253 ),
254 locations.clone(),
255 )
256 }
257
258 /// Useful for obtaining the kind of [`IndexMap`] this library uses for the
259 /// [`ShapeCase::Object`] variant.
260 #[must_use]
261 pub fn empty_map() -> IndexMap<String, Self> {
262 IndexMap::new()
263 }
264
265 /// Returns an open object [`Shape`] with no declared fields, which accepts
266 /// any object value because the unknown `rest` shape permits any dynamic
267 /// property. Useful as an "is this any object?" probe via
268 /// [`Shape::accepts`], or directly via [`Shape::is_object`].
269 /// For the strictly closed `{}` shape (no fields, no rest — only the
270 /// literal empty object is accepted), use [`Shape::strict_empty_object`].
271 #[must_use]
272 pub fn any_object(locations: impl IntoIterator<Item = Location>) -> Self {
273 Shape::new(
274 ShapeCase::Object {
275 fields: Shape::empty_map(),
276 rest: Shape::unknown([]),
277 },
278 locations,
279 )
280 }
281
282 /// Previous name for [`Shape::any_object`]. The `empty_object` label was
283 /// misleading because the shape it produced has accepted any object value
284 /// since `Shape::record` and friends became open by default — only
285 /// [`Shape::strict_empty_object`] actually requires the empty literal `{}`.
286 /// Callers that meant the open form should migrate to [`Shape::any_object`];
287 /// callers that genuinely needed the closed empty-only form should migrate
288 /// to [`Shape::strict_empty_object`].
289 #[deprecated(
290 since = "0.7.0",
291 note = "use Shape::any_object for the open form, or Shape::strict_empty_object if extras should be rejected"
292 )]
293 #[must_use]
294 pub fn empty_object(locations: impl IntoIterator<Item = Location>) -> Self {
295 Shape::any_object(locations)
296 }
297
298 /// Returns a strictly closed empty object [`Shape`]: no declared fields and
299 /// no dynamic properties. Only the literal empty object `{}` satisfies it.
300 /// For the common case where an empty object is used as a placeholder that
301 /// should accept any object value, use [`Shape::any_object`] instead.
302 #[must_use]
303 pub fn strict_empty_object(locations: impl IntoIterator<Item = Location>) -> Self {
304 Shape::new(
305 ShapeCase::Object {
306 fields: Shape::empty_map(),
307 rest: Shape::none(),
308 },
309 locations,
310 )
311 }
312
313 /// To get a compatible empty mutable [`IndexMap`] without directly
314 /// depending on the [`indexmap`] crate yourself, use [`Shape::empty_map()`].
315 #[must_use]
316 pub fn object(
317 fields: IndexMap<String, Shape>,
318 rest: Shape,
319 locations: impl IntoIterator<Item = Location>,
320 ) -> Self {
321 Shape::new(ShapeCase::Object { fields, rest }, locations)
322 }
323
324 /// Returns an open record [`Shape`]: an object with the given static
325 /// fields plus an unknown `rest` shape, so any additional dynamic
326 /// properties are permitted. For a strictly closed record whose only
327 /// satisfying values carry exactly the declared fields, use
328 /// [`Shape::strict_record`].
329 #[must_use]
330 pub fn record(
331 fields: IndexMap<String, Shape>,
332 locations: impl IntoIterator<Item = Location>,
333 ) -> Self {
334 Shape::object(fields, Shape::unknown([]), locations)
335 }
336
337 /// Returns a strictly closed record [`Shape`]: an object with the given
338 /// static fields and no dynamic properties. Values with additional keys
339 /// not listed in `fields` are rejected. For the open variant that
340 /// permits additional properties, use [`Shape::record`].
341 #[must_use]
342 pub fn strict_record(
343 fields: IndexMap<String, Shape>,
344 locations: impl IntoIterator<Item = Location>,
345 ) -> Self {
346 Shape::object(fields, Shape::none(), locations)
347 }
348
349 /// Returns a [`Shape`] that accepts any dictionary-like object with dynamic
350 /// string properties having a given value shape.
351 #[must_use]
352 pub fn dict(value_shape: Shape, locations: impl IntoIterator<Item = Location>) -> Self {
353 Shape::object(Shape::empty_map(), value_shape, locations)
354 }
355
356 /// Arrays, tuples, and lists are all manifestations of the same underlying
357 /// [`ShapeCase::Array`] representation.
358 #[must_use]
359 pub fn array(
360 prefix: impl IntoIterator<Item = Shape>,
361 tail: Shape,
362 locations: impl IntoIterator<Item = Location>,
363 ) -> Self {
364 let prefix = prefix.into_iter().collect();
365 Self::new(ShapeCase::Array { prefix, tail }, locations)
366 }
367
368 /// Previous name for the open-tailed tuple constructor. The bare name
369 /// `tuple` is ambiguous — classical tuples are fixed-arity (closed), but
370 /// `Shape::tuple` has been open-tailed since [`Shape::strict_tuple`] was
371 /// introduced. Callers should pick explicitly: [`Shape::strict_tuple`]
372 /// for an exact n-tuple (no extras), or [`Shape::open_tuple`] when they
373 /// want to assert a prefix while allowing additional trailing elements.
374 /// The "any array?" probe is better served by [`Shape::any_array`] or
375 /// [`Shape::is_array`].
376 ///
377 /// The body of this function still produces the open form, so existing
378 /// callers' behavior is unchanged until they migrate.
379 #[deprecated(
380 since = "0.7.0",
381 note = "use Shape::strict_tuple for an exact n-tuple, Shape::open_tuple if extras should be permitted, or Shape::any_array / Shape::is_array for the bare \"any array?\" probe"
382 )]
383 pub fn tuple(
384 shapes: impl IntoIterator<Item = Shape>,
385 locations: impl IntoIterator<Item = Location>,
386 ) -> Self {
387 Shape::open_tuple(shapes, locations)
388 }
389
390 /// An open tuple is a [`ShapeCase::Array`] with statically known leading
391 /// element shapes and an open (`Unknown`) tail, so it accepts arrays
392 /// starting with the declared prefix and continuing with any number of
393 /// trailing elements of any shape. For the closed counterpart that
394 /// accepts only arrays of the exact declared length, use
395 /// [`Shape::strict_tuple`].
396 #[must_use]
397 pub fn open_tuple(
398 shapes: impl IntoIterator<Item = Shape>,
399 locations: impl IntoIterator<Item = Location>,
400 ) -> Self {
401 Shape::array(shapes, Shape::unknown([]), locations)
402 }
403
404 /// A strict tuple is a [`ShapeCase::Array`] with statically known (though
405 /// possibly empty) element shapes and no dynamic tail shape, so it accepts
406 /// only arrays of exactly the same length and element shapes. For the
407 /// open-tailed counterpart that permits extra trailing elements beyond
408 /// the declared prefix, use [`Shape::open_tuple`].
409 #[must_use]
410 pub fn strict_tuple(
411 shapes: impl IntoIterator<Item = Shape>,
412 locations: impl IntoIterator<Item = Location>,
413 ) -> Self {
414 Shape::array(shapes, Shape::none(), locations)
415 }
416
417 /// A `List<S>` is a [`ShapeCase::Array`] with an empty static `prefix` and a
418 /// dynamic element shape `S`.
419 #[must_use]
420 pub fn list(of: Shape, locations: impl IntoIterator<Item = Location>) -> Self {
421 Shape::array(empty(), of, locations)
422 }
423
424 /// Returns an open [`ShapeCase::Array`] with no required leading elements
425 /// and an unknown tail, so it accepts any array value. Useful as an "is
426 /// this any array?" probe via [`Shape::accepts`], or directly via
427 /// [`Shape::is_array`].
428 #[must_use]
429 pub fn any_array(locations: impl IntoIterator<Item = Location>) -> Self {
430 Shape::array(empty(), Shape::unknown([]), locations)
431 }
432
433 /// Returns a [`ShapeCase::One`] union of the given shapes, simplified.
434 ///
435 /// Note that `locations` in this case should _not_ refer to each individual inner shape, but
436 /// to the thing that caused all of these shapes to be combined, like maybe a `->match`. If
437 /// there is no obvious cause to point users to, then the location should be empty.
438 pub fn one(
439 shapes: impl IntoIterator<Item = Shape>,
440 locations: impl IntoIterator<Item = Location>,
441 ) -> Self {
442 one(shapes.into_iter(), locations.into_iter().collect())
443 }
444
445 /// Returns a [`ShapeCase::All`] intersection of the given shapes, simplified.
446 ///
447 /// Note that `locations` in this case should _not_ refer to each individual inner shape, but
448 /// to the thing that caused all of these shapes to be combined, like maybe a `IntfA & IntfB`.
449 /// If there is no obvious cause to point users to, then the location should be empty.
450 pub fn all(
451 shapes: impl IntoIterator<Item = Shape>,
452 locations: impl IntoIterator<Item = Location>,
453 ) -> Self {
454 all(shapes.into_iter(), locations.into_iter().collect())
455 }
456
457 /// Returns a shape that accepts any JSON value (including [`ShapeCase::None`]
458 /// and [`ShapeCase::Unknown`]), and is not accepted by any shape other than itself.
459 #[must_use]
460 pub fn unknown(locations: impl IntoIterator<Item = Location>) -> Self {
461 Self::new(ShapeCase::Unknown, locations)
462 }
463
464 #[must_use]
465 pub fn is_unknown(&self) -> bool {
466 matches!(self.case(), ShapeCase::Unknown)
467 }
468
469 /// Returns true iff this shape would be accepted by [`Shape::any_array`],
470 /// i.e. every value the shape can take is an array. Recurses through
471 /// `ShapeCase::One` (every branch must be an array), `ShapeCase::All`
472 /// (at least one intersection member must be an array), and bound
473 /// `ShapeCase::Name` shapes (the resolved shape must be an array).
474 #[must_use]
475 pub fn is_array(&self) -> bool {
476 match self.case() {
477 ShapeCase::Array { .. } => true,
478 ShapeCase::One(members) if !members.is_empty() => members.iter().all(Shape::is_array),
479 ShapeCase::All(members) => members.iter().any(Shape::is_array),
480 ShapeCase::Name(name, weak) => weak.upgrade(name).is_some_and(|s| s.is_array()),
481 _ => false,
482 }
483 }
484
485 /// Returns true iff this shape would be accepted by [`Shape::any_object`],
486 /// i.e. every value the shape can take is an object. Recurses through
487 /// `ShapeCase::One`, `ShapeCase::All`, and bound `ShapeCase::Name`
488 /// shapes in the same way as [`Shape::is_array`].
489 #[must_use]
490 pub fn is_object(&self) -> bool {
491 match self.case() {
492 ShapeCase::Object { .. } => true,
493 ShapeCase::One(members) if !members.is_empty() => members.iter().all(Shape::is_object),
494 ShapeCase::All(members) => members.iter().any(Shape::is_object),
495 ShapeCase::Name(name, weak) => weak.upgrade(name).is_some_and(|s| s.is_object()),
496 _ => false,
497 }
498 }
499
500 /// Returns a shape representing the absence of a JSON value, which is
501 /// satisfied/accepted only by itself.
502 ///
503 /// Because this represents the absence of a value, it shouldn't have a location. Basically,
504 /// nothing can produce none alone, and if it were a union, that union would have its own
505 /// location.
506 #[must_use]
507 pub fn none() -> Self {
508 Self::new(ShapeCase::None, [])
509 }
510
511 #[must_use]
512 pub fn is_none(&self) -> bool {
513 self.case.is_none()
514 }
515
516 /// Report a failure of shape processing. Creates an Unknown shape with
517 /// the error attached as metadata.
518 #[must_use]
519 pub fn error(
520 message: impl Into<String>,
521 locations: impl IntoIterator<Item = Location>,
522 ) -> Self {
523 let locations: Vec<_> = locations.into_iter().collect();
524 Self::new_with_errors(
525 ShapeCase::Unknown,
526 locations,
527 [Error {
528 message: message.into(),
529 }],
530 )
531 }
532
533 /// Returns true if this shape has any errors attached directly (not nested).
534 #[deprecated(
535 since = "0.7.0",
536 note = "use `has_errors()` for recursive check or `has_own_errors()` for own-only"
537 )]
538 #[must_use]
539 pub fn is_error(&self) -> bool {
540 self.meta.has_errors()
541 }
542
543 /// Returns true if this shape has errors attached to it (not nested children).
544 #[must_use]
545 pub fn has_own_errors(&self) -> bool {
546 self.meta.has_errors()
547 }
548
549 /// Returns true if this shape or any nested child has errors.
550 #[must_use]
551 pub fn has_errors(&self) -> bool {
552 if self.has_own_errors() {
553 return true;
554 }
555 match self.case() {
556 ShapeCase::Array { prefix, tail } => {
557 prefix.iter().any(Shape::has_errors) || tail.has_errors()
558 }
559 ShapeCase::Object { fields, rest } => {
560 fields.values().any(Shape::has_errors) || rest.has_errors()
561 }
562 ShapeCase::One(one) => one.iter().any(Shape::has_errors),
563 ShapeCase::All(all) => all.iter().any(Shape::has_errors),
564 // Name references are not followed (could cause cycles)
565 // Leaf cases have no children to traverse
566 ShapeCase::Name(_, _)
567 | ShapeCase::Bool(_)
568 | ShapeCase::String(_)
569 | ShapeCase::Int(_)
570 | ShapeCase::Float
571 | ShapeCase::Null
572 | ShapeCase::Unknown
573 | ShapeCase::None => false,
574 }
575 }
576
577 /// Iterate over errors attached to this shape (not nested children).
578 pub fn own_errors(&self) -> impl Iterator<Item = &Error> {
579 self.meta.errors()
580 }
581
582 /// Recursively collect all errors from this shape and its nested children.
583 /// This traverses Array elements, Object fields, One/All variants, but does
584 /// NOT follow Name references (to avoid cycles and because named shapes are
585 /// conceptually separate).
586 #[must_use]
587 pub fn errors(&self) -> Vec<&Error> {
588 let mut errors: Vec<&Error> = self.own_errors().collect();
589
590 match self.case() {
591 ShapeCase::Array { prefix, tail } => {
592 for child in prefix {
593 errors.extend(child.errors());
594 }
595 errors.extend(tail.errors());
596 }
597 ShapeCase::Object { fields, rest } => {
598 for child in fields.values() {
599 errors.extend(child.errors());
600 }
601 errors.extend(rest.errors());
602 }
603 ShapeCase::One(one) => {
604 for child in one.iter() {
605 errors.extend(child.errors());
606 }
607 }
608 ShapeCase::All(all) => {
609 for child in all.iter() {
610 errors.extend(child.errors());
611 }
612 }
613 // Name references are not followed (could cause cycles)
614 // Leaf cases have no children to traverse
615 ShapeCase::Name(_, _)
616 | ShapeCase::Bool(_)
617 | ShapeCase::String(_)
618 | ShapeCase::Int(_)
619 | ShapeCase::Float
620 | ShapeCase::Null
621 | ShapeCase::Unknown
622 | ShapeCase::None => {}
623 }
624
625 errors
626 }
627
628 /// Report a failure of shape processing associated with a
629 /// partial/best-guess shape that may still be useful. The error is
630 /// attached to the partial shape as metadata.
631 #[must_use]
632 pub fn error_with_partial(
633 message: impl Into<String>,
634 partial: Shape,
635 locations: impl IntoIterator<Item = Location>,
636 ) -> Self {
637 partial
638 .with_error(Error {
639 message: message.into(),
640 })
641 .with_locations(locations.into_iter().collect::<Vec<_>>().iter())
642 }
643
644 /// Clone the shape with an additional error attached.
645 #[must_use]
646 pub fn with_error(mut self, error: Error) -> Self {
647 Ref::make_mut(&mut self.meta).add_error(error);
648 self
649 }
650
651 /// Clone the shape, adding the provided `locations` to the existing locations.
652 #[must_use]
653 pub fn with_locations<'a>(mut self, locations: impl IntoIterator<Item = &'a Location>) -> Self {
654 for loc in locations {
655 if !self.meta.has_location(loc) {
656 Ref::make_mut(&mut self.meta).add_location(loc);
657 }
658 }
659 self
660 }
661}
662
663#[cfg(test)]
664mod test_errors {
665 use super::*;
666
667 #[test]
668 fn error_shape_has_error() {
669 let error_shape = Shape::error("Expected a string", []);
670 let errors = error_shape.errors();
671 assert_eq!(errors.len(), 1);
672 assert_eq!(errors[0].message, "Expected a string");
673 assert!(error_shape.has_errors());
674 }
675
676 #[test]
677 fn error_with_partial_preserves_shape() {
678 let error_shape = Shape::error_with_partial("Parse failed", Shape::bool([]), []);
679 // The shape should be Bool with an error attached
680 assert!(matches!(error_shape.case(), ShapeCase::Bool(None)));
681 assert!(error_shape.has_errors());
682 let errors = error_shape.errors();
683 assert_eq!(errors.len(), 1);
684 assert_eq!(errors[0].message, "Parse failed");
685 }
686
687 #[test]
688 fn multiple_errors_can_attach() {
689 let shape = Shape::bool([])
690 .with_error(Error {
691 message: "First error".to_string(),
692 })
693 .with_error(Error {
694 message: "Second error".to_string(),
695 });
696 let errors = shape.errors();
697 assert_eq!(errors.len(), 2);
698 assert_eq!(errors[0].message, "First error");
699 assert_eq!(errors[1].message, "Second error");
700 }
701
702 #[test]
703 fn plain_shapes_have_no_errors() {
704 let int_shape = Shape::int([]);
705 assert!(!int_shape.has_errors());
706 assert_eq!(int_shape.errors().len(), 0);
707 }
708
709 #[test]
710 fn errors_collects_from_nested_shapes() {
711 // Create an array with an error on a nested element
712 let error_element = Shape::string([]).with_error(Error {
713 message: "nested error".to_string(),
714 });
715 let array = Shape::array([], error_element.clone(), []);
716
717 // own_errors() should be empty on the array itself
718 assert!(!array.has_own_errors());
719 assert!(array.own_errors().next().is_none());
720
721 // errors() should find the nested error (recursive)
722 let all = array.errors();
723 assert_eq!(all.len(), 1);
724 assert_eq!(all[0].message, "nested error");
725
726 // has_errors() should return true (recursive check)
727 assert!(array.has_errors());
728 }
729
730 #[test]
731 fn errors_collects_from_object_fields() {
732 let error_field = Shape::int([]).with_error(Error {
733 message: "field error".to_string(),
734 });
735 let mut fields = Shape::empty_map();
736 fields.insert("count".to_string(), error_field);
737 let obj = Shape::object(fields, Shape::none(), []);
738
739 // No own errors on obj, but nested field has one
740 assert!(!obj.has_own_errors());
741 assert!(obj.has_errors());
742
743 let all = obj.errors();
744 assert_eq!(all.len(), 1);
745 assert_eq!(all[0].message, "field error");
746 }
747
748 #[test]
749 fn errors_collects_from_multiple_levels() {
750 // Object with error -> array with error -> element with error
751 let deep_error = Shape::bool([]).with_error(Error {
752 message: "deep".to_string(),
753 });
754 let array_with_error = Shape::array([], deep_error, []).with_error(Error {
755 message: "middle".to_string(),
756 });
757 let mut fields = Shape::empty_map();
758 fields.insert("items".to_string(), array_with_error);
759 let obj = Shape::object(fields, Shape::none(), []).with_error(Error {
760 message: "top".to_string(),
761 });
762
763 let all = obj.errors();
764 assert_eq!(all.len(), 3);
765 // Errors collected in traversal order: top, middle, deep
766 assert_eq!(all[0].message, "top");
767 assert_eq!(all[1].message, "middle");
768 assert_eq!(all[2].message, "deep");
769 }
770
771 #[test]
772 fn has_errors_short_circuits() {
773 // Should return true as soon as it finds an error
774 let clean_shape = Shape::int([]);
775 assert!(!clean_shape.has_errors());
776
777 let error_shape = Shape::error("oops", []);
778 assert!(error_shape.has_errors());
779 }
780
781 #[test]
782 fn errors_and_names_single_each() {
783 // One error, one name
784 let shape = Shape::string([])
785 .with_error(Error {
786 message: "invalid value".to_string(),
787 })
788 .with_base_name("MyString", []);
789
790 assert!(shape.has_errors());
791 assert_eq!(shape.errors().len(), 1);
792 assert!(shape.has_base_name("MyString"));
793
794 assert_eq!(shape.pretty_print(), r#"String (err "invalid value")"#);
795 assert_eq!(shape.pretty_print_without_errors(), "String");
796 assert_eq!(
797 shape.pretty_print_with_names(),
798 r#"String (err "invalid value") (aka MyString)"#
799 );
800 }
801
802 #[test]
803 fn multiple_errors_no_names() {
804 let shape = Shape::int([])
805 .with_error(Error {
806 message: "first error".to_string(),
807 })
808 .with_error(Error {
809 message: "second error".to_string(),
810 });
811
812 assert!(shape.has_errors());
813 assert_eq!(shape.errors().len(), 2);
814 assert_eq!(shape.names().count(), 0);
815
816 assert_eq!(
817 shape.pretty_print(),
818 r#"Int (err "first error", "second error")"#
819 );
820 assert_eq!(shape.pretty_print_without_errors(), "Int");
821 assert_eq!(
822 shape.pretty_print_with_names(),
823 r#"Int (err "first error", "second error")"#
824 );
825 }
826
827 #[test]
828 fn no_errors_multiple_names() {
829 // Create a shape, apply one name, then apply another
830 // Note: with_base_name propagates names to children, but for a leaf
831 // shape like Int, multiple calls add multiple names
832 let shape = Shape::int([])
833 .with_base_name("Count", [])
834 .with_base_name("Total", []);
835
836 assert!(!shape.has_errors());
837 assert!(shape.has_base_name("Count"));
838 assert!(shape.has_base_name("Total"));
839
840 assert_eq!(shape.pretty_print(), "Int");
841 assert_eq!(shape.pretty_print_without_errors(), "Int");
842 // Names are collected via MergeSet, order may vary
843 let with_names = shape.pretty_print_with_names();
844 assert!(with_names.contains("(aka"));
845 assert!(with_names.contains("Count"));
846 assert!(with_names.contains("Total"));
847 }
848
849 #[test]
850 fn multiple_errors_multiple_names() {
851 let shape = Shape::bool([])
852 .with_error(Error {
853 message: "err1".to_string(),
854 })
855 .with_error(Error {
856 message: "err2".to_string(),
857 })
858 .with_base_name("Flag", [])
859 .with_base_name("Toggle", []);
860
861 assert!(shape.has_errors());
862 assert_eq!(shape.errors().len(), 2);
863 assert!(shape.has_base_name("Flag"));
864 assert!(shape.has_base_name("Toggle"));
865
866 // pretty_print shows errors only
867 assert_eq!(shape.pretty_print(), r#"Bool (err "err1", "err2")"#);
868
869 // pretty_print_without_errors shows neither
870 assert_eq!(shape.pretty_print_without_errors(), "Bool");
871
872 // pretty_print_with_names shows errors first, then names
873 let with_names = shape.pretty_print_with_names();
874 assert!(with_names.starts_with(r#"Bool (err "err1", "err2") (aka "#));
875 assert!(with_names.contains("Flag"));
876 assert!(with_names.contains("Toggle"));
877 }
878
879 #[test]
880 fn nested_errors_and_names() {
881 // Create an object with a field that has both error and name
882 let field_with_error = Shape::string([])
883 .with_error(Error {
884 message: "field error".to_string(),
885 })
886 .with_base_name("FieldName", []);
887
888 let mut fields = Shape::empty_map();
889 fields.insert("field".to_string(), field_with_error);
890 let obj = Shape::object(fields, Shape::none(), [])
891 .with_error(Error {
892 message: "object error".to_string(),
893 })
894 .with_base_name("MyObject", []);
895
896 // Object has its own error, field has its own error
897 assert!(obj.has_own_errors());
898 assert!(obj.has_errors());
899 // errors() is recursive - should find both
900 assert_eq!(obj.errors().len(), 2);
901
902 // Check object-level pretty print
903 let pp = obj.pretty_print();
904 assert!(pp.contains(r#"(err "object error")"#));
905 assert!(pp.contains(r#"(err "field error")"#));
906
907 // With names shows errors first, then names at each level
908 let with_names = obj.pretty_print_with_names();
909 assert!(with_names.contains("MyObject"));
910 assert!(with_names.contains("FieldName"));
911 }
912}