salvo_oapi/openapi/schema/
object.rs

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