json_schema_diff/
types.rs

1use schemars::schema::InstanceType;
2use serde::Serialize;
3use thiserror::Error;
4
5/// An "atomic" change made to the JSON schema in question, going from LHS to RHS.
6///
7/// Just a wrapper container for `ChangeKind`
8#[derive(Debug, PartialEq, Serialize)]
9pub struct Change {
10    /// JSON path for the given change. `""` for "root schema". `".foo"` for property foo.
11    pub path: String,
12    /// Data specific to the kind of change.
13    pub change: ChangeKind,
14}
15
16/// The kind of change + data relevant to the change.
17#[derive(Debug, PartialEq, Serialize)]
18pub enum ChangeKind {
19    /// A type has been added and is now additionally allowed.
20    TypeAdd {
21        /// The type in question.
22        added: JsonSchemaType,
23    },
24    /// A type has been removed and is no longer allowed.
25    TypeRemove {
26        /// The type in question.
27        removed: JsonSchemaType,
28    },
29    /// A const value has been added as an allowed value.
30    ConstAdd {
31        /// The value of the added const.
32        added: serde_json::Value,
33    },
34    /// A const has been removed as an allowed value.
35    ConstRemove {
36        /// The value of the removed const.
37        removed: serde_json::Value,
38    },
39    /// A property has been added and (depending on additionalProperties) is now additionally
40    /// allowed.
41    PropertyAdd {
42        /// The value of additionalProperties within the current JSON object.
43        lhs_additional_properties: bool,
44        /// The name of the added property.
45        added: String,
46    },
47    /// A property has been removed and (depending on additionalProperties) might now no longer be
48    /// allowed.
49    PropertyRemove {
50        /// The value of additionalProperties within the current JSON object.
51        lhs_additional_properties: bool,
52        /// The name of the added property.
53        removed: String,
54    },
55    /// A minimum/maximum constraint has been added.
56    RangeAdd {
57        /// The value of the added constraint.
58        added: Range,
59    },
60    /// A minimum/maximum constraint has been removed.
61    RangeRemove {
62        /// The value of the removed constraint.
63        removed: Range,
64    },
65    /// A minimum/maximum constraint has been updated.
66    RangeChange {
67        /// The old constraint value.
68        old_value: Range,
69        /// The new constraint value.
70        new_value: Range,
71    },
72    /// An array-type item has been changed from tuple validation to array validation.
73    ///
74    /// See https://json-schema.org/understanding-json-schema/reference/array.html
75    ///
76    /// Changes will still be emitted for inner items.
77    TupleToArray {
78        /// The length of the (old) tuple
79        old_length: usize,
80    },
81    /// An array-type item has been changed from array validation to tuple validation.
82    ///
83    /// See https://json-schema.org/understanding-json-schema/reference/array.html
84    ///
85    /// Changes will still be emitted for inner items.
86    ArrayToTuple {
87        /// The length of the (new) tuple
88        new_length: usize,
89    },
90    /// An array-type item with tuple validation has changed its length ("items" array got longer
91    /// or shorter.
92    ///
93    /// See https://json-schema.org/understanding-json-schema/reference/array.html
94    ///
95    /// Changes will still be emitted for inner items.
96    TupleChange {
97        /// The new length of the tuple
98        new_length: usize,
99    },
100    /// A previously required property has been removed
101    RequiredRemove {
102        /// The property that is no longer required
103        property: String,
104    },
105    /// A previously optional property has been made required
106    RequiredAdd {
107        /// The property that is now required
108        property: String,
109    },
110}
111
112impl ChangeKind {
113    /// Whether the change is breaking.
114    ///
115    /// What is considered breaking is WIP. Changes are intentionally exposed as-is in public API
116    /// so that the user can develop their own logic as to what they consider breaking.
117    ///
118    /// Currently the rule of thumb is, a change is breaking if it would cause messages that used
119    /// to validate fine under RHS to no longer validate under LHS.
120    pub fn is_breaking(&self) -> bool {
121        match self {
122            Self::TypeAdd { .. } => false,
123            Self::TypeRemove { .. } => true,
124            Self::ConstAdd { .. } => true,
125            Self::ConstRemove { .. } => false,
126            Self::PropertyAdd {
127                lhs_additional_properties,
128                ..
129            } => *lhs_additional_properties,
130            Self::PropertyRemove {
131                lhs_additional_properties,
132                ..
133            } => !*lhs_additional_properties,
134            Self::RangeAdd { .. } => true,
135            Self::RangeRemove { .. } => false,
136            Self::RangeChange {
137                old_value,
138                new_value,
139            } => match (old_value, new_value) {
140                (Range::ExclusiveMinimum(exc), Range::Minimum(min)) if exc >= min => false,
141                (Range::ExclusiveMaximum(exc), Range::Maximum(max)) if exc <= max => false,
142                (Range::Minimum(l), Range::Minimum(r)) if l >= r => false,
143                (Range::ExclusiveMinimum(l), Range::ExclusiveMinimum(r)) if l >= r => false,
144                (Range::Maximum(l), Range::Maximum(r)) if l <= r => false,
145                (Range::ExclusiveMaximum(l), Range::ExclusiveMaximum(r)) if l <= r => false,
146                _ => true,
147            },
148            Self::TupleToArray { .. } => false,
149            Self::ArrayToTuple { .. } => true,
150            Self::TupleChange { .. } => true,
151            Self::RequiredRemove { .. } => false,
152            Self::RequiredAdd { .. } => true,
153        }
154    }
155}
156
157/// The errors that can happen in this crate.
158#[derive(Error, Debug)]
159pub enum Error {
160    /// Failed to parse the JSON schema.
161    ///
162    /// Any deserialization errors from serde that happen while converting the value into our AST
163    /// end up here.
164    #[error("failed to parse schema")]
165    Serde(#[from] serde_json::Error),
166}
167
168/// All primitive types defined in JSON schema.
169#[derive(Serialize, Clone, Ord, Eq, PartialEq, PartialOrd, Debug)]
170#[allow(missing_docs)]
171pub enum JsonSchemaType {
172    #[serde(rename = "string")]
173    String,
174    #[serde(rename = "number")]
175    Number,
176    #[serde(rename = "integer")]
177    Integer,
178    #[serde(rename = "object")]
179    Object,
180    #[serde(rename = "array")]
181    Array,
182    #[serde(rename = "boolean")]
183    Boolean,
184    #[serde(rename = "null")]
185    Null,
186}
187
188impl From<JsonSchemaType> for InstanceType {
189    fn from(t: JsonSchemaType) -> Self {
190        match t {
191            JsonSchemaType::String => InstanceType::String,
192            JsonSchemaType::Number => InstanceType::Number,
193            JsonSchemaType::Integer => InstanceType::Integer,
194            JsonSchemaType::Object => InstanceType::Object,
195            JsonSchemaType::Array => InstanceType::Array,
196            JsonSchemaType::Boolean => InstanceType::Boolean,
197            JsonSchemaType::Null => InstanceType::Null,
198        }
199    }
200}
201
202impl From<InstanceType> for JsonSchemaType {
203    fn from(t: InstanceType) -> Self {
204        match t {
205            InstanceType::String => JsonSchemaType::String,
206            InstanceType::Number => JsonSchemaType::Number,
207            InstanceType::Integer => JsonSchemaType::Integer,
208            InstanceType::Object => JsonSchemaType::Object,
209            InstanceType::Array => JsonSchemaType::Array,
210            InstanceType::Boolean => JsonSchemaType::Boolean,
211            InstanceType::Null => JsonSchemaType::Null,
212        }
213    }
214}
215
216/// Range constraints in JSON schema.
217#[derive(Serialize, Clone, PartialEq, PartialOrd, Debug)]
218#[serde(rename_all = "camelCase")]
219#[allow(missing_docs)]
220pub enum Range {
221    Minimum(f64),
222    Maximum(f64),
223    ExclusiveMinimum(f64),
224    ExclusiveMaximum(f64),
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    #[test]
231    fn is_range_change_breaking() {
232        assert!(!ChangeKind::RangeChange {
233            old_value: Range::Minimum(1.0),
234            new_value: Range::Minimum(1.0),
235        }
236        .is_breaking());
237
238        assert!(ChangeKind::RangeChange {
239            old_value: Range::Minimum(1.0),
240            new_value: Range::Minimum(2.0),
241        }
242        .is_breaking());
243
244        assert!(!ChangeKind::RangeChange {
245            old_value: Range::Minimum(2.0),
246            new_value: Range::Minimum(1.0),
247        }
248        .is_breaking());
249
250        assert!(ChangeKind::RangeChange {
251            old_value: Range::Minimum(1.0),
252            new_value: Range::ExclusiveMinimum(1.0),
253        }
254        .is_breaking());
255
256        assert!(ChangeKind::RangeChange {
257            old_value: Range::Minimum(1.0),
258            new_value: Range::ExclusiveMinimum(2.0),
259        }
260        .is_breaking());
261
262        assert!(ChangeKind::RangeChange {
263            old_value: Range::Minimum(2.0),
264            new_value: Range::ExclusiveMinimum(1.0),
265        }
266        .is_breaking());
267
268        assert!(!ChangeKind::RangeChange {
269            old_value: Range::ExclusiveMinimum(1.0),
270            new_value: Range::ExclusiveMinimum(1.0),
271        }
272        .is_breaking());
273
274        assert!(ChangeKind::RangeChange {
275            old_value: Range::ExclusiveMinimum(1.0),
276            new_value: Range::ExclusiveMinimum(2.0),
277        }
278        .is_breaking());
279
280        assert!(!ChangeKind::RangeChange {
281            old_value: Range::ExclusiveMinimum(2.0),
282            new_value: Range::ExclusiveMinimum(1.0),
283        }
284        .is_breaking());
285
286        assert!(!ChangeKind::RangeChange {
287            old_value: Range::Maximum(1.0),
288            new_value: Range::Maximum(1.0),
289        }
290        .is_breaking());
291
292        assert!(!ChangeKind::RangeChange {
293            old_value: Range::Maximum(1.0),
294            new_value: Range::Maximum(2.0),
295        }
296        .is_breaking());
297
298        assert!(ChangeKind::RangeChange {
299            old_value: Range::Maximum(2.0),
300            new_value: Range::Maximum(1.0),
301        }
302        .is_breaking());
303
304        assert!(ChangeKind::RangeChange {
305            old_value: Range::Maximum(1.0),
306            new_value: Range::ExclusiveMaximum(1.0),
307        }
308        .is_breaking());
309
310        assert!(ChangeKind::RangeChange {
311            old_value: Range::Maximum(1.0),
312            new_value: Range::ExclusiveMaximum(2.0),
313        }
314        .is_breaking());
315
316        assert!(ChangeKind::RangeChange {
317            old_value: Range::Maximum(2.0),
318            new_value: Range::ExclusiveMaximum(1.0),
319        }
320        .is_breaking());
321
322        assert!(!ChangeKind::RangeChange {
323            old_value: Range::ExclusiveMaximum(1.0),
324            new_value: Range::ExclusiveMaximum(1.0),
325        }
326        .is_breaking());
327
328        assert!(!ChangeKind::RangeChange {
329            old_value: Range::ExclusiveMaximum(1.0),
330            new_value: Range::ExclusiveMaximum(2.0),
331        }
332        .is_breaking());
333
334        assert!(ChangeKind::RangeChange {
335            old_value: Range::ExclusiveMaximum(2.0),
336            new_value: Range::ExclusiveMaximum(1.0),
337        }
338        .is_breaking());
339    }
340}