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