Skip to main content

apistos_schemars/
visit.rs

1/*!
2Contains the [`Visitor`] trait, used to recursively modify a constructed schema and its subschemas.
3
4Sometimes you may want to apply a change to a schema, as well as all schemas contained within it.
5The easiest way to achieve this is by defining a type that implements [`Visitor`].
6All methods of `Visitor` have a default implementation that makes no change but recursively visits all subschemas.
7When overriding one of these methods, you will *usually* want to still call this default implementation.
8
9# Example
10To add a custom property to all schemas:
11```
12# extern crate apistos_schemars as schemars;
13use schemars::schema::SchemaObject;
14use schemars::visit::{Visitor, visit_schema_object};
15
16pub struct MyVisitor;
17
18impl Visitor for MyVisitor {
19    fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
20        // First, make our change to this schema
21        schema.extensions.insert("my_property".to_string(), serde_json::json!("hello world"));
22
23        // Then delegate to default implementation to visit any subschemas
24        visit_schema_object(self, schema);
25    }
26}
27```
28*/
29use crate::schema::{RootSchema, Schema, SchemaObject, SingleOrVec};
30
31/// Trait used to recursively modify a constructed schema and its subschemas.
32pub trait Visitor {
33    /// Override this method to modify a [`RootSchema`] and (optionally) its subschemas.
34    ///
35    /// When overriding this method, you will usually want to call the [`visit_root_schema`] function to visit subschemas.
36    fn visit_root_schema(&mut self, root: &mut RootSchema) {
37        visit_root_schema(self, root)
38    }
39
40    /// Override this method to modify a [`Schema`] and (optionally) its subschemas.
41    ///
42    /// When overriding this method, you will usually want to call the [`visit_schema`] function to visit subschemas.
43    fn visit_schema(&mut self, schema: &mut Schema) {
44        visit_schema(self, schema)
45    }
46
47    /// Override this method to modify a [`SchemaObject`] and (optionally) its subschemas.
48    ///
49    /// When overriding this method, you will usually want to call the [`visit_schema_object`] function to visit subschemas.
50    fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
51        visit_schema_object(self, schema)
52    }
53}
54
55/// Visits all subschemas of the [`RootSchema`].
56pub fn visit_root_schema<V: Visitor + ?Sized>(v: &mut V, root: &mut RootSchema) {
57    v.visit_schema_object(&mut root.schema);
58    visit_map_values(v, &mut root.definitions);
59}
60
61/// Visits all subschemas of the [`Schema`].
62pub fn visit_schema<V: Visitor + ?Sized>(v: &mut V, schema: &mut Schema) {
63    if let Schema::Object(schema) = schema {
64        v.visit_schema_object(schema)
65    }
66}
67
68/// Visits all subschemas of the [`SchemaObject`].
69pub fn visit_schema_object<V: Visitor + ?Sized>(v: &mut V, schema: &mut SchemaObject) {
70    if let Some(sub) = &mut schema.subschemas {
71        visit_vec(v, &mut sub.all_of);
72        visit_vec(v, &mut sub.any_of);
73        visit_vec(v, &mut sub.one_of);
74        visit_box(v, &mut sub.not);
75        visit_box(v, &mut sub.if_schema);
76        visit_box(v, &mut sub.then_schema);
77        visit_box(v, &mut sub.else_schema);
78    }
79
80    if let Some(arr) = &mut schema.array {
81        visit_single_or_vec(v, &mut arr.items);
82        visit_box(v, &mut arr.additional_items);
83        visit_box(v, &mut arr.contains);
84    }
85
86    if let Some(obj) = &mut schema.object {
87        visit_map_values(v, &mut obj.properties);
88        visit_map_values(v, &mut obj.pattern_properties);
89        visit_box(v, &mut obj.additional_properties);
90        visit_box(v, &mut obj.property_names);
91    }
92}
93
94fn visit_box<V: Visitor + ?Sized>(v: &mut V, target: &mut Option<Box<Schema>>) {
95    if let Some(s) = target {
96        v.visit_schema(s)
97    }
98}
99
100fn visit_vec<V: Visitor + ?Sized>(v: &mut V, target: &mut Option<Vec<Schema>>) {
101    if let Some(vec) = target {
102        for s in vec {
103            v.visit_schema(s)
104        }
105    }
106}
107
108fn visit_map_values<V: Visitor + ?Sized>(v: &mut V, target: &mut crate::Map<String, Schema>) {
109    for s in target.values_mut() {
110        v.visit_schema(s)
111    }
112}
113
114fn visit_single_or_vec<V: Visitor + ?Sized>(v: &mut V, target: &mut Option<SingleOrVec<Schema>>) {
115    match target {
116        None => {}
117        Some(SingleOrVec::Single(s)) => v.visit_schema(s),
118        Some(SingleOrVec::Vec(vec)) => {
119            for s in vec {
120                v.visit_schema(s)
121            }
122        }
123    }
124}
125
126/// This visitor will replace all boolean JSON Schemas with equivalent object schemas.
127///
128/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support booleans as schemas.
129#[derive(Debug, Clone)]
130pub struct ReplaceBoolSchemas {
131    /// When set to `true`, a schema's `additionalProperties` property will not be changed from a boolean.
132    pub skip_additional_properties: bool,
133}
134
135impl Visitor for ReplaceBoolSchemas {
136    fn visit_schema(&mut self, schema: &mut Schema) {
137        visit_schema(self, schema);
138
139        if let Schema::Bool(b) = *schema {
140            *schema = Schema::Bool(b).into_object().into()
141        }
142    }
143
144    fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
145        if self.skip_additional_properties {
146            if let Some(obj) = &mut schema.object {
147                if let Some(ap) = &obj.additional_properties {
148                    if let Schema::Bool(_) = ap.as_ref() {
149                        let additional_properties = obj.additional_properties.take();
150                        visit_schema_object(self, schema);
151                        schema.object().additional_properties = additional_properties;
152
153                        return;
154                    }
155                }
156            }
157        }
158
159        visit_schema_object(self, schema);
160    }
161}
162
163/// This visitor will restructure JSON Schema objects so that the `$ref` property will never appear alongside any other properties.
164///
165/// This is useful for dialects of JSON Schema (e.g. Draft 7) that do not support other properties alongside `$ref`.
166#[derive(Debug, Clone)]
167pub struct RemoveRefSiblings;
168
169impl Visitor for RemoveRefSiblings {
170    fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
171        visit_schema_object(self, schema);
172
173        if let Some(reference) = schema.reference.take() {
174            if schema == &SchemaObject::default() {
175                schema.reference = Some(reference);
176            } else {
177                let ref_schema = Schema::new_ref(reference);
178                let all_of = &mut schema.subschemas().all_of;
179                match all_of {
180                    Some(vec) => vec.push(ref_schema),
181                    None => *all_of = Some(vec![ref_schema]),
182                }
183            }
184        }
185    }
186}
187
188/// This visitor will remove the `examples` schema property and (if present) set its first value as the `example` property.
189///
190/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `examples` property.
191#[derive(Debug, Clone)]
192pub struct SetSingleExample {
193    /// When set to `true`, the `examples` property will not be removed, but its first value will still be copied to `example`.
194    pub retain_examples: bool,
195}
196
197impl Visitor for SetSingleExample {
198    fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
199        visit_schema_object(self, schema);
200
201        let first_example = schema.metadata.as_mut().and_then(|m| {
202            if self.retain_examples {
203                m.examples.first().cloned()
204            } else {
205                m.examples.drain(..).next()
206            }
207        });
208
209        if let Some(example) = first_example {
210            schema.extensions.insert("example".to_owned(), example);
211        }
212    }
213}