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}