Skip to main content

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}