diff --git a/schemars/src/schema.rs b/schemars/src/schema.rs index 4bef66e0..464f055d 100644 --- a/schemars/src/schema.rs +++ b/schemars/src/schema.rs @@ -4,7 +4,6 @@ JSON Schema types. #[cfg(feature = "impl_json_schema")] use crate as schemars; -#[cfg(feature = "impl_json_schema")] use crate::JsonSchema; use crate::{Map, Set}; use serde::{Deserialize, Serialize}; @@ -26,6 +25,32 @@ pub enum Schema { Object(SchemaObject), } +macro_rules! with_metadata_fn { + ($method:ident, $name:ident, $ty:ty) => { + with_metadata_fn!( + concat!( + "Returns the schema with the ", stringify!($name), " metadata field set." + ), + $method, + $name, + $ty + ); + }; + ($doc:expr, $method:ident, $name:ident, $ty:ty) => { + #[doc = $doc] + pub fn $method(self, $name: impl Into<$ty>) -> Self { + let value = $name.into(); + if value == <$ty>::default() { + self + } else { + let mut schema_obj = self.into_object(); + schema_obj.metadata().$name = value.into(); + Schema::Object(schema_obj) + } + } + }; +} + impl Schema { /// Creates a new `$ref` schema. /// @@ -71,6 +96,86 @@ impl Schema { }, } } + + with_metadata_fn!(with_description, description, String); + with_metadata_fn!(with_id, id, String); + with_metadata_fn!(with_title, title, String); + with_metadata_fn!(with_deprecated, deprecated, bool); + with_metadata_fn!(with_read_only, read_only, bool); + with_metadata_fn!(with_write_only, write_only, bool); + with_metadata_fn!(with_default, default, Value); + + pub fn with_examples>(self, examples: I) -> Self { + let mut schema_obj = self.into_object(); + schema_obj.metadata().examples.extend(examples); + Schema::Object(schema_obj) + } + + /// Create a schema for a unit enum + pub fn new_unit_enum(variant: &str) -> Self { + Self::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + enum_values: Some(vec![variant.into()]), + ..SchemaObject::default() + }) + } + + /// Create a schema for an externally tagged enum + pub fn new_externally_tagged_enum(variant: &str, sub_schema: Schema) -> Self { + Self::Object(SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some(Box::new(ObjectValidation { + properties: { + let mut props = Map::new(); + props.insert(variant.to_owned(), sub_schema); + props + }, + required: { + let mut required = Set::new(); + required.insert(variant.to_owned()); + required + }, + // Externally tagged variants must prohibit additional + // properties irrespective of the disposition of + // `deny_unknown_fields`. If additional properties were allowed + // one could easily construct an object that validated against + // multiple variants since here it's the properties rather than + // the values of a property that distingish between variants. + additional_properties: Some(Box::new(false.into())), + ..Default::default() + })), + ..SchemaObject::default() + }) + } + + /// Create a schema for an internally tagged enum + pub fn new_internally_tagged_enum(tag_name: &str, variant: &str, deny_unknown_fields: bool) -> Self { + let tag_schema = Schema::Object(SchemaObject { + instance_type: Some( + InstanceType::String.into(), + ), + enum_values: Some(vec![variant.into()]), + ..Default::default() + }); + Self::Object(SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some(Box::new(ObjectValidation { + properties: { + let mut props = Map::new(); + props.insert(tag_name.to_owned(), tag_schema); + props + }, + required: { + let mut required = Set::new(); + required.insert(tag_name.to_owned()); + required + }, + additional_properties: deny_unknown_fields.then(|| Box::new(false.into())), + ..Default::default() + })), + ..SchemaObject::default() + }) + } } impl From for Schema { @@ -486,6 +591,16 @@ pub struct ObjectValidation { pub property_names: Option>, } +impl ObjectValidation { + pub fn insert_property(&mut self, key: &str, mut has_default: bool, required: bool, schema: Schema) { + self.properties.insert(key.to_owned(), schema); + has_default |= T::_schemars_private_is_option(); + if required || !has_default { + self.required.insert(key.to_owned()); + } + } +} + /// The possible types of values in JSON Schema documents. /// /// See [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1). diff --git a/schemars_derive/src/metadata.rs b/schemars_derive/src/metadata.rs index e9637044..36317885 100644 --- a/schemars_derive/src/metadata.rs +++ b/schemars_derive/src/metadata.rs @@ -17,10 +17,7 @@ impl<'a> SchemaMetadata<'a> { if !setters.is_empty() { *schema_expr = quote! {{ let schema = #schema_expr; - schemars::_private::apply_metadata(schema, schemars::schema::Metadata { - #(#setters)* - ..Default::default() - }) + schema #(#setters)* }} } } @@ -30,29 +27,29 @@ impl<'a> SchemaMetadata<'a> { if let Some(title) = &self.title { setters.push(quote! { - title: Some(#title.to_owned()), + .with_title(#title) }); } if let Some(description) = &self.description { setters.push(quote! { - description: Some(#description.to_owned()), + .with_description(#description) }); } if self.deprecated { setters.push(quote! { - deprecated: true, + .with_deprecated(true) }); } if self.read_only { setters.push(quote! { - read_only: true, + .with_read_only(true) }); } if self.write_only { setters.push(quote! { - write_only: true, + .with_write_only(true) }); } @@ -63,13 +60,13 @@ impl<'a> SchemaMetadata<'a> { } }); setters.push(quote! { - examples: vec![#(#examples),*].into_iter().flatten().collect(), + .with_examples([#(#examples),*].into_iter().flatten()) }); } if let Some(default) = &self.default { setters.push(quote! { - default: #default.and_then(|d| schemars::_schemars_maybe_to_value!(d)), + .with_default(#default.and_then(|d| schemars::_schemars_maybe_to_value!(d))) }); } diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index 9bd81325..138a7f47 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -162,7 +162,7 @@ fn expr_for_external_tagged_enum<'a>( let unit_names = unit_variants.iter().map(|v| v.name()); let unit_schema = schema_object(quote! { instance_type: Some(schemars::schema::InstanceType::String.into()), - enum_values: Some(vec![#(#unit_names.into()),*]), + enum_values: Some([#(#unit_names),*].into_iter().map(|v| v.into()).collect()), }); if complex_variants.is_empty() { @@ -178,35 +178,14 @@ fn expr_for_external_tagged_enum<'a>( let name = variant.name(); let mut schema_expr = if variant.is_unit() && variant.attrs.with.is_none() { - schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::String.into()), - enum_values: Some(vec![#name.into()]), - }) + quote! { + schemars::schema::Schema::new_unit_enum(#name) + } } else { let sub_schema = expr_for_untagged_enum_variant(variant, deny_unknown_fields); - schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::Object.into()), - object: Some(Box::new(schemars::schema::ObjectValidation { - properties: { - let mut props = schemars::Map::new(); - props.insert(#name.to_owned(), #sub_schema); - props - }, - required: { - let mut required = schemars::Set::new(); - required.insert(#name.to_owned()); - required - }, - // Externally tagged variants must prohibit additional - // properties irrespective of the disposition of - // `deny_unknown_fields`. If additional properties were allowed - // one could easily construct an object that validated against - // multiple variants since here it's the properties rather than - // the values of a property that distingish between variants. - additional_properties: Some(Box::new(false.into())), - ..Default::default() - })), - }) + quote! { + schemars::schema::Schema::new_externally_tagged_enum(#name, #sub_schema) + } }; variant @@ -227,43 +206,16 @@ fn expr_for_internal_tagged_enum<'a>( ) -> TokenStream { let mut unique_names = HashSet::new(); let mut count = 0; - let set_additional_properties = if deny_unknown_fields { - quote! { - additional_properties: Some(Box::new(false.into())), - } - } else { - TokenStream::new() - }; let variant_schemas = variants .map(|variant| { unique_names.insert(variant.name()); count += 1; let name = variant.name(); - let type_schema = schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::String.into()), - enum_values: Some(vec![#name.into()]), - }); - let mut tag_schema = schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::Object.into()), - object: Some(Box::new(schemars::schema::ObjectValidation { - properties: { - let mut props = schemars::Map::new(); - props.insert(#tag_name.to_owned(), #type_schema); - props - }, - required: { - let mut required = schemars::Set::new(); - required.insert(#tag_name.to_owned()); - required - }, - // As we're creating a "wrapper" object, we can honor the - // disposition of deny_unknown_fields. - #set_additional_properties - ..Default::default() - })), - }); + let mut tag_schema = quote! { + schemars::schema::Schema::new_internally_tagged_enum(#tag_name, #name, #deny_unknown_fields) + }; variant.attrs.as_metadata().apply_to_schema(&mut tag_schema); @@ -498,19 +450,8 @@ fn expr_for_struct( let (ty, type_def) = type_for_field_schema(field); - let maybe_insert_required = match (&default, field.validation_attrs.required()) { - (Some(_), _) => TokenStream::new(), - (None, false) => { - quote! { - if !<#ty as schemars::JsonSchema>::_schemars_private_is_option() { - object_validation.required.insert(#name.to_owned()); - } - } - } - (None, true) => quote! { - object_validation.required.insert(#name.to_owned()); - }, - }; + let has_default = default.is_some(); + let required = field.validation_attrs.required(); let metadata = SchemaMetadata { read_only: field.serde_attrs.skip_deserializing(), @@ -536,8 +477,7 @@ fn expr_for_struct( quote! { { #type_def - object_validation.properties.insert(#name.to_owned(), #schema_expr); - #maybe_insert_required + object_validation.insert_property::<#ty>(#name, #has_default, #required, #schema_expr); } } })