Skip to main content

salvo_oapi/openapi/schema/
object.rs

1use indexmap::IndexSet;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5use super::AdditionalProperties;
6use super::number::Number;
7use crate::{Array, Deprecated, PropMap, RefOr, Schema, SchemaFormat, SchemaType, Xml};
8
9/// Implements subset of [OpenAPI Schema Object][schema] which allows
10/// adding other [`Schema`]s as **properties** to this [`Schema`].
11///
12/// This is a generic OpenAPI schema object which can used to present `object`, `field` or an
13/// `enum`.
14///
15/// [schema]: https://spec.openapis.org/oas/latest.html#schema-object
16#[non_exhaustive]
17#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
18#[serde(rename_all = "camelCase")]
19pub struct Object {
20    /// Type of [`Object`] e.g. [`BasicType::Object`][crate::BasicType] for `object` and
21    /// [`BasicType::String`][crate::BasicType] for `string` types.
22    #[serde(rename = "type", skip_serializing_if = "SchemaType::is_any_value")]
23    pub schema_type: SchemaType,
24
25    /// Changes the [`Object`] name.
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub name: Option<String>,
28
29    /// Additional format for detailing the schema type.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub format: Option<SchemaFormat>,
32
33    /// Description of the [`Object`]. Markdown syntax is supported.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub description: Option<String>,
36
37    /// Default value which is provided when user has not provided the input in Swagger UI.
38    #[serde(rename = "default", skip_serializing_if = "Option::is_none")]
39    pub default_value: Option<Value>,
40
41    /// Enum variants of fields that can be represented as `unit` type `enums`
42    #[serde(default, rename = "enum", skip_serializing_if = "Option::is_none")]
43    pub enum_values: Option<Vec<Value>>,
44
45    /// Vector of required field names.
46    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
47    pub required: IndexSet<String>,
48
49    /// Map of fields with their [`Schema`] types.
50    ///
51    /// With **preserve-order** feature flag [`indexmap::IndexMap`] will be used as
52    /// properties map backing implementation to retain property order of [`ToSchema`][to_schema].
53    /// By default [`PropMap`] will be used.
54    ///
55    /// [to_schema]: crate::ToSchema
56    #[serde(default, skip_serializing_if = "PropMap::is_empty")]
57    pub properties: PropMap<String, RefOr<Schema>>,
58
59    /// Additional [`Schema`] for non specified fields (Useful for typed maps).
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub additional_properties: Option<Box<AdditionalProperties<Schema>>>,
62
63    /// Schema to describe property names of an object such as a map.
64    /// See <https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#name-propertynames>
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub property_names: Option<Box<RefOr<Schema>>>,
67
68    /// Changes the [`Object`] deprecated status.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub deprecated: Option<Deprecated>,
71
72    /// Examples shown in UI of the value for richer documentation.
73    #[serde(skip_serializing_if = "Vec::is_empty", default)]
74    pub examples: Vec<Value>,
75
76    /// Write only property will be only sent in _write_ requests like _POST, PUT_.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub write_only: Option<bool>,
79
80    /// Read only property will be only sent in _read_ requests like _GET_.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub read_only: Option<bool>,
83
84    /// Additional [`Xml`] formatting of the [`Object`].
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub xml: Option<Xml>,
87
88    /// Must be a number strictly greater than `0`. Numeric value is considered valid if value
89    /// divided by the _`multiple_of`_ value results an integer.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub multiple_of: Option<Number>,
92
93    /// Specify inclusive upper limit for the [`Object`]'s value. Number is considered valid if
94    /// it is equal or less than the _`maximum`_.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub maximum: Option<Number>,
97
98    /// Specify inclusive lower limit for the [`Object`]'s value. Number value is considered
99    /// valid if it is equal or greater than the _`minimum`_.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub minimum: Option<Number>,
102
103    /// Specify exclusive upper limit for the [`Object`]'s value. Number value is considered
104    /// valid if it is strictly less than _`exclusive_maximum`_.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub exclusive_maximum: Option<Number>,
107
108    /// Specify exclusive lower limit for the [`Object`]'s value. Number value is considered
109    /// valid if it is strictly above the _`exclusive_minimum`_.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub exclusive_minimum: Option<Number>,
112
113    /// Specify maximum length for `string` values. _`max_length`_ cannot be a negative integer
114    /// value. Value is considered valid if content length is equal or less than the
115    /// _`max_length`_.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub max_length: Option<usize>,
118
119    /// Specify minimum length for `string` values. _`min_length`_ cannot be a negative integer
120    /// value. Setting this to _`0`_ has the same effect as omitting this field. Value is
121    /// considered valid if content length is equal or more than the _`min_length`_.
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub min_length: Option<usize>,
124
125    /// Define a valid `ECMA-262` dialect regular expression. The `string` content is
126    /// considered valid if the _`pattern`_ matches the value successfully.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub pattern: Option<String>,
129
130    /// Specify inclusive maximum amount of properties an [`Object`] can hold.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub max_properties: Option<usize>,
133
134    /// Specify inclusive minimum amount of properties an [`Object`] can hold. Setting this to
135    /// `0` will have same effect as omitting the attribute.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub min_properties: Option<usize>,
138
139    /// Optional extensions `x-something`.
140    #[serde(default, skip_serializing_if = "PropMap::is_empty", flatten)]
141    pub extensions: PropMap<String, serde_json::Value>,
142
143    /// The `content_encoding` keyword specifies the encoding used to store the contents, as
144    /// specified in [RFC 2054, part 6.1](https://tools.ietf.org/html/rfc2045) and [RFC 4648](RFC 2054, part 6.1).
145    ///
146    /// Typically this is either unset for _`string`_ content types which then uses the content
147    /// encoding of the underlying JSON document. If the content is in _`binary`_ format such as an
148    /// image or an audio set it to `base64` to encode it as _`Base64`_.
149    ///
150    /// See more details at <https://json-schema.org/understanding-json-schema/reference/non_json_data#contentencoding>
151    #[serde(skip_serializing_if = "String::is_empty", default)]
152    pub content_encoding: String,
153
154    /// The _`content_media_type`_ keyword specifies the MIME type of the contents of a string,
155    /// as described in [RFC 2046](https://tools.ietf.org/html/rfc2046).
156    ///
157    /// See more details at <https://json-schema.org/understanding-json-schema/reference/non_json_data#contentmediatype>
158    #[serde(skip_serializing_if = "String::is_empty", default)]
159    pub content_media_type: String,
160}
161
162impl Object {
163    /// Initialize a new [`Object`] with default [`SchemaType`]. This effectively same as calling
164    /// `Object::with_type(SchemaType::Object)`.
165    #[must_use]
166    pub fn new() -> Self {
167        Default::default()
168    }
169
170    /// Initialize new [`Object`] with given [`SchemaType`].
171    ///
172    /// Create [`std::string`] object type which can be used to define `string` field of an object.
173    /// ```
174    /// # use salvo_oapi::schema::{Object, BasicType};
175    /// let object = Object::with_type(BasicType::String);
176    /// ```
177    #[must_use]
178    pub fn with_type<T: Into<SchemaType>>(schema_type: T) -> Self {
179        Self {
180            schema_type: schema_type.into(),
181            ..Default::default()
182        }
183    }
184
185    /// Add or change type of the object e.g. to change type to _`string`_
186    /// use value `SchemaType::Type(Type::String)`.
187    #[must_use]
188    pub fn schema_type<T: Into<SchemaType>>(mut self, schema_type: T) -> Self {
189        self.schema_type = schema_type.into();
190        self
191    }
192
193    /// Add or change additional format for detailing the schema type.
194    #[must_use]
195    pub fn format(mut self, format: SchemaFormat) -> Self {
196        self.format = Some(format);
197        self
198    }
199
200    /// Add new property to the [`Object`].
201    ///
202    /// Method accepts property name and property component as an arguments.
203    #[must_use]
204    pub fn property<S: Into<String>, I: Into<RefOr<Schema>>>(
205        mut self,
206        property_name: S,
207        component: I,
208    ) -> Self {
209        self.properties
210            .insert(property_name.into(), component.into());
211
212        self
213    }
214
215    /// Add additional properties to the [`Object`].
216    #[must_use]
217    pub fn additional_properties<I: Into<AdditionalProperties<Schema>>>(
218        mut self,
219        additional_properties: I,
220    ) -> Self {
221        self.additional_properties = Some(Box::new(additional_properties.into()));
222        self
223    }
224
225    /// Add [`Schema`] to describe property names of an object such as a map.
226    /// See <https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#name-propertynames>
227    #[must_use]
228    pub fn property_names(mut self, property_names: impl Into<RefOr<Schema>>) -> Self {
229        self.property_names = Some(Box::new(property_names.into()));
230        self
231    }
232
233    /// Add field to the required fields of [`Object`].
234    #[must_use]
235    pub fn required(mut self, required_field: impl Into<String>) -> Self {
236        self.required.insert(required_field.into());
237        self
238    }
239
240    /// Add or change the name of the [`Object`].
241    #[must_use]
242    pub fn name(mut self, name: impl Into<String>) -> Self {
243        self.name = Some(name.into());
244        self
245    }
246
247    /// Add or change description of the property. Markdown syntax is supported.
248    #[must_use]
249    pub fn description(mut self, description: impl Into<String>) -> Self {
250        self.description = Some(description.into());
251        self
252    }
253
254    /// Add or change default value for the object which is provided when user has not provided the
255    /// input in Swagger UI.
256    #[must_use]
257    pub fn default_value(mut self, default: Value) -> Self {
258        self.default_value = Some(default);
259        self
260    }
261
262    /// Add or change deprecated status for [`Object`].
263    #[must_use]
264    pub fn deprecated(mut self, deprecated: Deprecated) -> Self {
265        self.deprecated = Some(deprecated);
266        self
267    }
268
269    /// Add or change enum property variants.
270    #[must_use]
271    pub fn enum_values<I, E>(mut self, enum_values: I) -> Self
272    where
273        I: IntoIterator<Item = E>,
274        E: Into<Value>,
275    {
276        self.enum_values = Some(
277            enum_values
278                .into_iter()
279                .map(|enum_value| enum_value.into())
280                .collect(),
281        );
282        self
283    }
284
285    /// Add or change example shown in UI of the value for richer documentation.
286    #[must_use]
287    pub fn example<V: Into<Value>>(mut self, example: V) -> Self {
288        self.examples.push(example.into());
289        self
290    }
291
292    /// Add or change examples shown in UI of the value for richer documentation.
293    #[must_use]
294    pub fn examples<I: IntoIterator<Item = V>, V: Into<Value>>(mut self, examples: I) -> Self {
295        self.examples = examples.into_iter().map(Into::into).collect();
296        self
297    }
298
299    /// Add or change write only flag for [`Object`].
300    #[must_use]
301    pub fn write_only(mut self, write_only: bool) -> Self {
302        self.write_only = Some(write_only);
303        self
304    }
305
306    /// Add or change read only flag for [`Object`].
307    #[must_use]
308    pub fn read_only(mut self, read_only: bool) -> Self {
309        self.read_only = Some(read_only);
310        self
311    }
312
313    /// Add or change additional [`Xml`] formatting of the [`Object`].
314    #[must_use]
315    pub fn xml(mut self, xml: Xml) -> Self {
316        self.xml = Some(xml);
317        self
318    }
319
320    /// Set or change _`multiple_of`_ validation flag for `number` and `integer` type values.
321    #[must_use]
322    pub fn multiple_of<N: Into<Number>>(mut self, multiple_of: N) -> Self {
323        self.multiple_of = Some(multiple_of.into());
324        self
325    }
326
327    /// Set or change inclusive maximum value for `number` and `integer` values.
328    #[must_use]
329    pub fn maximum<N: Into<Number>>(mut self, maximum: N) -> Self {
330        self.maximum = Some(maximum.into());
331        self
332    }
333
334    /// Set or change inclusive minimum value for `number` and `integer` values.
335    #[must_use]
336    pub fn minimum<N: Into<Number>>(mut self, minimum: N) -> Self {
337        self.minimum = Some(minimum.into());
338        self
339    }
340
341    /// Set or change exclusive maximum value for `number` and `integer` values.
342    #[must_use]
343    pub fn exclusive_maximum<N: Into<Number>>(mut self, exclusive_maximum: N) -> Self {
344        self.exclusive_maximum = Some(exclusive_maximum.into());
345        self
346    }
347
348    /// Set or change exclusive minimum value for `number` and `integer` values.
349    #[must_use]
350    pub fn exclusive_minimum<N: Into<Number>>(mut self, exclusive_minimum: N) -> Self {
351        self.exclusive_minimum = Some(exclusive_minimum.into());
352        self
353    }
354
355    /// Set or change maximum length for `string` values.
356    #[must_use]
357    pub fn max_length(mut self, max_length: usize) -> Self {
358        self.max_length = Some(max_length);
359        self
360    }
361
362    /// Set or change minimum length for `string` values.
363    #[must_use]
364    pub fn min_length(mut self, min_length: usize) -> Self {
365        self.min_length = Some(min_length);
366        self
367    }
368
369    /// Set or change a valid regular expression for `string` value to match.
370    #[must_use]
371    pub fn pattern<I: Into<String>>(mut self, pattern: I) -> Self {
372        self.pattern = Some(pattern.into());
373        self
374    }
375
376    /// Set or change maximum number of properties the [`Object`] can hold.
377    #[must_use]
378    pub fn max_properties(mut self, max_properties: usize) -> Self {
379        self.max_properties = Some(max_properties);
380        self
381    }
382
383    /// Set or change minimum number of properties the [`Object`] can hold.
384    #[must_use]
385    pub fn min_properties(mut self, min_properties: usize) -> Self {
386        self.min_properties = Some(min_properties);
387        self
388    }
389
390    /// Add openapi extension (`x-something`) for [`Object`].
391    #[must_use]
392    pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
393        self.extensions.insert(key.into(), value);
394        self
395    }
396
397    /// Set of change [`Object::content_encoding`]. Typically left empty but could be `base64` for
398    /// example.
399    #[must_use]
400    pub fn content_encoding<S: Into<String>>(mut self, content_encoding: S) -> Self {
401        self.content_encoding = content_encoding.into();
402        self
403    }
404
405    /// Set of change [`Object::content_media_type`]. Value must be valid MIME type e.g.
406    /// `application/json`.
407    #[must_use]
408    pub fn content_media_type<S: Into<String>>(mut self, content_media_type: S) -> Self {
409        self.content_media_type = content_media_type.into();
410        self
411    }
412
413    /// Convert type to [`Array`].
414    #[must_use]
415    pub fn to_array(self) -> Array {
416        Array::new().items(self)
417    }
418}
419
420impl From<Object> for Schema {
421    fn from(s: Object) -> Self {
422        Self::Object(Box::new(s))
423    }
424}
425
426// impl ToArray for Object {}
427
428impl From<Object> for RefOr<Schema> {
429    fn from(obj: Object) -> Self {
430        Self::Type(Schema::Object(Box::new(obj)))
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use assert_json_diff::assert_json_eq;
437    use serde_json::json;
438
439    use super::*;
440    use crate::BasicType;
441
442    #[test]
443    fn test_build_string_object() {
444        let object = Object::new()
445            .schema_type(BasicType::String)
446            .deprecated(Deprecated::True)
447            .write_only(false)
448            .read_only(true)
449            .xml(Xml::new())
450            .max_length(10)
451            .min_length(1)
452            .pattern(r"^[a-z]+$");
453
454        assert_json_eq!(
455            object,
456            json!({
457                "type": "string",
458                "deprecated": true,
459                "readOnly": true,
460                "writeOnly": false,
461                "xml": {},
462                "minLength": 1,
463                "maxLength": 10,
464                "pattern": "^[a-z]+$"
465            })
466        );
467    }
468
469    #[test]
470    fn test_build_number_object() {
471        let object = Object::new()
472            .schema_type(BasicType::Number)
473            .deprecated(Deprecated::True)
474            .write_only(false)
475            .read_only(true)
476            .xml(Xml::new())
477            .multiple_of(10.0)
478            .minimum(0.0)
479            .maximum(1000.0)
480            .exclusive_minimum(0.0)
481            .exclusive_maximum(1000.0);
482
483        assert_json_eq!(
484            object,
485            json!({
486                "type": "number",
487                "deprecated": true,
488                "readOnly": true,
489                "writeOnly": false,
490                "xml": {},
491                "multipleOf": 10,
492                "minimum": 0,
493                "maximum": 1000,
494                "exclusiveMinimum": 0,
495                "exclusiveMaximum": 1000
496            })
497        );
498    }
499
500    #[test]
501    fn test_build_object_object() {
502        let object = Object::new()
503            .schema_type(BasicType::Object)
504            .deprecated(Deprecated::True)
505            .write_only(false)
506            .read_only(true)
507            .xml(Xml::new())
508            .min_properties(1)
509            .max_properties(10);
510
511        assert_json_eq!(
512            object,
513            json!({
514                "type": "object",
515                "deprecated": true,
516                "readOnly": true,
517                "writeOnly": false,
518                "xml": {},
519                "minProperties": 1,
520                "maxProperties": 10
521            })
522        );
523    }
524
525    #[test]
526    fn test_object_with_extensions() {
527        let expected = json!("value");
528        let json_value = Object::new().add_extension("x-some-extension", expected.clone());
529
530        let value = serde_json::to_value(&json_value).unwrap();
531        assert_eq!(value.get("x-some-extension"), Some(&expected));
532    }
533}