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}