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 underying 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    pub fn new() -> Self {
158        Default::default()
159    }
160
161    /// Initialize new [`Object`] with given [`SchemaType`].
162    ///
163    /// Create [`std::string`] object type which can be used to define `string` field of an object.
164    /// ```
165    /// # use salvo_oapi::schema::{Object, BasicType};
166    /// let object = Object::with_type(BasicType::String);
167    /// ```
168    pub fn with_type<T: Into<SchemaType>>(schema_type: T) -> Self {
169        Self {
170            schema_type: schema_type.into(),
171            ..Default::default()
172        }
173    }
174
175    /// Add or change type of the object e.g. to change type to _`string`_
176    /// use value `SchemaType::Type(Type::String)`.
177    pub fn schema_type<T: Into<SchemaType>>(mut self, schema_type: T) -> Self {
178        self.schema_type = schema_type.into();
179        self
180    }
181
182    /// Add or change additional format for detailing the schema type.
183    pub fn format(mut self, format: SchemaFormat) -> Self {
184        self.format = Some(format);
185        self
186    }
187
188    /// Add new property to the [`Object`].
189    ///
190    /// Method accepts property name and property component as an arguments.
191    pub fn property<S: Into<String>, I: Into<RefOr<Schema>>>(
192        mut self,
193        property_name: S,
194        component: I,
195    ) -> Self {
196        self.properties
197            .insert(property_name.into(), component.into());
198
199        self
200    }
201
202    /// Add additional properties to the [`Object`].
203    pub fn additional_properties<I: Into<AdditionalProperties<Schema>>>(
204        mut self,
205        additional_properties: I,
206    ) -> Self {
207        self.additional_properties = Some(Box::new(additional_properties.into()));
208        self
209    }
210
211    /// Add field to the required fields of [`Object`].
212    pub fn required(mut self, required_field: impl Into<String>) -> Self {
213        self.required.insert(required_field.into());
214        self
215    }
216
217    /// Add or change the name of the [`Object`].
218    pub fn name(mut self, name: impl Into<String>) -> Self {
219        self.name = Some(name.into());
220        self
221    }
222
223    /// Add or change description of the property. Markdown syntax is supported.
224    pub fn description(mut self, description: impl Into<String>) -> Self {
225        self.description = Some(description.into());
226        self
227    }
228
229    /// Add or change default value for the object which is provided when user has not provided the input in Swagger UI.
230    pub fn default_value(mut self, default: Value) -> Self {
231        self.default_value = Some(default);
232        self
233    }
234
235    /// Add or change deprecated status for [`Object`].
236    pub fn deprecated(mut self, deprecated: Deprecated) -> Self {
237        self.deprecated = Some(deprecated);
238        self
239    }
240
241    /// Add or change enum property variants.
242    pub fn enum_values<I, E>(mut self, enum_values: I) -> Self
243    where
244        I: IntoIterator<Item = E>,
245        E: Into<Value>,
246    {
247        self.enum_values = enum_values
248            .into_iter()
249            .map(|enum_value| enum_value.into())
250            .collect();
251        self
252    }
253
254    /// Add or change example shown in UI of the value for richer documentation.
255    pub fn example<V: Into<Value>>(mut self, example: V) -> Self {
256        self.examples.push(example.into());
257        self
258    }
259
260    /// Add or change examples shown in UI of the value for richer documentation.
261    pub fn examples<I: IntoIterator<Item = V>, V: Into<Value>>(mut self, examples: I) -> Self {
262        self.examples = examples.into_iter().map(Into::into).collect();
263        self
264    }
265
266    /// Add or change write only flag for [`Object`].
267    pub fn write_only(mut self, write_only: bool) -> Self {
268        self.write_only = Some(write_only);
269        self
270    }
271
272    /// Add or change read only flag for [`Object`].
273    pub fn read_only(mut self, read_only: bool) -> Self {
274        self.read_only = Some(read_only);
275        self
276    }
277
278    /// Add or change additional [`Xml`] formatting of the [`Object`].
279    pub fn xml(mut self, xml: Xml) -> Self {
280        self.xml = Some(xml);
281        self
282    }
283
284    /// Set or change _`multiple_of`_ validation flag for `number` and `integer` type values.
285    pub fn multiple_of(mut self, multiple_of: f64) -> Self {
286        self.multiple_of = Some(multiple_of);
287        self
288    }
289
290    /// Set or change inclusive maximum value for `number` and `integer` values.
291    pub fn maximum(mut self, maximum: f64) -> Self {
292        self.maximum = Some(maximum);
293        self
294    }
295
296    /// Set or change inclusive minimum value for `number` and `integer` values.
297    pub fn minimum(mut self, minimum: f64) -> Self {
298        self.minimum = Some(minimum);
299        self
300    }
301
302    /// Set or change exclusive maximum value for `number` and `integer` values.
303    pub fn exclusive_maximum(mut self, exclusive_maximum: f64) -> Self {
304        self.exclusive_maximum = Some(exclusive_maximum);
305        self
306    }
307
308    /// Set or change exclusive minimum value for `number` and `integer` values.
309    pub fn exclusive_minimum(mut self, exclusive_minimum: f64) -> Self {
310        self.exclusive_minimum = Some(exclusive_minimum);
311        self
312    }
313
314    /// Set or change maximum length for `string` values.
315    pub fn max_length(mut self, max_length: usize) -> Self {
316        self.max_length = Some(max_length);
317        self
318    }
319
320    /// Set or change minimum length for `string` values.
321    pub fn min_length(mut self, min_length: usize) -> Self {
322        self.min_length = Some(min_length);
323        self
324    }
325
326    /// Set or change a valid regular expression for `string` value to match.
327    pub fn pattern<I: Into<String>>(mut self, pattern: I) -> Self {
328        self.pattern = Some(pattern.into());
329        self
330    }
331
332    /// Set or change maximum number of properties the [`Object`] can hold.
333    pub fn max_properties(mut self, max_properties: usize) -> Self {
334        self.max_properties = Some(max_properties);
335        self
336    }
337
338    /// Set or change minimum number of properties the [`Object`] can hold.
339    pub fn min_properties(mut self, min_properties: usize) -> Self {
340        self.min_properties = Some(min_properties);
341        self
342    }
343
344    /// Add openapi extension (`x-something`) for [`Object`].
345    pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
346        self.extensions.insert(key.into(), value);
347        self
348    }
349
350    /// Set of change [`Object::content_encoding`]. Typically left empty but could be `base64` for
351    /// example.
352    pub fn content_encoding<S: Into<String>>(mut self, content_encoding: S) -> Self {
353        self.content_encoding = content_encoding.into();
354        self
355    }
356
357    /// Set of change [`Object::content_media_type`]. Value must be valid MIME type e.g.
358    /// `application/json`.
359    pub fn content_media_type<S: Into<String>>(mut self, content_media_type: S) -> Self {
360        self.content_media_type = content_media_type.into();
361        self
362    }
363}
364
365impl From<Object> for Schema {
366    fn from(s: Object) -> Self {
367        Self::Object(Box::new(s))
368    }
369}
370
371impl ToArray for Object {}
372
373impl From<Object> for RefOr<Schema> {
374    fn from(obj: Object) -> Self {
375        Self::Type(Schema::Object(Box::new(obj)))
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use assert_json_diff::assert_json_eq;
382    use serde_json::json;
383
384    use super::*;
385    use crate::BasicType;
386
387    #[test]
388    fn test_build_string_object() {
389        let object = Object::new()
390            .schema_type(BasicType::String)
391            .deprecated(Deprecated::True)
392            .write_only(false)
393            .read_only(true)
394            .xml(Xml::new())
395            .max_length(10)
396            .min_length(1)
397            .pattern(r"^[a-z]+$");
398
399        assert_json_eq!(
400            object,
401            json!({
402                "type": "string",
403                "deprecated": true,
404                "readOnly": true,
405                "writeOnly": false,
406                "xml": {},
407                "minLength": 1,
408                "maxLength": 10,
409                "pattern": "^[a-z]+$"
410            })
411        );
412    }
413
414    #[test]
415    fn test_build_number_object() {
416        let object = Object::new()
417            .schema_type(BasicType::Number)
418            .deprecated(Deprecated::True)
419            .write_only(false)
420            .read_only(true)
421            .xml(Xml::new())
422            .multiple_of(10.0)
423            .minimum(0.0)
424            .maximum(1000.0)
425            .exclusive_minimum(0.0)
426            .exclusive_maximum(1000.0);
427
428        assert_json_eq!(
429            object,
430            json!({
431                "type": "number",
432                "deprecated": true,
433                "readOnly": true,
434                "writeOnly": false,
435                "xml": {},
436                "multipleOf": 10.0,
437                "minimum": 0.0,
438                "maximum": 1000.0,
439                "exclusiveMinimum": 0.0,
440                "exclusiveMaximum": 1000.0
441            })
442        );
443    }
444
445    #[test]
446    fn test_build_object_object() {
447        let object = Object::new()
448            .schema_type(BasicType::Object)
449            .deprecated(Deprecated::True)
450            .write_only(false)
451            .read_only(true)
452            .xml(Xml::new())
453            .min_properties(1)
454            .max_properties(10);
455
456        assert_json_eq!(
457            object,
458            json!({
459                "type": "object",
460                "deprecated": true,
461                "readOnly": true,
462                "writeOnly": false,
463                "xml": {},
464                "minProperties": 1,
465                "maxProperties": 10
466            })
467        );
468    }
469
470    #[test]
471    fn test_object_with_extensions() {
472        let expected = json!("value");
473        let json_value = Object::new().add_extension("x-some-extension", expected.clone());
474
475        let value = serde_json::to_value(&json_value).unwrap();
476        assert_eq!(value.get("x-some-extension"), Some(&expected));
477    }
478}