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::schema::*;
use crate::{MakeSchemaError, Result};
use std::collections::BTreeMap as Map;
use std::collections::BTreeSet as Set;
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() {
return T::make_schema(self);
}
@ -97,30 +98,40 @@ impl SchemaGenerator {
let name = self
.definitions
.get(&type_id)
.map(|(n, _)| n.clone())
.map(|(n, _)| Ok(n.clone()))
.unwrap_or_else(|| {
let name = self.make_unique_name::<T>();
self.insert_new_subschema_for::<T>(type_id, name.clone());
name
});
self.insert_new_subschema_for::<T>(type_id, name.clone())?;
Ok(name)
})?;
let reference = format!("{}{}", self.settings().definitions_path, name);
SchemaRef { reference }.into()
Ok(SchemaRef { reference }.into())
}
fn insert_new_subschema_for<T: ?Sized + MakeSchema>(
&mut self,
type_id: SchemaTypeId,
name: String,
) {
) -> Result<String> {
self.names.insert(name.clone());
let dummy = Schema::Bool(false);
// 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
.entry(type_id)
.and_modify(|(_, s)| *s = schema);
.insert(type_id.clone(), (name.clone(), dummy));
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> {
@ -131,52 +142,64 @@ impl SchemaGenerator {
Map::from_iter(self.definitions.into_iter().map(|(_, v)| v))
}
pub fn root_schema_for<T: ?Sized + MakeSchema>(&mut self) -> Schema {
let schema = T::make_schema(self);
if let Schema::Object(mut o) = schema {
o.schema = Some("http://json-schema.org/draft-07/schema#".to_owned());
o.title = Some(T::schema_name());
o.definitions.extend(self.definitions());
return Schema::Object(o);
}
schema
pub fn root_schema_for<T: ?Sized + MakeSchema>(&mut self) -> Result {
let schema = T::make_schema(self)?;
Ok(match schema {
Schema::Object(mut o) => {
o.schema = Some("http://json-schema.org/draft-07/schema#".to_owned());
o.title = Some(T::schema_name());
o.definitions.extend(self.definitions());
Schema::Object(o)
}
schema => schema,
})
}
pub fn into_root_schema_for<T: ?Sized + MakeSchema>(mut self) -> Schema {
let schema = T::make_schema(&mut self);
if let Schema::Object(mut o) = schema {
o.schema = Some("http://json-schema.org/draft-07/schema#".to_owned());
o.title = Some(T::schema_name());
o.definitions.extend(self.into_definitions());
return Schema::Object(o);
}
schema
pub fn into_root_schema_for<T: ?Sized + MakeSchema>(mut self) -> Result {
let schema = T::make_schema(&mut self)?;
Ok(match schema {
Schema::Object(mut o) => {
o.schema = Some("http://json-schema.org/draft-07/schema#".to_owned());
o.title = Some(T::schema_name());
o.definitions.extend(self.into_definitions());
Schema::Object(o)
}
schema => schema,
})
}
pub(crate) fn try_get_schema_object<'a>(
&'a self,
mut schema: &'a Schema,
) -> Option<SchemaObject> {
pub(crate) fn get_schema_object<'a>(&'a self, mut schema: &'a Schema) -> Result<SchemaObject> {
loop {
match schema {
Schema::Object(o) => return Some(o.clone()),
Schema::Bool(true) => return Some(Default::default()),
Schema::Object(o) => return Ok(o.clone()),
Schema::Bool(true) => return Ok(Default::default()),
Schema::Bool(false) => {
return Some(SchemaObject {
return Ok(SchemaObject {
not: Some(Schema::Bool(true).into()),
..Default::default()
})
}
Schema::Ref(r) => {
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
schema = self
.definitions
.values()
.filter(|(n, _)| n == name)
.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 schema;
mod error;
#[macro_use]
mod macros;
pub use error::*;
pub use make_schema::MakeSchema;
pub use schemars_derive::*;

View file

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

View file

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

View file

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

View file

@ -3,13 +3,14 @@ use schemars::schema::*;
use schemars::{gen, schema_for};
use serde_json::{from_str, to_string_pretty};
use std::fs;
use std::error::Error;
#[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: 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)?)?;
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]
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: Schema = from_str(&expected_json)?;
let actual = gen::SchemaSettings::openapi3()
.into_generator()
.into_root_schema_for::<Schema>();
.into_root_schema_for::<Schema>()?;
fs::write(
"tests/schema-openapi3.actual.json",
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! {
#[automatically_derived]
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
}
};
@ -53,11 +53,11 @@ fn add_trait_bounds(generics: &mut Generics) {
fn wrap_schema_fields(schema_contents: TokenStream) -> TokenStream {
quote! {
schemars::schema::SchemaObject {
Ok(schemars::schema::SchemaObject {
#schema_contents
..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 {
match variant.style {
Style::Unit => quote! {
gen.subschema_for::<()>()
gen.subschema_for::<()>()?
},
Style::Newtype => {
let f = &variant.fields[0];
let ty = f.ty;
quote_spanned! {f.original.span()=>
gen.subschema_for::<#ty>()
gen.subschema_for::<#ty>()?
}
}
Style::Tuple => {
let types = variant.fields.iter().map(|f| f.ty);
quote! {
gen.subschema_for::<(#(#types),*)>()
gen.subschema_for::<(#(#types),*)>()?
}
}
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 ty = f.ty;
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! {