shape/child_shape.rs
1use std::fmt::{Display, Formatter};
2
3use super::Shape;
4use super::ShapeCase;
5use crate::case_enum::Error;
6use crate::helpers::quote_non_identifier;
7use crate::location::{Located, Location};
8use indexmap::IndexSet;
9
10/// `NamedShapePathKey` represents a single step in a subpath associated with a
11/// [`ShapeCase::Name`] shape reference. When pretty-printed, these subpaths are
12/// delimited by `.` characters (with `"..."`-quoting as necessary for
13/// non-identifier field names), and can be either `::Field` names or array
14/// `::Index` values.
15///
16/// As a special form of catch-all `::Index` value, the step may also be the
17/// wildcard `::AnyIndex`, which denotes a union of all the element shapes of an
18/// array, or just the given shape if not an array, which is useful to support
19/// GraphQL-like array mapping. When pretty-printed, these wildcard keys look
20/// like `.*`, and if multiple wildcards are used in a row, they will be
21/// coalesced/simplified down to just one logical `.*`.
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub enum NamedShapePathKey {
24 // The value shape of an object field with a specific name.
25 Field(String),
26 // The .** wildcard denoting a union of all object field value shapes,
27 // automatically mapped over arrays, and returning the shape itself for
28 // non-object, non-array shapes.
29 AnyField,
30
31 // The shape of an array element at a specific index.
32 Index(usize),
33 // The .* wildcard, which represents a union of all array element shapes
34 // (ignoring None) or just the shape itself for non-array shapes.
35 AnyIndex,
36
37 // Represents the `?` optional chaining operator, which replaces any
38 // possibility of `null` in the input shape with `None` (and silences some
39 // errors at runtime), but otherwise leaves its input unmodified.
40 Question,
41
42 // Represents a hypothetical `!` non-`None` assertion operator, which
43 // removes `None` from the input type. If you want to represent a shape that
44 // is neither `null` nor `None`/missing, you can use `::Question` to map
45 // `null` to `None` and then `::NotNone` to remove `None`, so both `null`
46 // and `None` will be removed from the resulting shape. If you apply
47 // `::NotNone` to a `None` shape, the result will be the empty union shape
48 // `One<>`, which we pretty-print as `Never`, analogous to TypeScript's
49 // `never` type. When included in a larger union, this `One<>`/`Never` shape
50 // will be removed during union simplification.
51 NotNone,
52}
53
54impl From<String> for NamedShapePathKey {
55 fn from(value: String) -> Self {
56 NamedShapePathKey::Field(value)
57 }
58}
59
60impl From<&str> for NamedShapePathKey {
61 fn from(value: &str) -> Self {
62 NamedShapePathKey::Field(value.to_string())
63 }
64}
65
66impl From<usize> for NamedShapePathKey {
67 fn from(value: usize) -> Self {
68 NamedShapePathKey::Index(value)
69 }
70}
71
72impl NamedShapePathKey {
73 #[must_use]
74 pub fn path_to_string(path: &[Located<NamedShapePathKey>]) -> String {
75 let mut dotted = String::new();
76
77 for key in path {
78 dotted.push_str(&key.value.to_string());
79 }
80
81 dotted
82 }
83}
84
85impl Display for NamedShapePathKey {
86 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
87 match self {
88 NamedShapePathKey::Field(name) => {
89 write!(f, ".{}", quote_non_identifier(name.as_str()))
90 }
91 NamedShapePathKey::AnyField => {
92 write!(f, ".**")
93 }
94 NamedShapePathKey::Index(index) => {
95 write!(f, ".{index}")
96 }
97 NamedShapePathKey::AnyIndex => {
98 write!(f, ".*")
99 }
100 NamedShapePathKey::Question => {
101 write!(f, "?")
102 }
103 NamedShapePathKey::NotNone => {
104 write!(f, "!")
105 }
106 }
107 }
108}
109
110impl Shape {
111 /// Returns a new [`Shape`] representing the shape of a given subproperty
112 /// (field name) of the `self` shape.
113 #[must_use]
114 pub fn field(&self, field_name: &str, locations: impl IntoIterator<Item = Location>) -> Shape {
115 self.child(Located::new(NamedShapePathKey::from(field_name), locations))
116 }
117
118 /// Returns a new [`Shape`] representing the union of all field shapes of
119 /// object shapes, or just the shape itself for non-object shapes.
120 #[must_use]
121 pub fn any_field(&self, locations: impl IntoIterator<Item = Location>) -> Shape {
122 self.child(Located::new(NamedShapePathKey::AnyField, locations))
123 }
124
125 /// Returns a new [`Shape`] representing the shape of a given element of an
126 /// array shape.
127 #[must_use]
128 pub fn item(&self, index: usize, locations: impl IntoIterator<Item = Location>) -> Shape {
129 self.child(Located::new(NamedShapePathKey::from(index), locations))
130 }
131
132 /// Returns a new [`Shape`] representing the union of all element shapes of
133 /// array shapes, or just the shape itself for non-array shapes.
134 #[must_use]
135 pub fn any_item(&self, locations: impl IntoIterator<Item = Location>) -> Shape {
136 self.child(Located::new(NamedShapePathKey::AnyIndex, locations))
137 }
138
139 /// Returns a new [`Shape`] representing the input shape with any
140 /// possibility of `null` replaced by `None`, but otherwise unchanged. This
141 /// models the behavior of a `?` optional chainining operator, which
142 /// additionally silences some errors related to missing fields at runtime.
143 /// When a [`ShapeCase::Name`] shape reference has a `?` step in its
144 /// subpath, that `?` step can be applied to the shape when/if the named
145 /// shape is declared/resolved, so the effect of the `?` is not lost.
146 #[must_use]
147 pub fn question(&self, locations: impl IntoIterator<Item = Location>) -> Shape {
148 self.child(Located::new(NamedShapePathKey::Question, locations))
149 }
150
151 /// Returns a new [`Shape`] representing the input shape with any
152 /// possibility of `None` removed, but otherwise unchanged. This models the
153 /// behavior of a hypothetical `!` non-`None` assertion operator. When a
154 /// [`ShapeCase::Name`] shape reference has a `!` step in its subpath, that
155 /// `!` step can be applied to the shape when/if the named shape is later
156 /// declared/resolved, so the effect of the `!` is not lost.
157 #[must_use]
158 pub fn not_none(&self, locations: impl IntoIterator<Item = Location>) -> Shape {
159 self.child(Located::new(NamedShapePathKey::NotNone, locations))
160 }
161
162 #[allow(clippy::too_many_lines)]
163 #[must_use]
164 /// Get a child shape, and add the provided `locations` to it.
165 pub fn child(&self, key: Located<NamedShapePathKey>) -> Shape {
166 match self.case() {
167 ShapeCase::Object { fields, rest, .. } => match &key.value {
168 NamedShapePathKey::Field(field_name) => {
169 if let Some(shape) = fields.get(field_name) {
170 shape.with_locations(key.locations)
171 } else {
172 // The rest shape might be ShapeCase::None, so the
173 // ShapeCase::One will simplify to just ShapeCase::None.
174 Shape::one(
175 [
176 rest.with_locations(key.locations.clone()),
177 Shape::none().with_locations(key.locations.clone()),
178 ],
179 key.locations.clone(),
180 )
181 }
182 }
183
184 NamedShapePathKey::AnyField => {
185 let mut subshapes = IndexSet::new();
186 for shape in fields.values() {
187 subshapes.insert(shape.clone());
188 }
189 if !rest.is_none() {
190 subshapes.insert(rest.clone());
191 }
192 Shape::one(subshapes, key.locations)
193 }
194
195 // Object shapes have no specific indexes.
196 NamedShapePathKey::Index(_) => Shape::none().with_locations(key.locations),
197
198 // The .* AnyIndex wildcard is defined to return the input shape if it
199 // is not an array that can have indexes.
200 NamedShapePathKey::AnyIndex
201 // Object shapes are neither null nor None, so ? and ! have no
202 // effect.
203 | NamedShapePathKey::Question
204 | NamedShapePathKey::NotNone => self.clone(),
205 },
206
207 ShapeCase::Array { prefix, tail } => match &key.value {
208 NamedShapePathKey::Index(index) => {
209 if let Some(shape) = prefix.get(*index) {
210 shape.with_locations(key.locations)
211 } else {
212 // The rest shape might be ShapeCase::None, so the
213 // ShapeCase::One will simplify to just ShapeCase::None.
214 Shape::one(
215 [
216 tail.clone(),
217 Shape::none().with_locations(key.locations.clone()),
218 ],
219 key.locations.clone(),
220 )
221 }
222 }
223
224 NamedShapePathKey::AnyIndex => {
225 let mut subshapes = IndexSet::new();
226 for shape in prefix {
227 subshapes.insert(shape.clone());
228 }
229 if !tail.is_none() {
230 subshapes.insert(tail.clone());
231 }
232 Shape::one(subshapes, key.locations)
233 }
234
235 NamedShapePathKey::AnyField | NamedShapePathKey::Field(_) => {
236 // Following GraphQL logic, map key over the array and make
237 // a new ShapeCase::Array with the resulting shapes.
238 let new_items = prefix
239 .iter()
240 .map(|shape| shape.child(key.clone()))
241 .collect::<Vec<_>>();
242
243 let new_rest = tail.child(key.clone());
244
245 // If we tried mapping a field name over an array, and all
246 // we got back was an empty array, then we can simplify to
247 // ShapeCase::None.
248 if new_rest.is_none() && new_items.iter().all(Shape::is_none) {
249 Shape::none().with_locations(self.locations.clone())
250 } else {
251 Shape::array(new_items, new_rest, self.locations.clone())
252 }
253 }
254
255 // Array shapes are neither null nor None, so ? and ! have no effect.
256 NamedShapePathKey::Question | NamedShapePathKey::NotNone => self.clone(),
257 },
258
259 ShapeCase::String(value) => match &key.value {
260 NamedShapePathKey::Index(index) => {
261 if let Some(singleton) = value {
262 if let Some(ch) = singleton.chars().nth(*index) {
263 Shape::string_value(ch.to_string().as_str(), key.locations)
264 } else {
265 Shape::none().with_locations(key.locations)
266 }
267 } else {
268 Shape::one(
269 [
270 Shape::string(self.locations.clone()),
271 Shape::none().with_locations(key.locations.clone()),
272 ],
273 key.locations.clone(),
274 )
275 }
276 }
277
278 NamedShapePathKey::AnyIndex => {
279 if let Some(singleton) = value {
280 let mut subshapes = IndexSet::new();
281 for ch in singleton.chars() {
282 subshapes.insert(Shape::string_value(ch.to_string().as_str(), []));
283 }
284 Shape::one(subshapes, key.locations)
285 } else {
286 Shape::string(key.locations)
287 }
288 }
289
290 // String shapes have no named fields.
291 NamedShapePathKey::Field(_) => Shape::none(),
292
293 // The .** wildcard is defined to return the input shape if it
294 // is not an object that can have fields.
295 NamedShapePathKey::AnyField
296 // String shapes are neither null nor None, so ? and ! have no
297 // effect.
298 | NamedShapePathKey::Question
299 | NamedShapePathKey::NotNone => self.clone(),
300 },
301
302 ShapeCase::One(shapes) => {
303 let mut subshapes = IndexSet::new();
304 for shape in shapes {
305 subshapes.insert(shape.child(key.clone()));
306 }
307 // Note that if/when subshapes.is_empty(), Shape::one will
308 // return an empty One<> union, which is analogous to
309 // TypeScript's `never` type, so we pretty-print One<> as
310 // "Never" for clarity.
311 Shape::one(subshapes, self.locations.clone())
312 }
313
314 ShapeCase::All(shapes) => {
315 let mut subshapes = IndexSet::new();
316 for shape in shapes {
317 // When shape.is_none() and key is ::NotNone,
318 // shape.child(key.clone()) will return the One<>/Never
319 // shape, which will be removed from the intersection during
320 // simplification, as desired, per the rules of intersection
321 // simplification.
322 let subshape = shape.child(key.clone());
323 if matches!(subshape.case.as_ref(), ShapeCase::None) {
324 continue;
325 }
326 subshapes.insert(subshape);
327 }
328 if subshapes.is_empty() {
329 Shape::none().with_locations(key.locations)
330 } else {
331 Shape::all(subshapes, key.locations)
332 }
333 }
334
335 // For ShapeCase::Name, the subproperties accumulate in the subpath
336 // vector, to be evaluated once the named shape has been declared.
337 // For all other ShapeCase variants, the child shape can be
338 // evaluated immediately.
339 ShapeCase::Name(name, subpath) => match &key.value {
340 NamedShapePathKey::Field(_) | NamedShapePathKey::Index(_) => {
341 let mut new_subpath = subpath.clone();
342 new_subpath.push(key.clone());
343 Shape::new(
344 ShapeCase::Name(name.clone(), new_subpath),
345 self.locations.clone(),
346 )
347 }
348 NamedShapePathKey::AnyIndex => {
349 // Disallow multiple consecutive .*.*... wildcards.
350 if subpath
351 .last()
352 .is_some_and(|last| last.value == NamedShapePathKey::AnyIndex)
353 {
354 Shape::new(
355 ShapeCase::Name(name.clone(), subpath.clone()),
356 key.locations,
357 )
358 } else {
359 let mut new_subpath = subpath.clone();
360 new_subpath.push(key.clone());
361 Shape::new(ShapeCase::Name(name.clone(), new_subpath), key.locations)
362 }
363 }
364 NamedShapePathKey::AnyField => {
365 // Unlike with .*, multiple consecutive .** wildcards do not
366 // collapse to one.
367 let mut new_subpath = subpath.clone();
368 new_subpath.push(key.clone());
369 Shape::new(ShapeCase::Name(name.clone(), new_subpath), key.locations)
370 }
371 NamedShapePathKey::Question => {
372 // Disallow multiple consecutive field??? operators, as
373 // their effect should be idempotent, and ?? is ambiguous
374 // with the nullish coalescing operator.
375 if subpath
376 .last()
377 .is_some_and(|last| last.value == NamedShapePathKey::Question)
378 {
379 Shape::new(
380 ShapeCase::Name(name.clone(), subpath.clone()),
381 key.locations,
382 )
383 } else {
384 let mut new_subpath = subpath.clone();
385 new_subpath.push(key.clone());
386 Shape::new(ShapeCase::Name(name.clone(), new_subpath), key.locations)
387 }
388 }
389 NamedShapePathKey::NotNone => {
390 // Disallow multiple consecutive field!!! operators, as
391 // their effect should be idempotent.
392 if subpath
393 .last()
394 .is_some_and(|last| last.value == NamedShapePathKey::NotNone)
395 {
396 Shape::new(
397 ShapeCase::Name(name.clone(), subpath.clone()),
398 key.locations,
399 )
400 } else {
401 let mut new_subpath = subpath.clone();
402 new_subpath.push(key.clone());
403 Shape::new(ShapeCase::Name(name.clone(), new_subpath), key.locations)
404 }
405 }
406 },
407
408 // ShapeCase::Error allows access to subproperty shapes of its
409 // partial field.
410 ShapeCase::Error(Error { partial, .. }) => partial
411 .as_ref()
412 .map_or(Shape::none(), |shape| shape.child(key.clone())),
413
414 ShapeCase::Null => match &key.value {
415 // Null shapes have no specific named fields or numeric indexes.
416 NamedShapePathKey::Field(_) | NamedShapePathKey::Index(_) => Shape::none(),
417 // The .* and .** wildcards are defined to return the input
418 // shape if it is not an array or object that can have
419 // indexes or fields.
420 NamedShapePathKey::AnyIndex
421 | NamedShapePathKey::AnyField
422 // Null shapes are not None, so ! has no effect.
423 | NamedShapePathKey::NotNone => self.clone(),
424 // Null shapes are null, so ? replaces them with None.
425 NamedShapePathKey::Question => Shape::none().with_locations(key.locations),
426 },
427
428 // ShapeCase::None has no subproperties, as it represents the
429 // absence of a value, or (to put it another way) the shape of any
430 // subproperty of ShapeCase::None is also ShapeCase::None.
431 ShapeCase::None => match &key.value {
432 NamedShapePathKey::NotNone => Shape::never(key.locations),
433 // Any other NamedShapePathKey applied to ShapeCase::None just
434 // returns ShapeCase::None.
435 _ => self.clone(),
436 },
437
438 // ShapeCase::Unknown has no known subproperties, which means any
439 // child access also results in ShapeCase::Unknown.
440 ShapeCase::Unknown => Shape::unknown(key.locations),
441
442 // Non-String primitive shapes like ::Bool and ::Int and ::Float do
443 // not have subproperties, but a wildcard .* subproperty produces
444 // the same shape (as if it was the only element of an array).
445 _ => match &key.value {
446 NamedShapePathKey::AnyIndex
447 | NamedShapePathKey::AnyField
448 // Primitive shapes are neither null nor None, so ? and ! have
449 // no effect.
450 | NamedShapePathKey::Question
451 | NamedShapePathKey::NotNone => self.clone(),
452 _ => Shape::none().with_locations(key.locations),
453 },
454 }
455 }
456}