Allow making a schema to fail by returning an Err

This commit is contained in:
Graham Esau 2019-08-06 20:56:04 +01:00
parent 2aa1835240
commit 51ed13218c
8 changed files with 149 additions and 99 deletions

26
schemars/src/error.rs Normal file
View file

@ -0,0 +1,26 @@
use std::fmt;
use std::error::Error;
use crate::schema::Schema;
pub type Result<T = Schema> = std::result::Result<T, MakeSchemaError>;
#[derive(Debug, Clone)]
pub struct MakeSchemaError {
msg: &'static str,
schema: Schema
}
impl MakeSchemaError {
pub fn new(msg: &'static str, schema: Schema) -> MakeSchemaError {
MakeSchemaError { msg, schema }
}
}
impl fmt::Display for MakeSchemaError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} Schema: {:?}", self.msg, self.schema)
}
}
impl Error for MakeSchemaError {
}

View file

@ -1,5 +1,6 @@
use crate::make_schema::{MakeSchema, SchemaTypeId}; use crate::make_schema::{MakeSchema, SchemaTypeId};
use crate::schema::*; use crate::schema::*;
use crate::{MakeSchemaError, Result};
use std::collections::BTreeMap as Map; use std::collections::BTreeMap as Map;
use std::collections::BTreeSet as Set; use std::collections::BTreeSet as Set;
use std::iter::FromIterator; use std::iter::FromIterator;
@ -88,7 +89,7 @@ impl SchemaGenerator {
} }
} }
pub fn subschema_for<T: ?Sized + MakeSchema>(&mut self) -> Schema { pub fn subschema_for<T: ?Sized + MakeSchema>(&mut self) -> Result {
if !T::is_referenceable() { if !T::is_referenceable() {
return T::make_schema(self); return T::make_schema(self);
} }
@ -97,30 +98,40 @@ impl SchemaGenerator {
let name = self let name = self
.definitions .definitions
.get(&type_id) .get(&type_id)
.map(|(n, _)| n.clone()) .map(|(n, _)| Ok(n.clone()))
.unwrap_or_else(|| { .unwrap_or_else(|| {
let name = self.make_unique_name::<T>(); let name = self.make_unique_name::<T>();
self.insert_new_subschema_for::<T>(type_id, name.clone()); self.insert_new_subschema_for::<T>(type_id, name.clone())?;
name Ok(name)
}); })?;
let reference = format!("{}{}", self.settings().definitions_path, name); let reference = format!("{}{}", self.settings().definitions_path, name);
SchemaRef { reference }.into() Ok(SchemaRef { reference }.into())
} }
fn insert_new_subschema_for<T: ?Sized + MakeSchema>( fn insert_new_subschema_for<T: ?Sized + MakeSchema>(
&mut self, &mut self,
type_id: SchemaTypeId, type_id: SchemaTypeId,
name: String, name: String,
) { ) -> Result<String> {
self.names.insert(name.clone()); self.names.insert(name.clone());
let dummy = Schema::Bool(false); let dummy = Schema::Bool(false);
// insert into definitions BEFORE calling make_schema to avoid infinite recursion // insert into definitions BEFORE calling make_schema to avoid infinite recursion
self.definitions.insert(type_id.clone(), (name, dummy));
let schema = T::make_schema(self);
self.definitions self.definitions
.entry(type_id) .insert(type_id.clone(), (name.clone(), dummy));
.and_modify(|(_, s)| *s = schema);
match T::make_schema(self) {
Ok(schema) => {
self.definitions
.entry(type_id)
.and_modify(|(_, s)| *s = schema);
Ok(name)
}
Err(e) => {
self.names.remove(&name);
self.definitions.remove(&type_id);
Err(e)
}
}
} }
pub fn definitions(&self) -> Map<String, Schema> { pub fn definitions(&self) -> Map<String, Schema> {
@ -131,52 +142,64 @@ impl SchemaGenerator {
Map::from_iter(self.definitions.into_iter().map(|(_, v)| v)) Map::from_iter(self.definitions.into_iter().map(|(_, v)| v))
} }
pub fn root_schema_for<T: ?Sized + MakeSchema>(&mut self) -> Schema { pub fn root_schema_for<T: ?Sized + MakeSchema>(&mut self) -> Result {
let schema = T::make_schema(self); let schema = T::make_schema(self)?;
if let Schema::Object(mut o) = schema { Ok(match schema {
o.schema = Some("http://json-schema.org/draft-07/schema#".to_owned()); Schema::Object(mut o) => {
o.title = Some(T::schema_name()); o.schema = Some("http://json-schema.org/draft-07/schema#".to_owned());
o.definitions.extend(self.definitions()); o.title = Some(T::schema_name());
return Schema::Object(o); o.definitions.extend(self.definitions());
} Schema::Object(o)
schema }
schema => schema,
})
} }
pub fn into_root_schema_for<T: ?Sized + MakeSchema>(mut self) -> Schema { pub fn into_root_schema_for<T: ?Sized + MakeSchema>(mut self) -> Result {
let schema = T::make_schema(&mut self); let schema = T::make_schema(&mut self)?;
if let Schema::Object(mut o) = schema { Ok(match schema {
o.schema = Some("http://json-schema.org/draft-07/schema#".to_owned()); Schema::Object(mut o) => {
o.title = Some(T::schema_name()); o.schema = Some("http://json-schema.org/draft-07/schema#".to_owned());
o.definitions.extend(self.into_definitions()); o.title = Some(T::schema_name());
return Schema::Object(o); o.definitions.extend(self.into_definitions());
} Schema::Object(o)
schema }
schema => schema,
})
} }
pub(crate) fn try_get_schema_object<'a>( pub(crate) fn get_schema_object<'a>(&'a self, mut schema: &'a Schema) -> Result<SchemaObject> {
&'a self,
mut schema: &'a Schema,
) -> Option<SchemaObject> {
loop { loop {
match schema { match schema {
Schema::Object(o) => return Some(o.clone()), Schema::Object(o) => return Ok(o.clone()),
Schema::Bool(true) => return Some(Default::default()), Schema::Bool(true) => return Ok(Default::default()),
Schema::Bool(false) => { Schema::Bool(false) => {
return Some(SchemaObject { return Ok(SchemaObject {
not: Some(Schema::Bool(true).into()), not: Some(Schema::Bool(true).into()),
..Default::default() ..Default::default()
}) })
} }
Schema::Ref(r) => { Schema::Ref(r) => {
let definitions_path_len = self.settings().definitions_path.len(); let definitions_path_len = self.settings().definitions_path.len();
let name = r.reference.get(definitions_path_len..)?; let name = r.reference.get(definitions_path_len..).ok_or_else(|| {
MakeSchemaError::new(
"Could not extract referenced schema name.",
Schema::Ref(r.clone()),
)
})?;
// FIXME this is pretty inefficient // FIXME this is pretty inefficient
schema = self schema = self
.definitions .definitions
.values() .values()
.filter(|(n, _)| n == name) .filter(|(n, _)| n == name)
.map(|(_, s)| s) .map(|(_, s)| s)
.next()?; .next()
.ok_or_else(|| {
MakeSchemaError::new(
"Could not find referenced schema.",
Schema::Ref(r.clone()),
)
})?;
} }
} }
} }

View file

@ -2,9 +2,12 @@ pub mod gen;
pub mod make_schema; pub mod make_schema;
pub mod schema; pub mod schema;
mod error;
#[macro_use] #[macro_use]
mod macros; mod macros;
pub use error::*;
pub use make_schema::MakeSchema; pub use make_schema::MakeSchema;
pub use schemars_derive::*; pub use schemars_derive::*;

View file

@ -1,9 +1,8 @@
use schemars::schema_for; use schemars::{schema_for, schema::Schema};
use schemars::schema::Schema; use std::error::Error;
use serde_json::Result;
fn main() -> Result<()> { fn main() -> Result<(), Box<dyn Error>> {
let schema = schema_for!(Schema); let schema = schema_for!(Schema)?;
let json = serde_json::to_string_pretty(&schema)?; let json = serde_json::to_string_pretty(&schema)?;
println!("{}", json); println!("{}", json);

View file

@ -1,5 +1,6 @@
use crate::gen::{BoolSchemas, SchemaGenerator}; use crate::gen::{BoolSchemas, SchemaGenerator};
use crate::schema::*; use crate::schema::*;
use crate::Result;
use serde_json::json; use serde_json::json;
use std::collections::BTreeMap as Map; use std::collections::BTreeMap as Map;
@ -24,7 +25,7 @@ pub trait MakeSchema {
true true
} }
fn make_schema(gen: &mut SchemaGenerator) -> Schema; fn make_schema(gen: &mut SchemaGenerator) -> Result;
} }
macro_rules! no_ref_schema { macro_rules! no_ref_schema {
@ -52,12 +53,12 @@ macro_rules! simple_impl {
impl MakeSchema for $type { impl MakeSchema for $type {
no_ref_schema!(); no_ref_schema!();
fn make_schema(_: &mut SchemaGenerator) -> Schema { fn make_schema(_: &mut SchemaGenerator) -> Result {
SchemaObject { Ok(SchemaObject {
instance_type: Some(InstanceType::$instance_type.into()), instance_type: Some(InstanceType::$instance_type.into()),
..Default::default() ..Default::default()
} }
.into() .into())
} }
} }
}; };
@ -85,16 +86,16 @@ simple_impl!(() => Null);
impl MakeSchema for char { impl MakeSchema for char {
no_ref_schema!(); no_ref_schema!();
fn make_schema(_: &mut SchemaGenerator) -> Schema { fn make_schema(_: &mut SchemaGenerator) -> Result {
let mut extensions = Map::new(); let mut extensions = Map::new();
extensions.insert("minLength".to_owned(), json!(1)); extensions.insert("minLength".to_owned(), json!(1));
extensions.insert("maxLength".to_owned(), json!(1)); extensions.insert("maxLength".to_owned(), json!(1));
SchemaObject { Ok(SchemaObject {
instance_type: Some(InstanceType::String.into()), instance_type: Some(InstanceType::String.into()),
extensions, extensions,
..Default::default() ..Default::default()
} }
.into() .into())
} }
} }
@ -104,15 +105,15 @@ impl MakeSchema for char {
impl<T> MakeSchema for [T; 0] { impl<T> MakeSchema for [T; 0] {
no_ref_schema!(); no_ref_schema!();
fn make_schema(_: &mut SchemaGenerator) -> Schema { fn make_schema(_: &mut SchemaGenerator) -> Result {
let mut extensions = Map::new(); let mut extensions = Map::new();
extensions.insert("maxItems".to_owned(), json!(0)); extensions.insert("maxItems".to_owned(), json!(0));
SchemaObject { Ok(SchemaObject {
instance_type: Some(InstanceType::Array.into()), instance_type: Some(InstanceType::Array.into()),
extensions, extensions,
..Default::default() ..Default::default()
} }
.into() .into())
} }
} }
@ -122,16 +123,16 @@ macro_rules! array_impls {
impl<T: MakeSchema> MakeSchema for [T; $len] { impl<T: MakeSchema> MakeSchema for [T; $len] {
no_ref_schema!(); no_ref_schema!();
fn make_schema(gen: &mut SchemaGenerator) -> Schema { fn make_schema(gen: &mut SchemaGenerator) -> Result {
let mut extensions = Map::new(); let mut extensions = Map::new();
extensions.insert("minItems".to_owned(), json!($len)); extensions.insert("minItems".to_owned(), json!($len));
extensions.insert("maxItems".to_owned(), json!($len)); extensions.insert("maxItems".to_owned(), json!($len));
SchemaObject { Ok(SchemaObject {
instance_type: Some(InstanceType::Array.into()), instance_type: Some(InstanceType::Array.into()),
items: Some(gen.subschema_for::<T>().into()), items: Some(gen.subschema_for::<T>()?.into()),
extensions, extensions,
..Default::default() ..Default::default()
}.into() }.into())
} }
} }
)+ )+
@ -153,19 +154,19 @@ macro_rules! tuple_impls {
impl<$($name: MakeSchema),+> MakeSchema for ($($name,)+) { impl<$($name: MakeSchema),+> MakeSchema for ($($name,)+) {
no_ref_schema!(); no_ref_schema!();
fn make_schema(gen: &mut SchemaGenerator) -> Schema { fn make_schema(gen: &mut SchemaGenerator) -> Result {
let mut extensions = Map::new(); let mut extensions = Map::new();
extensions.insert("minItems".to_owned(), json!($len)); extensions.insert("minItems".to_owned(), json!($len));
extensions.insert("maxItems".to_owned(), json!($len)); extensions.insert("maxItems".to_owned(), json!($len));
let items = vec![ let items = vec![
$(gen.subschema_for::<$name>()),+ $(gen.subschema_for::<$name>()?),+
]; ];
SchemaObject { Ok(SchemaObject {
instance_type: Some(InstanceType::Array.into()), instance_type: Some(InstanceType::Array.into()),
items: Some(items.into()), items: Some(items.into()),
extensions, extensions,
..Default::default() ..Default::default()
}.into() }.into())
} }
} }
)+ )+
@ -201,13 +202,12 @@ macro_rules! seq_impl {
{ {
no_ref_schema!(); no_ref_schema!();
fn make_schema(gen: &mut SchemaGenerator) -> Schema fn make_schema(gen: &mut SchemaGenerator) -> Result {
{ Ok(SchemaObject {
SchemaObject {
instance_type: Some(InstanceType::Array.into()), instance_type: Some(InstanceType::Array.into()),
items: Some(gen.subschema_for::<T>().into()), items: Some(gen.subschema_for::<T>()?.into()),
..Default::default() ..Default::default()
}.into() }.into())
} }
} }
}; };
@ -231,9 +231,8 @@ macro_rules! map_impl {
{ {
no_ref_schema!(); no_ref_schema!();
fn make_schema(gen: &mut SchemaGenerator) -> Schema fn make_schema(gen: &mut SchemaGenerator) -> Result {
{ let subschema = gen.subschema_for::<V>()?;
let subschema = gen.subschema_for::<V>();
let make_schema_bool = gen.settings().bool_schemas == BoolSchemas::AdditionalPropertiesOnly let make_schema_bool = gen.settings().bool_schemas == BoolSchemas::AdditionalPropertiesOnly
&& subschema == gen.schema_for_any(); && subschema == gen.schema_for_any();
let mut extensions = Map::new(); let mut extensions = Map::new();
@ -245,11 +244,11 @@ macro_rules! map_impl {
json!(subschema) json!(subschema)
} }
); );
SchemaObject { Ok(SchemaObject {
instance_type: Some(InstanceType::Object.into()), instance_type: Some(InstanceType::Object.into()),
extensions, extensions,
..Default::default() ..Default::default()
}.into() }.into())
} }
} }
}; };
@ -267,28 +266,25 @@ impl<T: MakeSchema> MakeSchema for Option<T> {
T::is_referenceable() T::is_referenceable()
} }
fn make_schema(gen: &mut SchemaGenerator) -> Schema { fn make_schema(gen: &mut SchemaGenerator) -> Result {
let mut schema = gen.subschema_for::<T>(); let mut schema = gen.subschema_for::<T>()?;
if gen.settings().option_add_null_type { if gen.settings().option_add_null_type {
schema = match schema { schema = match schema {
Schema::Bool(true) => Schema::Bool(true), Schema::Bool(true) => Schema::Bool(true),
Schema::Bool(false) => <()>::make_schema(gen), Schema::Bool(false) => <()>::make_schema(gen)?,
schema => SchemaObject { schema => SchemaObject {
any_of: Some(vec![schema, <()>::make_schema(gen)]), any_of: Some(vec![schema, <()>::make_schema(gen)?]),
..Default::default() ..Default::default()
} }
.into(), .into(),
} }
} }
if gen.settings().option_nullable { if gen.settings().option_nullable {
let deref = gen.try_get_schema_object(&schema); let mut deref = gen.get_schema_object(&schema)?;
debug_assert!(deref.is_some(), "Could not get schema object: {:?}", schema); deref.extensions.insert("nullable".to_owned(), json!(true));
if let Some(mut deref) = deref { schema = Schema::Object(deref);
deref.extensions.insert("nullable".to_owned(), json!(true));
schema = Schema::Object(deref);
}
}; };
schema Ok(schema)
} }
} }
@ -302,7 +298,7 @@ macro_rules! deref_impl {
{ {
no_ref_schema!(); no_ref_schema!();
fn make_schema(gen: &mut SchemaGenerator) -> Schema { fn make_schema(gen: &mut SchemaGenerator) -> Result {
gen.subschema_for::<T>() gen.subschema_for::<T>()
} }
} }
@ -321,7 +317,7 @@ deref_impl!(<'a, T: ToOwned> MakeSchema for std::borrow::Cow<'a, T>);
impl MakeSchema for serde_json::Value { impl MakeSchema for serde_json::Value {
no_ref_schema!(); no_ref_schema!();
fn make_schema(gen: &mut SchemaGenerator) -> Schema { fn make_schema(gen: &mut SchemaGenerator) -> Result {
gen.schema_for_any() Ok(gen.schema_for_any())
} }
} }

View file

@ -1,6 +1,7 @@
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use schemars::{schema_for, MakeSchema}; use schemars::{schema_for, MakeSchema};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::error::Error;
#[derive(Serialize, Deserialize, Debug, PartialEq, MakeSchema)] #[derive(Serialize, Deserialize, Debug, PartialEq, MakeSchema)]
struct Flat { struct Flat {
@ -32,6 +33,7 @@ struct Deep3 {
#[test] #[test]
#[ignore = "flattening is not yet implemented"] #[ignore = "flattening is not yet implemented"]
fn flatten_schema() { fn flatten_schema() -> Result<(), Box<dyn Error>> {
assert_eq!(schema_for!(Flat), schema_for!(Deep1)); assert_eq!(schema_for!(Flat)?, schema_for!(Deep1)?);
Ok(())
} }

View file

@ -3,13 +3,14 @@ use schemars::schema::*;
use schemars::{gen, schema_for}; use schemars::{gen, schema_for};
use serde_json::{from_str, to_string_pretty}; use serde_json::{from_str, to_string_pretty};
use std::fs; use std::fs;
use std::error::Error;
#[test] #[test]
fn schema_matches_default_settings() -> Result<(), Box<dyn std::error::Error>> { fn schema_matches_default_settings() -> Result<(), Box<dyn Error>> {
let expected_json = fs::read_to_string("tests/schema.json")?; let expected_json = fs::read_to_string("tests/schema.json")?;
let expected: Schema = from_str(&expected_json)?; let expected: Schema = from_str(&expected_json)?;
let actual = schema_for!(Schema); let actual = schema_for!(Schema)?;
fs::write("tests/schema.actual.json", to_string_pretty(&actual)?)?; fs::write("tests/schema.actual.json", to_string_pretty(&actual)?)?;
assert_eq!(actual, expected, "Generated schema did not match saved schema - generated schema has been written to \"tests/schema.actual.json\"."); assert_eq!(actual, expected, "Generated schema did not match saved schema - generated schema has been written to \"tests/schema.actual.json\".");
@ -17,13 +18,13 @@ fn schema_matches_default_settings() -> Result<(), Box<dyn std::error::Error>> {
} }
#[test] #[test]
fn schema_matches_openapi3() -> Result<(), Box<dyn std::error::Error>> { fn schema_matches_openapi3() -> Result<(), Box<dyn Error>> {
let expected_json = fs::read_to_string("tests/schema-openapi3.json")?; let expected_json = fs::read_to_string("tests/schema-openapi3.json")?;
let expected: Schema = from_str(&expected_json)?; let expected: Schema = from_str(&expected_json)?;
let actual = gen::SchemaSettings::openapi3() let actual = gen::SchemaSettings::openapi3()
.into_generator() .into_generator()
.into_root_schema_for::<Schema>(); .into_root_schema_for::<Schema>()?;
fs::write( fs::write(
"tests/schema-openapi3.actual.json", "tests/schema-openapi3.actual.json",
to_string_pretty(&actual)?, to_string_pretty(&actual)?,

View file

@ -35,7 +35,7 @@ pub fn derive_make_schema(input: proc_macro::TokenStream) -> proc_macro::TokenSt
let impl_block = quote! { let impl_block = quote! {
#[automatically_derived] #[automatically_derived]
impl #impl_generics schemars::MakeSchema for #name #ty_generics #where_clause { impl #impl_generics schemars::MakeSchema for #name #ty_generics #where_clause {
fn make_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { fn make_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Result {
#schema #schema
} }
}; };
@ -53,11 +53,11 @@ fn add_trait_bounds(generics: &mut Generics) {
fn wrap_schema_fields(schema_contents: TokenStream) -> TokenStream { fn wrap_schema_fields(schema_contents: TokenStream) -> TokenStream {
quote! { quote! {
schemars::schema::SchemaObject { Ok(schemars::schema::SchemaObject {
#schema_contents #schema_contents
..Default::default() ..Default::default()
} }
.into() .into())
} }
} }
@ -131,19 +131,19 @@ fn schema_for_untagged_enum(variants: &[Variant]) -> TokenStream {
fn schema_for_untagged_enum_variant(variant: &Variant) -> TokenStream { fn schema_for_untagged_enum_variant(variant: &Variant) -> TokenStream {
match variant.style { match variant.style {
Style::Unit => quote! { Style::Unit => quote! {
gen.subschema_for::<()>() gen.subschema_for::<()>()?
}, },
Style::Newtype => { Style::Newtype => {
let f = &variant.fields[0]; let f = &variant.fields[0];
let ty = f.ty; let ty = f.ty;
quote_spanned! {f.original.span()=> quote_spanned! {f.original.span()=>
gen.subschema_for::<#ty>() gen.subschema_for::<#ty>()?
} }
} }
Style::Tuple => { Style::Tuple => {
let types = variant.fields.iter().map(|f| f.ty); let types = variant.fields.iter().map(|f| f.ty);
quote! { quote! {
gen.subschema_for::<(#(#types),*)>() gen.subschema_for::<(#(#types),*)>()?
} }
} }
Style::Struct => schema_for_struct(&variant.fields), Style::Struct => schema_for_struct(&variant.fields),
@ -155,7 +155,7 @@ fn schema_for_struct(fields: &[Field]) -> TokenStream {
let name = f.attrs.name().deserialize_name(); let name = f.attrs.name().deserialize_name();
let ty = f.ty; let ty = f.ty;
quote_spanned! {f.original.span()=> quote_spanned! {f.original.span()=>
props.insert(#name.to_owned(), gen.subschema_for::<#ty>()); props.insert(#name.to_owned(), gen.subschema_for::<#ty>()?);
} }
}); });
wrap_schema_fields(quote! { wrap_schema_fields(quote! {