Skip to main content

json_schema_rs/json_schema/
spec_version.rs

1//! JSON Schema specification version.
2
3use super::settings::JsonSchemaSettings;
4
5/// JSON Schema specification version. One variant per vendored spec.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum SpecVersion {
8    Draft00,
9    Draft01,
10    Draft02,
11    Draft03,
12    Draft04,
13    Draft05,
14    Draft06,
15    Draft07,
16    Draft201909,
17    Draft202012,
18}
19
20impl SpecVersion {
21    /// Returns the canonical meta-schema URI for this draft (e.g. for use in `$schema`).
22    ///
23    /// URIs match the local specs (draft-00 through 2020-12). Drafts 00–02 used
24    /// hyper-schema URIs; draft-03 onward use schema# or draft/YYYY-MM/schema.
25    #[must_use]
26    pub fn schema_uri(self) -> &'static str {
27        match self {
28            SpecVersion::Draft00 => "http://json-schema.org/draft-00/hyper-schema#",
29            SpecVersion::Draft01 => "http://json-schema.org/draft-01/hyper-schema#",
30            SpecVersion::Draft02 => "http://json-schema.org/draft-02/hyper-schema#",
31            SpecVersion::Draft03 => "http://json-schema.org/draft-03/schema#",
32            SpecVersion::Draft04 => "http://json-schema.org/draft-04/schema#",
33            SpecVersion::Draft05 => "http://json-schema.org/draft-05/schema#",
34            SpecVersion::Draft06 => "http://json-schema.org/draft-06/schema#",
35            SpecVersion::Draft07 => "http://json-schema.org/draft-07/schema#",
36            SpecVersion::Draft201909 => "https://json-schema.org/draft/2019-09/schema",
37            SpecVersion::Draft202012 => "https://json-schema.org/draft/2020-12/schema",
38        }
39    }
40
41    /// Parses a `$schema` URI string and returns the corresponding [`SpecVersion`], if recognized.
42    ///
43    /// Matching is done by comparing the trimmed string to canonical URIs (with or without
44    /// trailing slash). The legacy draft-04 URI `http://json-schema.org/schema#` is
45    /// deprecated and returns [`Some(SpecVersion::Draft04)`] for compatibility.
46    ///
47    /// Returns `None` for empty, unknown, or malformed URIs.
48    #[must_use]
49    pub fn from_schema_uri(s: &str) -> Option<SpecVersion> {
50        let s = s.trim();
51        if s.is_empty() {
52            return None;
53        }
54        // Normalize: strip trailing slash for comparison (canonical URIs have no trailing slash except hyper-schema#)
55        let s_normalized = s.trim_end_matches('/');
56        match s_normalized {
57            "http://json-schema.org/draft-00/hyper-schema#" => Some(SpecVersion::Draft00),
58            "http://json-schema.org/draft-01/hyper-schema#" => Some(SpecVersion::Draft01),
59            "http://json-schema.org/draft-02/hyper-schema#" => Some(SpecVersion::Draft02),
60            "http://json-schema.org/draft-03/schema#" => Some(SpecVersion::Draft03),
61            "http://json-schema.org/draft-04/schema#" | "http://json-schema.org/schema#" => {
62                Some(SpecVersion::Draft04)
63            } // second is legacy deprecated
64            "http://json-schema.org/draft-05/schema#" => Some(SpecVersion::Draft05),
65            "http://json-schema.org/draft-06/schema#" => Some(SpecVersion::Draft06),
66            "http://json-schema.org/draft-07/schema#" => Some(SpecVersion::Draft07),
67            "https://json-schema.org/draft/2019-09/schema" => Some(SpecVersion::Draft201909),
68            "https://json-schema.org/draft/2020-12/schema" => Some(SpecVersion::Draft202012),
69            _ => None,
70        }
71    }
72
73    /// Returns [`JsonSchemaSettings`] tuned for this spec version.
74    /// Callers can use the builder to override individual options.
75    ///
76    /// **Default (latest) spec:** [`Draft202012`](SpecVersion::Draft202012) is
77    /// the latest supported spec; its settings match
78    /// `JsonSchemaSettings::default()` when no options are set.
79    #[must_use]
80    pub fn default_schema_settings(self) -> JsonSchemaSettings {
81        JsonSchemaSettings {
82            disallow_unknown_fields: false,
83            spec_version: None,
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use crate::json_schema::SpecVersion;
91    use crate::json_schema::settings::JsonSchemaSettings;
92
93    #[test]
94    fn spec_version_returns_settings() {
95        let settings: JsonSchemaSettings = SpecVersion::Draft07.default_schema_settings();
96        assert!(!settings.disallow_unknown_fields);
97    }
98
99    /// Default builder output matches Draft 2020-12 (latest spec) settings.
100    #[test]
101    fn default_settings_match_draft202012() {
102        let from_builder: JsonSchemaSettings = JsonSchemaSettings::default();
103        let from_spec: JsonSchemaSettings = SpecVersion::Draft202012.default_schema_settings();
104        assert_eq!(from_builder, from_spec);
105    }
106
107    // --- schema_uri() exhaustive: one expected URI per variant ---
108
109    #[test]
110    fn schema_uri_draft00() {
111        let expected: &str = "http://json-schema.org/draft-00/hyper-schema#";
112        let actual: &str = SpecVersion::Draft00.schema_uri();
113        assert_eq!(expected, actual);
114    }
115
116    #[test]
117    fn schema_uri_draft01() {
118        let expected: &str = "http://json-schema.org/draft-01/hyper-schema#";
119        let actual: &str = SpecVersion::Draft01.schema_uri();
120        assert_eq!(expected, actual);
121    }
122
123    #[test]
124    fn schema_uri_draft02() {
125        let expected: &str = "http://json-schema.org/draft-02/hyper-schema#";
126        let actual: &str = SpecVersion::Draft02.schema_uri();
127        assert_eq!(expected, actual);
128    }
129
130    #[test]
131    fn schema_uri_draft03() {
132        let expected: &str = "http://json-schema.org/draft-03/schema#";
133        let actual: &str = SpecVersion::Draft03.schema_uri();
134        assert_eq!(expected, actual);
135    }
136
137    #[test]
138    fn schema_uri_draft04() {
139        let expected: &str = "http://json-schema.org/draft-04/schema#";
140        let actual: &str = SpecVersion::Draft04.schema_uri();
141        assert_eq!(expected, actual);
142    }
143
144    #[test]
145    fn schema_uri_draft05() {
146        let expected: &str = "http://json-schema.org/draft-05/schema#";
147        let actual: &str = SpecVersion::Draft05.schema_uri();
148        assert_eq!(expected, actual);
149    }
150
151    #[test]
152    fn schema_uri_draft06() {
153        let expected: &str = "http://json-schema.org/draft-06/schema#";
154        let actual: &str = SpecVersion::Draft06.schema_uri();
155        assert_eq!(expected, actual);
156    }
157
158    #[test]
159    fn schema_uri_draft07() {
160        let expected: &str = "http://json-schema.org/draft-07/schema#";
161        let actual: &str = SpecVersion::Draft07.schema_uri();
162        assert_eq!(expected, actual);
163    }
164
165    #[test]
166    fn schema_uri_draft201909() {
167        let expected: &str = "https://json-schema.org/draft/2019-09/schema";
168        let actual: &str = SpecVersion::Draft201909.schema_uri();
169        assert_eq!(expected, actual);
170    }
171
172    #[test]
173    fn schema_uri_draft202012() {
174        let expected: &str = "https://json-schema.org/draft/2020-12/schema";
175        let actual: &str = SpecVersion::Draft202012.schema_uri();
176        assert_eq!(expected, actual);
177    }
178
179    // --- from_schema_uri() round-trip: every variant ---
180
181    #[test]
182    fn from_schema_uri_round_trip_draft00() {
183        let v: SpecVersion = SpecVersion::Draft00;
184        let expected: Option<SpecVersion> = Some(v);
185        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri(v.schema_uri());
186        assert_eq!(expected, actual);
187    }
188
189    #[test]
190    fn from_schema_uri_round_trip_draft01() {
191        let v: SpecVersion = SpecVersion::Draft01;
192        let expected: Option<SpecVersion> = Some(v);
193        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri(v.schema_uri());
194        assert_eq!(expected, actual);
195    }
196
197    #[test]
198    fn from_schema_uri_round_trip_draft02() {
199        let v: SpecVersion = SpecVersion::Draft02;
200        let expected: Option<SpecVersion> = Some(v);
201        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri(v.schema_uri());
202        assert_eq!(expected, actual);
203    }
204
205    #[test]
206    fn from_schema_uri_round_trip_draft03() {
207        let v: SpecVersion = SpecVersion::Draft03;
208        let expected: Option<SpecVersion> = Some(v);
209        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri(v.schema_uri());
210        assert_eq!(expected, actual);
211    }
212
213    #[test]
214    fn from_schema_uri_round_trip_draft04() {
215        let v: SpecVersion = SpecVersion::Draft04;
216        let expected: Option<SpecVersion> = Some(v);
217        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri(v.schema_uri());
218        assert_eq!(expected, actual);
219    }
220
221    #[test]
222    fn from_schema_uri_round_trip_draft05() {
223        let v: SpecVersion = SpecVersion::Draft05;
224        let expected: Option<SpecVersion> = Some(v);
225        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri(v.schema_uri());
226        assert_eq!(expected, actual);
227    }
228
229    #[test]
230    fn from_schema_uri_round_trip_draft06() {
231        let v: SpecVersion = SpecVersion::Draft06;
232        let expected: Option<SpecVersion> = Some(v);
233        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri(v.schema_uri());
234        assert_eq!(expected, actual);
235    }
236
237    #[test]
238    fn from_schema_uri_round_trip_draft07() {
239        let v: SpecVersion = SpecVersion::Draft07;
240        let expected: Option<SpecVersion> = Some(v);
241        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri(v.schema_uri());
242        assert_eq!(expected, actual);
243    }
244
245    #[test]
246    fn from_schema_uri_round_trip_draft201909() {
247        let v: SpecVersion = SpecVersion::Draft201909;
248        let expected: Option<SpecVersion> = Some(v);
249        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri(v.schema_uri());
250        assert_eq!(expected, actual);
251    }
252
253    #[test]
254    fn from_schema_uri_round_trip_draft202012() {
255        let v: SpecVersion = SpecVersion::Draft202012;
256        let expected: Option<SpecVersion> = Some(v);
257        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri(v.schema_uri());
258        assert_eq!(expected, actual);
259    }
260
261    // --- from_schema_uri() from string: canonical URIs ---
262
263    #[test]
264    fn from_schema_uri_canonical_2020_12() {
265        let expected: Option<SpecVersion> = Some(SpecVersion::Draft202012);
266        let actual: Option<SpecVersion> =
267            SpecVersion::from_schema_uri("https://json-schema.org/draft/2020-12/schema");
268        assert_eq!(expected, actual);
269    }
270
271    #[test]
272    fn from_schema_uri_canonical_2019_09() {
273        let expected: Option<SpecVersion> = Some(SpecVersion::Draft201909);
274        let actual: Option<SpecVersion> =
275            SpecVersion::from_schema_uri("https://json-schema.org/draft/2019-09/schema");
276        assert_eq!(expected, actual);
277    }
278
279    #[test]
280    fn from_schema_uri_canonical_draft07() {
281        let expected: Option<SpecVersion> = Some(SpecVersion::Draft07);
282        let actual: Option<SpecVersion> =
283            SpecVersion::from_schema_uri("http://json-schema.org/draft-07/schema#");
284        assert_eq!(expected, actual);
285    }
286
287    #[test]
288    fn from_schema_uri_canonical_draft04() {
289        let expected: Option<SpecVersion> = Some(SpecVersion::Draft04);
290        let actual: Option<SpecVersion> =
291            SpecVersion::from_schema_uri("http://json-schema.org/draft-04/schema#");
292        assert_eq!(expected, actual);
293    }
294
295    #[test]
296    fn from_schema_uri_legacy_draft04_schema_hash() {
297        let expected: Option<SpecVersion> = Some(SpecVersion::Draft04);
298        let actual: Option<SpecVersion> =
299            SpecVersion::from_schema_uri("http://json-schema.org/schema#");
300        assert_eq!(expected, actual);
301    }
302
303    // --- Unknown / invalid ---
304
305    #[test]
306    fn from_schema_uri_empty_returns_none() {
307        let expected: Option<SpecVersion> = None;
308        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri("");
309        assert_eq!(expected, actual);
310    }
311
312    #[test]
313    fn from_schema_uri_whitespace_only_returns_none() {
314        let expected: Option<SpecVersion> = None;
315        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri("   ");
316        assert_eq!(expected, actual);
317    }
318
319    #[test]
320    fn from_schema_uri_unknown_returns_none() {
321        let expected: Option<SpecVersion> = None;
322        let actual: Option<SpecVersion> =
323            SpecVersion::from_schema_uri("https://unknown.example.com/schema");
324        assert_eq!(expected, actual);
325    }
326
327    #[test]
328    fn from_schema_uri_malformed_returns_none() {
329        let expected: Option<SpecVersion> = None;
330        let actual: Option<SpecVersion> = SpecVersion::from_schema_uri("not-a-uri");
331        assert_eq!(expected, actual);
332    }
333
334    #[test]
335    fn from_schema_uri_trailing_slash_normalized() {
336        // We trim trailing slash; draft/2020-12/schema has no trailing slash in canonical form,
337        // so with trailing slash it may not match unless we add that in from_schema_uri.
338        // Current implementation does exact match after trim_end_matches('/') only for the string
339        // that already has no trailing slash. So "https://json-schema.org/draft/2020-12/schema/"
340        // becomes "https://json-schema.org/draft/2020-12/schema" and matches.
341        let expected: Option<SpecVersion> = Some(SpecVersion::Draft202012);
342        let actual: Option<SpecVersion> =
343            SpecVersion::from_schema_uri("https://json-schema.org/draft/2020-12/schema/");
344        assert_eq!(expected, actual);
345    }
346}