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