shape/
case_enum.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
pub(crate) mod all;
mod deduplicate_shape;
pub(crate) mod one;

use std::iter::{empty, once};

use self::all::All;
use self::one::One;
use super::child_shape::NamedShapePathKey;
use super::Shape;
use crate::location::Located;
use indexmap::IndexMap;

/// The [`ShapeCase`] enum attempts to capture all the common shapes of JSON
/// values, not just the JSON types themselves, but also patterns of usage (such
/// as using JSON arrays as either static tuples or dynamic lists).
///
/// A [`ShapeCase`] enum variant like [`ShapeCase::One`] may temporarily
/// represent a structure that has not been fully simplified, but simplication
/// is required to turn the [`ShapeCase`] into a `Shape`, using the
/// `ShapeCase::simplify(&self) -> Shape` method.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ShapeCase {
    /// The `Bool`, `String`, and `Int` variants can either represent a general
    /// shape using `None` for the `Option`, or a specific shape using `Some`.
    /// For example, `ShapeCase::Bool(None)` is a shape satisfied by either true
    /// or false, like the Rust bool type, while `ShapeCase::Bool(Some(true))`
    /// is a shape satisfied only by `true` (and likewise for `false`).
    Bool(Option<bool>),

    /// Similar to [`ShapeCase::Bool`], [`ShapeCase::String`] represents a shape
    /// matching either any string (in the `None` case) or some specific string
    /// (in the `Some` case).
    String(Option<String>),

    /// Similar to [`ShapeCase::Bool`] and [`ShapeCase::String`],
    /// [`ShapeCase::Int`] can represent either any integer or some specific
    /// integer.
    Int(Option<i64>),

    /// [`ShapeCase::Float`] is a shape that captures the set of all floating
    /// point numbers. We do not allow singleton floating point value shapes,
    /// since floating point equality is unreliable.
    Float,

    /// [`ShapeCase::Null`] is a singleton shape whose only possible value is
    /// `null`. Note that [`ShapeCase::Null`] is different from
    /// [`ShapeCase::None`], which represents the absence of a value.
    Null,

    /// [`ShapeCase::Array`] represents a `prefix` of statically known shapes
    /// (like a tuple type), followed by an optional `tail` shape for all other
    /// (dynamic) elements. When only the `prefix` elements are defined, the
    /// `tail` shape is [`ShapeCase::None`]. `ShapeCase::Array(vec![],
    /// ShapeCase::None)` is the shape of an empty array.
    Array { prefix: Vec<Shape>, tail: Shape },

    /// [`ShapeCase::Object`] is a map of statically known field names to field
    /// shapes, together with an optional type for all other string keys. When
    /// dynamic string indexing is disabled, the rest shape will be
    /// [`ShapeCase::None`]. Note that accessing the dynamic map always returns
    /// `ShapeCase::One([rest_shape, ShapeCase::None])`, to reflect the
    /// uncertainty of the shape of dynamic keys not present in the static
    /// fields.
    Object {
        // TODO: track location of keys
        fields: IndexMap<String, Shape>,
        rest: Shape,
    },

    /// A union of shapes, satisfied by values that satisfy any of the shapes in
    /// the set.
    ///
    /// Create using [`Shape::one`].
    One(One),

    /// An intersection of shapes, satisfied by values that satisfy all of the
    /// shapes in the set. When applied to multiple [`ShapeCase::Object`]
    /// shapes, [`ShapeCase::All`] represents merging the fields of the objects,
    /// as reflected by the simplification logic.
    ///
    /// Crete using [`Shape::all`].
    All(All),

    /// [`ShapeCase::Name`] refers to a shape declared with the given name,
    /// possibly in the future. When shape processing needs to refer to the
    /// shape of some subproperty of a named shape, it uses the
    /// `Vec<NamedShapePathKey>` subpath to represent the nested shape. When the
    /// named shape is eventually declared, the subpath can be used to resolve
    /// the actual shape of the nested property path.
    //
    /// As of now, the name `String` is expected to be either an identifier or a
    /// variable name like `$root` or `$this` or `$args`, allowing
    /// [`ShapeCase::Name`] to represent types of subproperties of variables as
    /// well as named types from some schema.
    Name(Located<String>, Vec<Located<NamedShapePathKey>>),

    /// [`ShapeCase::Unknown`] is a shape that represents any possible JSON
    /// value (including `ShapeCase::None`).
    Unknown,

    /// Represents the absence of a value, or the shape of a property not
    /// present in an object. Used by the rest parameters of both
    /// [`ShapeCase::Array`] and [`ShapeCase::Object`] to indicate no additional
    /// dynamic elements are allowed, and with [`ShapeCase::One`] to represent
    /// optionality of values, e.g. `One<Bool, None>`.
    None,

    /// Represents a local failure of shape processing.
    Error(Error),
}

impl ShapeCase {
    #[must_use]
    pub fn is_none(&self) -> bool {
        matches!(self, Self::None)
    }

    #[must_use]
    pub fn is_null(&self) -> bool {
        matches!(self, Self::Null)
    }

    #[must_use]
    pub fn error(message: String) -> Self {
        Self::Error(Error {
            message,
            partial: None,
        })
    }

    #[must_use]
    pub fn error_with_partial(message: String, partial: Shape) -> Self {
        Self::Error(Error {
            message,
            partial: Some(partial),
        })
    }

    /// Iterate over all errors within this shape, recursively
    pub fn errors(&self) -> Box<dyn Iterator<Item = &Error> + '_> {
        match self {
            Self::Error(error) => Box::new(once(error)),
            Self::Array { prefix, tail } => {
                Box::new(prefix.iter().flat_map(Shape::errors).chain(tail.errors()))
            }
            Self::Object { fields, rest } => {
                Box::new(fields.values().flat_map(Shape::errors).chain(rest.errors()))
            }
            Self::One(shapes) => Box::new(shapes.iter().flat_map(Shape::errors)),
            Self::All(shapes) => Box::new(shapes.iter().flat_map(Shape::errors)),
            _ => Box::new(empty()),
        }
    }
}

impl From<bool> for ShapeCase {
    fn from(value: bool) -> Self {
        ShapeCase::Bool(Some(value))
    }
}

impl From<String> for ShapeCase {
    fn from(value: String) -> Self {
        ShapeCase::String(Some(value))
    }
}

impl From<&str> for ShapeCase {
    fn from(value: &str) -> Self {
        ShapeCase::String(Some(value.to_string()))
    }
}

impl From<i64> for ShapeCase {
    fn from(value: i64) -> Self {
        ShapeCase::Int(Some(value))
    }
}

impl PartialEq<&ShapeCase> for ShapeCase {
    fn eq(&self, other: &&ShapeCase) -> bool {
        self == *other
    }
}

impl PartialEq<ShapeCase> for &ShapeCase {
    fn eq(&self, other: &ShapeCase) -> bool {
        *self == other
    }
}

/// Represents a local failure of shape processing.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Error {
    pub message: String,
    /// This `partial` shape can be another `ShapeCase::Error` shape, which
    /// allows for chaining of multiple errors.
    pub partial: Option<Shape>,
}

impl From<Error> for ShapeCase {
    fn from(error: Error) -> Self {
        ShapeCase::Error(error)
    }
}