Skip to main content

openapi_nexus_spec/oas31/spec/
schema.rs

1//! Schema specification for OpenAPI 3.1
2
3use std::{collections::BTreeMap, fmt};
4
5use serde::{Deserialize, Deserializer, Serialize};
6use snafu::Snafu;
7
8use super::{
9    ErrorRef, FromRef, ObjectOrReference, OpenApiV31Spec, Ref, RefType,
10    discriminator::Discriminator, spec_extensions,
11};
12
13/// Schema errors.
14#[derive(Debug, Clone, PartialEq, Snafu)]
15#[snafu(visibility(pub))]
16pub enum ErrorSchema {
17    #[snafu(display("Missing type field"))]
18    NoType,
19
20    #[snafu(display("Unknown type: {}", type_name))]
21    UnknownType { type_name: String },
22
23    #[snafu(display("Required property list specified for a non-object schema"))]
24    RequiredSpecifiedOnNonObject,
25}
26
27/// Single schema type.
28#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
29#[serde(rename_all = "snake_case")]
30pub enum Type {
31    Boolean,
32    Integer,
33    Number,
34    String,
35    Array,
36    Object,
37    Null,
38}
39
40/// Set of schema types.
41#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
42#[serde(untagged)]
43pub enum TypeSet {
44    Single(Type),
45    Multiple(Vec<Type>),
46}
47
48impl TypeSet {
49    /// Returns `true` if this type-set contains the given type.
50    pub fn contains(&self, type_: Type) -> bool {
51        match self {
52            TypeSet::Single(single_type) => *single_type == type_,
53            TypeSet::Multiple(type_set) => type_set.contains(&type_),
54        }
55    }
56
57    /// Returns `true` if this type-set is `object` or `[object, 'null']`.
58    pub fn is_object_or_nullable_object(&self) -> bool {
59        match self {
60            TypeSet::Single(Type::Object) => true,
61            TypeSet::Multiple(set) if set == &[Type::Object] => true,
62            TypeSet::Multiple(set) if set == &[Type::Object, Type::Null] => true,
63            TypeSet::Multiple(set) if set == &[Type::Null, Type::Object] => true,
64            _ => false,
65        }
66    }
67
68    /// Returns `true` if this type-set is `array` or `[array, 'null']`.
69    pub fn is_array_or_nullable_array(&self) -> bool {
70        match self {
71            TypeSet::Single(Type::Array) => true,
72            TypeSet::Multiple(set) if set == &[Type::Array] => true,
73            TypeSet::Multiple(set) if set == &[Type::Array, Type::Null] => true,
74            TypeSet::Multiple(set) if set == &[Type::Null, Type::Array] => true,
75            _ => false,
76        }
77    }
78}
79
80/// A schema object allows the definition of input and output data types.
81#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
82pub struct ObjectSchema {
83    #[serde(rename = "allOf", default, skip_serializing_if = "Vec::is_empty")]
84    pub all_of: Vec<ObjectOrReference<ObjectSchema>>,
85
86    #[serde(rename = "anyOf", default, skip_serializing_if = "Vec::is_empty")]
87    pub any_of: Vec<ObjectOrReference<ObjectSchema>>,
88
89    #[serde(rename = "oneOf", default, skip_serializing_if = "Vec::is_empty")]
90    pub one_of: Vec<ObjectOrReference<ObjectSchema>>,
91
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub items: Option<Box<Schema>>,
94
95    #[serde(rename = "prefixItems", default, skip_serializing_if = "Vec::is_empty")]
96    pub prefix_items: Vec<ObjectOrReference<ObjectSchema>>,
97
98    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
99    pub properties: BTreeMap<String, ObjectOrReference<ObjectSchema>>,
100
101    #[serde(
102        rename = "additionalProperties",
103        skip_serializing_if = "Option::is_none"
104    )]
105    pub additional_properties: Option<Schema>,
106
107    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
108    pub schema_type: Option<TypeSet>,
109
110    #[serde(rename = "enum", default, skip_serializing_if = "Vec::is_empty")]
111    pub enum_values: Vec<serde_json::Value>,
112
113    #[serde(rename = "const", skip_serializing_if = "Option::is_none")]
114    pub const_value: Option<serde_json::Value>,
115
116    #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")]
117    pub multiple_of: Option<serde_json::Number>,
118
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub maximum: Option<serde_json::Number>,
121
122    #[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")]
123    pub exclusive_maximum: Option<serde_json::Number>,
124
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub minimum: Option<serde_json::Number>,
127
128    #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")]
129    pub exclusive_minimum: Option<serde_json::Number>,
130
131    #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")]
132    pub max_length: Option<u64>,
133
134    #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")]
135    pub min_length: Option<u64>,
136
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub pattern: Option<String>,
139
140    #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")]
141    pub max_items: Option<u64>,
142
143    #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")]
144    pub min_items: Option<u64>,
145
146    #[serde(rename = "uniqueItems", skip_serializing_if = "Option::is_none")]
147    pub unique_items: Option<bool>,
148
149    #[serde(rename = "maxProperties", skip_serializing_if = "Option::is_none")]
150    pub max_properties: Option<u64>,
151
152    #[serde(rename = "minProperties", skip_serializing_if = "Option::is_none")]
153    pub min_properties: Option<u64>,
154
155    #[serde(default, skip_serializing_if = "Vec::is_empty")]
156    pub required: Vec<String>,
157
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub format: Option<String>,
160
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub title: Option<String>,
163
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub description: Option<String>,
166
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub default: Option<serde_json::Value>,
169
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub deprecated: Option<bool>,
172
173    #[serde(rename = "readOnly", skip_serializing_if = "Option::is_none")]
174    pub read_only: Option<bool>,
175
176    #[serde(rename = "writeOnly", skip_serializing_if = "Option::is_none")]
177    pub write_only: Option<bool>,
178
179    #[serde(default, skip_serializing_if = "Vec::is_empty")]
180    pub examples: Vec<serde_json::Value>,
181
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub discriminator: Option<Discriminator>,
184
185    #[serde(
186        default,
187        deserialize_with = "distinguish_missing_and_null",
188        skip_serializing_if = "Option::is_none"
189    )]
190    pub example: Option<serde_json::Value>,
191
192    #[serde(flatten, with = "spec_extensions")]
193    pub extensions: BTreeMap<String, serde_json::Value>,
194}
195
196impl ObjectSchema {
197    /// Returns true if Null appears in set of schema types, or None if unspecified.
198    pub fn is_nullable(&self) -> Option<bool> {
199        Some(match self.schema_type.as_ref()? {
200            TypeSet::Single(type_) => *type_ == Type::Null,
201            TypeSet::Multiple(set) => set.contains(&Type::Null),
202        })
203    }
204}
205
206impl FromRef for ObjectSchema {
207    fn from_ref(spec: &OpenApiV31Spec, path: &str) -> Result<Self, ErrorRef> {
208        let refpath = path.parse::<Ref>()?;
209
210        match refpath.kind {
211            RefType::Schema => spec
212                .components
213                .as_ref()
214                .and_then(|cs| cs.schemas.get(&refpath.name))
215                .ok_or_else(|| ErrorRef::Unresolvable {
216                    path: path.to_owned(),
217                })
218                .and_then(|oor| oor.resolve(spec)),
219
220            typ => Err(ErrorRef::MismatchedType {
221                expected: typ,
222                actual: RefType::Schema,
223            }),
224        }
225    }
226}
227
228/// A boolean JSON schema.
229#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
230#[serde(transparent)]
231pub struct BooleanSchema(pub bool);
232
233/// A JSON schema document.
234#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
235#[serde(untagged)]
236pub enum Schema {
237    Boolean(BooleanSchema),
238    Object(Box<ObjectOrReference<ObjectSchema>>),
239}
240
241/// Considers any value that is present as `Some`, including `null`.
242fn distinguish_missing_and_null<'de, T, D>(de: D) -> Result<Option<T>, D::Error>
243where
244    T: Deserialize<'de> + fmt::Debug,
245    D: Deserializer<'de>,
246{
247    T::deserialize(de).map(Some)
248}
249
250#[cfg(test)]
251mod tests {
252    use super::{ObjectSchema, Type};
253
254    #[test]
255    fn type_set_contains() {
256        let spec = r#"{"type": "integer"}"#;
257        let schema = serde_json::from_str::<ObjectSchema>(spec).unwrap();
258        let schema_type = schema.schema_type.unwrap();
259        assert!(schema_type.contains(Type::Integer));
260
261        let spec = r#"{"type": ["integer", "null"]}"#;
262        let schema = serde_json::from_str::<ObjectSchema>(spec).unwrap();
263        let schema_type = schema.schema_type.unwrap();
264        assert!(schema_type.contains(Type::Integer));
265
266        let spec = r#"{"type": ["object", "null"]}"#;
267        let schema = serde_json::from_str::<ObjectSchema>(spec).unwrap();
268        let schema_type = schema.schema_type.unwrap();
269        assert!(schema_type.contains(Type::Object));
270        assert!(schema_type.is_object_or_nullable_object());
271    }
272
273    #[test]
274    fn example_can_be_explicit_null() {
275        let spec = r#"{"type": ["string", "null"]}"#;
276        let schema = serde_json::from_str::<ObjectSchema>(spec).unwrap();
277        assert_eq!(schema.example, None);
278
279        let spec = r#"{"type": ["string", "null"], "example": null}"#;
280        let schema = serde_json::from_str::<ObjectSchema>(spec).unwrap();
281        assert_eq!(schema.example, Some(serde_json::Value::Null));
282    }
283}