From a8292671114aabe6bce06fbdbeb3c5515c2fa873 Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Thu, 4 Jun 2020 19:23:36 +0100 Subject: [PATCH] Add Visitor trait, update changelog --- CHANGELOG.md | 12 ++ schemars/src/gen.rs | 204 ++++++++++-------- schemars/src/json_schema_impls/core.rs | 21 +- schemars/src/json_schema_impls/maps.rs | 77 +------ schemars/src/json_schema_impls/serdejson.rs | 4 +- schemars/src/lib.rs | 15 +- schemars/src/macros.rs | 2 +- schemars/src/schema.rs | 27 ++- schemars/src/visit.rs | 84 ++++++++ schemars/tests/docs.rs | 2 +- .../doc_comments_struct_ref_siblings.json | 2 +- 11 files changed, 244 insertions(+), 206 deletions(-) create mode 100644 schemars/src/visit.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c1e0827..1a8e4d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## In-dev - version TBC +### Added: +- `visit::Visitor`, a trait for updating a schema and all schemas it contains recursively. A `SchemaSettings` can now contain a list of visitors. +- `into_object()` method added to `Schema` as a shortcut for `into::()` + +### Removed (**BREAKING CHANGES**): +- `SchemaSettings::bool_schemas` - this has been superseded by the `ReplaceBoolSchemas` visitor +- `SchemaSettings::allow_ref_siblings` - this has been superseded by the `RemoveRefSiblings` visitor + +### Deprecated: +- `make_extensible`, `schema_for_any`, and `schema_for_none` methods on `SchemaGenerator` + ## [0.7.6] - 2020-05-17 ### Added: - `#[schemars(example = "...")]` attribute for setting examples on generated schemas (https://github.com/GREsau/schemars/issues/23) diff --git a/schemars/src/gen.rs b/schemars/src/gen.rs index 81a9c7a..313ad1c 100644 --- a/schemars/src/gen.rs +++ b/schemars/src/gen.rs @@ -1,6 +1,7 @@ use crate::flatten::Merge; use crate::schema::*; -use crate::{JsonSchema, Map}; +use crate::{visit::*, JsonSchema, Map}; +use std::{fmt::Debug, sync::Arc}; /// Settings to customize how Schemas are generated. /// @@ -18,10 +19,6 @@ pub struct SchemaSettings { /// /// Defaults to `true`. pub option_add_null_type: bool, - /// Controls whether trivial [`Bool`](../schema/enum.Schema.html#variant.Bool) schemas may be generated. - /// - /// Defaults to [`BoolSchemas::Enabled`]. - pub bool_schemas: BoolSchemas, /// A JSON pointer to the expected location of referenceable subschemas within the resulting root schema. /// /// Defaults to `"#/definitions/"`. @@ -30,24 +27,11 @@ pub struct SchemaSettings { /// /// Defaults to `"http://json-schema.org/draft-07/schema#"`. pub meta_schema: Option, - /// Whether schemas with a `$ref` property may have other properties set. - /// - /// Defaults to `false`. - pub allow_ref_siblings: bool, + /// TODO document + pub visitors: Visitors, _hidden: (), } -/// Controls whether trivial [`Bool`](../schema/enum.Schema.html#variant.Bool) schemas may be generated. -#[derive(Debug, PartialEq, Copy, Clone)] -pub enum BoolSchemas { - /// `Bool` schemas may be used. - Enabled, - /// `Bool` schemas may only be used in a schema's [`additionalProperties`](../schema/struct.ObjectValidation.html#structfield.additional_properties) field. - AdditionalPropertiesOnly, - /// `Bool` schemas will never be used. - Disabled, -} - impl Default for SchemaSettings { fn default() -> SchemaSettings { SchemaSettings::draft07() @@ -60,10 +44,9 @@ impl SchemaSettings { SchemaSettings { option_nullable: false, option_add_null_type: true, - bool_schemas: BoolSchemas::Enabled, definitions_path: "#/definitions/".to_owned(), meta_schema: Some("http://json-schema.org/draft-07/schema#".to_owned()), - allow_ref_siblings: false, + visitors: Visitors(vec![Arc::new(RemoveRefSiblings)]), _hidden: (), } } @@ -73,10 +56,9 @@ impl SchemaSettings { SchemaSettings { option_nullable: false, option_add_null_type: true, - bool_schemas: BoolSchemas::Enabled, definitions_path: "#/definitions/".to_owned(), meta_schema: Some("https://json-schema.org/draft/2019-09/schema".to_owned()), - allow_ref_siblings: true, + visitors: Visitors::default(), _hidden: (), } } @@ -86,13 +68,17 @@ impl SchemaSettings { SchemaSettings { option_nullable: true, option_add_null_type: false, - bool_schemas: BoolSchemas::AdditionalPropertiesOnly, definitions_path: "#/components/schemas/".to_owned(), meta_schema: Some( "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema" .to_owned(), ), - allow_ref_siblings: false, + visitors: Visitors(vec![ + Arc::new(RemoveRefSiblings), + Arc::new(ReplaceBoolSchemas { + skip_additional_properties: true, + }), + ]), _hidden: (), } } @@ -114,12 +100,35 @@ impl SchemaSettings { self } + /// TODO document + pub fn with_visitor(mut self, visitor: impl Visitor) -> Self { + self.visitors.0.push(Arc::new(visitor)); + self + } + /// Creates a new [`SchemaGenerator`] using these settings. pub fn into_generator(self) -> SchemaGenerator { SchemaGenerator::new(self) } } +/// TODO document +#[derive(Debug, Clone, Default)] +pub struct Visitors(Vec>); + +impl PartialEq for Visitors { + fn eq(&self, other: &Self) -> bool { + if self.0.len() != other.0.len() { + return false; + } + + self.0 + .iter() + .zip(other.0.iter()) + .all(|(a, b)| Arc::ptr_eq(a, b)) + } +} + /// The main type used to generate JSON Schemas. /// /// # Example @@ -170,59 +179,17 @@ impl SchemaGenerator { &self.settings } - /// Modifies the given `SchemaObject` so that it may have validation, metadata or other properties set on it. - /// - /// If `schema` is not a `$ref` schema, then this does not modify `schema`. Otherwise, depending on this generator's settings, - /// this may wrap the `$ref` in another schema. This is required because in many JSON Schema implementations, a schema with `$ref` - /// set may not include other properties. - /// - /// # Example - /// ``` - /// use schemars::{gen::SchemaGenerator, schema::SchemaObject}; - /// - /// let gen = SchemaGenerator::default(); - /// - /// let ref_schema = SchemaObject::new_ref("foo".to_owned()); - /// assert!(ref_schema.is_ref()); - /// - /// let mut extensible_schema = ref_schema.clone(); - /// gen.make_extensible(&mut extensible_schema); - /// assert_ne!(ref_schema, extensible_schema); - /// assert!(!extensible_schema.is_ref()); - /// - /// let mut extensible_schema2 = extensible_schema.clone(); - /// gen.make_extensible(&mut extensible_schema); - /// assert_eq!(extensible_schema, extensible_schema2); - /// ``` - pub fn make_extensible(&self, schema: &mut SchemaObject) { - if schema.is_ref() && !self.settings().allow_ref_siblings { - let original = std::mem::replace(schema, SchemaObject::default()); - schema.subschemas().all_of = Some(vec![original.into()]); - } - } + #[deprecated = "This method no longer has any effect."] + pub fn make_extensible(&self, _schema: &mut SchemaObject) {} - /// Returns a `Schema` that matches everything, such as the empty schema `{}`. - /// - /// The exact value returned depends on this generator's [`BoolSchemas`](struct.SchemaSettings.html#structfield.bool_schemas) setting. + #[deprecated = "Use `Schema::Bool(true)` instead"] pub fn schema_for_any(&self) -> Schema { - let schema: Schema = true.into(); - if self.settings().bool_schemas == BoolSchemas::Enabled { - schema - } else { - Schema::Object(schema.into()) - } + Schema::Bool(true) } - /// Returns a `Schema` that matches nothing, such as the schema `{ "not":{} }`. - /// - /// The exact value returned depends on this generator's [`BoolSchemas`](struct.SchemaSettings.html#structfield.bool_schemas) setting. + #[deprecated = "Use `Schema::Bool(false)` instead"] pub fn schema_for_none(&self) -> Schema { - let schema: Schema = false.into(); - if self.settings().bool_schemas == BoolSchemas::Enabled { - schema - } else { - Schema::Object(schema.into()) - } + Schema::Bool(false) } /// Generates a JSON Schema for the type `T`, and returns either the schema itself or a `$ref` schema referencing `T`'s schema. @@ -253,7 +220,7 @@ impl SchemaGenerator { self.definitions.insert(name, schema); } - /// Returns the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated. + /// Borrows the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated. /// /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas /// themselves. @@ -275,14 +242,19 @@ impl SchemaGenerator { /// add them to the `SchemaGenerator`'s schema definitions and include them in the returned `SchemaObject`'s /// [`definitions`](../schema/struct.Metadata.html#structfield.definitions) pub fn root_schema_for(&mut self) -> RootSchema { - let mut schema = T::json_schema(self).into(); - self.make_extensible(&mut schema); + let mut schema = T::json_schema(self).into_object(); schema.metadata().title.get_or_insert_with(T::schema_name); - RootSchema { + let mut root = RootSchema { meta_schema: self.settings.meta_schema.clone(), definitions: self.definitions.clone(), schema, + }; + + for visitor in &self.settings.visitors.0 { + visitor.visit_root_schema(&mut root) } + + root } /// Consumes `self` and generates a root JSON Schema for the type `T`. @@ -290,14 +262,19 @@ impl SchemaGenerator { /// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will /// include them in the returned `SchemaObject`'s [`definitions`](../schema/struct.Metadata.html#structfield.definitions) pub fn into_root_schema_for(mut self) -> RootSchema { - let mut schema = T::json_schema(&mut self).into(); - self.make_extensible(&mut schema); + let mut schema = T::json_schema(&mut self).into_object(); schema.metadata().title.get_or_insert_with(T::schema_name); - RootSchema { + let mut root = RootSchema { meta_schema: self.settings.meta_schema, definitions: self.definitions, schema, + }; + + for visitor in &self.settings.visitors.0 { + visitor.visit_root_schema(&mut root) } + + root } /// Attemps to find the schema that the given `schema` is referencing. @@ -352,13 +329,70 @@ impl SchemaGenerator { None => return schema, Some(ref metadata) if *metadata == Metadata::default() => return schema, Some(metadata) => { - let mut schema_obj = schema.into(); - - self.make_extensible(&mut schema_obj); + let mut schema_obj = schema.into_object(); schema_obj.metadata = Some(Box::new(metadata)).merge(schema_obj.metadata); - Schema::Object(schema_obj) } } } } + +/// TODO document +#[derive(Debug)] +pub struct ReplaceBoolSchemas { + pub skip_additional_properties: bool, +} + +impl Visitor for ReplaceBoolSchemas { + fn visit_schema(&self, schema: &mut Schema) { + if let Schema::Bool(b) = *schema { + *schema = Schema::Bool(b).into_object().into() + } + + visit_schema(self, schema) + } + + fn visit_schema_object(&self, schema: &mut SchemaObject) { + if self.skip_additional_properties { + let mut additional_properties = None; + if let Some(obj) = &mut schema.object { + if let Some(ap) = &obj.additional_properties { + if let Schema::Bool(_) = ap.as_ref() { + additional_properties = obj.additional_properties.take(); + } + } + } + + visit_schema_object(self, schema); + + if additional_properties.is_some() { + schema.object().additional_properties = additional_properties; + } + } else { + visit_schema_object(self, schema); + } + } +} + +/// TODO document +#[derive(Debug)] +pub struct RemoveRefSiblings; + +impl Visitor for RemoveRefSiblings { + fn visit_schema_object(&self, schema: &mut SchemaObject) { + visit_schema_object(self, schema); + + if let Some(reference) = schema.reference.take() { + if schema == &SchemaObject::default() { + schema.reference = Some(reference); + } else { + let ref_schema = Schema::new_ref(reference); + let all_of = &mut schema.subschemas().all_of; + match all_of { + Some(vec) => vec.push(ref_schema), + None => *all_of = Some(vec![ref_schema]), + } + } + } + } +} diff --git a/schemars/src/json_schema_impls/core.rs b/schemars/src/json_schema_impls/core.rs index 84917c5..2c5b4b6 100644 --- a/schemars/src/json_schema_impls/core.rs +++ b/schemars/src/json_schema_impls/core.rs @@ -35,8 +35,7 @@ impl JsonSchema for Option { } } if gen.settings().option_nullable { - let mut schema_obj = schema.into(); - gen.make_extensible(&mut schema_obj); + let mut schema_obj = schema.into_object(); schema_obj .extensions .insert("nullable".to_owned(), json!(true)); @@ -178,8 +177,7 @@ forward_impl!((<'a> JsonSchema for std::fmt::Arguments<'a>) => String); #[cfg(test)] mod tests { use super::*; - use crate::gen::*; - use crate::tests::{custom_schema_object_for, schema_for, schema_object_for}; + use crate::tests::{schema_for, schema_object_for}; use pretty_assertions::assert_eq; #[test] @@ -209,21 +207,6 @@ mod tests { assert_eq!(any_of[1], schema_for::<()>()); } - #[test] - fn schema_for_option_with_nullable() { - let settings = SchemaSettings::default().with(|s| { - s.option_nullable = true; - s.option_add_null_type = false; - }); - let schema = custom_schema_object_for::>(settings); - assert_eq!( - schema.instance_type, - Some(SingleOrVec::from(InstanceType::Integer)) - ); - assert_eq!(schema.extensions.get("nullable"), Some(&json!(true))); - assert_eq!(schema.subschemas.is_none(), true); - } - #[test] fn schema_for_result() { let schema = schema_object_for::>(); diff --git a/schemars/src/json_schema_impls/maps.rs b/schemars/src/json_schema_impls/maps.rs index f3f08fa..356464f 100644 --- a/schemars/src/json_schema_impls/maps.rs +++ b/schemars/src/json_schema_impls/maps.rs @@ -1,4 +1,4 @@ -use crate::gen::{BoolSchemas, SchemaGenerator}; +use crate::gen::SchemaGenerator; use crate::schema::*; use crate::JsonSchema; @@ -16,18 +16,10 @@ macro_rules! map_impl { fn json_schema(gen: &mut SchemaGenerator) -> Schema { let subschema = gen.subschema_for::(); - let json_schema_bool = gen.settings().bool_schemas == BoolSchemas::AdditionalPropertiesOnly - && subschema == gen.schema_for_any(); - let additional_properties = - if json_schema_bool { - true.into() - } else { - subschema.into() - }; SchemaObject { instance_type: Some(InstanceType::Object.into()), object: Some(Box::new(ObjectValidation { - additional_properties: Some(Box::new(additional_properties)), + additional_properties: Some(Box::new(subschema)), ..Default::default() })), ..Default::default() @@ -40,68 +32,3 @@ macro_rules! map_impl { map_impl!( JsonSchema for std::collections::BTreeMap); map_impl!( JsonSchema for std::collections::HashMap); - -#[cfg(test)] -mod tests { - use super::*; - use crate::gen::*; - use crate::tests::{custom_schema_object_for, schema_for}; - use pretty_assertions::assert_eq; - use std::collections::BTreeMap; - - #[test] - fn schema_for_map_any_value() { - for bool_schemas in &[BoolSchemas::Enabled, BoolSchemas::AdditionalPropertiesOnly] { - let settings = SchemaSettings::default().with(|s| s.bool_schemas = *bool_schemas); - let schema = custom_schema_object_for::>(settings); - assert_eq!( - schema.instance_type, - Some(SingleOrVec::from(InstanceType::Object)) - ); - let additional_properties = schema - .object - .unwrap() - .additional_properties - .expect("additionalProperties field present"); - assert_eq!(*additional_properties, Schema::Bool(true)); - } - } - - #[test] - fn schema_for_map_any_value_no_bool_schema() { - let settings = SchemaSettings::default().with(|s| s.bool_schemas = BoolSchemas::Disabled); - let schema = custom_schema_object_for::>(settings); - assert_eq!( - schema.instance_type, - Some(SingleOrVec::from(InstanceType::Object)) - ); - let additional_properties = schema - .object - .unwrap() - .additional_properties - .expect("additionalProperties field present"); - assert_eq!(*additional_properties, Schema::Object(Default::default())); - } - - #[test] - fn schema_for_map_int_value() { - for bool_schemas in &[ - BoolSchemas::Enabled, - BoolSchemas::Disabled, - BoolSchemas::AdditionalPropertiesOnly, - ] { - let settings = SchemaSettings::default().with(|s| s.bool_schemas = *bool_schemas); - let schema = custom_schema_object_for::>(settings); - assert_eq!( - schema.instance_type, - Some(SingleOrVec::from(InstanceType::Object)) - ); - let additional_properties = schema - .object - .unwrap() - .additional_properties - .expect("additionalProperties field present"); - assert_eq!(*additional_properties, schema_for::()); - } - } -} diff --git a/schemars/src/json_schema_impls/serdejson.rs b/schemars/src/json_schema_impls/serdejson.rs index 17f714c..d0cceff 100644 --- a/schemars/src/json_schema_impls/serdejson.rs +++ b/schemars/src/json_schema_impls/serdejson.rs @@ -11,8 +11,8 @@ impl JsonSchema for Value { "AnyValue".to_owned() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - gen.schema_for_any() + fn json_schema(_: &mut SchemaGenerator) -> Schema { + Schema::Bool(true) } } diff --git a/schemars/src/lib.rs b/schemars/src/lib.rs index 0aad758..eb54638 100644 --- a/schemars/src/lib.rs +++ b/schemars/src/lib.rs @@ -230,6 +230,8 @@ mod macros; pub mod gen; /// JSON Schema types. pub mod schema; +/// TODO document +pub mod visit; #[cfg(feature = "schemars_derive")] extern crate schemars_derive; @@ -323,18 +325,9 @@ pub mod tests { schema_object(schema_for::()) } - pub fn custom_schema_object_for( - settings: gen::SchemaSettings, - ) -> schema::SchemaObject { - schema_object(custom_schema_for::(settings)) - } - pub fn schema_for() -> schema::Schema { - custom_schema_for::(Default::default()) - } - - pub fn custom_schema_for(settings: gen::SchemaSettings) -> schema::Schema { - T::json_schema(&mut gen::SchemaGenerator::new(settings)) + let mut gen = gen::SchemaGenerator::default(); + T::json_schema(&mut gen) } pub fn schema_object(schema: schema::Schema) -> schema::SchemaObject { diff --git a/schemars/src/macros.rs b/schemars/src/macros.rs index 2268826..ba2dc4b 100644 --- a/schemars/src/macros.rs +++ b/schemars/src/macros.rs @@ -1,4 +1,4 @@ -/// Generates a [`Schema`](schema::Schema) for the given type using default settings. +/// Generates a [`RootSchema`](schema::RootSchema) for the given type using default settings. /// /// The type must implement [`JsonSchema`]. /// diff --git a/schemars/src/schema.rs b/schemars/src/schema.rs index cfbce53..939075d 100644 --- a/schemars/src/schema.rs +++ b/schemars/src/schema.rs @@ -40,6 +40,21 @@ impl Schema { _ => false, } } + + /// TODO document + pub fn into_object(self) -> SchemaObject { + match self { + Schema::Object(o) => o, + Schema::Bool(true) => SchemaObject::default(), + Schema::Bool(false) => SchemaObject { + subschemas: Some(Box::new(SubschemaValidation { + not: Some(Schema::Object(Default::default()).into()), + ..Default::default() + })), + ..Default::default() + }, + } + } } impl From for Schema { @@ -204,17 +219,7 @@ impl SchemaObject { impl From for SchemaObject { fn from(schema: Schema) -> Self { - match schema { - Schema::Object(o) => o, - Schema::Bool(true) => SchemaObject::default(), - Schema::Bool(false) => SchemaObject { - subschemas: Some(Box::new(SubschemaValidation { - not: Some(Schema::Object(Default::default()).into()), - ..Default::default() - })), - ..Default::default() - }, - } + schema.into_object() } } diff --git a/schemars/src/visit.rs b/schemars/src/visit.rs new file mode 100644 index 0000000..7308d42 --- /dev/null +++ b/schemars/src/visit.rs @@ -0,0 +1,84 @@ +use crate::schema::{RootSchema, Schema, SchemaObject, SingleOrVec}; +use std::{any::Any, fmt::Debug}; + +pub trait Visitor: Debug + Any { + fn visit_root_schema(&self, root: &mut RootSchema) { + visit_root_schema(self, root) + } + + fn visit_schema(&self, schema: &mut Schema) { + visit_schema(self, schema) + } + + fn visit_schema_object(&self, schema: &mut SchemaObject) { + visit_schema_object(self, schema) + } +} + +pub fn visit_root_schema(v: &V, root: &mut RootSchema) { + v.visit_schema_object(&mut root.schema); + visit_map_values(v, &mut root.definitions); +} + +pub fn visit_schema(v: &V, schema: &mut Schema) { + if let Schema::Object(schema) = schema { + v.visit_schema_object(schema) + } +} + +pub fn visit_schema_object(v: &V, schema: &mut SchemaObject) { + if let Some(sub) = &mut schema.subschemas { + visit_vec(v, &mut sub.all_of); + visit_vec(v, &mut sub.any_of); + visit_vec(v, &mut sub.one_of); + visit_box(v, &mut sub.not); + visit_box(v, &mut sub.if_schema); + visit_box(v, &mut sub.then_schema); + visit_box(v, &mut sub.else_schema); + } + + if let Some(arr) = &mut schema.array { + visit_single_or_vec(v, &mut arr.items); + visit_box(v, &mut arr.additional_items); + visit_box(v, &mut arr.contains); + } + + if let Some(obj) = &mut schema.object { + visit_map_values(v, &mut obj.properties); + visit_map_values(v, &mut obj.pattern_properties); + visit_box(v, &mut obj.additional_properties); + visit_box(v, &mut obj.property_names); + } +} + +fn visit_box(v: &V, target: &mut Option>) { + if let Some(s) = target { + v.visit_schema(s) + } +} + +fn visit_vec(v: &V, target: &mut Option>) { + if let Some(vec) = target { + for s in vec { + v.visit_schema(s) + } + } +} + +fn visit_map_values(v: &V, target: &mut crate::Map) { + for s in target.values_mut() { + v.visit_schema(s) + } +} + +fn visit_single_or_vec(v: &V, target: &mut Option>) { + match target { + None => {} + Some(SingleOrVec::Single(s)) => v.visit_schema(s), + Some(SingleOrVec::Vec(vec)) => { + for s in vec { + v.visit_schema(s) + } + } + } +} diff --git a/schemars/tests/docs.rs b/schemars/tests/docs.rs index df07f4e..be97e34 100644 --- a/schemars/tests/docs.rs +++ b/schemars/tests/docs.rs @@ -58,7 +58,7 @@ fn doc_comments_struct() -> TestResult { #[test] fn doc_comments_struct_ref_siblings() -> TestResult { - let settings = SchemaSettings::draft07().with(|s| s.allow_ref_siblings = true); + let settings = SchemaSettings::draft2019_09(); test_generated_schema::("doc_comments_struct_ref_siblings", settings) } diff --git a/schemars/tests/expected/doc_comments_struct_ref_siblings.json b/schemars/tests/expected/doc_comments_struct_ref_siblings.json index c215d43..35bb648 100644 --- a/schemars/tests/expected/doc_comments_struct_ref_siblings.json +++ b/schemars/tests/expected/doc_comments_struct_ref_siblings.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2019-09/schema", "title": "This is the struct's title", "description": "This is the struct's description.", "type": "object",