salvo_oapi/openapi/schema/
object.rs

1use indexmap::IndexSet;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5use super::AdditionalProperties;
6use crate::{Deprecated, PropMap, RefOr, Schema, SchemaFormat, SchemaType, ToArray, 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. [`Type::Object`] for `object` and [`Type::String`] 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
395impl From<Object> for Schema {
396    fn from(s: Object) -> Self {
397        Self::Object(Box::new(s))
398    }
399}
400
401impl ToArray for Object {}
402
403impl From<Object> for RefOr<Schema> {
404    fn from(obj: Object) -> Self {
405        Self::Type(Schema::Object(Box::new(obj)))
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use assert_json_diff::assert_json_eq;
412    use serde_json::json;
413
414    use super::*;
415    use crate::BasicType;
416
417    #[test]
418    fn test_build_string_object() {
419        let object = Object::new()
420            .schema_type(BasicType::String)
421            .deprecated(Deprecated::True)
422            .write_only(false)
423            .read_only(true)
424            .xml(Xml::new())
425            .max_length(10)
426            .min_length(1)
427            .pattern(r"^[a-z]+$");
428
429        assert_json_eq!(
430            object,
431            json!({
432                "type": "string",
433                "deprecated": true,
434                "readOnly": true,
435                "writeOnly": false,
436                "xml": {},
437                "minLength": 1,
438                "maxLength": 10,
439                "pattern": "^[a-z]+$"
440            })
441        );
442    }
443
444    #[test]
445    fn test_build_number_object() {
446        let object = Object::new()
447            .schema_type(BasicType::Number)
448            .deprecated(Deprecated::True)
449            .write_only(false)
450            .read_only(true)
451            .xml(Xml::new())
452            .multiple_of(10.0)
453            .minimum(0.0)
454            .maximum(1000.0)
455            .exclusive_minimum(0.0)
456            .exclusive_maximum(1000.0);
457
458        assert_json_eq!(
459            object,
460            json!({
461                "type": "number",
462                "deprecated": true,
463                "readOnly": true,
464                "writeOnly": false,
465                "xml": {},
466                "multipleOf": 10.0,
467                "minimum": 0.0,
468                "maximum": 1000.0,
469                "exclusiveMinimum": 0.0,
470                "exclusiveMaximum": 1000.0
471            })
472        );
473    }
474
475    #[test]
476    fn test_build_object_object() {
477        let object = Object::new()
478            .schema_type(BasicType::Object)
479            .deprecated(Deprecated::True)
480            .write_only(false)
481            .read_only(true)
482            .xml(Xml::new())
483            .min_properties(1)
484            .max_properties(10);
485
486        assert_json_eq!(
487            object,
488            json!({
489                "type": "object",
490                "deprecated": true,
491                "readOnly": true,
492                "writeOnly": false,
493                "xml": {},
494                "minProperties": 1,
495                "maxProperties": 10
496            })
497        );
498    }
499
500    #[test]
501    fn test_object_with_extensions() {
502        let expected = json!("value");
503        let json_value = Object::new().add_extension("x-some-extension", expected.clone());
504
505        let value = serde_json::to_value(&json_value).unwrap();
506        assert_eq!(value.get("x-some-extension"), Some(&expected));
507    }
508}