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, ToArray};
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
339impl From<Ref> for RefOr<Schema> {
340    fn from(r: Ref) -> Self {
341        Self::Ref(r)
342    }
343}
344
345impl<T> From<T> for RefOr<T> {
346    fn from(t: T) -> Self {
347        Self::Type(t)
348    }
349}
350
351impl Default for RefOr<Schema> {
352    fn default() -> Self {
353        Self::Type(Schema::object(Object::new()))
354    }
355}
356
357impl ToArray for RefOr<Schema> {}
358
359/// Represents type of [`Schema`].
360#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
361#[serde(untagged)]
362pub enum SchemaType {
363    /// Single type known from OpenAPI spec 3.0
364    Basic(BasicType),
365    /// Multiple types rendered as [`slice`]
366    Array(Vec<BasicType>),
367    /// Type that is considered typeless. _`AnyValue`_ will omit the type definition from the schema
368    /// making it to accept any type possible.
369    AnyValue,
370}
371
372impl Default for SchemaType {
373    fn default() -> Self {
374        Self::Basic(BasicType::default())
375    }
376}
377
378impl From<BasicType> for SchemaType {
379    fn from(value: BasicType) -> Self {
380        Self::basic(value)
381    }
382}
383
384impl FromIterator<BasicType> for SchemaType {
385    fn from_iter<T: IntoIterator<Item = BasicType>>(iter: T) -> Self {
386        Self::Array(iter.into_iter().collect())
387    }
388}
389impl SchemaType {
390    /// Instantiate new [`SchemaType`] of given [`BasicType`]
391    ///
392    /// Method accepets one argument `type` to create [`SchemaType`] for.
393    ///
394    /// # Examples
395    ///
396    /// _**Create string [`SchemaType`]**_
397    /// ```rust
398    /// # use salvo_oapi::schema::{SchemaType, BasicType};
399    /// let ty = SchemaType::basic(BasicType::String);
400    /// ```
401    #[must_use]
402    pub fn basic(r#type: BasicType) -> Self {
403        Self::Basic(r#type)
404    }
405
406    //// Instantiate new [`SchemaType::AnyValue`].
407    ///
408    /// This is same as calling [`SchemaType::AnyValue`] but in a function form `() -> SchemaType`
409    /// allowing it to be used as argument for _serde's_ _`default = "..."`_.
410    #[must_use]
411    pub fn any() -> Self {
412        Self::AnyValue
413    }
414
415    /// Check whether this [`SchemaType`] is any value _(typeless)_ returning true on any value
416    /// schema type.
417    #[must_use]
418    pub fn is_any_value(&self) -> bool {
419        matches!(self, Self::AnyValue)
420    }
421}
422
423/// Represents data type fragment of [`Schema`].
424///
425/// [`BasicType`] is used to create a [`SchemaType`] that defines the type of the [`Schema`].
426/// [`SchemaType`] can be created from a single [`BasicType`] or multiple [`BasicType`]s according to the
427/// OpenAPI 3.1 spec. Since the OpenAPI 3.1 is fully compatible with JSON schema the definition of
428/// the _**type**_ property comes from [JSON Schema type](https://json-schema.org/understanding-json-schema/reference/type).
429///
430/// # Examples
431/// _**Create nullable string [`SchemaType`]**_
432/// ```rust
433/// # use std::iter::FromIterator;
434/// # use salvo_oapi::schema::{BasicType, SchemaType};
435/// let _: SchemaType = [BasicType::String, BasicType::Null].into_iter().collect();
436/// ```
437/// _**Create string [`SchemaType`]**_
438/// ```rust
439/// # use salvo_oapi::schema::{BasicType, SchemaType};
440/// let _ = SchemaType::basic(BasicType::String);
441/// ```
442#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)]
443#[serde(rename_all = "lowercase")]
444pub enum BasicType {
445    /// Used with [`Object`] to describe schema that has _properties_ describing fields. have
446    #[default]
447    Object,
448    /// Indicates string type of content. Used with [`Object`] on a `string`
449    /// field.
450    String,
451    /// Indicates integer type of content. Used with [`Object`] on a `number`
452    /// field.
453    Integer,
454    /// Indicates floating point number type of content. Used with
455    /// [`Object`] on a `number` field.
456    Number,
457    /// Indicates boolean type of content. Used with [`Object`] on
458    /// a `bool` field.
459    Boolean,
460    /// Used with [`Array`]. Indicates array type of content.
461    Array,
462    /// Null type. Used together with other type to indicate nullable values.
463    Null,
464}
465
466/// Additional format for [`SchemaType`] to fine tune the data type used.
467///
468/// If the **format** is not supported by the UI it may default back to [`SchemaType`] alone.
469/// Format is an open value, so you can use any formats, even not those defined by the
470/// OpenAPI Specification.
471#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
472#[serde(rename_all = "lowercase", untagged)]
473pub enum SchemaFormat {
474    /// Use to define additional detail about the value.
475    KnownFormat(KnownFormat),
476    /// Can be used to provide additional detail about the value when [`SchemaFormat::KnownFormat`]
477    /// is not suitable.
478    Custom(String),
479}
480
481/// Known schema format modifier property to provide fine detail of the primitive type.
482///
483/// Known format is defined in <https://spec.openapis.org/oas/latest.html#data-types> and
484/// <https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00#section-7.3> as
485/// well as by few known data types that are enabled by specific feature flag e.g. _`uuid`_.
486#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
487#[serde(rename_all = "kebab-case")]
488pub enum KnownFormat {
489    /// 8 bit integer.
490    Int8,
491    /// 16 bit integer.
492    Int16,
493    /// 32 bit integer.
494    Int32,
495    /// 64 bit integer.
496    Int64,
497    /// 8 bit unsigned integer.
498    #[serde(rename = "uint8")]
499    UInt8,
500    /// 16 bit unsigned integer.
501    #[serde(rename = "uint16")]
502    UInt16,
503    /// 32 bit unsigned integer.
504    #[serde(rename = "uint32")]
505    UInt32,
506    /// 64 bit unsigned integer.
507    #[serde(rename = "uint64")]
508    UInt64,
509    /// floating point number.
510    Float,
511    /// double (floating point) number.
512    Double,
513    /// base64 encoded chars.
514    Byte,
515    /// binary data (octet).
516    Binary,
517    /// ISO-8601 full time format [RFC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14).
518    Time,
519    /// ISO-8601 full date [RFC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14).
520    Date,
521    /// ISO-8601 full date time [RFC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14).
522    DateTime,
523    /// duration format from [RFC3339 Appendix-A](https://datatracker.ietf.org/doc/html/rfc3339#appendix-A).
524    Duration,
525    /// Hint to UI to obscure input.
526    Password,
527    /// Use for compact string
528    String,
529    /// Used with [`String`] values to indicate value is in decimal format.
530    ///
531    /// **decimal** feature need to be enabled.
532    #[cfg(any(feature = "decimal", feature = "decimal-float"))]
533    #[cfg_attr(docsrs, doc(cfg(any(feature = "decimal", feature = "decimal-float"))))]
534    Decimal,
535    /// Used with [`String`] values to indicate value is in ULID format.
536    #[cfg(feature = "ulid")]
537    #[cfg_attr(docsrs, doc(cfg(feature = "ulid")))]
538    Ulid,
539
540    /// Used with [`String`] values to indicate value is in UUID format.
541    #[cfg(feature = "uuid")]
542    #[cfg_attr(docsrs, doc(cfg(feature = "uuid")))]
543    Uuid,
544    /// Used with [`String`] values to indicate value is in Url format.
545    ///
546    /// **url** feature need to be enabled.
547    #[cfg(feature = "url")]
548    #[cfg_attr(docsrs, doc(cfg(feature = "url")))]
549    Url,
550    /// A string instance is valid against this attribute if it is a valid URI Reference
551    /// (either a URI or a relative-reference) according to
552    /// [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986).
553    #[cfg(feature = "url")]
554    #[cfg_attr(docsrs, doc(cfg(feature = "url")))]
555    UriReference,
556    /// A string instance is valid against this attribute if it is a
557    /// valid IRI, according to [RFC3987](https://datatracker.ietf.org/doc/html/rfc3987).
558    #[cfg(feature = "url")]
559    #[cfg_attr(docsrs, doc(cfg(feature = "url")))]
560    Iri,
561    /// A string instance is valid against this attribute if it is a valid IRI Reference
562    /// (either an IRI or a relative-reference)
563    /// according to [RFC3987](https://datatracker.ietf.org/doc/html/rfc3987).
564    #[cfg(feature = "url")]
565    #[cfg_attr(docsrs, doc(cfg(feature = "url")))]
566    IriReference,
567    /// As defined in "Mailbox" rule [RFC5321](https://datatracker.ietf.org/doc/html/rfc5321#section-4.1.2).
568    Email,
569    /// As defined by extended "Mailbox" rule [RFC6531](https://datatracker.ietf.org/doc/html/rfc6531#section-3.3).
570    IdnEmail,
571    /// As defined by [RFC1123](https://datatracker.ietf.org/doc/html/rfc1123#section-2.1), including host names
572    /// produced using the Punycode algorithm
573    /// specified in [RFC5891](https://datatracker.ietf.org/doc/html/rfc5891#section-4.4).
574    Hostname,
575    /// As defined by either [RFC1123](https://datatracker.ietf.org/doc/html/rfc1123#section-2.1) as for hostname,
576    /// or an internationalized hostname as defined by [RFC5890](https://datatracker.ietf.org/doc/html/rfc5890#section-2.3.2.3).
577    IdnHostname,
578    /// An IPv4 address according to [RFC2673](https://datatracker.ietf.org/doc/html/rfc2673#section-3.2).
579    Ipv4,
580    /// An IPv6 address according to [RFC4291](https://datatracker.ietf.org/doc/html/rfc4291#section-2.2).
581    Ipv6,
582    /// A string instance is a valid URI Template if it is according to
583    /// [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570).
584    ///
585    /// _**Note!**_ There are no separate IRL template.
586    UriTemplate,
587    /// A valid JSON string representation of a JSON Pointer according to [RFC6901](https://datatracker.ietf.org/doc/html/rfc6901#section-5).
588    JsonPointer,
589    /// 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).
590    RelativeJsonPointer,
591    /// Regular expression, which SHOULD be valid according to the
592    /// [ECMA-262](https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00#ref-ecma262).
593    Regex,
594}
595
596#[cfg(test)]
597mod tests {
598    use assert_json_diff::assert_json_eq;
599    use serde_json::{Value, json};
600
601    use super::*;
602    use crate::*;
603
604    #[test]
605    fn create_schema_serializes_json() -> Result<(), serde_json::Error> {
606        let openapi = OpenApi::new("My api", "1.0.0").components(
607            Components::new()
608                .add_schema("Person", Ref::new("#/components/PersonModel"))
609                .add_schema(
610                    "Credential",
611                    Schema::from(
612                        Object::new()
613                            .property(
614                                "id",
615                                Object::new()
616                                    .schema_type(BasicType::Integer)
617                                    .format(SchemaFormat::KnownFormat(KnownFormat::Int32))
618                                    .description("Id of credential")
619                                    .default_value(json!(1i32)),
620                            )
621                            .property(
622                                "name",
623                                Object::new()
624                                    .schema_type(BasicType::String)
625                                    .description("Name of credential"),
626                            )
627                            .property(
628                                "status",
629                                Object::new()
630                                    .schema_type(BasicType::String)
631                                    .default_value(json!("Active"))
632                                    .description("Credential status")
633                                    .enum_values(["Active", "NotActive", "Locked", "Expired"]),
634                            )
635                            .property(
636                                "history",
637                                Array::new().items(Ref::from_schema_name("UpdateHistory")),
638                            )
639                            .property("tags", Object::with_type(BasicType::String).to_array()),
640                    ),
641                ),
642        );
643
644        let serialized = serde_json::to_string_pretty(&openapi)?;
645        println!("serialized json:\n {serialized}");
646
647        let value = serde_json::to_value(&openapi)?;
648        let credential = get_json_path(&value, "components.schemas.Credential.properties");
649        let person = get_json_path(&value, "components.schemas.Person");
650
651        assert!(
652            credential.get("id").is_some(),
653            "could not find path: components.schemas.Credential.properties.id"
654        );
655        assert!(
656            credential.get("status").is_some(),
657            "could not find path: components.schemas.Credential.properties.status"
658        );
659        assert!(
660            credential.get("name").is_some(),
661            "could not find path: components.schemas.Credential.properties.name"
662        );
663        assert!(
664            credential.get("history").is_some(),
665            "could not find path: components.schemas.Credential.properties.history"
666        );
667        assert_json_eq!(
668            credential
669                .get("id")
670                .unwrap_or(&serde_json::value::Value::Null),
671            json!({"type":"integer","format":"int32","description":"Id of credential","default":1})
672        );
673        assert_json_eq!(
674            credential
675                .get("name")
676                .unwrap_or(&serde_json::value::Value::Null),
677            json!({"type":"string","description":"Name of credential"})
678        );
679        assert_json_eq!(
680            credential
681                .get("status")
682                .unwrap_or(&serde_json::value::Value::Null),
683            json!({"default":"Active","description":"Credential status","enum":["Active","NotActive","Locked","Expired"],"type":"string"})
684        );
685        assert_json_eq!(
686            credential
687                .get("history")
688                .unwrap_or(&serde_json::value::Value::Null),
689            json!({"items":{"$ref":"#/components/schemas/UpdateHistory"},"type":"array"})
690        );
691        assert_eq!(person, &json!({"$ref":"#/components/PersonModel"}));
692
693        Ok(())
694    }
695
696    // Examples taken from https://spec.openapis.org/oas/latest.html#model-with-map-dictionary-properties
697    #[test]
698    fn test_property_order() {
699        let json_value = Object::new()
700            .property(
701                "id",
702                Object::new()
703                    .schema_type(BasicType::Integer)
704                    .format(SchemaFormat::KnownFormat(KnownFormat::Int32))
705                    .description("Id of credential")
706                    .default_value(json!(1i32)),
707            )
708            .property(
709                "name",
710                Object::new()
711                    .schema_type(BasicType::String)
712                    .description("Name of credential"),
713            )
714            .property(
715                "status",
716                Object::new()
717                    .schema_type(BasicType::String)
718                    .default_value(json!("Active"))
719                    .description("Credential status")
720                    .enum_values(["Active", "NotActive", "Locked", "Expired"]),
721            )
722            .property(
723                "history",
724                Array::new().items(Ref::from_schema_name("UpdateHistory")),
725            )
726            .property("tags", Object::with_type(BasicType::String).to_array());
727
728        #[cfg(not(feature = "preserve-order"))]
729        assert_eq!(
730            json_value.properties.keys().collect::<Vec<_>>(),
731            vec!["history", "id", "name", "status", "tags"]
732        );
733
734        #[cfg(feature = "preserve-order")]
735        assert_eq!(
736            json_value.properties.keys().collect::<Vec<_>>(),
737            vec!["id", "name", "status", "history", "tags"]
738        );
739    }
740
741    // Examples taken from https://spec.openapis.org/oas/latest.html#model-with-map-dictionary-properties
742    #[test]
743    fn test_additional_properties() {
744        let json_value =
745            Object::new().additional_properties(Object::new().schema_type(BasicType::String));
746        assert_json_eq!(
747            json_value,
748            json!({
749                "type": "object",
750                "additionalProperties": {
751                    "type": "string"
752                }
753            })
754        );
755
756        let json_value = Object::new().additional_properties(
757            Array::new().items(Object::new().schema_type(BasicType::Number)),
758        );
759        assert_json_eq!(
760            json_value,
761            json!({
762                "type": "object",
763                "additionalProperties": {
764                    "items": {
765                        "type": "number",
766                    },
767                    "type": "array",
768                }
769            })
770        );
771
772        let json_value = Object::new().additional_properties(Ref::from_schema_name("ComplexModel"));
773        assert_json_eq!(
774            json_value,
775            json!({
776                "type": "object",
777                "additionalProperties": {
778                    "$ref": "#/components/schemas/ComplexModel"
779                }
780            })
781        )
782    }
783
784    #[test]
785    fn test_object_with_name() {
786        let json_value = Object::new().name("SomeName");
787        assert_json_eq!(
788            json_value,
789            json!({
790                "type": "object",
791                "name": "SomeName"
792            })
793        );
794    }
795
796    #[test]
797    fn test_derive_object_with_examples() {
798        let expected = r#"{"type":"object","examples":[{"age":20,"name":"bob the cat"}]}"#;
799        let json_value = Object::new().examples([json!({"age": 20, "name": "bob the cat"})]);
800
801        let value_string = serde_json::to_string(&json_value).unwrap();
802        assert_eq!(
803            value_string, expected,
804            "value string != expected string, {value_string} != {expected}"
805        );
806    }
807
808    fn get_json_path<'a>(value: &'a Value, path: &str) -> &'a Value {
809        path.split('.').fold(value, |acc, fragment| {
810            acc.get(fragment).unwrap_or(&serde_json::value::Value::Null)
811        })
812    }
813
814    #[test]
815    fn test_array_new() {
816        let array = Array::new().items(
817            Object::new().property(
818                "id",
819                Object::new()
820                    .schema_type(BasicType::Integer)
821                    .format(SchemaFormat::KnownFormat(KnownFormat::Int32))
822                    .description("Id of credential")
823                    .default_value(json!(1i32)),
824            ),
825        );
826
827        assert!(matches!(
828            array.schema_type,
829            SchemaType::Basic(BasicType::Array)
830        ));
831    }
832
833    #[test]
834    fn test_array_builder() {
835        let array: Array = Array::new().items(
836            Object::new().property(
837                "id",
838                Object::new()
839                    .schema_type(BasicType::Integer)
840                    .format(SchemaFormat::KnownFormat(KnownFormat::Int32))
841                    .description("Id of credential")
842                    .default_value(json!(1i32)),
843            ),
844        );
845
846        assert!(matches!(
847            array.schema_type,
848            SchemaType::Basic(BasicType::Array)
849        ));
850    }
851
852    #[test]
853    fn reserialize_deserialized_schema_components() {
854        let components = Components::new()
855            .extend_schemas(vec![(
856                "Comp",
857                Schema::from(
858                    Object::new()
859                        .property("name", Object::new().schema_type(BasicType::String))
860                        .required("name"),
861                ),
862            )])
863            .response("204", Response::new("No Content"))
864            .extend_responses(vec![("200", Response::new("Okay"))])
865            .add_security_scheme("TLS", SecurityScheme::MutualTls { description: None })
866            .extend_security_schemes(vec![(
867                "APIKey",
868                SecurityScheme::Http(security::Http::default()),
869            )]);
870
871        let serialized_components = serde_json::to_string(&components).unwrap();
872
873        let deserialized_components: Components =
874            serde_json::from_str(serialized_components.as_str()).unwrap();
875
876        assert_eq!(
877            serialized_components,
878            serde_json::to_string(&deserialized_components).unwrap()
879        )
880    }
881
882    #[test]
883    fn reserialize_deserialized_object_component() {
884        let prop = Object::new()
885            .property("name", Object::new().schema_type(BasicType::String))
886            .required("name");
887
888        let serialized_components = serde_json::to_string(&prop).unwrap();
889        let deserialized_components: Object =
890            serde_json::from_str(serialized_components.as_str()).unwrap();
891
892        assert_eq!(
893            serialized_components,
894            serde_json::to_string(&deserialized_components).unwrap()
895        )
896    }
897
898    #[test]
899    fn reserialize_deserialized_property() {
900        let prop = Object::new().schema_type(BasicType::String);
901
902        let serialized_components = serde_json::to_string(&prop).unwrap();
903        let deserialized_components: Object =
904            serde_json::from_str(serialized_components.as_str()).unwrap();
905
906        assert_eq!(
907            serialized_components,
908            serde_json::to_string(&deserialized_components).unwrap()
909        )
910    }
911
912    #[test]
913    fn serialize_deserialize_array_within_ref_or_t_object_builder() {
914        let ref_or_schema = RefOr::Type(Schema::object(Object::new().property(
915            "test",
916            RefOr::Type(Schema::Array(Array::new().items(RefOr::Type(
917                Schema::object(Object::new().property("element", RefOr::Ref(Ref::new("#/test")))),
918            )))),
919        )));
920
921        let json_str = serde_json::to_string(&ref_or_schema).expect("");
922        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
923        let json_de_str = serde_json::to_string(&deserialized).expect("");
924        assert_eq!(json_str, json_de_str);
925    }
926
927    #[test]
928    fn serialize_deserialize_one_of_within_ref_or_t_object_builder() {
929        let ref_or_schema = RefOr::Type(Schema::object(
930            Object::new().property(
931                "test",
932                RefOr::Type(Schema::OneOf(
933                    OneOf::new()
934                        .item(Schema::Array(Array::new().items(RefOr::Type(
935                            Schema::object(
936                                Object::new().property("element", RefOr::Ref(Ref::new("#/test"))),
937                            ),
938                        ))))
939                        .item(Schema::Array(Array::new().items(RefOr::Type(
940                            Schema::object(
941                                Object::new().property("foobar", RefOr::Ref(Ref::new("#/foobar"))),
942                            ),
943                        )))),
944                )),
945            ),
946        ));
947
948        let json_str = serde_json::to_string(&ref_or_schema).expect("");
949        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
950        let json_de_str = serde_json::to_string(&deserialized).expect("");
951        assert_eq!(json_str, json_de_str);
952    }
953
954    #[test]
955    fn serialize_deserialize_all_of_of_within_ref_or_t_object() {
956        let ref_or_schema = RefOr::Type(Schema::object(
957            Object::new().property(
958                "test",
959                RefOr::Type(Schema::AllOf(
960                    AllOf::new()
961                        .item(Schema::Array(Array::new().items(RefOr::Type(
962                            Schema::object(
963                                Object::new().property("element", RefOr::Ref(Ref::new("#/test"))),
964                            ),
965                        ))))
966                        .item(RefOr::Type(Schema::object(
967                            Object::new().property("foobar", RefOr::Ref(Ref::new("#/foobar"))),
968                        ))),
969                )),
970            ),
971        ));
972
973        let json_str = serde_json::to_string(&ref_or_schema).expect("");
974        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
975        let json_de_str = serde_json::to_string(&deserialized).expect("");
976        assert_eq!(json_str, json_de_str);
977    }
978
979    #[test]
980    fn serialize_deserialize_any_of_of_within_ref_or_t_object() {
981        let ref_or_schema = RefOr::Type(Schema::object(
982            Object::new().property(
983                "test",
984                RefOr::Type(Schema::AnyOf(
985                    AnyOf::new()
986                        .item(Schema::Array(Array::new().items(RefOr::Type(
987                            Schema::object(
988                                Object::new().property("element", RefOr::Ref(Ref::new("#/test"))),
989                            ),
990                        ))))
991                        .item(RefOr::Type(Schema::object(
992                            Object::new().property("foobar", RefOr::Ref(Ref::new("#/foobar"))),
993                        ))),
994                )),
995            ),
996        ));
997
998        let json_str = serde_json::to_string(&ref_or_schema).expect("");
999        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
1000        let json_de_str = serde_json::to_string(&deserialized).expect("");
1001        assert!(json_str.contains("\"anyOf\""));
1002        assert_eq!(json_str, json_de_str);
1003    }
1004
1005    #[test]
1006    fn serialize_deserialize_schema_array_ref_or_t() {
1007        let ref_or_schema = RefOr::Type(Schema::Array(Array::new().items(RefOr::Type(
1008            Schema::Object(Box::new(
1009                Object::new().property("element", RefOr::Ref(Ref::new("#/test"))),
1010            )),
1011        ))));
1012
1013        let json_str = serde_json::to_string(&ref_or_schema).expect("");
1014        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
1015        let json_de_str = serde_json::to_string(&deserialized).expect("");
1016        assert_eq!(json_str, json_de_str);
1017    }
1018
1019    #[test]
1020    fn serialize_deserialize_schema_array() {
1021        let ref_or_schema = Array::new().items(RefOr::Type(Schema::object(
1022            Object::new().property("element", RefOr::Ref(Ref::new("#/test"))),
1023        )));
1024
1025        let json_str = serde_json::to_string(&ref_or_schema).expect("");
1026        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).expect("");
1027        let json_de_str = serde_json::to_string(&deserialized).expect("");
1028        assert_eq!(json_str, json_de_str);
1029    }
1030
1031    #[test]
1032    fn serialize_deserialize_schema_with_additional_properties() {
1033        let schema = Schema::object(Object::new().property(
1034            "map",
1035            Object::new().additional_properties(AdditionalProperties::FreeForm(true)),
1036        ));
1037
1038        let json_str = serde_json::to_string(&schema).unwrap();
1039        let deserialized: RefOr<Schema> = serde_json::from_str(&json_str).unwrap();
1040        let json_de_str = serde_json::to_string(&deserialized).unwrap();
1041        assert_eq!(json_str, json_de_str);
1042    }
1043
1044    #[test]
1045    fn serialize_deserialize_schema_with_additional_properties_object() {
1046        let schema = Schema::object(Object::new().property(
1047            "map",
1048            Object::new().additional_properties(
1049                Object::new().property("name", Object::with_type(BasicType::String)),
1050            ),
1051        ));
1052
1053        let json_str = serde_json::to_string(&schema).expect("serde json should success");
1054        let deserialized: RefOr<Schema> =
1055            serde_json::from_str(&json_str).expect("serde json should success");
1056        let json_de_str = serde_json::to_string(&deserialized).expect("serde json should success");
1057        assert_eq!(json_str, json_de_str);
1058    }
1059
1060    #[test]
1061    fn serialize_discriminator_with_mapping() {
1062        let mut discriminator = Discriminator::new("type");
1063        discriminator.mapping = [("int".to_owned(), "#/components/schemas/MyInt".to_owned())]
1064            .into_iter()
1065            .collect::<PropMap<_, _>>();
1066        let one_of = OneOf::new()
1067            .item(Ref::from_schema_name("MyInt"))
1068            .discriminator(discriminator);
1069        let json_value = serde_json::to_value(one_of).expect("serde json should success");
1070
1071        assert_json_eq!(
1072            json_value,
1073            json!({
1074                "oneOf": [
1075                    {
1076                        "$ref": "#/components/schemas/MyInt"
1077                    }
1078                ],
1079                "discriminator": {
1080                    "propertyName": "type",
1081                    "mapping": {
1082                        "int": "#/components/schemas/MyInt"
1083                    }
1084                }
1085            })
1086        );
1087    }
1088
1089    #[test]
1090    fn deserialize_reserialize_one_of_default_type() {
1091        let a = OneOf::new()
1092            .item(Schema::Array(Array::new().items(RefOr::Type(
1093                Schema::object(Object::new().property("element", RefOr::Ref(Ref::new("#/test")))),
1094            ))))
1095            .item(Schema::Array(Array::new().items(RefOr::Type(
1096                Schema::object(Object::new().property("foobar", RefOr::Ref(Ref::new("#/foobar")))),
1097            ))));
1098
1099        let serialized_json = serde_json::to_string(&a).expect("should serialize to json");
1100        let b: OneOf = serde_json::from_str(&serialized_json).expect("should deserialize OneOf");
1101        let reserialized_json = serde_json::to_string(&b).expect("reserialized json");
1102
1103        assert_eq!(serialized_json, reserialized_json);
1104    }
1105
1106    #[test]
1107    fn serialize_deserialize_object_with_multiple_schema_types() {
1108        let object =
1109            Object::new().schema_type(SchemaType::from_iter([BasicType::Object, BasicType::Null]));
1110
1111        let json_str = serde_json::to_string(&object).expect("serde json should success");
1112        let deserialized: Object =
1113            serde_json::from_str(&json_str).expect("serde json should success");
1114        let json_de_str = serde_json::to_string(&deserialized).expect("serde json should success");
1115        assert_eq!(json_str, json_de_str);
1116    }
1117
1118    #[test]
1119    fn test_empty_schema() {
1120        let schema = empty();
1121        assert_json_eq!(
1122            schema,
1123            json!({
1124                "default": null
1125            })
1126        )
1127    }
1128
1129    #[test]
1130    fn test_default_schema() {
1131        let schema = Schema::default();
1132        assert_json_eq!(
1133            schema,
1134            json!({
1135                "type": "object",
1136            })
1137        )
1138    }
1139
1140    #[test]
1141    fn test_ref_from_response_name() {
1142        let _ref = Ref::from_response_name("MyResponse");
1143        assert_json_eq!(
1144            _ref,
1145            json!({
1146                "$ref": "#/components/responses/MyResponse"
1147            })
1148        )
1149    }
1150
1151    #[test]
1152    fn test_additional_properties_from_ref_or() {
1153        let additional_properties =
1154            AdditionalProperties::from(RefOr::Type(Schema::Object(Box::default())));
1155        assert_json_eq!(
1156            additional_properties,
1157            json!({
1158                "type": "object",
1159            })
1160        )
1161    }
1162}