From f6482fd4600e10a19c9370747afc60056a3fa55a Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Thu, 25 Mar 2021 18:32:28 +0000 Subject: [PATCH] Generate schema from any serializable value (#75) Implement schema_for_value!(...) macro --- README.md | 62 +++ docs/3-generating.md | 4 +- docs/_includes/examples/from_value.rs | 24 + .../_includes/examples/from_value.schema.json | 24 + schemars/examples/from_value.rs | 24 + schemars/examples/from_value.schema.json | 24 + schemars/src/flatten.rs | 3 + schemars/src/gen.rs | 67 ++- schemars/src/json_schema_impls/core.rs | 1 + schemars/src/lib.rs | 67 ++- schemars/src/macros.rs | 57 ++ schemars/src/ser.rs | 496 ++++++++++++++++++ .../tests/expected/from_value_2019_09.json | 80 +++ .../tests/expected/from_value_draft07.json | 80 +++ .../tests/expected/from_value_openapi3.json | 80 +++ schemars/tests/from_value.rs | 77 +++ schemars/tests/ui/schema_for_arg_value.rs | 5 + schemars/tests/ui/schema_for_arg_value.stderr | 7 + schemars/tests/util/mod.rs | 2 +- 19 files changed, 1179 insertions(+), 5 deletions(-) create mode 100644 docs/_includes/examples/from_value.rs create mode 100644 docs/_includes/examples/from_value.schema.json create mode 100644 schemars/examples/from_value.rs create mode 100644 schemars/examples/from_value.schema.json create mode 100644 schemars/src/ser.rs create mode 100644 schemars/tests/expected/from_value_2019_09.json create mode 100644 schemars/tests/expected/from_value_draft07.json create mode 100644 schemars/tests/expected/from_value_openapi3.json create mode 100644 schemars/tests/from_value.rs create mode 100644 schemars/tests/ui/schema_for_arg_value.rs create mode 100644 schemars/tests/ui/schema_for_arg_value.stderr diff --git a/README.md b/README.md index 324b26d..0ce3798 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,68 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); `#[serde(...)]` attributes can be overriden using `#[schemars(...)]` attributes, which behave identically (e.g. `#[schemars(rename_all = "camelCase")]`). You may find this useful if you want to change the generated schema without affecting Serde's behaviour, or if you're just not using Serde. +### Schema from Example Values + +If you want a schema for a type that can't/doesn't implement `JsonSchema`, but does implement `serde::Serialize`, then you can generate a JSON schema from a value of that type. However, this schema will generally be less precise than if the type implemented `JsonSchema` - particularly when it involves enums, since schemars will not make any assumptions about the structure of an enum based on a single variant. + +```rust +use schemars::schema_for_value; +use serde::Serialize; + +#[derive(Serialize)] +pub struct MyStruct { + pub my_int: i32, + pub my_bool: bool, + pub my_nullable_enum: Option, +} + +#[derive(Serialize)] +pub enum MyEnum { + StringNewType(String), + StructVariant { floats: Vec }, +} + +fn main() { + let schema = schema_for_value!(MyStruct { + my_int: 123, + my_bool: true, + my_nullable_enum: Some(MyEnum::StringNewType("foo".to_string())) + }); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} +``` + +
+Click to see the output JSON schema... + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MyStruct", + "examples": [ + { + "my_bool": true, + "my_int": 123, + "my_nullable_enum": { + "StringNewType": "foo" + } + } + ], + "type": "object", + "properties": { + "my_bool": { + "type": "boolean" + }, + "my_int": { + "type": "integer", + "format": "int32" + }, + "my_nullable_enum": true + } +} +``` +
+ ## Feature Flags - `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro - `impl_json_schema` - implements `JsonSchema` for Schemars types themselves diff --git a/docs/3-generating.md b/docs/3-generating.md index f5c2d83..29222f0 100644 --- a/docs/3-generating.md +++ b/docs/3-generating.md @@ -20,6 +20,8 @@ If you want more control over how the schema is generated, you can use the [`gen See the API documentation for more info on how to use those types for custom schema generation. - diff --git a/docs/_includes/examples/from_value.rs b/docs/_includes/examples/from_value.rs new file mode 100644 index 0000000..9d50961 --- /dev/null +++ b/docs/_includes/examples/from_value.rs @@ -0,0 +1,24 @@ +use schemars::schema_for_value; +use serde::Serialize; + +#[derive(Serialize)] +pub struct MyStruct { + pub my_int: i32, + pub my_bool: bool, + pub my_nullable_enum: Option, +} + +#[derive(Serialize)] +pub enum MyEnum { + StringNewType(String), + StructVariant { floats: Vec }, +} + +fn main() { + let schema = schema_for_value!(MyStruct { + my_int: 123, + my_bool: true, + my_nullable_enum: Some(MyEnum::StringNewType("foo".to_string())) + }); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/docs/_includes/examples/from_value.schema.json b/docs/_includes/examples/from_value.schema.json new file mode 100644 index 0000000..e7ca457 --- /dev/null +++ b/docs/_includes/examples/from_value.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MyStruct", + "examples": [ + { + "my_bool": true, + "my_int": 123, + "my_nullable_enum": { + "StringNewType": "foo" + } + } + ], + "type": "object", + "properties": { + "my_bool": { + "type": "boolean" + }, + "my_int": { + "type": "integer", + "format": "int32" + }, + "my_nullable_enum": true + } +} diff --git a/schemars/examples/from_value.rs b/schemars/examples/from_value.rs new file mode 100644 index 0000000..9d50961 --- /dev/null +++ b/schemars/examples/from_value.rs @@ -0,0 +1,24 @@ +use schemars::schema_for_value; +use serde::Serialize; + +#[derive(Serialize)] +pub struct MyStruct { + pub my_int: i32, + pub my_bool: bool, + pub my_nullable_enum: Option, +} + +#[derive(Serialize)] +pub enum MyEnum { + StringNewType(String), + StructVariant { floats: Vec }, +} + +fn main() { + let schema = schema_for_value!(MyStruct { + my_int: 123, + my_bool: true, + my_nullable_enum: Some(MyEnum::StringNewType("foo".to_string())) + }); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/schemars/examples/from_value.schema.json b/schemars/examples/from_value.schema.json new file mode 100644 index 0000000..e7ca457 --- /dev/null +++ b/schemars/examples/from_value.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MyStruct", + "examples": [ + { + "my_bool": true, + "my_int": 123, + "my_nullable_enum": { + "StringNewType": "foo" + } + } + ], + "type": "object", + "properties": { + "my_bool": { + "type": "boolean" + }, + "my_int": { + "type": "integer", + "format": "int32" + }, + "my_nullable_enum": true + } +} diff --git a/schemars/src/flatten.rs b/schemars/src/flatten.rs index 1e7dab3..646f614 100644 --- a/schemars/src/flatten.rs +++ b/schemars/src/flatten.rs @@ -2,6 +2,9 @@ use crate::schema::*; use crate::{Map, Set}; impl Schema { + /// This function is only public for use by schemars_derive. + /// + /// It should not be considered part of the public API. #[doc(hidden)] pub fn flatten(self, other: Self) -> Schema { if is_null_type(&self) { diff --git a/schemars/src/gen.rs b/schemars/src/gen.rs index aff686c..72c1700 100644 --- a/schemars/src/gen.rs +++ b/schemars/src/gen.rs @@ -1,7 +1,7 @@ /*! JSON Schema generator and settings. -This module is useful if you want more control over how the schema generated then the [`schema_for!`] macro gives you. +This module is useful if you want more control over how the schema generated than the [`schema_for!`] macro gives you. There are two main types in this module:two main types in this module: * [`SchemaSettings`], which defines what JSON Schema features should be used when generating schemas (for example, how `Option`s should be represented). * [`SchemaGenerator`], which manages the generation of a schema document. @@ -11,6 +11,7 @@ use crate::flatten::Merge; use crate::schema::*; use crate::{visit::*, JsonSchema, Map}; use dyn_clone::DynClone; +use serde::Serialize; use std::{any::Any, collections::HashSet, fmt::Debug}; /// Settings to customize how Schemas are generated. @@ -314,6 +315,70 @@ impl SchemaGenerator { root } + /// Generates a root JSON Schema for the given example value. + /// + /// If the value implements [`JsonSchema`](crate::JsonSchema), then prefer using the [`root_schema_for()`](Self::root_schema_for()) + /// function which will generally produce a more precise schema, particularly when the value contains any enums. + pub fn root_schema_for_value( + &mut self, + value: &T, + ) -> Result { + let mut schema = value + .serialize(crate::ser::Serializer { + gen: self, + include_title: true, + })? + .into_object(); + + if let Ok(example) = serde_json::to_value(value) { + schema.metadata().examples.push(example); + } + + let mut root = RootSchema { + meta_schema: self.settings.meta_schema.clone(), + definitions: self.definitions.clone(), + schema, + }; + + for visitor in &mut self.settings.visitors { + visitor.visit_root_schema(&mut root) + } + + Ok(root) + } + + /// Consumes `self` and generates a root JSON Schema for the given example value. + /// + /// If the value implements [`JsonSchema`](crate::JsonSchema), then prefer using the [`into_root_schema_for()!`](Self::into_root_schema_for()) + /// function which will generally produce a more precise schema, particularly when the value contains any enums. + pub fn into_root_schema_for_value( + mut self, + value: &T, + ) -> Result { + let mut schema = value + .serialize(crate::ser::Serializer { + gen: &mut self, + include_title: true, + })? + .into_object(); + + if let Ok(example) = serde_json::to_value(value) { + schema.metadata().examples.push(example); + } + + let mut root = RootSchema { + meta_schema: self.settings.meta_schema, + definitions: self.definitions, + schema, + }; + + for visitor in &mut self.settings.visitors { + visitor.visit_root_schema(&mut root) + } + + Ok(root) + } + /// Attemps to find the schema that the given `schema` is referencing. /// /// If the given `schema` has a [`$ref`](../schema/struct.SchemaObject.html#structfield.reference) property which refers diff --git a/schemars/src/json_schema_impls/core.rs b/schemars/src/json_schema_impls/core.rs index 2c5b4b6..9d5fee0 100644 --- a/schemars/src/json_schema_impls/core.rs +++ b/schemars/src/json_schema_impls/core.rs @@ -25,6 +25,7 @@ impl JsonSchema for Option { schema } schema => SchemaObject { + // TODO technically the schema already accepts null, so this may be unnecessary subschemas: Some(Box::new(SubschemaValidation { any_of: Some(vec![schema, <()>::json_schema(gen)]), ..Default::default() diff --git a/schemars/src/lib.rs b/schemars/src/lib.rs index a4372f8..e77558b 100644 --- a/schemars/src/lib.rs +++ b/schemars/src/lib.rs @@ -195,9 +195,71 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap()); `#[serde(...)]` attributes can be overriden using `#[schemars(...)]` attributes, which behave identically (e.g. `#[schemars(rename_all = "camelCase")]`). You may find this useful if you want to change the generated schema without affecting Serde's behaviour, or if you're just not using Serde. +### Schema from Example Values + +If you want a schema for a type that can't/doesn't implement `JsonSchema`, but does implement `serde::Serialize`, then you can generate a JSON schema from a value of that type. However, this schema will generally be less precise than if the type implemented `JsonSchema` - particularly when it involves enums, since schemars will not make any assumptions about the structure of an enum based on a single variant. + +```rust +use schemars::schema_for_value; +use serde::Serialize; + +#[derive(Serialize)] +pub struct MyStruct { + pub my_int: i32, + pub my_bool: bool, + pub my_nullable_enum: Option, +} + +#[derive(Serialize)] +pub enum MyEnum { + StringNewType(String), + StructVariant { floats: Vec }, +} + +fn main() { + let schema = schema_for_value!(MyStruct { + my_int: 123, + my_bool: true, + my_nullable_enum: Some(MyEnum::StringNewType("foo".to_string())) + }); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} +``` + +
+Click to see the output JSON schema... + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MyStruct", + "examples": [ + { + "my_bool": true, + "my_int": 123, + "my_nullable_enum": { + "StringNewType": "foo" + } + } + ], + "type": "object", + "properties": { + "my_bool": { + "type": "boolean" + }, + "my_int": { + "type": "integer", + "format": "int32" + }, + "my_nullable_enum": true + } +} +``` +
+ ## Feature Flags -- `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro. -- `impl_json_schema` - implements `JsonSchema` for Schemars types themselves. +- `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro +- `impl_json_schema` - implements `JsonSchema` for Schemars types themselves - `preserve_order` - keep the order of struct fields in `Schema` and `SchemaObject` ## Optional Dependencies @@ -236,6 +298,7 @@ pub type MapEntry<'a, K, V> = indexmap::map::Entry<'a, K, V>; mod flatten; mod json_schema_impls; +mod ser; #[macro_use] mod macros; diff --git a/schemars/src/macros.rs b/schemars/src/macros.rs index 226f2cc..18a6810 100644 --- a/schemars/src/macros.rs +++ b/schemars/src/macros.rs @@ -13,9 +13,66 @@ /// /// let my_schema = schema_for!(MyStruct); /// ``` +#[cfg(doc)] #[macro_export] macro_rules! schema_for { ($type:ty) => { $crate::gen::SchemaGenerator::default().into_root_schema_for::<$type>() }; } + +/// Generates a [`RootSchema`](crate::schema::RootSchema) for the given type using default settings. +/// +/// The type must implement [`JsonSchema`](crate::JsonSchema). +/// +/// # Example +/// ``` +/// use schemars::{schema_for, JsonSchema}; +/// +/// #[derive(JsonSchema)] +/// struct MyStruct { +/// foo: i32, +/// } +/// +/// let my_schema = schema_for!(MyStruct); +/// ``` +#[cfg(not(doc))] +#[macro_export] +macro_rules! schema_for { + ($type:ty) => { + $crate::gen::SchemaGenerator::default().into_root_schema_for::<$type>() + }; + ($_:expr) => { + compile_error!("This argument to `schema_for!` is not a type - did you mean to use `schema_for_value!` instead?") + }; +} + +/// Generates a [`RootSchema`](crate::schema::RootSchema) for the given example value using default settings. +/// +/// The value must implement [`Serialize`](serde::Serialize). If the value also implements [`JsonSchema`](crate::JsonSchema), +/// then prefer using the [`schema_for!`](schema_for) macro which will generally produce a more precise schema, +/// particularly when the value contains any enums. +/// +/// If the `Serialize` implementation of the value decides to fail, this macro will panic. +/// For a non-panicking alternative, create a [`SchemaGenerator`](crate::gen::SchemaGenerator) and use +/// its [`into_root_schema_for_value`](crate::gen::SchemaGenerator::into_root_schema_for_value) method. +/// +/// # Example +/// ``` +/// use schemars::schema_for_value; +/// +/// #[derive(serde::Serialize)] +/// struct MyStruct { +/// foo: i32, +/// } +/// +/// let my_schema = schema_for_value!(MyStruct { foo: 123 }); +/// ``` +#[macro_export] +macro_rules! schema_for_value { + ($value:expr) => { + $crate::gen::SchemaGenerator::default() + .into_root_schema_for_value(&$value) + .unwrap() + }; +} diff --git a/schemars/src/ser.rs b/schemars/src/ser.rs new file mode 100644 index 0000000..f70ae3e --- /dev/null +++ b/schemars/src/ser.rs @@ -0,0 +1,496 @@ +use crate::schema::*; +use crate::JsonSchema; +use crate::{gen::SchemaGenerator, Map}; +use serde_json::{Error, Value}; +use std::{convert::TryInto, fmt::Display}; + +pub(crate) struct Serializer<'a> { + pub(crate) gen: &'a mut SchemaGenerator, + pub(crate) include_title: bool, +} + +pub(crate) struct SerializeSeq<'a> { + gen: &'a mut SchemaGenerator, + items: Option, +} + +pub(crate) struct SerializeTuple<'a> { + gen: &'a mut SchemaGenerator, + items: Vec, + title: &'static str, +} + +pub(crate) struct SerializeMap<'a> { + gen: &'a mut SchemaGenerator, + properties: Map, + current_key: Option, + title: &'static str, +} + +macro_rules! forward_to_subschema_for { + ($fn:ident, $ty:ty) => { + fn $fn(self, _value: $ty) -> Result { + Ok(self.gen.subschema_for::<$ty>()) + } + }; +} + +impl<'a> serde::Serializer for Serializer<'a> { + type Ok = Schema; + type Error = Error; + + type SerializeSeq = SerializeSeq<'a>; + type SerializeTuple = SerializeTuple<'a>; + type SerializeTupleStruct = SerializeTuple<'a>; + type SerializeTupleVariant = Self; + type SerializeMap = SerializeMap<'a>; + type SerializeStruct = SerializeMap<'a>; + type SerializeStructVariant = Self; + + forward_to_subschema_for!(serialize_bool, bool); + forward_to_subschema_for!(serialize_i8, i8); + forward_to_subschema_for!(serialize_i16, i16); + forward_to_subschema_for!(serialize_i32, i32); + forward_to_subschema_for!(serialize_i64, i64); + forward_to_subschema_for!(serialize_i128, i128); + forward_to_subschema_for!(serialize_u8, u8); + forward_to_subschema_for!(serialize_u16, u16); + forward_to_subschema_for!(serialize_u32, u32); + forward_to_subschema_for!(serialize_u64, u64); + forward_to_subschema_for!(serialize_u128, u128); + forward_to_subschema_for!(serialize_f32, f32); + forward_to_subschema_for!(serialize_f64, f64); + forward_to_subschema_for!(serialize_char, char); + forward_to_subschema_for!(serialize_str, &str); + forward_to_subschema_for!(serialize_bytes, &[u8]); + + fn collect_str(self, _value: &T) -> Result + where + T: Display, + { + Ok(self.gen.subschema_for::()) + } + + fn collect_map(self, iter: I) -> Result + where + K: serde::Serialize, + V: serde::Serialize, + I: IntoIterator, + { + let value_schema = iter + .into_iter() + .try_fold(None, |acc, (_, v)| { + if acc == Some(Schema::Bool(true)) { + return Ok(acc); + } + + let schema = v.serialize(Serializer { + gen: self.gen, + include_title: false, + })?; + Ok(match &acc { + None => Some(schema), + Some(items) if items != &schema => Some(Schema::Bool(true)), + _ => acc, + }) + })? + .unwrap_or(Schema::Bool(true)); + + Ok(SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some(Box::new(ObjectValidation { + additional_properties: Some(Box::new(value_schema)), + ..ObjectValidation::default() + })), + ..SchemaObject::default() + } + .into()) + } + + fn serialize_none(self) -> Result { + Ok(self.gen.subschema_for::>()) + } + + fn serialize_some(mut self, value: &T) -> Result + where + T: serde::Serialize, + { + // FIXME nasty duplication of `impl JsonSchema for Option` + fn add_null_type(instance_type: &mut SingleOrVec) { + match instance_type { + SingleOrVec::Single(ty) if **ty != InstanceType::Null => { + *instance_type = vec![**ty, InstanceType::Null].into() + } + SingleOrVec::Vec(ty) if !ty.contains(&InstanceType::Null) => { + ty.push(InstanceType::Null) + } + _ => {} + }; + } + + let mut schema = value.serialize(Serializer { + gen: self.gen, + include_title: false, + })?; + + if self.gen.settings().option_add_null_type { + schema = match schema { + Schema::Bool(true) => Schema::Bool(true), + Schema::Bool(false) => <()>::json_schema(&mut self.gen), + Schema::Object(SchemaObject { + instance_type: Some(ref mut instance_type), + .. + }) => { + add_null_type(instance_type); + schema + } + schema => SchemaObject { + subschemas: Some(Box::new(SubschemaValidation { + any_of: Some(vec![schema, <()>::json_schema(&mut self.gen)]), + ..Default::default() + })), + ..Default::default() + } + .into(), + } + } + + if self.gen.settings().option_nullable { + let mut schema_obj = schema.into_object(); + schema_obj + .extensions + .insert("nullable".to_owned(), serde_json::json!(true)); + schema = Schema::Object(schema_obj); + }; + + Ok(schema) + } + + fn serialize_unit(self) -> Result { + Ok(self.gen.subschema_for::<()>()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + self.serialize_unit() + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + Ok(Schema::Bool(true)) + } + + fn serialize_newtype_struct( + self, + name: &'static str, + value: &T, + ) -> Result + where + T: serde::Serialize, + { + let include_title = self.include_title; + let mut result = value.serialize(self); + + if include_title { + if let Ok(Schema::Object(ref mut object)) = result { + object.metadata().title = Some(name.to_string()); + } + } + + result + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: serde::Serialize, + { + Ok(Schema::Bool(true)) + } + + fn serialize_seq(self, _len: Option) -> Result { + Ok(SerializeSeq { + gen: self.gen, + items: None, + }) + } + + fn serialize_tuple(self, len: usize) -> Result { + Ok(SerializeTuple { + gen: self.gen, + items: Vec::with_capacity(len), + title: "", + }) + } + + fn serialize_tuple_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + let title = if self.include_title { name } else { "" }; + Ok(SerializeTuple { + gen: self.gen, + items: Vec::with_capacity(len), + title, + }) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Ok(self) + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(SerializeMap { + gen: self.gen, + properties: Map::new(), + current_key: None, + title: "", + }) + } + + fn serialize_struct( + self, + name: &'static str, + _len: usize, + ) -> Result { + let title = if self.include_title { name } else { "" }; + Ok(SerializeMap { + gen: self.gen, + properties: Map::new(), + current_key: None, + title, + }) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Ok(self) + } +} + +impl serde::ser::SerializeTupleVariant for Serializer<'_> { + type Ok = Schema; + type Error = Error; + + fn serialize_field(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize, + { + Ok(()) + } + + fn end(self) -> Result { + Ok(Schema::Bool(true)) + } +} + +impl serde::ser::SerializeStructVariant for Serializer<'_> { + type Ok = Schema; + type Error = Error; + + fn serialize_field( + &mut self, + _key: &'static str, + _value: &T, + ) -> Result<(), Self::Error> + where + T: serde::Serialize, + { + Ok(()) + } + + fn end(self) -> Result { + Ok(Schema::Bool(true)) + } +} + +impl serde::ser::SerializeSeq for SerializeSeq<'_> { + type Ok = Schema; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize, + { + if self.items != Some(Schema::Bool(true)) { + let schema = value.serialize(Serializer { + gen: self.gen, + include_title: false, + })?; + match &self.items { + None => self.items = Some(schema), + Some(items) => { + if items != &schema { + self.items = Some(Schema::Bool(true)) + } + } + } + } + + Ok(()) + } + + fn end(self) -> Result { + let items = self.items.unwrap_or(Schema::Bool(true)); + Ok(SchemaObject { + instance_type: Some(InstanceType::Array.into()), + array: Some(Box::new(ArrayValidation { + items: Some(items.into()), + ..ArrayValidation::default() + })), + ..SchemaObject::default() + } + .into()) + } +} + +impl serde::ser::SerializeTuple for SerializeTuple<'_> { + type Ok = Schema; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize, + { + let schema = value.serialize(Serializer { + gen: self.gen, + include_title: false, + })?; + self.items.push(schema); + Ok(()) + } + + fn end(self) -> Result { + let len = self.items.len().try_into().ok(); + let mut schema = SchemaObject { + instance_type: Some(InstanceType::Array.into()), + array: Some(Box::new(ArrayValidation { + items: Some(SingleOrVec::Vec(self.items)), + max_items: len, + min_items: len, + ..ArrayValidation::default() + })), + ..SchemaObject::default() + }; + + if !self.title.is_empty() { + schema.metadata().title = Some(self.title.to_owned()); + } + + Ok(schema.into()) + } +} + +impl serde::ser::SerializeTupleStruct for SerializeTuple<'_> { + type Ok = Schema; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize, + { + serde::ser::SerializeTuple::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeTuple::end(self) + } +} + +impl serde::ser::SerializeMap for SerializeMap<'_> { + type Ok = Schema; + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: serde::Serialize, + { + // FIXME this is too lenient - we should return an error if serde_json + // doesn't allow T to be a key of a map. + let json = serde_json::to_string(key)?; + self.current_key = Some( + json.trim_start_matches('"') + .trim_end_matches('"') + .to_string(), + ); + + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize, + { + let key = self.current_key.take().unwrap_or_default(); + let schema = value.serialize(Serializer { + gen: self.gen, + include_title: false, + })?; + self.properties.insert(key, schema); + + Ok(()) + } + + fn end(self) -> Result { + let mut schema = SchemaObject { + instance_type: Some(InstanceType::Object.into()), + object: Some(Box::new(ObjectValidation { + properties: self.properties, + ..ObjectValidation::default() + })), + ..SchemaObject::default() + }; + + if !self.title.is_empty() { + schema.metadata().title = Some(self.title.to_owned()); + } + + Ok(schema.into()) + } +} + +impl serde::ser::SerializeStruct for SerializeMap<'_> { + type Ok = Schema; + type Error = Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: serde::Serialize, + { + let prop_schema = value.serialize(Serializer { + gen: self.gen, + include_title: false, + })?; + self.properties.insert(key.to_string(), prop_schema); + + Ok(()) + } + + fn end(self) -> Result { + serde::ser::SerializeMap::end(self) + } +} diff --git a/schemars/tests/expected/from_value_2019_09.json b/schemars/tests/expected/from_value_2019_09.json new file mode 100644 index 0000000..77e8953 --- /dev/null +++ b/schemars/tests/expected/from_value_2019_09.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "MyStruct", + "examples": [ + { + "myBool": true, + "myInnerStruct": { + "my_empty_map": {}, + "my_empty_vec": [], + "my_map": { + "": 0.0 + }, + "my_tuple": [ + "💩", + 42 + ], + "my_vec": [ + "hello", + "world" + ] + }, + "myInt": 123, + "myNullableEnum": null + } + ], + "type": "object", + "properties": { + "myInt": { + "type": "integer", + "format": "int32" + }, + "myBool": { + "type": "boolean" + }, + "myNullableEnum": true, + "myInnerStruct": { + "type": "object", + "properties": { + "my_map": { + "type": "object", + "additionalProperties": { + "type": "number", + "format": "double" + } + }, + "my_vec": { + "type": "array", + "items": { + "type": "string" + } + }, + "my_empty_map": { + "type": "object", + "additionalProperties": true + }, + "my_empty_vec": { + "type": "array", + "items": true + }, + "my_tuple": { + "type": "array", + "items": [ + { + "type": "string", + "maxLength": 1, + "minLength": 1 + }, + { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/from_value_draft07.json b/schemars/tests/expected/from_value_draft07.json new file mode 100644 index 0000000..936eeaf --- /dev/null +++ b/schemars/tests/expected/from_value_draft07.json @@ -0,0 +1,80 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MyStruct", + "examples": [ + { + "myBool": true, + "myInnerStruct": { + "my_empty_map": {}, + "my_empty_vec": [], + "my_map": { + "": 0.0 + }, + "my_tuple": [ + "💩", + 42 + ], + "my_vec": [ + "hello", + "world" + ] + }, + "myInt": 123, + "myNullableEnum": null + } + ], + "type": "object", + "properties": { + "myInt": { + "type": "integer", + "format": "int32" + }, + "myBool": { + "type": "boolean" + }, + "myNullableEnum": true, + "myInnerStruct": { + "type": "object", + "properties": { + "my_map": { + "type": "object", + "additionalProperties": { + "type": "number", + "format": "double" + } + }, + "my_vec": { + "type": "array", + "items": { + "type": "string" + } + }, + "my_empty_map": { + "type": "object", + "additionalProperties": true + }, + "my_empty_vec": { + "type": "array", + "items": true + }, + "my_tuple": { + "type": "array", + "items": [ + { + "type": "string", + "maxLength": 1, + "minLength": 1 + }, + { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/from_value_openapi3.json b/schemars/tests/expected/from_value_openapi3.json new file mode 100644 index 0000000..52514e2 --- /dev/null +++ b/schemars/tests/expected/from_value_openapi3.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", + "title": "MyStruct", + "type": "object", + "properties": { + "myInt": { + "type": "integer", + "format": "int32" + }, + "myBool": { + "type": "boolean" + }, + "myNullableEnum": { + "nullable": true + }, + "myInnerStruct": { + "type": "object", + "properties": { + "my_map": { + "type": "object", + "additionalProperties": { + "type": "number", + "format": "double" + } + }, + "my_vec": { + "type": "array", + "items": { + "type": "string" + } + }, + "my_empty_map": { + "type": "object", + "additionalProperties": true + }, + "my_empty_vec": { + "type": "array", + "items": {} + }, + "my_tuple": { + "type": "array", + "items": [ + { + "type": "string", + "maxLength": 1, + "minLength": 1 + }, + { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + }, + "example": { + "myBool": true, + "myInnerStruct": { + "my_empty_map": {}, + "my_empty_vec": [], + "my_map": { + "": 0.0 + }, + "my_tuple": [ + "💩", + 42 + ], + "my_vec": [ + "hello", + "world" + ] + }, + "myInt": 123, + "myNullableEnum": null + } +} \ No newline at end of file diff --git a/schemars/tests/from_value.rs b/schemars/tests/from_value.rs new file mode 100644 index 0000000..9e6943f --- /dev/null +++ b/schemars/tests/from_value.rs @@ -0,0 +1,77 @@ +mod util; +use std::collections::HashMap; + +use schemars::gen::SchemaSettings; +use serde::Serialize; +use util::*; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MyStruct { + pub my_int: i32, + pub my_bool: bool, + pub my_nullable_enum: Option, + pub my_inner_struct: MyInnerStruct, + #[serde(skip)] + pub skip: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub skip_if_none: Option, +} + +#[derive(Serialize)] +pub struct MyInnerStruct { + pub my_map: HashMap, + pub my_vec: Vec<&'static str>, + pub my_empty_map: HashMap, + pub my_empty_vec: Vec<&'static str>, + pub my_tuple: (char, u8), +} + +#[derive(Serialize)] +pub enum MyEnum { + StringNewType(String), + StructVariant { floats: Vec }, +} + +fn make_value() -> MyStruct { + let mut value = MyStruct { + my_int: 123, + my_bool: true, + my_nullable_enum: None, + my_inner_struct: MyInnerStruct { + my_map: HashMap::new(), + my_vec: vec!["hello", "world"], + my_empty_map: HashMap::new(), + my_empty_vec: vec![], + my_tuple: ('💩', 42), + }, + skip: 123, + skip_if_none: None, + }; + value.my_inner_struct.my_map.insert(String::new(), 0.0); + value +} + +#[test] +fn schema_from_value_matches_draft07() -> TestResult { + let gen = SchemaSettings::draft07().into_generator(); + let actual = gen.into_root_schema_for_value(&make_value())?; + + test_schema(&actual, "from_value_draft07") +} + +#[test] +fn schema_from_value_matches_2019_09() -> TestResult { + let gen = SchemaSettings::draft2019_09().into_generator(); + let actual = gen.into_root_schema_for_value(&make_value())?; + + test_schema(&actual, "from_value_2019_09") +} + +#[test] +fn schema_from_value_matches_openapi3() -> TestResult { + let gen = SchemaSettings::openapi3().into_generator(); + let actual = gen.into_root_schema_for_value(&make_value())?; + + test_schema(&actual, "from_value_openapi3") +} diff --git a/schemars/tests/ui/schema_for_arg_value.rs b/schemars/tests/ui/schema_for_arg_value.rs new file mode 100644 index 0000000..b5ead75 --- /dev/null +++ b/schemars/tests/ui/schema_for_arg_value.rs @@ -0,0 +1,5 @@ +use schemars::schema_for; + +fn main() { + let _schema = schema_for!(123); +} diff --git a/schemars/tests/ui/schema_for_arg_value.stderr b/schemars/tests/ui/schema_for_arg_value.stderr new file mode 100644 index 0000000..c787985 --- /dev/null +++ b/schemars/tests/ui/schema_for_arg_value.stderr @@ -0,0 +1,7 @@ +error: This argument to `schema_for!` is not a type - did you mean to use `schema_for_value!` instead? + --> $DIR/schema_for_arg_value.rs:4:19 + | +4 | let _schema = schema_for!(123); + | ^^^^^^^^^^^^^^^^ + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/schemars/tests/util/mod.rs b/schemars/tests/util/mod.rs index fa191fe..9f365d6 100644 --- a/schemars/tests/util/mod.rs +++ b/schemars/tests/util/mod.rs @@ -18,7 +18,7 @@ pub fn test_default_generated_schema(file: &str) -> TestResult { test_schema(&actual, file) } -fn test_schema(actual: &RootSchema, file: &str) -> TestResult { +pub fn test_schema(actual: &RootSchema, file: &str) -> TestResult { let expected_json = match fs::read_to_string(format!("tests/expected/{}.json", file)) { Ok(j) => j, Err(e) => {