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