Add Visitor trait, update changelog

This commit is contained in:
Graham Esau 2020-06-04 19:23:36 +01:00
parent 4b37f96c99
commit a829267111
11 changed files with 244 additions and 206 deletions

View file

@ -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::<SchemaObject>()`
### 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)

View file

@ -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<String>,
/// 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<Arc<dyn Visitor>>);
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<T: ?Sized + JsonSchema>(&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<T: ?Sized + JsonSchema>(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]),
}
}
}
}
}

View file

@ -35,8 +35,7 @@ impl<T: JsonSchema> JsonSchema for Option<T> {
}
}
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::<Option<i32>>(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::<Result<bool, String>>();

View file

@ -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::<V>();
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!(<K, V> JsonSchema for std::collections::BTreeMap<K, V>);
map_impl!(<K, V, H> JsonSchema for std::collections::HashMap<K, V, H>);
#[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::<BTreeMap<String, serde_json::Value>>(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::<BTreeMap<String, serde_json::Value>>(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::<BTreeMap<String, i32>>(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::<i32>());
}
}
}

View file

@ -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)
}
}

View file

@ -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::<T>())
}
pub fn custom_schema_object_for<T: JsonSchema>(
settings: gen::SchemaSettings,
) -> schema::SchemaObject {
schema_object(custom_schema_for::<T>(settings))
}
pub fn schema_for<T: JsonSchema>() -> schema::Schema {
custom_schema_for::<T>(Default::default())
}
pub fn custom_schema_for<T: JsonSchema>(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 {

View file

@ -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`].
///

View file

@ -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<SchemaObject> for Schema {
@ -204,17 +219,7 @@ impl SchemaObject {
impl From<Schema> 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()
}
}

84
schemars/src/visit.rs Normal file
View file

@ -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: Visitor + ?Sized>(v: &V, root: &mut RootSchema) {
v.visit_schema_object(&mut root.schema);
visit_map_values(v, &mut root.definitions);
}
pub fn visit_schema<V: Visitor + ?Sized>(v: &V, schema: &mut Schema) {
if let Schema::Object(schema) = schema {
v.visit_schema_object(schema)
}
}
pub fn visit_schema_object<V: Visitor + ?Sized>(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: Visitor + ?Sized>(v: &V, target: &mut Option<Box<Schema>>) {
if let Some(s) = target {
v.visit_schema(s)
}
}
fn visit_vec<V: Visitor + ?Sized>(v: &V, target: &mut Option<Vec<Schema>>) {
if let Some(vec) = target {
for s in vec {
v.visit_schema(s)
}
}
}
fn visit_map_values<V: Visitor + ?Sized>(v: &V, target: &mut crate::Map<String, Schema>) {
for s in target.values_mut() {
v.visit_schema(s)
}
}
fn visit_single_or_vec<V: Visitor + ?Sized>(v: &V, target: &mut Option<SingleOrVec<Schema>>) {
match target {
None => {}
Some(SingleOrVec::Single(s)) => v.visit_schema(s),
Some(SingleOrVec::Vec(vec)) => {
for s in vec {
v.visit_schema(s)
}
}
}
}

View file

@ -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::<MyStruct>("doc_comments_struct_ref_siblings", settings)
}

View file

@ -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",