schema_analysis/targets/
schemars.rs

1//! Integration with [schemars](https://github.com/GREsau/schemars)
2
3use std::error::Error;
4
5use schemars::schema as schemars_types;
6
7use crate::Schema;
8
9impl Schema {
10    /// Convert into a json_schema using the default settings.
11    pub fn to_json_schema_with_schemars(&self) -> Result<String, impl Error> {
12        self.to_json_schema_with_schemars_version(&Default::default())
13    }
14
15    /// Convert into a specific version of json_schema.
16    pub fn to_json_schema_with_schemars_version(
17        &self,
18        version: &JsonSchemaVersion,
19    ) -> Result<String, impl Error> {
20        let settings: schemars::gen::SchemaSettings = version.to_schemars_settings();
21        let mut generator: schemars::gen::SchemaGenerator = settings.into();
22
23        let root = self.to_schemars_schema(&mut generator);
24        serde_json::to_string_pretty(&root)
25    }
26
27    /// Convert using a provided generator (which also holds the settings) to a json schema.
28    pub fn to_schemars_schema(
29        &self,
30        generator: &mut schemars::gen::SchemaGenerator,
31    ) -> schemars_types::RootSchema {
32        let inner = helpers::inferred_to_schemars(generator, self);
33        helpers::wrap_in_root(inner, generator.settings())
34    }
35}
36
37/// The currently supported json schema versions.
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
39pub enum JsonSchemaVersion {
40    /// `schemars::gen::SchemaSettings::draft07`
41    Draft07,
42    /// `schemars::gen::SchemaSettings::draft2019_09`
43    Draft2019_09,
44    /// `schemars::gen::SchemaSettings::openapi3`
45    OpenApi3,
46}
47impl Default for JsonSchemaVersion {
48    fn default() -> Self {
49        Self::Draft2019_09
50    }
51}
52impl JsonSchemaVersion {
53    /// Convert the version to full settings.
54    pub fn to_schemars_settings(&self) -> schemars::gen::SchemaSettings {
55        use schemars::gen::SchemaSettings;
56        match self {
57            JsonSchemaVersion::Draft07 => SchemaSettings::draft07(),
58            JsonSchemaVersion::Draft2019_09 => SchemaSettings::draft2019_09(),
59            JsonSchemaVersion::OpenApi3 => SchemaSettings::openapi3(),
60        }
61    }
62}
63
64mod helpers {
65
66    use std::collections::BTreeSet;
67
68    use schemars::schema as schemars_types;
69
70    use crate::{Field, Schema};
71
72    /// Wraps a [Schema](schemars_types::Schema) in a [RootSchema](schemars_types::RootSchema).
73    pub fn wrap_in_root(
74        inner: schemars_types::Schema,
75        settings: &schemars::gen::SchemaSettings,
76    ) -> schemars_types::RootSchema {
77        schemars_types::RootSchema {
78            meta_schema: settings.meta_schema.clone(),
79            definitions: Default::default(),
80            schema: inner.into_object(),
81        }
82    }
83
84    /// Converts an inferred [Schema] to a schemars [Schema](schemars_types::Schema).
85    pub fn inferred_to_schemars(
86        generator: &mut schemars::gen::SchemaGenerator,
87        inferred: &Schema,
88    ) -> schemars_types::Schema {
89        // Note: we can use the generator even if we don't generate the final root schema
90        //  using it because simple values will not be referrenced.
91        //  Do not use for complex values.
92        match inferred {
93            Schema::Null(_) => generator.subschema_for::<()>(),
94            Schema::Boolean(_) => generator.subschema_for::<bool>(),
95
96            // Using specific integer/float types causes the schema to remember the
97            // specific representation.
98            Schema::Integer(_) => schemars_types::SchemaObject {
99                instance_type: Some(schemars_types::InstanceType::Integer.into()),
100                ..Default::default()
101            }
102            .into(),
103            Schema::Float(_) => schemars_types::SchemaObject {
104                instance_type: Some(schemars_types::InstanceType::Number.into()),
105                ..Default::default()
106            }
107            .into(),
108
109            Schema::String(_) => generator.subschema_for::<String>(),
110            Schema::Bytes(_) => generator.subschema_for::<Vec<u8>>(),
111
112            Schema::Sequence { field, .. } => schemars_types::SchemaObject {
113                instance_type: Some(schemars_types::InstanceType::Array.into()),
114                array: Some(Box::new(schemars_types::ArrayValidation {
115                    items: Some(internal_field_to_schemars_schema(generator, field).into()),
116                    ..Default::default()
117                })),
118                ..Default::default()
119            }
120            .into(),
121
122            Schema::Struct { fields, .. } => {
123                let required: BTreeSet<String> = fields
124                    .iter()
125                    // Null values are handled in the Field function.
126                    .filter(|(_, v)| !v.status.may_be_missing)
127                    .map(|(k, _)| k.clone())
128                    .collect();
129                let properties = fields
130                    .iter()
131                    .map(|(k, field)| {
132                        (
133                            k.clone(),
134                            internal_field_to_schemars_schema(generator, field),
135                        )
136                    })
137                    .collect();
138                schemars_types::SchemaObject {
139                    instance_type: Some(schemars_types::InstanceType::Object.into()),
140                    object: Some(Box::new(schemars_types::ObjectValidation {
141                        required,
142                        properties,
143                        ..Default::default()
144                    })),
145                    ..Default::default()
146                }
147                .into()
148            }
149
150            Schema::Union { variants } => {
151                let json_schemas = variants
152                    .iter()
153                    .map(|s| inferred_to_schemars(generator, s))
154                    .collect();
155                schemars_types::SchemaObject {
156                    subschemas: Some(Box::new(schemars_types::SubschemaValidation {
157                        any_of: Some(json_schemas),
158                        ..Default::default()
159                    })),
160                    ..Default::default()
161                }
162                .into()
163            }
164        }
165    }
166
167    /// Converts a [Field] into a [Schema](schemars_types::Schema).
168    fn internal_field_to_schemars_schema(
169        generator: &mut schemars::gen::SchemaGenerator,
170        field: &Field,
171    ) -> schemars_types::Schema {
172        // Note: we can use the generator even if we don't generate the final root schema
173        //  using it because simple values will not be referrenced.
174        //  Do not use for complex values.
175
176        let mut schema = match &field.schema {
177            Some(schema) => inferred_to_schemars(generator, schema),
178            None => schemars_types::Schema::Bool(true),
179        };
180
181        if field.status.may_be_null {
182            // Taken from:
183            // https://github.com/GREsau/schemars/blob/master/schemars/src/json_schema_impls/core.rs
184            if generator.settings().option_add_null_type {
185                schema = match schema {
186                    schemars_types::Schema::Bool(true) => schemars_types::Schema::Bool(true),
187                    schemars_types::Schema::Bool(false) => generator.subschema_for::<()>(),
188                    schemars_types::Schema::Object(schemars_types::SchemaObject {
189                        instance_type: Some(ref mut instance_type),
190                        ..
191                    }) => {
192                        add_null_type(instance_type);
193                        schema
194                    }
195                    schema => schemars_types::SchemaObject {
196                        // TODO technically the schema already accepts null, so this may be unnecessary
197                        subschemas: Some(Box::new(schemars_types::SubschemaValidation {
198                            any_of: Some(vec![schema, generator.subschema_for::<()>()]),
199                            ..Default::default()
200                        })),
201                        ..Default::default()
202                    }
203                    .into(),
204                }
205            }
206            if generator.settings().option_nullable {
207                let mut schema_obj = schema.into_object();
208                schema_obj
209                    .extensions
210                    .insert("nullable".to_owned(), serde_json::json!(true));
211                schema = schemars_types::Schema::Object(schema_obj);
212            };
213        }
214        schema
215    }
216
217    /// Taken from:
218    /// https://github.com/GREsau/schemars/blob/master/schemars/src/json_schema_impls/core.rs
219    fn add_null_type(
220        instance_type: &mut schemars_types::SingleOrVec<schemars_types::InstanceType>,
221    ) {
222        match instance_type {
223            schemars_types::SingleOrVec::Single(ty)
224                if **ty != schemars_types::InstanceType::Null =>
225            {
226                *instance_type = vec![**ty, schemars_types::InstanceType::Null].into()
227            }
228            schemars_types::SingleOrVec::Vec(ty)
229                if !ty.contains(&schemars_types::InstanceType::Null) =>
230            {
231                ty.push(schemars_types::InstanceType::Null)
232            }
233            _ => {}
234        };
235    }
236}