salvo_oapi/openapi/
schema.rs

1//! Implements [OpenAPI Schema Object][schema] types which can be
2//! used to define field properties, enum values, array or object types.
3//!
4//! [schema]: https://spec.openapis.org/oas/latest.html#schema-object
5mod all_of;
6mod any_of;
7mod array;
8mod object;
9mod one_of;
10
11use std::ops::{Deref, DerefMut};
12
13pub use all_of::AllOf;
14pub use any_of::AnyOf;
15pub use array::Array;
16pub use object::Object;
17pub use one_of::OneOf;
18use serde::{Deserialize, Serialize};
19
20use crate::{PropMap, RefOr};
21
22/// Schemas collection for OpenApi.
23#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
24#[serde(rename_all = "camelCase")]
25pub struct Schemas(pub PropMap<String, RefOr<Schema>>);
26
27impl<K, R> From<PropMap<K, R>> for Schemas
28where
29    K: Into<String>,
30    R: Into<RefOr<Schema>>,
31{
32    fn from(inner: PropMap<K, R>) -> Self {
33        Self(
34            inner
35                .into_iter()
36                .map(|(k, v)| (k.into(), v.into()))
37                .collect(),
38        )
39    }
40}
41impl<K, R, const N: usize> From<[(K, R); N]> for Schemas
42where
43    K: Into<String>,
44    R: Into<RefOr<Schema>>,
45{
46    fn from(inner: [(K, R); N]) -> Self {
47        Self(
48            <[(K, R)]>::into_vec(Box::new(inner))
49                .into_iter()
50                .map(|(k, v)| (k.into(), v.into()))
51                .collect(),
52        )
53    }
54}
55
56impl Deref for Schemas {
57    type Target = PropMap<String, RefOr<Schema>>;
58
59    fn deref(&self) -> &Self::Target {
60        &self.0
61    }
62}
63
64impl DerefMut for Schemas {
65    fn deref_mut(&mut self) -> &mut Self::Target {
66        &mut self.0
67    }
68}
69
70impl IntoIterator for Schemas {
71    type Item = (String, RefOr<Schema>);
72    type IntoIter = <PropMap<String, RefOr<Schema>> as IntoIterator>::IntoIter;
73
74    fn into_iter(self) -> Self::IntoIter {
75        self.0.into_iter()
76    }
77}
78
79impl Schemas {
80    /// Construct a new empty [`Schemas`]. This is effectively same as calling [`Schemas::default`].
81    #[must_use]
82    pub fn new() -> Self {
83        Default::default()
84    }
85    /// Inserts a key-value pair into the instance and returns `self`.
86    #[must_use]
87    pub fn schema<K: Into<String>, V: Into<RefOr<Schema>>>(mut self, key: K, value: V) -> Self {
88        self.insert(key, value);
89        self
90    }
91    /// Inserts a key-value pair into the instance.
92    pub fn insert<K: Into<String>, V: Into<RefOr<Schema>>>(&mut self, key: K, value: V) {
93        self.0.insert(key.into(), value.into());
94    }
95    /// Moves all elements from `other` into `self`, leaving `other` empty.
96    ///
97    /// If a key from `other` is already present in `self`, the respective
98    /// value from `self` will be overwritten with the respective value from `other`.
99    pub fn append(&mut self, other: &mut Self) {
100        let items = std::mem::take(&mut other.0);
101        for item in items {
102            self.insert(item.0, item.1);
103        }
104    }
105    /// Extends a collection with the contents of an iterator.
106    pub fn extend<I, K, V>(&mut self, iter: I)
107    where
108        I: IntoIterator<Item = (K, V)>,
109        K: Into<String>,
110        V: Into<RefOr<Schema>>,
111    {
112        for (k, v) in iter.into_iter() {
113            self.insert(k, v);
114        }
115    }
116}
117
118/// Create an _`empty`_ [`Schema`] that serializes to _`null`_.
119///
120/// Can be used in places where an item can be serialized as `null`. This is used with unit type
121/// enum variants and tuple unit types.
122#[must_use]
123pub fn empty() -> Schema {
124    Schema::object(
125        Object::new()
126            .schema_type(SchemaType::AnyValue)
127            .default_value(serde_json::Value::Null),
128    )
129}
130
131/// Is super type for [OpenAPI Schema Object][schemas]. Schema is reusable resource what can be
132/// referenced from path operations and other components using [`Ref`].
133///
134/// [schemas]: https://spec.openapis.org/oas/latest.html#schema-object
135#[non_exhaustive]
136#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
137#[serde(untagged, rename_all = "camelCase")]
138pub enum Schema {
139    /// Defines array schema from another schema. Typically used with
140    /// [`Schema::Object`]. Slice and Vec types are translated to [`Schema::Array`] types.
141    Array(Array),
142    /// Defines object schema. Object is either `object` holding **properties** which are other
143    /// [`Schema`]s or can be a field within the [`Object`].
144    Object(Box<Object>),
145    /// Creates a _OneOf_ type [composite Object][composite] schema. This schema
146    /// is used to map multiple schemas together where API endpoint could return any of them.
147    /// [`Schema::OneOf`] is created form complex enum where enum holds other than unit types.
148    ///
149    /// [composite]: https://spec.openapis.org/oas/latest.html#components-object
150    OneOf(OneOf),
151
152    /// Creates a _AnyOf_ type [composite Object][composite] schema.
153    ///
154    /// [composite]: https://spec.openapis.org/oas/latest.html#components-object
155    AllOf(AllOf),
156
157    /// Creates a _AnyOf_ type [composite Object][composite] schema.
158    ///
159    /// [composite]: https://spec.openapis.org/oas/latest.html#components-object
160    AnyOf(AnyOf),
161}
162
163impl Default for Schema {
164    fn default() -> Self {
165        Self::Object(Default::default())
166    }
167}
168
169impl Schema {
170    /// Construct a new [`Schema`] object.
171    #[must_use]
172    pub fn object(obj: Object) -> Self {
173        Self::Object(Box::new(obj))
174    }
175}
176
177/// OpenAPI [Discriminator][discriminator] object which can be optionally used together with
178/// [`OneOf`] composite object.
179///
180/// [discriminator]: https://spec.openapis.org/oas/latest.html#discriminator-object
181#[non_exhaustive]
182#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)]
183#[serde(rename_all = "camelCase")]
184pub struct Discriminator {
185    /// Defines a discriminator property name which must be found within all composite
186    /// objects.
187    pub property_name: String,
188
189    /// An object to hold mappings between payload values and schema names or references.
190    /// This field can only be populated manually. There is no macro support and no
191    /// validation.
192    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
193    pub mapping: PropMap<String, String>,
194}
195
196impl Discriminator {
197    /// Construct a new [`Discriminator`] object with property name.
198    ///
199    /// # Examples
200    ///
201    /// Create a new [`Discriminator`] object for `pet_type` property.
202    /// ```
203    /// # use salvo_oapi::schema::Discriminator;
204    /// let discriminator = Discriminator::new("pet_type");
205    /// ```
206    pub fn new<I: Into<String>>(property_name: I) -> Self {
207        Self {
208            property_name: property_name.into(),
209            mapping: PropMap::new(),
210        }
211    }
212}
213
214#[allow(clippy::trivially_copy_pass_by_ref)]
215fn is_false(value: &bool) -> bool {
216    !*value
217}
218
219/// AdditionalProperties is used to define values of map fields of the [`Schema`].
220///
221/// The value can either be [`RefOr`] or _`bool`_.
222#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
223#[serde(untagged)]
224pub enum AdditionalProperties<T> {
225    /// Use when value type of the map is a known [`Schema`] or [`Ref`] to the [`Schema`].
226    RefOr(RefOr<T>),
227    /// Use _`AdditionalProperties::FreeForm(true)`_ when any value is allowed in the map.
228    FreeForm(bool),
229}
230
231impl<T> From<RefOr<T>> for AdditionalProperties<T> {
232    fn from(value: RefOr<T>) -> Self {
233        Self::RefOr(value)
234    }
235}
236
237impl From<Object> for AdditionalProperties<Schema> {
238    fn from(value: Object) -> Self {
239        Self::RefOr(RefOr::Type(Schema::object(value)))
240    }
241}
242
243impl From<Array> for AdditionalProperties<Schema> {
244    fn from(value: Array) -> Self {
245        Self::RefOr(RefOr::Type(Schema::Array(value)))
246    }
247}
248
249impl From<Ref> for AdditionalProperties<Schema> {
250    fn from(value: Ref) -> Self {
251        Self::RefOr(RefOr::Ref(value))
252    }
253}
254
255/// Implements [OpenAPI Reference Object][reference] that can be used to reference
256/// reusable components such as [`Schema`]s or [`Response`](super::Response)s.
257///
258/// [reference]: https://spec.openapis.org/oas/latest.html#reference-object
259#[non_exhaustive]
260#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
261pub struct Ref {
262    /// Reference location of the actual component.
263    #[serde(rename = "$ref")]
264    pub ref_location: String,
265
266    /// A description which by default should override that of the referenced component.
267    /// Description supports markdown syntax. If referenced object type does not support
268    /// description this field does not have effect.
269    #[serde(skip_serializing_if = "String::is_empty", default)]
270    pub description: String,
271
272    /// A short summary which by default should override that of the referenced component. If
273    /// referenced component does not support summary field this does not have effect.
274    #[serde(skip_serializing_if = "String::is_empty", default)]
275    pub summary: String,
276}
277
278impl Ref {
279    /// Construct a new [`Ref`] with custom ref location. In most cases this is not necessary
280    /// and [`Ref::from_schema_name`] could be used instead.
281    #[must_use]
282    pub fn new<I: Into<String>>(ref_location: I) -> Self {
283        Self {
284            ref_location: ref_location.into(),
285            ..Default::default()
286        }
287    }
288
289    /// Construct a new [`Ref`] from provided schema name. This will create a [`Ref`] that
290    /// references the reusable schemas.
291    #[must_use]
292    pub fn from_schema_name<I: Into<String>>(schema_name: I) -> Self {
293        Self::new(format!("#/components/schemas/{}", schema_name.into()))
294    }
295
296    /// Construct a new [`Ref`] from provided response name. This will create a [`Ref`] that
297    /// references the reusable response.
298    #[must_use]
299    pub fn from_response_name<I: Into<String>>(response_name: I) -> Self {
300        Self::new(format!("#/components/responses/{}", response_name.into()))
301    }
302
303    /// Add or change reference location of the actual component.
304    #[must_use]
305    pub fn ref_location(mut self, ref_location: String) -> Self {
306        self.ref_location = ref_location;
307        self
308    }
309
310    /// Add or change reference location of the actual component automatically formatting the $ref
311    /// to `#/components/schemas/...` format.
312    #[must_use]
313    pub fn ref_location_from_schema_name<S: Into<String>>(mut self, schema_name: S) -> Self {
314        self.ref_location = format!("#/components/schemas/{}", schema_name.into());
315        self
316    }
317
318    // TODO: REMOVE THE unnecessary description Option wrapping.
319
320    /// Add or change description which by default should override that of the referenced component.
321    /// Description supports markdown syntax. If referenced object type does not support
322    /// description this field does not have effect.
323    #[must_use]
324    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
325        self.description = description.into();
326        self
327    }
328
329    /// Add or change short summary which by default should override that of the referenced
330    /// component. If referenced component does not support summary field this does not have
331    /// effect.
332    #[must_use]
333    pub fn summary<S: Into<String>>(mut self, summary: S) -> Self {
334        self.summary = summary.into();
335        self
336    }
337
338    /// Convert type to [`Array`].
339    #[must_use]
340    pub fn to_array(self) -> Array {
341        Array::new().items(self)
342    }
343}
344
345impl From<Ref> for RefOr<Schema> {
346    fn from(r: Ref) -> Self {
347        Self::Ref(r)
348    }
349}
350
351impl<T> From<T> for RefOr<T> {
352    fn from(t: T) -> Self {
353        Self::Type(t)
354    }
355}
356
357impl Default for RefOr<Schema> {
358    fn default() -> Self {
359        Self::Type(Schema::object(Object::new()))
360    }
361}
362
363// impl ToArray for RefOr<Schema> {}
364
365/// Represents type of [`Schema`].
366#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
367#[serde(untagged)]
368pub enum SchemaType {
369    /// Single type known from OpenAPI spec 3.0
370    Basic(BasicType),
371    /// Multiple types rendered as [`slice`]
372    Array(Vec<BasicType>),
373    /// Type that is considered typeless. _`AnyValue`_ will omit the type definition from the schema
374    /// making it to accept any type possible.
375    AnyValue,
376}
377
378impl Default for SchemaType {
379    fn default() -> Self {
380        Self::Basic(BasicType::default())
381    }
382}
383
384impl From<BasicType> for SchemaType {
385    fn from(value: BasicType) -> Self {
386        Self::basic(value)
387    }
388}
389
390impl FromIterator<BasicType> for SchemaType {
391    fn from_iter<T: IntoIterator<Item = BasicType>>(iter: T) -> Self {
392        Self::Array(iter.into_iter().collect())
393    }
394}
395impl SchemaType {
396    /// Instantiate new [`SchemaType`] of given [`BasicType`]
397    ///
398    /// Method accepts one argument `type` to create [`SchemaType`] for.
399    ///
400    /// # Examples
401    ///
402    /// _**Create string [`SchemaType`]**_
403    /// ```rust
404    /// # use salvo_oapi::schema::{SchemaType, BasicType};
405    /// let ty = SchemaType::basic(BasicType::String);
406    /// ```
407    #[must_use]
408    pub fn basic(r#type: BasicType) -> Self {
409        Self::Basic(r#type)
410    }
411
412    //// Instantiate new [`SchemaType::AnyValue`].
413    /// This is same as calling [`SchemaType::AnyValue`] but in a function form `() -> SchemaType`
414    /// allowing it to be used as argument for _serde's_ _`default = "..."`_.
415    #[must_use]
416    pub fn any() -> Self {
417        Self::AnyValue
418    }
419
420    /// Check whether this [`SchemaType`] is any value _(typeless)_ returning true on any value
421    /// schema type.
422    #[must_use]
423    pub fn is_any_value(&self) -> bool {
424        matches!(self, Self::AnyValue)
425    }
426}
427
428/// Represents data type fragment of [`Schema`].
429///
430/// [`BasicType`] is used to create a [`SchemaType`] that defines the type of the [`Schema`].
431/// [`SchemaType`] can be created from a single [`BasicType`] or multiple [`BasicType`]s according
432/// to the OpenAPI 3.1 spec. Since the OpenAPI 3.1 is fully compatible with JSON schema the
433/// definition of the _**type**_ property comes from [JSON Schema type](https://json-schema.org/understanding-json-schema/reference/type).
434///
435/// # Examples
436/// _**Create nullable string [`SchemaType`]**_
437/// ```rust
438/// # use std::iter::FromIterator;
439/// # use salvo_oapi::schema::{BasicType, SchemaType};
440/// let _: SchemaType = [BasicType::String, BasicType::Null].into_iter().collect();
441/// ```
442/// _**Create string [`SchemaType`]**_
443/// ```rust
444/// # use salvo_oapi::schema::{BasicType, SchemaType};
445/// let _ = SchemaType::basic(BasicType::String);
446/// ```
447#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)]
448#[serde(rename_all = "lowercase")]
449pub enum BasicType {
450    /// Used with [`Object`] to describe schema that has _properties_ describing fields. have
451    #[default]
452    Object,
453    /// Indicates string type of content. Used with [`Object`] on a `string`
454    /// field.
455    String,
456    /// Indicates integer type of content. Used with [`Object`] on a `number`
457    /// field.
458    Integer,
459    /// Indicates floating point number type of content. Used with
460    /// [`Object`] on a `number` field.
461    Number,
462    /// Indicates boolean type of content. Used with [`Object`] on
463    /// a `bool` field.
464    Boolean,
465    /// Used with [`Array`]. Indicates array type of content.
466    Array,
467    /// Null type. Used together with other type to indicate nullable values.
468    Null,
469}
470
471/// Additional format for [`SchemaType`] to fine tune the data type used.
472///
473/// If the **format** is not supported by the UI it may default back to [`SchemaType`] alone.
474/// Format is an open value, so you can use any formats, even not those defined by the
475/// OpenAPI Specification.
476#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
477#[serde(rename_all = "lowercase", untagged)]
478pub enum SchemaFormat {
479    /// Use to define additional detail about the value.
480    KnownFormat(KnownFormat),
481    /// Can be used to provide additional detail about the value when [`SchemaFormat::KnownFormat`]
482    /// is not suitable.
483    Custom(String),
484}
485
486/// Known schema format modifier property to provide fine detail of the primitive type.
487///
488/// Known format is defined in <https://spec.openapis.org/oas/latest.html#data-types> and
489/// <https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00#section-7.3> as
490/// well as by few known data types that are enabled by specific feature flag e.g. _`uuid`_.
491#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
492#[serde(rename_all = "kebab-case")]
493pub enum KnownFormat {
494    /// 8 bit integer.
495    Int8,
496    /// 16 bit integer.
497    Int16,
498    /// 32 bit integer.
499    Int32,
500    /// 64 bit integer.
501    Int64,
502    /// 8 bit unsigned integer.
503    #[serde(rename = "uint8")]
504    UInt8,
505    /// 16 bit unsigned integer.
506    #[serde(rename = "uint16")]
507    UInt16,
508    /// 32 bit unsigned integer.
509    #[serde(rename = "uint32")]
510    UInt32,
511    /// 64 bit unsigned integer.
512    #[serde(rename = "uint64")]
513    UInt64,
514    /// floating point number.
515    Float,
516    /// double (floating point) number.
517    Double,
518    /// base64 encoded chars.
519    Byte,
520    /// binary data (octet).
521    Binary,
522    /// ISO-8601 full time format [RFC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14).
523    Time,
524    /// ISO-8601 full date [RFC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14).
525    Date,
526    /// ISO-8601 full date time [RFC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14).
527    DateTime,
528    /// duration format from [RFC3339 Appendix-A](https://datatracker.ietf.org/doc/html/rfc3339#appendix-A).
529    Duration,
530    /// Hint to UI to obscure input.
531    Password,
532    /// Use for compact string
533    String,
534    /// Used with [`String`] values to indicate value is in decimal format.
535    ///
536    /// **decimal** feature need to be enabled.
537    #[cfg(any(feature = "decimal", feature = "decimal-float"))]
538    #[cfg_attr(docsrs, doc(cfg(any(feature = "decimal", feature = "decimal-float"))))]
539    Decimal,
540    /// Used with [`String`] values to indicate value is in ULID format.
541    #[cfg(feature = "ulid")]
542    #[cfg_attr(docsrs, doc(cfg(feature = "ulid")))]
543    Ulid,
544
545    /// Used with [`String`] values to indicate value is in UUID format.
546    #[cfg(feature = "uuid")]
547    #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
548    Uuid,
549    /// Used with [`String`] values to indicate value is in Url format.
550    ///
551    /// **url** feature need to be enabled.
552    #[cfg(feature = "url")]
553    #[cfg_attr(docsrs, doc(cfg(feature = "url")))]
554    Url,
555    /// A string instance is valid against this attribute if it is a valid URI Reference
556    /// (either a URI or a relative-reference) according to
557    /// [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986).
558    #[cfg(feature = "url")]
559    #[cfg_attr(docsrs, doc(cfg(feature = "url")))]
560    UriReference,
561    /// A string instance is valid against this attribute if it is a
562    /// valid IRI, according to [RFC3987](https://datatracker.ietf.org/doc/html/rfc3987).
563    #[cfg(feature = "url")]
564    #[cfg_attr(docsrs, doc(cfg(feature = "url")))]
565    Iri,
566    /// A string instance is valid against this attribute if it is a valid IRI Reference
567    /// (either an IRI or a relative-reference)
568    /// according to [RFC3987](https://datatracker.ietf.org/doc/html/rfc3987).
569    #[cfg(feature = "url")]
570    #[cfg_attr(docsrs, doc(cfg(feature = "url")))]
571    IriReference,
572    /// As defined in "Mailbox" rule [RFC5321](https://datatracker.ietf.org/doc/html/rfc5321#section-4.1.2).
573    Email,
574    /// As defined by extended "Mailbox" rule [RFC6531](https://datatracker.ietf.org/doc/html/rfc6531#section-3.3).
575    IdnEmail,
576    /// As defined by [RFC1123](https://datatracker.ietf.org/doc/html/rfc1123#section-2.1), including host names
577    /// produced using the Punycode algorithm
578    /// specified in [RFC5891](https://datatracker.ietf.org/doc/html/rfc5891#section-4.4).
579    Hostname,
580    /// As defined by either [RFC1123](https://datatracker.ietf.org/doc/html/rfc1123#section-2.1) as for hostname,
581    /// or an internationalized hostname as defined by [RFC5890](https://datatracker.ietf.org/doc/html/rfc5890#section-2.3.2.3).
582    IdnHostname,
583    /// An IPv4 address according to [RFC2673](https://datatracker.ietf.org/doc/html/rfc2673#section-3.2).
584    Ipv4,
585    /// An IPv6 address according to [RFC4291](https://datatracker.ietf.org/doc/html/rfc4291#section-2.2).
586    Ipv6,
587    /// A string instance is a valid URI Template if it is according to
588    /// [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570).
589    ///
590    /// _**Note!**_ There are no separate IRL template.
591    UriTemplate,
592    /// A valid JSON string representation of a JSON Pointer according to [RFC6901](https://datatracker.ietf.org/doc/html/rfc6901#section-5).
593    JsonPointer,
594    /// A valid relative JSON Pointer according to [draft-handrews-relative-json-pointer-01](https://datatracker.ietf.org/doc/html/draft-handrews-relative-json-pointer-01).
595    RelativeJsonPointer,
596    /// Regular expression, which SHOULD be valid according to the
597    /// [ECMA-262](https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00#ref-ecma262).
598    Regex,
599}
600
601#[cfg(test)]
602mod tests {
603    use assert_json_diff::assert_json_eq;
604    use serde_json::{Value, json};
605
606    use super::*;
607    use crate::*;
608
609    #[test]
610    fn create_schema_serializes_json() -> Result<(), serde_json::Error> {
611        let openapi = OpenApi::new("My api", "1.0.0").components(
612            Components::new()
613                .add_schema("Person", Ref::new("#/components/PersonModel"))
614                .add_schema(
615                    "Credential",
616                    Schema::from(
617                        Object::new()
618                            .property(
619                                "id",
620                                Object::new()
621                                    .schema_type(BasicType::Integer)
622                                    .format(SchemaFormat::KnownFormat(KnownFormat::Int32))
623                                    .description("Id of credential")
624                                    .default_value(json!(1i32)),
625                            )
626                            .property(
627                                "name",
628                                Object::new()
629                                    .schema_type(BasicType::String)
630                                    .description("Name of credential"),
631                            )
632                            .property(
633                                "status",
634                                Object::new()
635                                    .schema_type(BasicType::String)
636                                    .default_value(json!("Active"))
637                                    .description("Credential status")
638                                    .enum_values(["Active", "NotActive", "Locked", "Expired"]),
639                            )
640                            .property(
641                                "history",
642                                Array::new().items(Ref::from_schema_name("UpdateHistory")),
643                            )
644                            .property("tags", Object::with_type(BasicType::String).to_array()),
645                    ),
646                ),
647        );
648
649        let serialized = serde_json::to_string_pretty(&openapi)?;
650        println!("serialized json:\n {serialized}");
651
652        let value = serde_json::to_value(&openapi)?;
653        let credential = get_json_path(&value, "components.schemas.Credential.properties");
654        let person = get_json_path(&value, "components.schemas.Person");
655
656        assert!(
657            credential.get("id").is_some(),
658            "could not find path: components.schemas.Credential.properties.id"
659        );
660        assert!(
661            credential.get("status").is_some(),
662            "could not find path: components.schemas.Credential.properties.status"
663        );
664        assert!(
665            credential.get("name").is_some(),
666            "could not find path: components.schemas.Credential.properties.name"
667        );
668        assert!(
669            credential.get("history").is_some(),
670            "could not find path: components.schemas.Credential.properties.history"
671        );
672        assert_json_eq!(
673            credential
674                .get("id")
675                .unwrap_or(&serde_json::value::Value::Null),
676            json!({"type":"integer","format":"int32","description":"Id of credential","default":1})
677        );
678        assert_json_eq!(
679            credential
680                .get("name")
681                .unwrap_or(&serde_json::value::Value::Null),
682            json!({"type":"string","description":"Name of credential"})
683        );
684        assert_json_eq!(
685            credential
686                .get("status")
687                .unwrap_or(&serde_json::value::Value::Null),
688            json!({"default":"Active","description":"Credential status","enum":["Active","NotActive","Locked","Expired"],"type":"string"})
689        );
690        assert_json_eq!(
691            credential
692                .get("history")
693                .unwrap_or(&serde_json::value::Value::Null),
694            json!({"items":{"$ref":"#/components/schemas/UpdateHistory"},"type":"array"})
695        );
696        assert_eq!(person, &json!({"$ref":"#/components/PersonModel"}));
697
698        Ok(())
699    }
700
701    // Examples taken from https://spec.openapis.org/oas/latest.html#model-with-map-dictionary-properties
702    #[test]
703    fn test_property_order() {
704        let json_value = Object::new()
705            .property(
706                "id",
707                Object::new()
708                    .schema_type(BasicType::Integer)
709                    .format(SchemaFormat::KnownFormat(KnownFormat::Int32))
710                    .description("Id of credential")
711                    .default_value(json!(1i32)),
712            )
713            .property(
714                "name",
715                Object::new()
716                    .schema_type(BasicType::String)
717                    .description("Name of credential"),
718            )
719            .property(
720                "status",
721                Object::new()
722                    .schema_type(BasicType::String)
723                    .default_value(json!("Active"))
724                    .description("Credential status")
725                    .enum_values(["Active", "NotActive", "Locked", "Expired"]),
726            )
727            .property(
728                "history",
729                Array::new().items(Ref::from_schema_name("UpdateHistory")),
730            )
731            .property("tags", Object::with_type(BasicType::String).to_array());
732
733        #[cfg(not(feature = "preserve-order"))]
734        assert_eq!(
735            json_value.properties.keys().collect::<Vec<_>>(),
736            vec!["history", "id", "name", "status", "tags"]
737        );
738
739        #[cfg(feature = "preserve-order")]
740        assert_eq!(
741            json_value.properties.keys().collect::<Vec<_>>(),
742            vec!["id", "name", "status", "history", "tags"]
743        );
744    }
745
746    // Examples taken from https://spec.openapis.org/oas/latest.html#model-with-map-dictionary-properties
747    #[test]
748    fn test_additional_properties() {
749        let json_value =
750            Object::new().additional_properties(Object::new().schema_type(BasicType::String));
751        assert_json_eq!(
752            json_value,
753            json!({
754                "type": "object",
755                "additionalProperties": {
756                    "type": "string"
757                }
758            })
759        );
760
761        let json_value = Object::new().additional_properties(
762            Array::new().items(Object::new().schema_type(BasicType::Number)),
763        );
764        assert_json_eq!(
765            json_value,
766            json!({
767                "type": "object",
768                "additionalProperties": {
769                    "items": {
770                        "type": "number",
771                    },
772                    "type": "array",
773                }
774            })
775        );
776
777        let json_value = Object::new().additional_properties(Ref::from_schema_name("ComplexModel"));
778        assert_json_eq!(
779            json_value,
780            json!({
781                "type": "object",
782                "additionalProperties": {
783                    "$ref": "#/components/schemas/ComplexModel"
784                }
785            })
786        )
787    }
788
789    #[test]
790    fn test_object_with_name() {
791        let json_value = Object::new().name("SomeName");
792        assert_json_eq!(
793            json_value,
794            json!({
795                "type": "object",
796                "name": "SomeName"
797            })
798        );
799    }
800
801    #[test]
802    fn test_derive_object_with_examples() {
803        let expected = r#"{"type":"object","examples":[{"age":20,"name":"bob the cat"}]}"#;
804        let json_value = Object::new().examples([json!({"age": 20, "name": "bob the cat"})]);
805
806        let value_string = serde_json::to_string(&json_value).unwrap();
807        assert_eq!(
808            value_string, expected,
809            "value string != expected string, {value_string} != {expected}"
810        );
811    }
812
813    fn get_json_path<'a>(value: &'a Value, path: &str) -> &'a Value {
814        path.split('.').fold(value, |acc, fragment| {
815            acc.get(fragment).unwrap_or(&serde_json::value::Value::Null)
816        })
817    }
818
819    #[test]
820    fn test_array_new() {
821        let array = Array::new().items(
822            Object::new().property(
823                "id",
824                Object::new()
825                    .schema_type(BasicType::Integer)
826                    .format(SchemaFormat::KnownFormat(KnownFormat::Int32))
827                    .description("Id of credential")
828                    .default_value(json!(1i32)),
829            ),
830        );
831
832        assert!(matches!(
833            array.schema_type,
834            SchemaType::Basic(BasicType::Array)
835        ));
836    }
837
838    #[test]
839    fn test_array_builder() {
840        let array: Array = Array::new().items(
841            Object::new().property(
842                "id",
843                Object::new()
844                    .schema_type(BasicType::Integer)
845                    .format(SchemaFormat::KnownFormat(KnownFormat::Int32))
846                    .description("Id of credential")
847                    .default_value(json!(1i32)),
848            ),
849        );
850
851        assert!(matches!(
852            array.schema_type,
853            SchemaType::Basic(BasicType::Array)
854        ));
855    }
856
857    #[test]
858    fn reserialize_deserialized_schema_components() {
859        let components = Components::new()
860            .extend_schemas(vec![(
861                "Comp",
862                Schema::from(
863                    Object::new()
864                        .property("name", Object::new().schema_type(BasicType::String))
865                        .required("name"),
866                ),
867            )])
868            .response("204", Response::new("No Content"))
869            .extend_responses(vec![("200", Response::new("Okay"))])
870            .add_security_scheme("TLS", SecurityScheme::MutualTls { description: None })
871            .extend_security_schemes(vec![(
872                "APIKey",
873                SecurityScheme::Http(security::Http::default()),
874            )]);
875
876        let serialized_components = serde_json::to_string(&components).unwrap();
877
878        let deserialized_components: Components =
879            serde_json::from_str(serialized_components.as_str()).unwrap();
880
881        assert_eq!(
882            serialized_components,
883            serde_json::to_string(&deserialized_components).unwrap()
884        )
885    }
886
887    #[test]
888    fn reserialize_deserialized_object_component() {
889        let prop = Object::new()
890            .property("name", Object::new().schema_type(BasicType::String))
891            .required("name");
892
893        let serialized_components = serde_json::to_string(&prop).unwrap();
894        let deserialized_components: Object =
895            serde_json::from_str(serialized_components.as_str()).unwrap();
896
897        assert_eq!(
898            serialized_components,
899            serde_json::to_string(&deserialized_components).unwrap()
900        )
901    }
902
903    #[test]
904    fn reserialize_deserialized_property() {
905        let prop = Object::new().schema_type(BasicType::String);
906
907        let serialized_components = serde_json::to_string(&prop).unwrap();
908        let deserialized_components: Object =
909            serde_json::from_str(serialized_components.as_str()).unwrap();
910
911        assert_eq!(
912            serialized_components,
913            serde_json::to_string(&deserialized_components).unwrap()
914        )
915    }
916
917    #[test]
918    fn serialize_deserialize_array_within_ref_or_t_object_builder() {
919        let ref_or_schema = RefOr::Type(Schema::object(Object::new().property(
920            "test",
921            RefOr::Type(Schema::Array(Array::new().items(RefOr::Type(
922                Schema::object(Object::new().property("element", RefOr::Ref(Ref::new("#/test")))),
923            )))),
924        )));
925
926        let json_str = serde_json::to_string(&ref_or_schema).expect("");
927        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
928        let json_de_str = serde_json::to_string(&deserialized).expect("");
929        assert_eq!(json_str, json_de_str);
930    }
931
932    #[test]
933    fn serialize_deserialize_one_of_within_ref_or_t_object_builder() {
934        let ref_or_schema = RefOr::Type(Schema::object(
935            Object::new().property(
936                "test",
937                RefOr::Type(Schema::OneOf(
938                    OneOf::new()
939                        .item(Schema::Array(Array::new().items(RefOr::Type(
940                            Schema::object(
941                                Object::new().property("element", RefOr::Ref(Ref::new("#/test"))),
942                            ),
943                        ))))
944                        .item(Schema::Array(Array::new().items(RefOr::Type(
945                            Schema::object(
946                                Object::new().property("foobar", RefOr::Ref(Ref::new("#/foobar"))),
947                            ),
948                        )))),
949                )),
950            ),
951        ));
952
953        let json_str = serde_json::to_string(&ref_or_schema).expect("");
954        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
955        let json_de_str = serde_json::to_string(&deserialized).expect("");
956        assert_eq!(json_str, json_de_str);
957    }
958
959    #[test]
960    fn serialize_deserialize_all_of_of_within_ref_or_t_object() {
961        let ref_or_schema = RefOr::Type(Schema::object(
962            Object::new().property(
963                "test",
964                RefOr::Type(Schema::AllOf(
965                    AllOf::new()
966                        .item(Schema::Array(Array::new().items(RefOr::Type(
967                            Schema::object(
968                                Object::new().property("element", RefOr::Ref(Ref::new("#/test"))),
969                            ),
970                        ))))
971                        .item(RefOr::Type(Schema::object(
972                            Object::new().property("foobar", RefOr::Ref(Ref::new("#/foobar"))),
973                        ))),
974                )),
975            ),
976        ));
977
978        let json_str = serde_json::to_string(&ref_or_schema).expect("");
979        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
980        let json_de_str = serde_json::to_string(&deserialized).expect("");
981        assert_eq!(json_str, json_de_str);
982    }
983
984    #[test]
985    fn serialize_deserialize_any_of_of_within_ref_or_t_object() {
986        let ref_or_schema = RefOr::Type(Schema::object(
987            Object::new().property(
988                "test",
989                RefOr::Type(Schema::AnyOf(
990                    AnyOf::new()
991                        .item(Schema::Array(Array::new().items(RefOr::Type(
992                            Schema::object(
993                                Object::new().property("element", RefOr::Ref(Ref::new("#/test"))),
994                            ),
995                        ))))
996                        .item(RefOr::Type(Schema::object(
997                            Object::new().property("foobar", RefOr::Ref(Ref::new("#/foobar"))),
998                        ))),
999                )),
1000            ),
1001        ));
1002
1003        let json_str = serde_json::to_string(&ref_or_schema).expect("");
1004        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
1005        let json_de_str = serde_json::to_string(&deserialized).expect("");
1006        assert!(json_str.contains("\"anyOf\""));
1007        assert_eq!(json_str, json_de_str);
1008    }
1009
1010    #[test]
1011    fn serialize_deserialize_schema_array_ref_or_t() {
1012        let ref_or_schema = RefOr::Type(Schema::Array(Array::new().items(RefOr::Type(
1013            Schema::Object(Box::new(
1014                Object::new().property("element", RefOr::Ref(Ref::new("#/test"))),
1015            )),
1016        ))));
1017
1018        let json_str = serde_json::to_string(&ref_or_schema).expect("");
1019        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
1020        let json_de_str = serde_json::to_string(&deserialized).expect("");
1021        assert_eq!(json_str, json_de_str);
1022    }
1023
1024    #[test]
1025    fn serialize_deserialize_schema_array() {
1026        let ref_or_schema = Array::new().items(RefOr::Type(Schema::object(
1027            Object::new().property("element", RefOr::Ref(Ref::new("#/test"))),
1028        )));
1029
1030        let json_str = serde_json::to_string(&ref_or_schema).expect("");
1031        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
1032        let json_de_str = serde_json::to_string(&deserialized).expect("");
1033        assert_eq!(json_str, json_de_str);
1034    }
1035
1036    #[test]
1037    fn serialize_deserialize_schema_with_additional_properties() {
1038        let schema = Schema::object(Object::new().property(
1039            "map",
1040            Object::new().additional_properties(AdditionalProperties::FreeForm(true)),
1041        ));
1042
1043        let json_str = serde_json::to_string(&schema).unwrap();
1044        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).unwrap();
1045        let json_de_str = serde_json::to_string(&deserialized).unwrap();
1046        assert_eq!(json_str, json_de_str);
1047    }
1048
1049    #[test]
1050    fn serialize_deserialize_schema_with_additional_properties_object() {
1051        let schema = Schema::object(Object::new().property(
1052            "map",
1053            Object::new().additional_properties(
1054                Object::new().property("name", Object::with_type(BasicType::String)),
1055            ),
1056        ));
1057
1058        let json_str = serde_json::to_string(&schema).expect("serde json should success");
1059        let deserialized: RefOr<Schema> =
1060            serde_json::from_str(&json_str).expect("serde json should success");
1061        let json_de_str = serde_json::to_string(&deserialized).expect("serde json should success");
1062        assert_eq!(json_str, json_de_str);
1063    }
1064
1065    #[test]
1066    fn serialize_discriminator_with_mapping() {
1067        let mut discriminator = Discriminator::new("type");
1068        discriminator.mapping = [("int".to_owned(), "#/components/schemas/MyInt".to_owned())]
1069            .into_iter()
1070            .collect::<PropMap<_, _>>();
1071        let one_of = OneOf::new()
1072            .item(Ref::from_schema_name("MyInt"))
1073            .discriminator(discriminator);
1074        let json_value = serde_json::to_value(one_of).expect("serde json should success");
1075
1076        assert_json_eq!(
1077            json_value,
1078            json!({
1079                "oneOf": [
1080                    {
1081                        "$ref": "#/components/schemas/MyInt"
1082                    }
1083                ],
1084                "discriminator": {
1085                    "propertyName": "type",
1086                    "mapping": {
1087                        "int": "#/components/schemas/MyInt"
1088                    }
1089                }
1090            })
1091        );
1092    }
1093
1094    #[test]
1095    fn deserialize_reserialize_one_of_default_type() {
1096        let a = OneOf::new()
1097            .item(Schema::Array(Array::new().items(RefOr::Type(
1098                Schema::object(Object::new().property("element", RefOr::Ref(Ref::new("#/test")))),
1099            ))))
1100            .item(Schema::Array(Array::new().items(RefOr::Type(
1101                Schema::object(Object::new().property("foobar", RefOr::Ref(Ref::new("#/foobar")))),
1102            ))));
1103
1104        let serialized_json = serde_json::to_string(&a).expect("should serialize to json");
1105        let b: OneOf = serde_json::from_str(&serialized_json).expect("should deserialize OneOf");
1106        let reserialized_json = serde_json::to_string(&b).expect("reserialized json");
1107
1108        assert_eq!(serialized_json, reserialized_json);
1109    }
1110
1111    #[test]
1112    fn serialize_deserialize_object_with_multiple_schema_types() {
1113        let object =
1114            Object::new().schema_type(SchemaType::from_iter([BasicType::Object, BasicType::Null]));
1115
1116        let json_str = serde_json::to_string(&object).expect("serde json should success");
1117        let deserialized: Object =
1118            serde_json::from_str(&json_str).expect("serde json should success");
1119        let json_de_str = serde_json::to_string(&deserialized).expect("serde json should success");
1120        assert_eq!(json_str, json_de_str);
1121    }
1122
1123    #[test]
1124    fn test_empty_schema() {
1125        let schema = empty();
1126        assert_json_eq!(
1127            schema,
1128            json!({
1129                "default": null
1130            })
1131        )
1132    }
1133
1134    #[test]
1135    fn test_default_schema() {
1136        let schema = Schema::default();
1137        assert_json_eq!(
1138            schema,
1139            json!({
1140                "type": "object",
1141            })
1142        )
1143    }
1144
1145    #[test]
1146    fn test_ref_from_response_name() {
1147        let _ref = Ref::from_response_name("MyResponse");
1148        assert_json_eq!(
1149            _ref,
1150            json!({
1151                "$ref": "#/components/responses/MyResponse"
1152            })
1153        )
1154    }
1155
1156    #[test]
1157    fn test_additional_properties_from_ref_or() {
1158        let additional_properties =
1159            AdditionalProperties::from(RefOr::Type(Schema::Object(Box::default())));
1160        assert_json_eq!(
1161            additional_properties,
1162            json!({
1163                "type": "object",
1164            })
1165        )
1166    }
1167}