openapiv3/
schema.rs

1use crate::*;
2use indexmap::IndexMap;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
7#[serde(rename_all = "camelCase")]
8pub struct SchemaData {
9    #[serde(default, skip_serializing_if = "is_false")]
10    pub nullable: bool,
11    #[serde(default, skip_serializing_if = "is_false")]
12    pub read_only: bool,
13    #[serde(default, skip_serializing_if = "is_false")]
14    pub write_only: bool,
15    #[serde(default, skip_serializing_if = "is_false")]
16    pub deprecated: bool,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub external_docs: Option<ExternalDocumentation>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub example: Option<serde_json::Value>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub title: Option<String>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub description: Option<String>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub discriminator: Option<Discriminator>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub default: Option<serde_json::Value>,
29    /// All extensions must be prefixed with `x-`, see
30    /// section Specification Extensions on https://swagger.io/specification/
31    /// for more information. So you could add a custom field `name` like:
32    /// `x-name: value` rather than `name: value`
33    /// In code, the `x-` prefix remains as part of the key: `extensions.get("x-name")`
34    #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")]
35    pub extensions: IndexMap<String, serde_json::Value>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct Schema {
40    #[serde(flatten)]
41    pub data: SchemaData,
42    #[serde(flatten)]
43    pub kind: SchemaKind,
44}
45
46impl std::ops::Deref for Schema {
47    type Target = SchemaData;
48
49    fn deref(&self) -> &Self::Target {
50        &self.data
51    }
52}
53
54impl std::ops::DerefMut for Schema {
55    fn deref_mut(&mut self) -> &mut Self::Target {
56        &mut self.data
57    }
58}
59
60#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)]
61#[serde(untagged)]
62pub enum SchemaKind {
63    Type(Type),
64    OneOf {
65        #[serde(rename = "oneOf")]
66        one_of: Vec<RefOr<Schema>>,
67    },
68    AllOf {
69        #[serde(rename = "allOf")]
70        all_of: Vec<RefOr<Schema>>,
71    },
72    AnyOf {
73        #[serde(rename = "anyOf")]
74        any_of: Vec<RefOr<Schema>>,
75    },
76    Not {
77        not: Box<RefOr<Schema>>,
78    },
79    Any(AnySchema),
80}
81
82
83impl Schema {
84    fn new_kind(kind: SchemaKind) -> Self {
85        Self { data: SchemaData::default(), kind }
86    }
87
88    pub fn new_number() -> Self {
89        Self::new_kind(SchemaKind::Type(Type::Number(NumberType::default())))
90    }
91
92    pub fn new_integer() -> Self {
93        Self::new_kind(SchemaKind::Type(Type::Integer(IntegerType::default())))
94    }
95
96    pub fn new_bool() -> Self {
97        Self::new_kind(SchemaKind::Type(Type::Boolean {}))
98    }
99
100    pub fn new_str_enum(enumeration: Vec<String>) -> Self {
101        Self::new_kind(SchemaKind::Type(Type::String(StringType {
102            enumeration,
103            ..StringType::default()
104        })))
105    }
106
107    pub fn new_string() -> Self {
108        Self::new_kind(SchemaKind::Type(Type::String(StringType::default())))
109    }
110
111    /// Create a schemaless object schema
112    pub fn new_object() -> Self {
113        Self::new_kind(SchemaKind::Type(Type::Object(ObjectType::default())))
114    }
115
116    /// Create a Map<String, inner> schema
117    pub fn new_map(inner: impl Into<RefOr<Schema>>) -> Self {
118        let inner = inner.into().boxed();
119        Self::new_kind(SchemaKind::Type(Type::Object(ObjectType {
120            additional_properties: Some(AdditionalProperties::Schema(inner)),
121            ..ObjectType::default()
122        })))
123    }
124
125    /// Create a Map<String, Any> schema
126    pub fn new_map_any() -> Self {
127        Self::new_kind(SchemaKind::Type(Type::Object(ObjectType {
128            additional_properties: Some(AdditionalProperties::Any(true)),
129            ..ObjectType::default()
130        })))
131    }
132
133    /// Create an Array<Any> schema
134    pub fn new_array_any() -> Self {
135        Self::new_kind(SchemaKind::Type(Type::Array(ArrayType::default())))
136    }
137
138    /// Create a new array schema with items of the given type
139    pub fn new_array(inner: impl Into<RefOr<Schema>>) -> Self {
140        let inner = inner.into().boxed();
141        Self::new_kind(SchemaKind::Type(Type::Array(ArrayType {
142            items: Some(inner),
143            ..ArrayType::default()
144        })))
145    }
146
147    pub fn new_one_of(one_of: Vec<RefOr<Schema>>) -> Self {
148        Self::new_kind(SchemaKind::OneOf { one_of })
149    }
150
151    pub fn new_all_of(all_of: Vec<RefOr<Schema>>) -> Self {
152        Self::new_kind(SchemaKind::AllOf { all_of })
153    }
154
155    pub fn new_any_of(any_of: Vec<RefOr<Schema>>) -> Self {
156        Self::new_kind(SchemaKind::AnyOf { any_of })
157    }
158
159    /// Create an Any schema
160    pub fn new_any() -> Self {
161        Self {
162            data: SchemaData::default(),
163            kind: SchemaKind::Any(AnySchema::default()),
164        }
165    }
166
167    pub fn with_format(mut self, format: &str) -> Self {
168        if let SchemaKind::Type(Type::String(s)) = &mut self.kind {
169            s.format = serde_json::from_value(Value::String(format.to_string())).unwrap();
170        }
171        self
172    }
173
174    pub fn is_empty(&self) -> bool {
175        match &self.kind {
176            SchemaKind::Type(Type::Object(o)) => {
177                o.properties.is_empty() && o.additional_properties.is_none()
178            }
179            _ => false,
180        }
181    }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
185#[serde(tag = "type", rename_all = "lowercase")]
186pub enum Type {
187    String(StringType),
188    Number(NumberType),
189    Integer(IntegerType),
190    Object(ObjectType),
191    Array(ArrayType),
192    Boolean {},
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
196#[serde(untagged)]
197pub enum AdditionalProperties {
198    Any(bool),
199    Schema(Box<RefOr<Schema>>),
200}
201
202/// Catch-all for any combination of properties that doesn't correspond to one
203/// of the predefined subsets.
204#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
205#[serde(rename_all = "camelCase")]
206pub struct AnySchema {
207    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
208    pub typ: Option<String>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub pattern: Option<String>,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub multiple_of: Option<f64>,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub exclusive_minimum: Option<bool>,
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub exclusive_maximum: Option<bool>,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub minimum: Option<f64>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub maximum: Option<f64>,
221    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
222    pub properties: RefOrMap<Schema>,
223    #[serde(default, skip_serializing_if = "Vec::is_empty")]
224    pub required: Vec<String>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub additional_properties: Option<AdditionalProperties>,
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub min_properties: Option<usize>,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub max_properties: Option<usize>,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub items: Option<Box<RefOr<Schema>>>,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub min_items: Option<usize>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub max_items: Option<usize>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub unique_items: Option<bool>,
239    #[serde(rename = "enum", default, skip_serializing_if = "Vec::is_empty")]
240    pub enumeration: Vec<serde_json::Value>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub format: Option<String>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub min_length: Option<usize>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub max_length: Option<usize>,
247    #[serde(default, skip_serializing_if = "Vec::is_empty")]
248    pub one_of: Vec<RefOr<Schema>>,
249    #[serde(default, skip_serializing_if = "Vec::is_empty")]
250    pub all_of: Vec<RefOr<Schema>>,
251    #[serde(default, skip_serializing_if = "Vec::is_empty")]
252    pub any_of: Vec<RefOr<Schema>>,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub not: Option<Box<RefOr<Schema>>>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
258#[serde(rename_all = "camelCase")]
259pub struct StringType {
260    #[serde(default, skip_serializing_if = "VariantOrUnknownOrEmpty::is_empty")]
261    pub format: VariantOrUnknownOrEmpty<StringFormat>,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub pattern: Option<String>,
264    #[serde(rename = "enum", default, skip_serializing_if = "Vec::is_empty")]
265    pub enumeration: Vec<String>,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub min_length: Option<usize>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub max_length: Option<usize>,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
273#[serde(rename_all = "camelCase")]
274pub struct NumberType {
275    #[serde(default, skip_serializing_if = "VariantOrUnknownOrEmpty::is_empty")]
276    pub format: VariantOrUnknownOrEmpty<NumberFormat>,
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub multiple_of: Option<f64>,
279    #[serde(default, skip_serializing_if = "is_false")]
280    pub exclusive_minimum: bool,
281    #[serde(default, skip_serializing_if = "is_false")]
282    pub exclusive_maximum: bool,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub minimum: Option<f64>,
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub maximum: Option<f64>,
287    #[serde(rename = "enum", default, skip_serializing_if = "Vec::is_empty")]
288    pub enumeration: Vec<Option<f64>>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
292#[serde(rename_all = "camelCase")]
293pub struct IntegerType {
294    #[serde(default, skip_serializing_if = "VariantOrUnknownOrEmpty::is_empty")]
295    pub format: VariantOrUnknownOrEmpty<IntegerFormat>,
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub multiple_of: Option<i64>,
298    #[serde(default, skip_serializing_if = "is_false")]
299    pub exclusive_minimum: bool,
300    #[serde(default, skip_serializing_if = "is_false")]
301    pub exclusive_maximum: bool,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub minimum: Option<i64>,
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub maximum: Option<i64>,
306    #[serde(rename = "enum", default, skip_serializing_if = "Vec::is_empty")]
307    pub enumeration: Vec<Option<i64>>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
311#[serde(rename_all = "camelCase")]
312pub struct ObjectType {
313    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
314    pub properties: RefOrMap<Schema>,
315    #[serde(default, skip_serializing_if = "Vec::is_empty")]
316    pub required: Vec<String>,
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub additional_properties: Option<AdditionalProperties>,
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub min_properties: Option<usize>,
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub max_properties: Option<usize>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
326#[serde(rename_all = "camelCase")]
327pub struct ArrayType {
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub items: Option<Box<RefOr<Schema>>>,
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub min_items: Option<usize>,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub max_items: Option<usize>,
334    #[serde(default, skip_serializing_if = "is_false")]
335    pub unique_items: bool,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
339#[serde(rename_all = "lowercase")]
340pub enum NumberFormat {
341    Float,
342    Double,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
346#[serde(rename_all = "lowercase")]
347pub enum IntegerFormat {
348    Int32,
349    Int64,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
353#[serde(rename_all = "lowercase")]
354pub enum StringFormat {
355    Date,
356    #[serde(rename = "date-time")]
357    DateTime,
358    Password,
359    Byte,
360    Binary,
361}
362
363impl VariantOrUnknownOrEmpty<StringFormat> {
364    pub fn as_str(&self) -> &str {
365        match self {
366            VariantOrUnknownOrEmpty::Item(StringFormat::Date) => "date",
367            VariantOrUnknownOrEmpty::Item(StringFormat::DateTime) => "date-time",
368            VariantOrUnknownOrEmpty::Item(StringFormat::Password) => "password",
369            VariantOrUnknownOrEmpty::Item(StringFormat::Byte) => "byte",
370            VariantOrUnknownOrEmpty::Item(StringFormat::Binary) => "binary",
371            VariantOrUnknownOrEmpty::Unknown(s) => s.as_str(),
372            VariantOrUnknownOrEmpty::Empty => "",
373        }
374    }
375}
376
377
378impl Schema {
379    pub fn properties(&self) -> &RefOrMap<Schema> {
380        self.get_properties().expect("Schema is not an object")
381    }
382
383    pub fn get_properties(&self) -> Option<&RefOrMap<Schema>> {
384        match &self.kind {
385            SchemaKind::Type(Type::Object(o)) => Some(&o.properties),
386            SchemaKind::Any(AnySchema { properties, .. }) => Some(properties),
387            _ => None,
388        }
389    }
390
391    pub fn get_properties_mut(&mut self) -> Option<&mut RefOrMap<Schema>> {
392        match &mut self.kind {
393            SchemaKind::Type(Type::Object(ref mut o)) => Some(&mut o.properties),
394            SchemaKind::Any(AnySchema { ref mut properties, .. }) => Some(properties),
395            _ => None,
396        }
397    }
398
399    pub fn properties_mut(&mut self) -> &mut RefOrMap<Schema> {
400        self.get_properties_mut().expect("Schema is not an object")
401    }
402
403    pub fn properties_iter<'a>(&'a self, spec: &'a OpenAPI) -> Box<dyn Iterator<Item=(&'a String, &'a RefOr<Schema>)> + 'a> {
404        match &self.kind {
405            SchemaKind::Type(Type::Object(o)) => Box::new(o.properties.iter()),
406            SchemaKind::Any(AnySchema { properties, .. }) => Box::new(properties.iter()),
407            SchemaKind::AllOf { all_of } => Box::new(all_of
408                .iter()
409                .map(move |schema| schema.resolve(spec).properties_iter(spec))
410                .flatten()),
411            _ => Box::new(std::iter::empty())
412        }
413    }
414
415    pub fn is_required(&self, field: &str) -> bool {
416        match &self.kind {
417            SchemaKind::Type(Type::Object(o)) => o.required.iter().any(|s| s == field),
418            SchemaKind::Any(AnySchema { required, .. }) => required.iter().any(|s| s == field),
419            _ => true,
420        }
421    }
422
423    pub fn get_required(&self) -> Option<&Vec<String>> {
424        match &self.kind {
425            SchemaKind::Type(Type::Object(o)) => Some(&o.required),
426            SchemaKind::Any(AnySchema { required, .. }) => Some(required),
427            _ => None,
428        }
429    }
430
431    pub fn required(&self) -> &Vec<String> {
432        self.get_required().expect("Schema is not an object")
433    }
434
435    pub fn get_required_mut(&mut self) -> Option<&mut Vec<String>> {
436        match &mut self.kind {
437            SchemaKind::Type(Type::Object(ref mut o)) => Some(&mut o.required),
438            SchemaKind::Any(AnySchema { ref mut required, .. }) => Some(required),
439            _ => None,
440        }
441    }
442
443    pub fn required_mut(&mut self) -> &mut Vec<String> {
444        self.get_required_mut().expect("Schema is not an object")
445    }
446
447    pub fn add_required(&mut self, field: &str) {
448        if let Some(req) = self.get_required_mut() {
449            if req.iter().any(|f| f == field) {
450                return;
451            }
452            req.push(field.to_string());
453        }
454    }
455
456    pub fn remove_required(&mut self, field: &str) {
457        if let Some(req) = self.get_required_mut() {
458            req.retain(|f| f != field);
459        }
460    }
461
462    pub fn is_anonymous_object(&self) -> bool {
463        match &self.kind {
464            SchemaKind::Type(Type::Object(o)) => o.properties.is_empty(),
465            _ => false,
466        }
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use assert_matches::assert_matches;
473    use serde_json::json;
474
475    use crate::{AnySchema, Schema, SchemaData, SchemaKind};
476
477    #[test]
478    fn test_schema_with_extensions() {
479        let schema = serde_json::from_str::<Schema>(
480            r#"{
481                "type": "boolean",
482                "x-foo": "bar"
483            }"#,
484        )
485            .unwrap();
486
487        assert_eq!(
488            schema.data.extensions.get("x-foo"),
489            Some(&json!("bar"))
490        );
491    }
492
493    #[test]
494    fn test_any() {
495        let value = json! { {} };
496        serde_json::from_value::<AnySchema>(value).unwrap();
497    }
498
499    #[test]
500    fn test_not() {
501        let value = json! {
502            {
503                "not": {}
504            }
505        };
506
507        let schema = serde_json::from_value::<Schema>(value).unwrap();
508        assert!(matches!(schema.kind, SchemaKind::Not { not: _ }));
509    }
510
511    #[test]
512    fn test_null() {
513        let value = json! {
514            {
515                "nullable": true,
516                "enum": [ null ],
517            }
518        };
519
520        let schema = serde_json::from_value::<Schema>(value).unwrap();
521        assert!(matches!(
522            &schema.data,
523            SchemaData { nullable: true, .. }
524        ));
525        assert!(matches!(
526            &schema.kind,
527            SchemaKind::Any(AnySchema { enumeration, .. }) if enumeration[0] == json!(null)));
528    }
529
530    #[test]
531    fn test_default_to_object() {
532        let s = r##"
533required:
534  - definition
535properties:
536  definition:
537    type: string
538    description: >
539      Serialized definition of the version. This should be an OpenAPI 2.x, 3.x or AsyncAPI 2.x file
540      serialized as a string, in YAML or JSON.
541    example: |
542      {asyncapi: "2.0", "info": { "title: … }}
543  references:
544    type: array
545    description: Import external references used by `definition`. It's usually resources not accessible by Bump servers, like local files or internal URLs.
546    items:
547      $ref: "#/components/schemas/Reference"
548"##.trim();
549        let s = serde_yaml::from_str::<Schema>(s).unwrap();
550        // assert!(matches!(s.schema_kind, SchemaKind::Type(crate::Type::Object(_))), "Schema kind was not expected {:?}", s.schema_kind);
551        assert!(matches!(s.kind, SchemaKind::Any(crate::AnySchema{ ref properties, ..}) if properties.len() == 2), "Schema kind was not expected {:?}", s.kind);
552    }
553
554    #[test]
555    fn test_all_of() {
556        let s = r##"
557allOf:
558  - $ref: "#/components/schemas/DocumentationRequest"
559  - $ref: "#/components/schemas/PreviewRequest"
560        "##.trim();
561        let s = serde_yaml::from_str::<Schema>(s).unwrap();
562        match &s.kind {
563            SchemaKind::AllOf { all_of } => {
564                assert_eq!(all_of.len(), 2);
565                assert!(matches!(all_of[0].as_ref_str(), Some("#/components/schemas/DocumentationRequest")));
566                assert!(matches!(all_of[1].as_ref_str(), Some("#/components/schemas/PreviewRequest")));
567            }
568            _ => panic!("Schema kind was not expected {:?}", s.kind)
569        }
570    }
571
572    #[test]
573    fn test_with_format() {
574        use crate::variant_or::VariantOrUnknownOrEmpty;
575        let s = Schema::new_string().with_format("date-time");
576        let SchemaKind::Type(crate::Type::String(s)) = s.kind else { panic!() };
577        assert_matches!(s.format, VariantOrUnknownOrEmpty::Item(crate::StringFormat::DateTime));
578
579        let s = Schema::new_string().with_format("uuid");
580        let SchemaKind::Type(crate::Type::String(s)) = s.kind else { panic!() };
581        assert_matches!(s.format, VariantOrUnknownOrEmpty::Unknown(s) if s == "uuid");
582    }
583}
584