Skip to main content

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