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