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