Replace visit::Visitor
with transform::Transform
This commit is contained in:
parent
71b45a8ba3
commit
324be32de6
5 changed files with 432 additions and 273 deletions
|
@ -8,7 +8,7 @@ There are two main types in this module:
|
|||
*/
|
||||
|
||||
use crate::Schema;
|
||||
use crate::{visit::*, JsonSchema};
|
||||
use crate::{transform::*, JsonSchema};
|
||||
use dyn_clone::DynClone;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Map, Value};
|
||||
|
@ -44,8 +44,8 @@ pub struct SchemaSettings {
|
|||
///
|
||||
/// Defaults to `"https://json-schema.org/draft/2020-12/schema"`.
|
||||
pub meta_schema: Option<String>,
|
||||
/// A list of visitors that get applied to all generated schemas.
|
||||
pub visitors: Vec<Box<dyn GenVisitor>>,
|
||||
/// A list of [`Transform`]s that get applied to generated root schemas.
|
||||
pub transforms: Vec<Box<dyn GenTransform>>,
|
||||
/// Inline all subschemas instead of using references.
|
||||
///
|
||||
/// Some references may still be generated in schemas for recursive types.
|
||||
|
@ -70,7 +70,7 @@ impl SchemaSettings {
|
|||
option_add_null_type: true,
|
||||
definitions_path: "/definitions".to_owned(),
|
||||
meta_schema: Some("http://json-schema.org/draft-07/schema#".to_owned()),
|
||||
visitors: vec![Box::new(RemoveRefSiblings), Box::new(ReplacePrefixItems)],
|
||||
transforms: vec![Box::new(RemoveRefSiblings), Box::new(ReplacePrefixItems)],
|
||||
inline_subschemas: false,
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ impl SchemaSettings {
|
|||
option_add_null_type: true,
|
||||
definitions_path: "/$defs".to_owned(),
|
||||
meta_schema: Some("https://json-schema.org/draft/2019-09/schema".to_owned()),
|
||||
visitors: vec![Box::new(ReplacePrefixItems)],
|
||||
transforms: vec![Box::new(ReplacePrefixItems)],
|
||||
inline_subschemas: false,
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ impl SchemaSettings {
|
|||
option_add_null_type: true,
|
||||
definitions_path: "/$defs".to_owned(),
|
||||
meta_schema: Some("https://json-schema.org/draft/2020-12/schema".to_owned()),
|
||||
visitors: Vec::new(),
|
||||
transforms: Vec::new(),
|
||||
inline_subschemas: false,
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ impl SchemaSettings {
|
|||
"https://spec.openapis.org/oas/3.0/schema/2021-09-28#/definitions/Schema"
|
||||
.to_owned(),
|
||||
),
|
||||
visitors: vec for these `SchemaSettings`.
|
||||
pub fn with_visitor(mut self, visitor: impl Visitor + Debug + Clone + 'static) -> Self {
|
||||
self.visitors.push(Box::new(visitor));
|
||||
/// Appends the given transform to the list of [transforms](SchemaSettings::transforms) for these `SchemaSettings`.
|
||||
pub fn with_transform(mut self, transform: impl Transform + Clone + 'static) -> Self {
|
||||
self.transforms.push(Box::new(transform));
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -297,9 +297,9 @@ impl SchemaGenerator {
|
|||
std::mem::take(&mut self.definitions)
|
||||
}
|
||||
|
||||
/// Returns an iterator over the [visitors](SchemaSettings::visitors) being used by this `SchemaGenerator`.
|
||||
pub fn visitors_mut(&mut self) -> impl Iterator<Item = &mut dyn GenVisitor> {
|
||||
self.settings.visitors.iter_mut().map(|v| v.as_mut())
|
||||
/// Returns an iterator over the [transforms](SchemaSettings::transforms) being used by this `SchemaGenerator`.
|
||||
pub fn transforms_mut(&mut self) -> impl Iterator<Item = &mut dyn GenTransform> {
|
||||
self.settings.transforms.iter_mut().map(|v| v.as_mut())
|
||||
}
|
||||
|
||||
/// Generates a JSON Schema for the type `T`.
|
||||
|
@ -320,7 +320,7 @@ impl SchemaGenerator {
|
|||
}
|
||||
|
||||
self.add_definitions(object, self.definitions.clone());
|
||||
self.run_visitors(&mut schema);
|
||||
self.apply_transforms(&mut schema);
|
||||
|
||||
schema
|
||||
}
|
||||
|
@ -344,7 +344,7 @@ impl SchemaGenerator {
|
|||
|
||||
let definitions = self.take_definitions();
|
||||
self.add_definitions(object, definitions);
|
||||
self.run_visitors(&mut schema);
|
||||
self.apply_transforms(&mut schema);
|
||||
|
||||
schema
|
||||
}
|
||||
|
@ -375,7 +375,7 @@ impl SchemaGenerator {
|
|||
}
|
||||
|
||||
self.add_definitions(object, self.definitions.clone());
|
||||
self.run_visitors(&mut schema);
|
||||
self.apply_transforms(&mut schema);
|
||||
|
||||
Ok(schema)
|
||||
}
|
||||
|
@ -407,7 +407,7 @@ impl SchemaGenerator {
|
|||
|
||||
let definitions = self.take_definitions();
|
||||
self.add_definitions(object, definitions);
|
||||
self.run_visitors(&mut schema);
|
||||
self.apply_transforms(&mut schema);
|
||||
|
||||
Ok(schema)
|
||||
}
|
||||
|
@ -456,26 +456,9 @@ impl SchemaGenerator {
|
|||
target.append(&mut definitions);
|
||||
}
|
||||
|
||||
fn run_visitors(&mut self, schema: &mut Schema) {
|
||||
for visitor in self.visitors_mut() {
|
||||
visitor.visit_schema(schema);
|
||||
}
|
||||
|
||||
let pointer = self.definitions_path_stripped();
|
||||
// `$defs` and `definitions` are both handled internally by `Visitor::visit_schema`.
|
||||
// If the definitions are in any other location, explicitly visit them here to ensure
|
||||
// they're run against any referenced subschemas.
|
||||
if pointer != "/$defs" && pointer != "/definitions" {
|
||||
if let Some(definitions) = schema
|
||||
.as_object_mut()
|
||||
.and_then(|so| json_pointer_mut(so, pointer, false))
|
||||
{
|
||||
for subschema in definitions.values_mut().flat_map(<&mut Schema>::try_from) {
|
||||
for visitor in self.visitors_mut() {
|
||||
visitor.visit_schema(subschema);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn apply_transforms(&mut self, schema: &mut Schema) {
|
||||
for transform in self.transforms_mut() {
|
||||
transform.transform(schema);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -518,43 +501,48 @@ fn json_pointer_mut<'a>(
|
|||
Some(object)
|
||||
}
|
||||
|
||||
/// A [Visitor] which implements additional traits required to be included in a [SchemaSettings].
|
||||
/// A [Transform] which implements additional traits required to be included in a [SchemaSettings].
|
||||
///
|
||||
/// You will rarely need to use this trait directly as it is automatically implemented for any type which implements all of:
|
||||
/// - [`Visitor`]
|
||||
/// - [`std::fmt::Debug`]
|
||||
/// - [`Transform`]
|
||||
/// - [`std::any::Any`] (implemented for all `'static` types)
|
||||
/// - [`std::clone::Clone`]
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use schemars::visit::Visitor;
|
||||
/// use schemars::gen::GenVisitor;
|
||||
/// use schemars::transform::Transform;
|
||||
/// use schemars::gen::GenTransform;
|
||||
///
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// struct MyVisitor;
|
||||
/// struct MyTransform;
|
||||
///
|
||||
/// impl Visitor for MyVisitor {
|
||||
/// fn visit_schema(&mut self, schema: &mut schemars::Schema) {
|
||||
/// impl Transform for MyTransform {
|
||||
/// fn transform(&mut self, schema: &mut schemars::Schema) {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let v: &dyn GenVisitor = &MyVisitor;
|
||||
/// assert!(v.as_any().is::<MyVisitor>());
|
||||
/// let v: &dyn GenTransform = &MyTransform;
|
||||
/// assert!(v.as_any().is::<MyTransform>());
|
||||
/// ```
|
||||
pub trait GenVisitor: Visitor + Debug + DynClone + Any {
|
||||
/// Upcasts this visitor into an `Any`, which can be used to inspect and manipulate it as its concrete type.
|
||||
pub trait GenTransform: Transform + DynClone + Any {
|
||||
/// Upcasts this transform into an [`Any`], which can be used to inspect and manipulate it as its concrete type.
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(GenVisitor);
|
||||
dyn_clone::clone_trait_object!(GenTransform);
|
||||
|
||||
impl<T> GenVisitor for T
|
||||
impl<T> GenTransform for T
|
||||
where
|
||||
T: Visitor + Debug + Clone + Any,
|
||||
T: Transform + Clone + Any,
|
||||
{
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Box<dyn GenTransform> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self._debug_type_name(f)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ mod macros;
|
|||
pub mod _private;
|
||||
/// Types for generating JSON schemas.
|
||||
pub mod gen;
|
||||
/// Types for recursively modifying JSON schemas.
|
||||
pub mod visit;
|
||||
/// Types for defining modifications to JSON schemas.
|
||||
pub mod transform;
|
||||
|
||||
#[cfg(feature = "schemars_derive")]
|
||||
extern crate schemars_derive;
|
||||
|
|
371
schemars/src/transform.rs
Normal file
371
schemars/src/transform.rs
Normal file
|
@ -0,0 +1,371 @@
|
|||
/*!
|
||||
Contains the [`Transform`] trait, used to modify a constructed schema and optionally its subschemas.
|
||||
This trait is automatically implemented for functions of the form `fn(&mut Schema) -> ()`.
|
||||
|
||||
# Recursive Transforms
|
||||
|
||||
To make a transform recursive (i.e. apply it to subschemas), you have two options:
|
||||
1. call the [`transform_subschemas`] function within the transform function
|
||||
2. wrap the `Transform` in a [`RecursiveTransform`]
|
||||
|
||||
# Examples
|
||||
|
||||
To add a custom property to all object schemas:
|
||||
|
||||
```
|
||||
# use schemars::{Schema, json_schema};
|
||||
use schemars::transform::{Transform, transform_subschemas};
|
||||
|
||||
pub struct MyTransform;
|
||||
|
||||
impl Transform for MyTransform {
|
||||
fn transform(&mut self, schema: &mut Schema) {
|
||||
// First, make our change to this schema
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
obj.insert("my_property".to_string(), serde_json::json!("hello world"));
|
||||
}
|
||||
|
||||
// Then apply the transform to any subschemas
|
||||
transform_subschemas(self, schema);
|
||||
}
|
||||
}
|
||||
|
||||
let mut schema = json_schema!({
|
||||
"type": "array",
|
||||
"items": {}
|
||||
});
|
||||
|
||||
MyTransform.transform(&mut schema);
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
json_schema!({
|
||||
"type": "array",
|
||||
"items": {
|
||||
"my_property": "hello world"
|
||||
},
|
||||
"my_property": "hello world"
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
The same example with a `fn` transform`:
|
||||
```
|
||||
# use schemars::{Schema, json_schema};
|
||||
use schemars::transform::transform_subschemas;
|
||||
|
||||
fn add_property(schema: &mut Schema) {
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
obj.insert("my_property".to_string(), serde_json::json!("hello world"));
|
||||
}
|
||||
|
||||
transform_subschemas(&mut add_property, schema)
|
||||
}
|
||||
|
||||
let mut schema = json_schema!({
|
||||
"type": "array",
|
||||
"items": {}
|
||||
});
|
||||
|
||||
add_property(&mut schema);
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
json_schema!({
|
||||
"type": "array",
|
||||
"items": {
|
||||
"my_property": "hello world"
|
||||
},
|
||||
"my_property": "hello world"
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
And the same example using a closure wrapped in a `RecursiveTransform`:
|
||||
```
|
||||
# use schemars::{Schema, json_schema};
|
||||
use schemars::transform::{Transform, RecursiveTransform};
|
||||
|
||||
let mut transform = RecursiveTransform(|schema: &mut Schema| {
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
obj.insert("my_property".to_string(), serde_json::json!("hello world"));
|
||||
}
|
||||
});
|
||||
|
||||
let mut schema = json_schema!({
|
||||
"type": "array",
|
||||
"items": {}
|
||||
});
|
||||
|
||||
transform.transform(&mut schema);
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
json_schema!({
|
||||
"type": "array",
|
||||
"items": {
|
||||
"my_property": "hello world"
|
||||
},
|
||||
"my_property": "hello world"
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
*/
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::Schema;
|
||||
|
||||
/// Trait used to modify a constructed schema and optionally its subschemas.
|
||||
///
|
||||
/// See the [module documentation](self) for more details on implementing this trait.
|
||||
pub trait Transform {
|
||||
/// Applies the transform to the given [`Schema`].
|
||||
///
|
||||
/// When overriding this method, you may want to call the [`transform_subschemas`] function to also transform any subschemas.
|
||||
fn transform(&mut self, schema: &mut Schema);
|
||||
|
||||
// Not public API
|
||||
// Hack to enable implementing Debug on Box<dyn GenTransform> even though closures don't implement Debug
|
||||
#[doc(hidden)]
|
||||
fn _debug_type_name(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(std::any::type_name::<Self>())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Transform for F
|
||||
where
|
||||
F: FnMut(&mut Schema),
|
||||
{
|
||||
fn transform(&mut self, schema: &mut Schema) {
|
||||
self(schema)
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the given [`Transform`] to all direct subschemas of the [`Schema`].
|
||||
pub fn transform_subschemas<T: Transform + ?Sized>(t: &mut T, schema: &mut Schema) {
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
for (key, value) in obj {
|
||||
// This is intentionally written to work with multiple JSON Schema versions, so that
|
||||
// users can add their own transforms on the end of e.g. `SchemaSettings::draft07()` and
|
||||
// they will still apply to all subschemas "as expected".
|
||||
// This is why this match statement contains both `additionalProperties` (which was
|
||||
// dropped in draft 2020-12) and `prefixItems` (which was added in draft 2020-12).
|
||||
match key.as_str() {
|
||||
"not"
|
||||
| "if"
|
||||
| "then"
|
||||
| "else"
|
||||
| "contains"
|
||||
| "additionalProperties"
|
||||
| "propertyNames"
|
||||
| "additionalItems" => {
|
||||
if let Ok(subschema) = value.try_into() {
|
||||
t.transform(subschema)
|
||||
}
|
||||
}
|
||||
"allOf" | "anyOf" | "oneOf" | "prefixItems" => {
|
||||
if let Some(array) = value.as_array_mut() {
|
||||
for value in array {
|
||||
if let Ok(subschema) = value.try_into() {
|
||||
t.transform(subschema)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Support `items` array even though this is not allowed in draft 2020-12 (see above comment)
|
||||
"items" => {
|
||||
if let Some(array) = value.as_array_mut() {
|
||||
for value in array {
|
||||
if let Ok(subschema) = value.try_into() {
|
||||
t.transform(subschema)
|
||||
}
|
||||
}
|
||||
} else if let Ok(subschema) = value.try_into() {
|
||||
t.transform(subschema)
|
||||
}
|
||||
}
|
||||
"properties" | "patternProperties" | "$defs" | "definitions" => {
|
||||
if let Some(obj) = value.as_object_mut() {
|
||||
for value in obj.values_mut() {
|
||||
if let Ok(subschema) = value.try_into() {
|
||||
t.transform(subschema)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper struct that can wrap a non-recursive [`Transform`] (i.e. one that does not apply to subschemas) into a recursive one.
|
||||
///
|
||||
/// Its implementation of `Transform` will first apply the inner transform to the "parent" schema, and then its subschemas (and their subschemas, and so on).
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use schemars::{Schema, json_schema};
|
||||
/// use schemars::transform::{Transform, RecursiveTransform};
|
||||
///
|
||||
/// let mut transform = RecursiveTransform(|schema: &mut Schema| {
|
||||
/// if let Some(obj) = schema.as_object_mut() {
|
||||
/// obj.insert("my_property".to_string(), serde_json::json!("hello world"));
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// let mut schema = json_schema!({
|
||||
/// "type": "array",
|
||||
/// "items": {}
|
||||
/// });
|
||||
///
|
||||
/// transform.transform(&mut schema);
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// schema,
|
||||
/// json_schema!({
|
||||
/// "type": "array",
|
||||
/// "items": {
|
||||
/// "my_property": "hello world"
|
||||
/// },
|
||||
/// "my_property": "hello world"
|
||||
/// })
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RecursiveTransform<T>(pub T);
|
||||
|
||||
impl<T> Transform for RecursiveTransform<T>
|
||||
where
|
||||
T: Transform,
|
||||
{
|
||||
fn transform(&mut self, schema: &mut Schema) {
|
||||
self.0.transform(schema);
|
||||
transform_subschemas(self, schema);
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces boolean JSON Schemas with equivalent object schemas.
|
||||
/// This also applies to subschemas.
|
||||
///
|
||||
/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support booleans as schemas.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplaceBoolSchemas {
|
||||
/// When set to `true`, a schema's `additionalProperties` property will not be changed from a boolean.
|
||||
pub skip_additional_properties: bool,
|
||||
}
|
||||
|
||||
impl Transform for ReplaceBoolSchemas {
|
||||
fn transform(&mut self, schema: &mut Schema) {
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
if self.skip_additional_properties {
|
||||
if let Some((ap_key, ap_value)) = obj.remove_entry("additionalProperties") {
|
||||
transform_subschemas(self, schema);
|
||||
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
obj.insert(ap_key, ap_value);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
transform_subschemas(self, schema);
|
||||
} else {
|
||||
schema.ensure_object();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Restructures JSON Schema objects so that the `$ref` property will never appear alongside any other properties.
|
||||
/// This also applies to subschemas.
|
||||
///
|
||||
/// This is useful for versions of JSON Schema (e.g. Draft 7) that do not support other properties alongside `$ref`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RemoveRefSiblings;
|
||||
|
||||
impl Transform for RemoveRefSiblings {
|
||||
fn transform(&mut self, schema: &mut Schema) {
|
||||
transform_subschemas(self, schema);
|
||||
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
if obj.len() > 1 {
|
||||
if let Some(ref_value) = obj.remove("$ref") {
|
||||
if let Value::Array(all_of) =
|
||||
obj.entry("allOf").or_insert(Value::Array(Vec::new()))
|
||||
{
|
||||
all_of.push(json!({
|
||||
"$ref": ref_value
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the `examples` schema property and (if present) set its first value as the `example` property.
|
||||
/// This also applies to subschemas.
|
||||
///
|
||||
/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `examples` property.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SetSingleExample;
|
||||
|
||||
impl Transform for SetSingleExample {
|
||||
fn transform(&mut self, schema: &mut Schema) {
|
||||
transform_subschemas(self, schema);
|
||||
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
if let Some(Value::Array(examples)) = obj.remove("examples") {
|
||||
if let Some(first_example) = examples.into_iter().next() {
|
||||
obj.insert("example".into(), first_example);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the `const` schema property with a single-valued `enum` property.
|
||||
/// This also applies to subschemas.
|
||||
///
|
||||
/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `const` property.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplaceConstValue;
|
||||
|
||||
impl Transform for ReplaceConstValue {
|
||||
fn transform(&mut self, schema: &mut Schema) {
|
||||
transform_subschemas(self, schema);
|
||||
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
if let Some(value) = obj.remove("const") {
|
||||
obj.insert("enum".into(), Value::Array(vec![value]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rename the `prefixItems` schema property to `items`.
|
||||
/// This also applies to subschemas.
|
||||
///
|
||||
/// If the schema contains both `prefixItems` and `items`, then this additionally renames `items` to `additionalItems`.
|
||||
///
|
||||
/// This is useful for versions of JSON Schema (e.g. Draft 7) that do not support the `prefixItems` property.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplacePrefixItems;
|
||||
|
||||
impl Transform for ReplacePrefixItems {
|
||||
fn transform(&mut self, schema: &mut Schema) {
|
||||
transform_subschemas(self, schema);
|
||||
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
if let Some(prefix_items) = obj.remove("prefixItems") {
|
||||
let previous_items = obj.insert("items".to_owned(), prefix_items);
|
||||
|
||||
if let Some(previous_items) = previous_items {
|
||||
obj.insert("additionalItems".to_owned(), previous_items);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,217 +0,0 @@
|
|||
/*!
|
||||
Contains the [`Visitor`] trait, used to recursively modify a constructed schema and its subschemas.
|
||||
|
||||
Sometimes you may want to apply a change to a schema, as well as all schemas contained within it.
|
||||
The easiest way to achieve this is by defining a type that implements [`Visitor`].
|
||||
All methods of `Visitor` have a default implementation that makes no change but recursively visits all subschemas.
|
||||
When overriding one of these methods, you will *usually* want to still call this default implementation.
|
||||
|
||||
# Example
|
||||
To add a custom property to all object schemas:
|
||||
```
|
||||
use schemars::Schema;
|
||||
use schemars::visit::{Visitor, visit_schema};
|
||||
|
||||
pub struct MyVisitor;
|
||||
|
||||
impl Visitor for MyVisitor {
|
||||
fn visit_schema(&mut self, schema: &mut Schema) {
|
||||
// First, make our change to this schema
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
obj.insert("my_property".to_string(), serde_json::json!("hello world"));
|
||||
}
|
||||
|
||||
// Then delegate to default implementation to visit any subschemas
|
||||
visit_schema(self, schema);
|
||||
}
|
||||
}
|
||||
```
|
||||
*/
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::Schema;
|
||||
|
||||
/// Trait used to recursively modify a constructed schema and its subschemas.
|
||||
pub trait Visitor {
|
||||
/// Override this method to modify a [`Schema`] and (optionally) its subschemas.
|
||||
///
|
||||
/// When overriding this method, you will usually want to call the [`visit_schema`] function to visit subschemas.
|
||||
fn visit_schema(&mut self, schema: &mut Schema);
|
||||
}
|
||||
|
||||
/// Visits all subschemas of the [`Schema`].
|
||||
pub fn visit_schema<V: Visitor + ?Sized>(v: &mut V, schema: &mut Schema) {
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
for (key, value) in obj {
|
||||
// This is intentionally written to work with multiple JSON Schema versions, so that
|
||||
// users can add their own visitors on the end of e.g. `SchemaSettings::draft07()` and
|
||||
// they will still apply to all subschemas "as expected".
|
||||
// This is why this match statement contains both `additionalProperties` (which was
|
||||
// dropped in draft 2020-12) and `prefixItems` (which was added in draft 2020-12).
|
||||
match key.as_str() {
|
||||
"not"
|
||||
| "if"
|
||||
| "then"
|
||||
| "else"
|
||||
| "contains"
|
||||
| "additionalProperties"
|
||||
| "propertyNames"
|
||||
| "additionalItems" => {
|
||||
if let Ok(subschema) = value.try_into() {
|
||||
v.visit_schema(subschema)
|
||||
}
|
||||
}
|
||||
"allOf" | "anyOf" | "oneOf" | "prefixItems" => {
|
||||
if let Some(array) = value.as_array_mut() {
|
||||
for value in array {
|
||||
if let Ok(subschema) = value.try_into() {
|
||||
v.visit_schema(subschema)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Support `items` array even though this is not allowed in draft 2020-12 (see above comment)
|
||||
"items" => {
|
||||
if let Some(array) = value.as_array_mut() {
|
||||
for value in array {
|
||||
if let Ok(subschema) = value.try_into() {
|
||||
v.visit_schema(subschema)
|
||||
}
|
||||
}
|
||||
} else if let Ok(subschema) = value.try_into() {
|
||||
v.visit_schema(subschema)
|
||||
}
|
||||
}
|
||||
"properties" | "patternProperties" | "$defs" | "definitions" => {
|
||||
if let Some(obj) = value.as_object_mut() {
|
||||
for value in obj.values_mut() {
|
||||
if let Ok(subschema) = value.try_into() {
|
||||
v.visit_schema(subschema)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This visitor will replace all boolean JSON Schemas with equivalent object schemas.
|
||||
///
|
||||
/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support booleans as schemas.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplaceBoolSchemas {
|
||||
/// When set to `true`, a schema's `additionalProperties` property will not be changed from a boolean.
|
||||
pub skip_additional_properties: bool,
|
||||
}
|
||||
|
||||
impl Visitor for ReplaceBoolSchemas {
|
||||
fn visit_schema(&mut self, schema: &mut Schema) {
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
if self.skip_additional_properties {
|
||||
if let Some((ap_key, ap_value)) = obj.remove_entry("additionalProperties") {
|
||||
visit_schema(self, schema);
|
||||
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
obj.insert(ap_key, ap_value);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
visit_schema(self, schema);
|
||||
} else {
|
||||
schema.ensure_object();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This visitor will restructure JSON Schema objects so that the `$ref` property will never appear alongside any other properties.
|
||||
///
|
||||
/// This is useful for versions of JSON Schema (e.g. Draft 7) that do not support other properties alongside `$ref`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RemoveRefSiblings;
|
||||
|
||||
impl Visitor for RemoveRefSiblings {
|
||||
fn visit_schema(&mut self, schema: &mut Schema) {
|
||||
visit_schema(self, schema);
|
||||
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
if obj.len() > 1 {
|
||||
if let Some(ref_value) = obj.remove("$ref") {
|
||||
if let Value::Array(all_of) =
|
||||
obj.entry("allOf").or_insert(Value::Array(Vec::new()))
|
||||
{
|
||||
all_of.push(json!({
|
||||
"$ref": ref_value
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This visitor will remove the `examples` schema property and (if present) set its first value as the `example` property.
|
||||
///
|
||||
/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `examples` property.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SetSingleExample;
|
||||
|
||||
impl Visitor for SetSingleExample {
|
||||
fn visit_schema(&mut self, schema: &mut Schema) {
|
||||
visit_schema(self, schema);
|
||||
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
if let Some(Value::Array(examples)) = obj.remove("examples") {
|
||||
if let Some(first_example) = examples.into_iter().next() {
|
||||
obj.insert("example".into(), first_example);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This visitor will replace the `const` schema property with a single-valued `enum` property.
|
||||
///
|
||||
/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `const` property.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplaceConstValue;
|
||||
|
||||
impl Visitor for ReplaceConstValue {
|
||||
fn visit_schema(&mut self, schema: &mut Schema) {
|
||||
visit_schema(self, schema);
|
||||
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
if let Some(value) = obj.remove("const") {
|
||||
obj.insert("enum".into(), Value::Array(vec![value]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This visitor will rename the `prefixItems` schema property to `items`.
|
||||
///
|
||||
/// If the schema contains both `prefixItems` and `items`, then this additionally renames `items` to `additionalItems`.
|
||||
///
|
||||
/// This is useful for versions of JSON Schema (e.g. Draft 7) that do not support the `prefixItems` property.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplacePrefixItems;
|
||||
|
||||
impl Visitor for ReplacePrefixItems {
|
||||
fn visit_schema(&mut self, schema: &mut Schema) {
|
||||
visit_schema(self, schema);
|
||||
|
||||
if let Some(obj) = schema.as_object_mut() {
|
||||
if let Some(prefix_items) = obj.remove("prefixItems") {
|
||||
let previous_items = obj.insert("items".to_owned(), prefix_items);
|
||||
|
||||
if let Some(previous_items) = previous_items {
|
||||
obj.insert("additionalItems".to_owned(), previous_items);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
mod util;
|
||||
use schemars::gen::SchemaSettings;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::{JsonSchema, Schema};
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
use util::*;
|
||||
|
@ -47,5 +47,22 @@ fn schema_matches_2020_12() -> TestResult {
|
|||
|
||||
#[test]
|
||||
fn schema_matches_openapi3() -> TestResult {
|
||||
test_generated_schema::<Outer>("schema_settings-openapi3", SchemaSettings::openapi3())
|
||||
let mut settings = SchemaSettings::openapi3();
|
||||
|
||||
// Hack to apply recursive transforms to schemas at components.schemas:
|
||||
// First, move them to $defs, then run the transforms, then move them back again.
|
||||
settings.transforms.insert(
|
||||
0,
|
||||
Box::new(|s: &mut Schema| {
|
||||
let obj = s.ensure_object();
|
||||
let defs = obj["components"]["schemas"].take();
|
||||
obj.insert("$defs".to_owned(), defs);
|
||||
}),
|
||||
);
|
||||
settings.transforms.push(Box::new(|s: &mut Schema| {
|
||||
let obj = s.ensure_object();
|
||||
obj["components"]["schemas"] = obj.remove("$defs").unwrap();
|
||||
}));
|
||||
|
||||
test_generated_schema::<Outer>("schema_settings-openapi3", settings)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue