Skip to main content

shape/
lib.rs

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