diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d37634e9..7c723066 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - nightly include: - rust: 1.60.0 - test_features: "--features impl_json_schema" + test_features: "" allow_failure: false - rust: stable test_features: "--all-features" diff --git a/Cargo.lock b/Cargo.lock index 516f9798..757013a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - [[package]] name = "arrayvec" version = "0.7.4" @@ -29,17 +23,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bigdecimal" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "bigdecimal" version = "0.4.2" @@ -168,12 +151,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.2" @@ -196,17 +173,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - [[package]] name = "indexmap" version = "2.0.2" @@ -214,8 +180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.2", - "serde", + "hashbrown", ] [[package]] @@ -284,29 +249,49 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] +[[package]] +name = "ref-cast" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "rust_decimal" version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" dependencies = [ - "arrayvec 0.7.4", + "arrayvec", "num-traits", ] @@ -320,18 +305,16 @@ checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" name = "schemars" version = "0.8.19" dependencies = [ - "arrayvec 0.5.2", - "arrayvec 0.7.4", - "bigdecimal 0.3.1", - "bigdecimal 0.4.2", + "arrayvec", + "bigdecimal", "bytes", "chrono", "dyn-clone", "either", "enumset", - "indexmap 1.9.3", - "indexmap 2.0.2", + "indexmap", "pretty_assertions", + "ref-cast", "rust_decimal", "schemars_derive", "semver", @@ -341,8 +324,7 @@ dependencies = [ "smol_str", "trybuild", "url", - "uuid 0.8.2", - "uuid 1.5.0", + "uuid", ] [[package]] @@ -361,9 +343,6 @@ name = "semver" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" -dependencies = [ - "serde", -] [[package]] name = "serde" @@ -415,18 +394,15 @@ checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smol_str" -version = "0.1.24" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" -dependencies = [ - "serde", -] +checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49" [[package]] name = "syn" -version = "2.0.38" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -504,12 +480,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" - [[package]] name = "uuid" version = "1.5.0" diff --git a/schemars/Cargo.toml b/schemars/Cargo.toml index 579e8c04..47dc643a 100644 --- a/schemars/Cargo.toml +++ b/schemars/Cargo.toml @@ -15,117 +15,91 @@ rust-version = "1.60" [dependencies] schemars_derive = { version = "=0.8.19", optional = true, path = "../schemars_derive" } -serde = { version = "1.0", features = ["derive"] } +serde = "1.0" serde_json = "1.0.25" dyn-clone = "1.0" +ref-cast = "1.0.22" -chrono = { version = "0.4", default-features = false, optional = true } -indexmap = { version = "1.2", features = ["serde-1"], optional = true } -indexmap2 = { version = "2.0", features = ["serde"], optional = true, package = "indexmap" } -either = { version = "1.3", default-features = false, optional = true } -uuid08 = { version = "0.8", default-features = false, optional = true, package = "uuid" } +# optional dependencies +chrono04 = { version = "0.4", default-features = false, optional = true, package = "chrono" } +indexmap2 = { version = "2.0", default-features = false, optional = true, package = "indexmap" } +either1 = { version = "1.3", default-features = false, optional = true, package = "either" } uuid1 = { version = "1.0", default-features = false, optional = true, package = "uuid" } -smallvec = { version = "1.0", optional = true } -arrayvec05 = { version = "0.5", default-features = false, optional = true, package = "arrayvec" } +smallvec1 = { version = "1.0", default-features = false, optional = true, package = "smallvec" } arrayvec07 = { version = "0.7", default-features = false, optional = true, package = "arrayvec" } -url = { version = "2.0", default-features = false, optional = true } -bytes = { version = "1.0", optional = true } -rust_decimal = { version = "1", default-features = false, optional = true } -bigdecimal03 = { version = "0.3", default-features = false, optional = true, package = "bigdecimal" } +url2 = { version = "2.0", default-features = false, optional = true, package = "url" } +bytes1 = { version = "1.0", default-features = false, optional = true, package = "bytes" } +rust_decimal1 = { version = "1", default-features = false, optional = true, package = "rust_decimal"} bigdecimal04 = { version = "0.4", default-features = false, optional = true, package = "bigdecimal" } -enumset = { version = "1.0", optional = true } -smol_str = { version = "0.1.17", optional = true } -semver = { version = "1.0.9", features = ["serde"], optional = true } +enumset1 = { version = "1.0", default-features = false, optional = true, package = "enumset" } +smol_str02 = { version = "0.2.1", default-features = false, optional = true, package = "smol_str" } +semver1 = { version = "1.0.9", default-features = false, optional = true, package = "semver" } [dev-dependencies] pretty_assertions = "1.2.1" trybuild = "1.0" +serde = { version = "1.0", features = ["derive"] } [features] default = ["derive"] derive = ["schemars_derive"] -# Use a different representation for the map type of Schemars. -# This allows data to be read into a Value and written back to a JSON string -# while preserving the order of map keys in the input. -preserve_order = ["indexmap"] - -impl_json_schema = ["derive"] -# derive_json_schema will be removed in a later version -derive_json_schema = ["impl_json_schema"] - -# `uuid` feature contains `uuid08` only for back-compat - will be changed to include uuid 1.0 instead in a later version -uuid = ["uuid08"] -# `arrayvec` feature without version suffix is included only for back-compat - will be removed in a later version -arrayvec = ["arrayvec05"] -indexmap1 = ["indexmap"] - raw_value = ["serde_json/raw_value"] -# `bigdecimal` feature without version suffix is included only for back-compat - will be removed in a later version -bigdecimal = ["bigdecimal03"] ui_test = [] [[test]] -name = "chrono" -required-features = ["chrono"] +name = "ui" +required-features = ["ui_test"] [[test]] -name = "indexmap" -required-features = ["indexmap"] +name = "chrono" +required-features = ["chrono04"] [[test]] -name = "indexmap2" +name = "indexmap" required-features = ["indexmap2"] [[test]] name = "either" -required-features = ["either"] +required-features = ["either1"] [[test]] name = "uuid" -required-features = ["uuid08", "uuid1"] +required-features = ["uuid1"] [[test]] name = "smallvec" -required-features = ["smallvec"] +required-features = ["smallvec1"] [[test]] name = "bytes" -required-features = ["bytes"] +required-features = ["bytes1"] [[test]] name = "arrayvec" -required-features = ["arrayvec05", "arrayvec07"] - -[[test]] -name = "schema_for_schema" -required-features = ["impl_json_schema"] - -[[test]] -name = "ui" -required-features = ["ui_test"] +required-features = ["arrayvec07"] [[test]] name = "url" -required-features = ["url"] +required-features = ["url2"] [[test]] name = "enumset" -required-features = ["enumset"] +required-features = ["enumset1"] [[test]] name = "smol_str" -required-features = ["smol_str"] +required-features = ["smol_str02"] [[test]] name = "semver" -required-features = ["semver"] +required-features = ["semver1"] [[test]] name = "decimal" -required-features = ["rust_decimal", "bigdecimal03", "bigdecimal04"] +required-features = ["rust_decimal1", "bigdecimal04"] [package.metadata.docs.rs] all-features = true diff --git a/schemars/examples/custom_serialization.rs b/schemars/examples/custom_serialization.rs index 53c78fa6..c119ea5d 100644 --- a/schemars/examples/custom_serialization.rs +++ b/schemars/examples/custom_serialization.rs @@ -1,4 +1,4 @@ -use schemars::schema::{Schema, SchemaObject}; +use schemars::Schema; use schemars::{gen::SchemaGenerator, schema_for, JsonSchema}; use serde::{Deserialize, Serialize}; @@ -21,9 +21,11 @@ pub struct MyStruct { } fn make_custom_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema: SchemaObject = ::json_schema(gen).into(); - schema.format = Some("boolean".to_owned()); - schema.into() + let mut schema = String::json_schema(gen); + schema + .ensure_object() + .insert("format".into(), "boolean".into()); + schema } fn eight() -> i32 { diff --git a/schemars/src/_private.rs b/schemars/src/_private.rs index c61ffc15..d4ac4a7b 100644 --- a/schemars/src/_private.rs +++ b/schemars/src/_private.rs @@ -1,7 +1,8 @@ use crate::gen::SchemaGenerator; -use crate::schema::{InstanceType, ObjectValidation, Schema, SchemaObject}; -use crate::{JsonSchema, Map, Set}; +use crate::JsonSchema; +use crate::Schema; use serde::Serialize; +use serde_json::Map; use serde_json::Value; // Helper for generating schemas for flattened `Option` fields. @@ -12,12 +13,8 @@ pub fn json_schema_for_flatten( let mut schema = T::_schemars_private_non_optional_json_schema(gen); if T::_schemars_private_is_option() && !required { - if let Schema::Object(SchemaObject { - object: Some(ref mut object_validation), - .. - }) = schema - { - object_validation.required.clear(); + if let Some(object) = schema.as_object_mut() { + object.remove("required"); } } @@ -57,38 +54,22 @@ impl MaybeSerializeWrapper { /// Create a schema for a unit enum pub fn new_unit_enum(variant: &str) -> Schema { - Schema::Object(SchemaObject { - instance_type: Some(InstanceType::String.into()), - enum_values: Some(vec![variant.into()]), - ..SchemaObject::default() + // TODO switch from single-valued "enum" to "const" + json_schema!({ + "type": "string", + "enum": [variant], }) } /// Create a schema for an externally tagged enum pub fn new_externally_tagged_enum(variant: &str, sub_schema: Schema) -> Schema { - Schema::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() + json_schema!({ + "type": "object", + "properties": { + variant: sub_schema + }, + "required": [variant], + "additionalProperties": false, }) } @@ -98,74 +79,87 @@ pub fn new_internally_tagged_enum( variant: &str, deny_unknown_fields: bool, ) -> Schema { - let tag_schema = Schema::Object(SchemaObject { - instance_type: Some(InstanceType::String.into()), - enum_values: Some(vec![variant.into()]), - ..Default::default() + // TODO switch from single-valued "enum" to "const" + let mut schema = json_schema!({ + "type": "object", + "properties": { + tag_name: { + "type": "string", + "enum": [variant], + } + }, + "required": [tag_name], }); - Schema::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() - }) + + if deny_unknown_fields { + schema + .as_object_mut() + .unwrap() + .insert("additionalProperties".into(), false.into()); + } + + schema } pub fn insert_object_property( - obj: &mut ObjectValidation, + schema: &mut Schema, key: &str, has_default: bool, required: bool, - schema: Schema, + sub_schema: Schema, ) { - obj.properties.insert(key.to_owned(), schema); + let obj = schema.ensure_object(); + if let Some(properties) = obj + .entry("properties") + .or_insert(Value::Object(Map::new())) + .as_object_mut() + { + properties.insert(key.to_owned(), sub_schema.into()); + } + if required || !(has_default || T::_schemars_private_is_option()) { - obj.required.insert(key.to_owned()); + if let Some(req) = obj + .entry("required") + .or_insert(Value::Array(Vec::new())) + .as_array_mut() + { + req.push(key.into()); + } } } -pub mod metadata { - use crate::Schema; - use serde_json::Value; - - macro_rules! add_metadata_fn { - ($method:ident, $name:ident, $ty:ty) => { - pub fn $method(schema: Schema, $name: impl Into<$ty>) -> Schema { - let value = $name.into(); - if value == <$ty>::default() { - schema - } else { - let mut schema_obj = schema.into_object(); - schema_obj.metadata().$name = value.into(); - Schema::Object(schema_obj) - } +pub fn insert_validation_property( + schema: &mut Schema, + required_type: &str, + key: &str, + value: impl Into, +) { + if schema.has_type(required_type) || (required_type == "number" && schema.has_type("integer")) { + schema.ensure_object().insert(key.to_owned(), value.into()); + } +} + +pub fn append_required(schema: &mut Schema, key: &str) { + if schema.has_type("object") { + if let Value::Array(array) = schema + .ensure_object() + .entry("required") + .or_insert(Value::Array(Vec::new())) + { + let value = Value::from(key); + if !array.contains(&value) { + array.push(value); } - }; + } } +} - add_metadata_fn!(add_description, description, String); - add_metadata_fn!(add_id, id, String); - add_metadata_fn!(add_title, title, String); - add_metadata_fn!(add_deprecated, deprecated, bool); - add_metadata_fn!(add_read_only, read_only, bool); - add_metadata_fn!(add_write_only, write_only, bool); - add_metadata_fn!(add_default, default, Value); - - pub fn add_examples>(schema: Schema, examples: I) -> Schema { - let mut schema_obj = schema.into_object(); - schema_obj.metadata().examples.extend(examples); - Schema::Object(schema_obj) +pub fn apply_inner_validation(schema: &mut Schema, f: fn(&mut Schema) -> ()) { + if let Some(inner_schema) = schema + .as_object_mut() + .and_then(|o| o.get_mut("items")) + .and_then(|i| <&mut Schema>::try_from(i).ok()) + { + f(inner_schema); } } diff --git a/schemars/src/flatten.rs b/schemars/src/flatten.rs index e55cb7fc..fc66b74d 100644 --- a/schemars/src/flatten.rs +++ b/schemars/src/flatten.rs @@ -1,180 +1,111 @@ -use crate::schema::*; -use crate::{Map, Set}; +use serde_json::map::Entry; +use serde_json::Value; + +use crate::Schema; 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) { - return other; - } else if is_null_type(&other) { + pub fn flatten(mut self, other: Self) -> Schema { + // This special null-type-schema handling is here for backward-compatibility, but needs reviewing. + // I think it's only needed to make internally-tagged enum unit variants behave correctly, but that + // should be handled entirely within schemars_derive. + if other + .as_object() + .and_then(|o| o.get("type")) + .and_then(|t| t.as_str()) + == Some("null") + { return self; } - let s1: SchemaObject = self.into(); - let s2: SchemaObject = other.into(); - Schema::Object(s1.merge(s2)) - } -} -pub(crate) trait Merge: Sized { - fn merge(self, other: Self) -> Self; -} - -macro_rules! impl_merge { - ($ty:ident { merge: $($merge_field:ident)*, or: $($or_field:ident)*, }) => { - impl Merge for $ty { - fn merge(self, other: Self) -> Self { - $ty { - $($merge_field: self.$merge_field.merge(other.$merge_field),)* - $($or_field: self.$or_field.or(other.$or_field),)* + if let Value::Object(mut obj2) = other.to_value() { + let obj1 = self.ensure_object(); + + let ap2 = obj2.remove("additionalProperties"); + if let Entry::Occupied(mut ap1) = obj1.entry("additionalProperties") { + match ap2 { + Some(ap2) => { + flatten_additional_properties(ap1.get_mut(), ap2); + } + None => { + ap1.remove(); + } } } - } - }; - ($ty:ident { or: $($or_field:ident)*, }) => { - impl_merge!( $ty { merge: , or: $($or_field)*, }); - }; -} - -// For ObjectValidation::additional_properties. -impl Merge for Option> { - fn merge(self, other: Self) -> Self { - match (self.map(|x| *x), other.map(|x| *x)) { - // Perfer permissive schemas. - (Some(Schema::Bool(true)), _) => Some(Box::new(true.into())), - (_, Some(Schema::Bool(true))) => Some(Box::new(true.into())), - (None, _) => None, - (_, None) => None, - // Merge if we have two non-trivial schemas. - (Some(Schema::Object(s1)), Some(Schema::Object(s2))) => { - Some(Box::new(Schema::Object(s1.merge(s2)))) + for (key, value2) in obj2 { + match obj1.entry(key) { + Entry::Vacant(vacant) => { + vacant.insert(value2); + } + Entry::Occupied(mut occupied) => { + match occupied.key().as_str() { + // This special "type" handling can probably be removed once the enum variant `with`/`schema_with` behaviour is fixed + "type" => match (occupied.get_mut(), value2) { + (Value::Array(a1), Value::Array(mut a2)) => { + a2.retain(|v2| !a1.contains(v2)); + a1.extend(a2); + } + (v1, Value::Array(mut a2)) => { + if !a2.contains(v1) { + a2.push(std::mem::take(v1)); + *occupied.get_mut() = Value::Array(a2); + } + } + (Value::Array(a1), v2) => { + if !a1.contains(&v2) { + a1.push(v2); + } + } + (v1, v2) => { + if v1 != &v2 { + *occupied.get_mut() = + Value::Array(vec![std::mem::take(v1), v2]); + } + } + }, + "required" => { + if let Value::Array(a1) = occupied.into_mut() { + if let Value::Array(a2) = value2 { + a1.extend(a2); + } + } + } + "properties" | "patternProperties" => { + if let Value::Object(o1) = occupied.into_mut() { + if let Value::Object(o2) = value2 { + o1.extend(o2); + } + } + } + _ => { + // leave the original value as it is (don't modify `self`) + } + }; + } + } } - - // Perfer the more permissive schema. - (Some(s1 @ Schema::Object(_)), Some(Schema::Bool(false))) => Some(Box::new(s1)), - (Some(Schema::Bool(false)), Some(s2 @ Schema::Object(_))) => Some(Box::new(s2)), - - // Default to the null schema. - (Some(Schema::Bool(false)), Some(Schema::Bool(false))) => Some(Box::new(false.into())), - } - } -} - -impl_merge!(SchemaObject { - merge: extensions instance_type enum_values - metadata subschemas number string array object, - or: format const_value reference, -}); - -impl Merge for Metadata { - fn merge(self, other: Self) -> Self { - Metadata { - id: self.id.or(other.id), - title: self.title.or(other.title), - description: self.description.or(other.description), - default: self.default.or(other.default), - deprecated: self.deprecated || other.deprecated, - read_only: self.read_only || other.read_only, - write_only: self.write_only || other.write_only, - examples: self.examples.merge(other.examples), - } - } -} - -impl_merge!(SubschemaValidation { - or: all_of any_of one_of not if_schema then_schema else_schema, -}); - -impl_merge!(NumberValidation { - or: multiple_of maximum exclusive_maximum minimum exclusive_minimum, -}); - -impl_merge!(StringValidation { - or: max_length min_length pattern, -}); - -impl_merge!(ArrayValidation { - or: items additional_items max_items min_items unique_items contains, -}); - -impl_merge!(ObjectValidation { - merge: required properties pattern_properties additional_properties, - or: max_properties min_properties property_names, -}); - -impl Merge for Option { - fn merge(self, other: Self) -> Self { - match (self, other) { - (Some(x), Some(y)) => Some(x.merge(y)), - (None, y) => y, - (x, None) => x, } - } -} - -impl Merge for Box { - fn merge(mut self, other: Self) -> Self { - *self = (*self).merge(*other); - self - } -} - -impl Merge for Vec { - fn merge(mut self, other: Self) -> Self { - self.extend(other); - self - } -} -impl Merge for Map -where - K: std::hash::Hash + Eq + Ord, -{ - fn merge(mut self, other: Self) -> Self { - self.extend(other); self } } -impl Merge for Set { - fn merge(mut self, other: Self) -> Self { - self.extend(other); - self - } -} - -impl Merge for SingleOrVec { - fn merge(self, other: Self) -> Self { - if self == other { - return self; +// TODO validate behaviour when flattening a normal struct into a struct with deny_unknown_fields +fn flatten_additional_properties(v1: &mut Value, v2: Value) { + match (v1, v2) { + (v1, Value::Bool(true)) => { + *v1 = Value::Bool(true); } - let mut vec = match (self, other) { - (SingleOrVec::Vec(v1), SingleOrVec::Vec(v2)) => v1.merge(v2), - (SingleOrVec::Vec(mut v), SingleOrVec::Single(s)) - | (SingleOrVec::Single(s), SingleOrVec::Vec(mut v)) => { - v.push(*s); - v - } - (SingleOrVec::Single(s1), SingleOrVec::Single(s2)) => vec![*s1, *s2], - }; - vec.sort(); - vec.dedup(); - SingleOrVec::Vec(vec) + (v1 @ Value::Bool(false), v2) => { + *v1 = v2; + } + (Value::Object(o1), Value::Object(o2)) => { + o1.extend(o2); + } + _ => {} } } - -fn is_null_type(schema: &Schema) -> bool { - let s = match schema { - Schema::Object(s) => s, - _ => return false, - }; - let instance_type = match &s.instance_type { - Some(SingleOrVec::Single(t)) => t, - _ => return false, - }; - - **instance_type == InstanceType::Null -} diff --git a/schemars/src/gen.rs b/schemars/src/gen.rs index a31839a0..d19f80b9 100644 --- a/schemars/src/gen.rs +++ b/schemars/src/gen.rs @@ -7,12 +7,12 @@ There are two main types in this module: * [`SchemaGenerator`], which manages the generation of a schema document. */ -use crate::schema::*; -use crate::{visit::*, JsonSchema, Map}; +use crate::Schema; +use crate::{visit::*, JsonSchema}; use dyn_clone::DynClone; use serde::Serialize; use std::borrow::Cow; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::{any::Any, collections::HashSet, fmt::Debug}; /// Settings to customize how Schemas are generated. @@ -76,7 +76,7 @@ impl SchemaSettings { option_add_null_type: true, definitions_path: "#/definitions/".to_owned(), meta_schema: Some("https://json-schema.org/draft/2019-09/schema".to_owned()), - visitors: Vec::default(), + visitors: Vec::new(), inline_subschemas: false, } } @@ -96,9 +96,7 @@ impl SchemaSettings { Box::new(ReplaceBoolSchemas { skip_additional_properties: true, }), - Box::new(SetSingleExample { - retain_examples: false, - }), + Box::new(SetSingleExample), ], inline_subschemas: false, } @@ -150,7 +148,7 @@ impl SchemaSettings { #[derive(Debug, Default)] pub struct SchemaGenerator { settings: SchemaSettings, - definitions: Map, + definitions: BTreeMap, pending_schema_ids: HashSet>, schema_id_to_name: HashMap, String>, used_schema_names: HashSet, @@ -198,19 +196,6 @@ impl SchemaGenerator { &self.settings } - #[deprecated = "This method no longer has any effect."] - pub fn make_extensible(&self, _schema: &mut SchemaObject) {} - - #[deprecated = "Use `Schema::Bool(true)` instead"] - pub fn schema_for_any(&self) -> Schema { - Schema::Bool(true) - } - - #[deprecated = "Use `Schema::Bool(false)` instead"] - pub fn schema_for_none(&self) -> Schema { - 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. /// /// If `T` is [referenceable](JsonSchema::is_referenceable), this will add `T`'s schema to this generator's definitions, and @@ -262,7 +247,7 @@ impl SchemaGenerator { name: String, id: Cow<'static, str>, ) { - let dummy = Schema::Bool(false); + let dummy = false.into(); // insert into definitions BEFORE calling json_schema to avoid infinite recursion self.definitions.insert(name.clone(), dummy); @@ -273,26 +258,26 @@ impl SchemaGenerator { /// 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 + /// The keys of the returned `BTreeMap` are the [schema names](JsonSchema::schema_name), and the values are the schemas /// themselves. - pub fn definitions(&self) -> &Map { + pub fn definitions(&self) -> &BTreeMap { &self.definitions } /// Mutably 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 + /// The keys of the returned `BTreeMap` are the [schema names](JsonSchema::schema_name), and the values are the schemas /// themselves. - pub fn definitions_mut(&mut self) -> &mut Map { + pub fn definitions_mut(&mut self) -> &mut BTreeMap { &mut self.definitions } /// Returns the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated, /// leaving an empty map in its place. /// - /// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas + /// The keys of the returned `BTreeMap` are the [schema names](JsonSchema::schema_name), and the values are the schemas /// themselves. - pub fn take_definitions(&mut self) -> Map { + pub fn take_definitions(&mut self) -> BTreeMap { std::mem::take(&mut self.definitions) } @@ -306,40 +291,72 @@ impl SchemaGenerator { /// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will /// 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(&mut self) -> RootSchema { - let mut schema = self.json_schema_internal::(T::schema_id()).into_object(); - schema.metadata().title.get_or_insert_with(T::schema_name); - let mut root = RootSchema { - meta_schema: self.settings.meta_schema.clone(), - definitions: self.definitions.clone(), - schema, - }; + pub fn root_schema_for(&mut self) -> Schema { + let mut schema = self.json_schema_internal::(T::schema_id()); + + let object = schema.ensure_object(); + + object + .entry("title") + .or_insert_with(|| T::schema_name().into()); + + if let Some(meta_schema) = self.settings.meta_schema.as_deref() { + object.insert("$schema".into(), meta_schema.into()); + } + + if !self.definitions.is_empty() { + object.insert( + "definitions".into(), + serde_json::Value::Object( + self.definitions + .iter() + .map(|(k, v)| (k.clone(), v.clone().into())) + .collect(), + ), + ); + } for visitor in &mut self.settings.visitors { - visitor.visit_root_schema(&mut root) + visitor.visit_schema(&mut schema); } - root + schema } /// Consumes `self` and generates a root JSON Schema for the type `T`. /// /// 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(mut self) -> RootSchema { - let mut schema = self.json_schema_internal::(T::schema_id()).into_object(); - schema.metadata().title.get_or_insert_with(T::schema_name); - let mut root = RootSchema { - meta_schema: self.settings.meta_schema, - definitions: self.definitions, - schema, - }; + pub fn into_root_schema_for(mut self) -> Schema { + let mut schema = self.json_schema_internal::(T::schema_id()); + + let object = schema.ensure_object(); + + object + .entry("title") + .or_insert_with(|| T::schema_name().into()); + + if let Some(meta_schema) = self.settings.meta_schema { + object.insert("$schema".into(), meta_schema.into()); + } + + if !self.definitions.is_empty() { + object.insert( + "definitions".into(), + serde_json::Value::Object( + self.definitions + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + ), + ); + } for visitor in &mut self.settings.visitors { - visitor.visit_root_schema(&mut root) + visitor.visit_schema(&mut schema); } - root + schema } /// Generates a root JSON Schema for the given example value. @@ -349,29 +366,39 @@ impl SchemaGenerator { 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(); + ) -> Result { + let mut schema = value.serialize(crate::ser::Serializer { + gen: self, + include_title: true, + })?; + + let object = schema.ensure_object(); if let Ok(example) = serde_json::to_value(value) { - schema.metadata().examples.push(example); + object.insert("examples".into(), vec![example].into()); } - let mut root = RootSchema { - meta_schema: self.settings.meta_schema.clone(), - definitions: self.definitions.clone(), - schema, - }; + if let Some(meta_schema) = self.settings.meta_schema.as_deref() { + object.insert("$schema".into(), meta_schema.into()); + } + + if !self.definitions.is_empty() { + object.insert( + "definitions".into(), + serde_json::Value::Object( + self.definitions + .iter() + .map(|(k, v)| (k.clone(), v.clone().into())) + .collect(), + ), + ); + } for visitor in &mut self.settings.visitors { - visitor.visit_root_schema(&mut root) + visitor.visit_schema(&mut schema); } - Ok(root) + Ok(schema) } /// Consumes `self` and generates a root JSON Schema for the given example value. @@ -381,72 +408,39 @@ impl SchemaGenerator { 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(); + ) -> Result { + let mut schema = value.serialize(crate::ser::Serializer { + gen: &mut self, + include_title: true, + })?; + + let object = schema.ensure_object(); if let Ok(example) = serde_json::to_value(value) { - schema.metadata().examples.push(example); + object.insert("examples".into(), vec![example].into()); } - 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) + if let Some(meta_schema) = self.settings.meta_schema { + object.insert("$schema".into(), meta_schema.into()); } - Ok(root) - } + if !self.definitions.is_empty() { + object.insert( + "definitions".into(), + serde_json::Value::Object( + self.definitions + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + ), + ); + } - /// 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 - /// to another schema in `self`'s schema definitions, the referenced schema will be returned. Otherwise, returns `None`. - /// - /// # Example - /// ``` - /// use schemars::{JsonSchema, gen::SchemaGenerator}; - /// - /// #[derive(JsonSchema)] - /// struct MyStruct { - /// foo: i32, - /// } - /// - /// let mut gen = SchemaGenerator::default(); - /// let ref_schema = gen.subschema_for::(); - /// - /// assert!(ref_schema.is_ref()); - /// - /// let dereferenced = gen.dereference(&ref_schema); - /// - /// assert!(dereferenced.is_some()); - /// assert!(!dereferenced.unwrap().is_ref()); - /// assert_eq!(dereferenced, gen.definitions().get("MyStruct")); - /// ``` - pub fn dereference<'a>(&'a self, schema: &Schema) -> Option<&'a Schema> { - match schema { - Schema::Object(SchemaObject { - reference: Some(ref schema_ref), - .. - }) => { - let definitions_path = &self.settings().definitions_path; - if schema_ref.starts_with(definitions_path) { - let name = &schema_ref[definitions_path.len()..]; - self.definitions.get(name) - } else { - None - } - } - _ => None, + for visitor in &mut self.settings.visitors { + visitor.visit_schema(&mut schema); } + + Ok(schema) } fn json_schema_internal(&mut self, id: Cow<'static, str>) -> Schema { diff --git a/schemars/src/json_schema_impls/array.rs b/schemars/src/json_schema_impls/array.rs index 036d0423..bacc079d 100644 --- a/schemars/src/json_schema_impls/array.rs +++ b/schemars/src/json_schema_impls/array.rs @@ -1,6 +1,5 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use std::borrow::Cow; // Does not require T: JsonSchema. @@ -16,15 +15,10 @@ impl JsonSchema for [T; 0] { } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - max_items: Some(0), - ..Default::default() - })), - ..Default::default() - } - .into() + json_schema!({ + "type": "array", + "maxItems": 0, + }) } } @@ -44,17 +38,12 @@ macro_rules! array_impls { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(gen.subschema_for::().into()), - max_items: Some($len), - min_items: Some($len), - ..Default::default() - })), - ..Default::default() - } - .into() + json_schema!({ + "type": "array", + "items": serde_json::Value::from(gen.subschema_for::()), + "minItems": $len, + "maxItems": $len, + }) } } )+ @@ -67,40 +56,3 @@ array_impls! { 21 22 23 24 25 26 27 28 29 30 31 32 } - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::{schema_for, schema_object_for}; - use pretty_assertions::assert_eq; - - #[test] - fn schema_for_array() { - let schema = schema_object_for::<[i32; 8]>(); - assert_eq!( - schema.instance_type, - Some(SingleOrVec::from(InstanceType::Array)) - ); - let array_validation = schema.array.unwrap(); - assert_eq!( - array_validation.items, - Some(SingleOrVec::from(schema_for::())) - ); - assert_eq!(array_validation.max_items, Some(8)); - assert_eq!(array_validation.min_items, Some(8)); - } - - // SomeStruct does not implement JsonSchema - struct SomeStruct; - - #[test] - fn schema_for_empty_array() { - let schema = schema_object_for::<[SomeStruct; 0]>(); - assert_eq!( - schema.instance_type, - Some(SingleOrVec::from(InstanceType::Array)) - ); - let array_validation = schema.array.unwrap(); - assert_eq!(array_validation.max_items, Some(0)); - } -} diff --git a/schemars/src/json_schema_impls/arrayvec05.rs b/schemars/src/json_schema_impls/arrayvec05.rs deleted file mode 100644 index 281bde79..00000000 --- a/schemars/src/json_schema_impls/arrayvec05.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use arrayvec05::{Array, ArrayString, ArrayVec}; -use std::convert::TryInto; - -// Do not set maxLength on the schema as that describes length in characters, but we only -// know max length in bytes. -forward_impl!(( JsonSchema for ArrayString where A: Array + Copy) => String); - -impl JsonSchema for ArrayVec -where - A::Item: JsonSchema, -{ - no_ref_schema!(); - - fn schema_name() -> String { - format!( - "Array_up_to_size_{}_of_{}", - A::CAPACITY, - A::Item::schema_name() - ) - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(gen.subschema_for::().into()), - max_items: A::CAPACITY.try_into().ok(), - ..Default::default() - })), - ..Default::default() - } - .into() - } -} diff --git a/schemars/src/json_schema_impls/arrayvec07.rs b/schemars/src/json_schema_impls/arrayvec07.rs index e2d92c5b..3d4d0505 100644 --- a/schemars/src/json_schema_impls/arrayvec07.rs +++ b/schemars/src/json_schema_impls/arrayvec07.rs @@ -1,8 +1,6 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use arrayvec07::{ArrayString, ArrayVec}; -use std::convert::TryInto; // Do not set maxLength on the schema as that describes length in characters, but we only // know max length in bytes. @@ -19,15 +17,10 @@ where } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(gen.subschema_for::().into()), - max_items: CAP.try_into().ok(), - ..Default::default() - })), - ..Default::default() - } - .into() + json_schema!({ + "type": "array", + "items": gen.subschema_for::(), + "maxItems": CAP + }) } } diff --git a/schemars/src/json_schema_impls/atomic.rs b/schemars/src/json_schema_impls/atomic.rs index 5d8f6d91..53d55c62 100644 --- a/schemars/src/json_schema_impls/atomic.rs +++ b/schemars/src/json_schema_impls/atomic.rs @@ -1,6 +1,3 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; use std::sync::atomic::*; forward_impl!(AtomicBool => bool); @@ -22,12 +19,12 @@ forward_impl!(AtomicUsize => usize); #[cfg(test)] mod tests { use super::*; - use crate::tests::schema_object_for; + use crate::schema_for; use pretty_assertions::assert_eq; #[test] fn schema_for_atomics() { - let atomic_schema = schema_object_for::<( + let atomic_schema = schema_for!(( AtomicBool, AtomicI8, AtomicI16, @@ -39,9 +36,8 @@ mod tests { AtomicU32, AtomicU64, AtomicUsize, - )>(); - let basic_schema = - schema_object_for::<(bool, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize)>(); + )); + let basic_schema = schema_for!((bool, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize)); assert_eq!(atomic_schema, basic_schema); } } diff --git a/schemars/src/json_schema_impls/bytes.rs b/schemars/src/json_schema_impls/bytes.rs deleted file mode 100644 index f1a0f290..00000000 --- a/schemars/src/json_schema_impls/bytes.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use bytes::{Bytes, BytesMut}; - -forward_impl!((JsonSchema for Bytes) => Vec); -forward_impl!((JsonSchema for BytesMut) => Vec); diff --git a/schemars/src/json_schema_impls/chrono.rs b/schemars/src/json_schema_impls/chrono04.rs similarity index 61% rename from schemars/src/json_schema_impls/chrono.rs rename to schemars/src/json_schema_impls/chrono04.rs index 3a1731b5..5fd23e8b 100644 --- a/schemars/src/json_schema_impls/chrono.rs +++ b/schemars/src/json_schema_impls/chrono04.rs @@ -1,8 +1,6 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use chrono::prelude::*; -use serde_json::json; +use crate::{json_schema, JsonSchema, Schema}; +use chrono04::prelude::*; use std::borrow::Cow; impl JsonSchema for Weekday { @@ -17,20 +15,18 @@ impl JsonSchema for Weekday { } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - enum_values: Some(vec![ - json!("Mon"), - json!("Tue"), - json!("Wed"), - json!("Thu"), - json!("Fri"), - json!("Sat"), - json!("Sun"), - ]), - ..Default::default() - } - .into() + json_schema!({ + "type": "string", + "enum": [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + ] + }) } } @@ -51,12 +47,10 @@ macro_rules! formatted_string_impl { } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - format: Some($format.to_owned()), - ..Default::default() - } - .into() + json_schema!({ + "type": "string", + "format": $format + }) } } }; diff --git a/schemars/src/json_schema_impls/core.rs b/schemars/src/json_schema_impls/core.rs index 955ead67..16104f80 100644 --- a/schemars/src/json_schema_impls/core.rs +++ b/schemars/src/json_schema_impls/core.rs @@ -1,7 +1,6 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use serde_json::json; +use crate::{json_schema, JsonSchema, Schema}; +use serde_json::Value; use std::borrow::Cow; use std::ops::{Bound, Range, RangeInclusive}; @@ -18,35 +17,45 @@ impl JsonSchema for Option { fn json_schema(gen: &mut SchemaGenerator) -> Schema { let mut schema = gen.subschema_for::(); + if gen.settings().option_add_null_type { - schema = match schema { - Schema::Bool(true) => Schema::Bool(true), - Schema::Bool(false) => <()>::json_schema(gen), - Schema::Object(SchemaObject { - instance_type: Some(ref mut instance_type), - .. - }) => { - add_null_type(instance_type); - 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() - })), - ..Default::default() + schema = match schema.try_to_object() { + Ok(mut obj) => { + let instance_type = obj.get_mut("type"); + match instance_type { + Some(Value::Array(array)) => { + let null = Value::from("null"); + if !array.contains(&null) { + array.push(null); + } + obj.into() + } + Some(Value::String(string)) => { + if string != "null" { + *instance_type.unwrap() = + Value::Array(vec![std::mem::take(string).into(), "null".into()]) + } + obj.into() + } + _ => json_schema!({ + "anyOf": [ + obj, + <()>::json_schema(gen) + ] + }), + } } - .into(), + Err(true) => true.into(), + Err(false) => <()>::json_schema(gen), } } + if gen.settings().option_nullable { - let mut schema_obj = schema.into_object(); - schema_obj - .extensions - .insert("nullable".to_owned(), json!(true)); - schema = Schema::Object(schema_obj); + schema + .ensure_object() + .insert("nullable".into(), true.into()); }; + schema } @@ -59,16 +68,6 @@ 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), - _ => {} - }; -} - impl JsonSchema for Result { fn schema_name() -> String { format!("Result_of_{}_or_{}", T::schema_name(), E::schema_name()) @@ -79,27 +78,24 @@ impl JsonSchema for Result { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut ok_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = ok_schema.object(); - obj.required.insert("Ok".to_owned()); - obj.properties - .insert("Ok".to_owned(), gen.subschema_for::()); - - let mut err_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = err_schema.object(); - obj.required.insert("Err".to_owned()); - obj.properties - .insert("Err".to_owned(), gen.subschema_for::()); - - let mut schema = SchemaObject::default(); - schema.subschemas().one_of = Some(vec![ok_schema.into(), err_schema.into()]); - schema.into() + json_schema!({ + "oneOf": [ + { + "type": "object", + "properties": { + "Ok": gen.subschema_for::() + }, + "required": ["Ok"] + }, + { + "type": "object", + "properties": { + "Err": gen.subschema_for::() + }, + "required": ["Err"] + } + ] + }) } } @@ -113,37 +109,28 @@ impl JsonSchema for Bound { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut included_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = included_schema.object(); - obj.required.insert("Included".to_owned()); - obj.properties - .insert("Included".to_owned(), gen.subschema_for::()); - - let mut excluded_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = excluded_schema.object(); - obj.required.insert("Excluded".to_owned()); - obj.properties - .insert("Excluded".to_owned(), gen.subschema_for::()); - - let unbounded_schema = SchemaObject { - instance_type: Some(InstanceType::String.into()), - const_value: Some(json!("Unbounded")), - ..Default::default() - }; - - let mut schema = SchemaObject::default(); - schema.subschemas().one_of = Some(vec![ - included_schema.into(), - excluded_schema.into(), - unbounded_schema.into(), - ]); - schema.into() + json_schema!({ + "oneOf": [ + { + "type": "object", + "properties": { + "Included": gen.subschema_for::() + }, + "required": ["Included"] + }, + { + "type": "object", + "properties": { + "Excluded": gen.subschema_for::() + }, + "required": ["Excluded"] + }, + { + "type": "string", + "const": "Unbounded" + } + ] + }) } } @@ -157,18 +144,15 @@ impl JsonSchema for Range { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = schema.object(); - obj.required.insert("start".to_owned()); - obj.required.insert("end".to_owned()); - obj.properties - .insert("start".to_owned(), gen.subschema_for::()); - obj.properties - .insert("end".to_owned(), gen.subschema_for::()); - schema.into() + let subschema = gen.subschema_for::(); + json_schema!({ + "type": "object", + "properties": { + "start": subschema, + "end": subschema + }, + "required": ["start", "end"] + }) } } @@ -177,54 +161,3 @@ forward_impl!(( JsonSchema for RangeInclusive) => Range); forward_impl!(( JsonSchema for std::marker::PhantomData) => ()); forward_impl!((<'a> JsonSchema for std::fmt::Arguments<'a>) => String); - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::{schema_for, schema_object_for}; - use pretty_assertions::assert_eq; - - #[test] - fn schema_for_option() { - let schema = schema_object_for::>(); - assert_eq!( - schema.instance_type, - Some(vec![InstanceType::Integer, InstanceType::Null].into()) - ); - assert_eq!(schema.extensions.get("nullable"), None); - assert_eq!(schema.subschemas.is_none(), true); - } - - #[test] - fn schema_for_option_with_ref() { - use crate as schemars; - #[derive(JsonSchema)] - struct Foo; - - let schema = schema_object_for::>(); - assert_eq!(schema.instance_type, None); - assert_eq!(schema.extensions.get("nullable"), None); - assert_eq!(schema.subschemas.is_some(), true); - let any_of = schema.subschemas.unwrap().any_of.unwrap(); - assert_eq!(any_of.len(), 2); - assert_eq!(any_of[0], Schema::new_ref("#/definitions/Foo".to_string())); - assert_eq!(any_of[1], schema_for::<()>()); - } - - #[test] - fn schema_for_result() { - let schema = schema_object_for::>(); - let one_of = schema.subschemas.unwrap().one_of.unwrap(); - assert_eq!(one_of.len(), 2); - - let ok_schema: SchemaObject = one_of[0].clone().into(); - let obj = ok_schema.object.unwrap(); - assert!(obj.required.contains("Ok")); - assert_eq!(obj.properties["Ok"], schema_for::()); - - let err_schema: SchemaObject = one_of[1].clone().into(); - let obj = err_schema.object.unwrap(); - assert!(obj.required.contains("Err")); - assert_eq!(obj.properties["Err"], schema_for::()); - } -} diff --git a/schemars/src/json_schema_impls/decimal.rs b/schemars/src/json_schema_impls/decimal.rs index 643ca44d..462e3d52 100644 --- a/schemars/src/json_schema_impls/decimal.rs +++ b/schemars/src/json_schema_impls/decimal.rs @@ -1,6 +1,5 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use std::borrow::Cow; macro_rules! decimal_impl { @@ -17,23 +16,16 @@ macro_rules! decimal_impl { } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - string: Some(Box::new(StringValidation { - pattern: Some(r"^-?[0-9]+(\.[0-9]+)?$".to_owned()), - ..Default::default() - })), - ..Default::default() - } - .into() + json_schema!({ + "type": "string", + "pattern": r"^-?[0-9]+(\.[0-9]+)?$", + }) } } }; } -#[cfg(feature = "rust_decimal")] -decimal_impl!(rust_decimal::Decimal); -#[cfg(feature = "bigdecimal03")] -decimal_impl!(bigdecimal03::BigDecimal); +#[cfg(feature = "rust_decimal1")] +decimal_impl!(rust_decimal1::Decimal); #[cfg(feature = "bigdecimal04")] decimal_impl!(bigdecimal04::BigDecimal); diff --git a/schemars/src/json_schema_impls/either.rs b/schemars/src/json_schema_impls/either1.rs similarity index 67% rename from schemars/src/json_schema_impls/either.rs rename to schemars/src/json_schema_impls/either1.rs index 957fdd16..96ed8a88 100644 --- a/schemars/src/json_schema_impls/either.rs +++ b/schemars/src/json_schema_impls/either1.rs @@ -1,7 +1,6 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use either::Either; +use crate::{json_schema, JsonSchema, Schema}; +use either1::Either; use std::borrow::Cow; impl JsonSchema for Either { @@ -20,8 +19,8 @@ impl JsonSchema for Either { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema = SchemaObject::default(); - schema.subschemas().any_of = Some(vec![gen.subschema_for::(), gen.subschema_for::()]); - schema.into() + json_schema!({ + "anyOf": [gen.subschema_for::(), gen.subschema_for::()], + }) } } diff --git a/schemars/src/json_schema_impls/enumset.rs b/schemars/src/json_schema_impls/enumset.rs deleted file mode 100644 index 22a3ebcf..00000000 --- a/schemars/src/json_schema_impls/enumset.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use enumset::{EnumSet, EnumSetType}; - -forward_impl!(( JsonSchema for EnumSet where T: EnumSetType + JsonSchema) => std::collections::BTreeSet); diff --git a/schemars/src/json_schema_impls/ffi.rs b/schemars/src/json_schema_impls/ffi.rs index 55b50125..4ad85c24 100644 --- a/schemars/src/json_schema_impls/ffi.rs +++ b/schemars/src/json_schema_impls/ffi.rs @@ -1,6 +1,5 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use std::borrow::Cow; use std::ffi::{CStr, CString, OsStr, OsString}; @@ -14,27 +13,24 @@ impl JsonSchema for OsString { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut unix_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = unix_schema.object(); - obj.required.insert("Unix".to_owned()); - obj.properties - .insert("Unix".to_owned(), >::json_schema(gen)); - - let mut win_schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = win_schema.object(); - obj.required.insert("Windows".to_owned()); - obj.properties - .insert("Windows".to_owned(), >::json_schema(gen)); - - let mut schema = SchemaObject::default(); - schema.subschemas().one_of = Some(vec![unix_schema.into(), win_schema.into()]); - schema.into() + json_schema!({ + "oneOf": [ + { + "type": "object", + "properties": { + "Unix": >::json_schema(gen) + }, + "required": ["Unix"] + }, + { + "type": "object", + "properties": { + "Windows": >::json_schema(gen) + }, + "required": ["Windows"] + }, + ] + }) } } diff --git a/schemars/src/json_schema_impls/indexmap.rs b/schemars/src/json_schema_impls/indexmap.rs deleted file mode 100644 index 3bd3e545..00000000 --- a/schemars/src/json_schema_impls/indexmap.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use indexmap::{IndexMap, IndexSet}; -use std::collections::{HashMap, HashSet}; - -forward_impl!(( JsonSchema for IndexMap) => HashMap); -forward_impl!(( JsonSchema for IndexSet) => HashSet); diff --git a/schemars/src/json_schema_impls/indexmap2.rs b/schemars/src/json_schema_impls/indexmap2.rs index 44eeedb3..9473dead 100644 --- a/schemars/src/json_schema_impls/indexmap2.rs +++ b/schemars/src/json_schema_impls/indexmap2.rs @@ -1,5 +1,3 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; use crate::JsonSchema; use indexmap2::{IndexMap, IndexSet}; use std::collections::{HashMap, HashSet}; diff --git a/schemars/src/json_schema_impls/maps.rs b/schemars/src/json_schema_impls/maps.rs index 7c27808f..b934f8bb 100644 --- a/schemars/src/json_schema_impls/maps.rs +++ b/schemars/src/json_schema_impls/maps.rs @@ -1,6 +1,5 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use std::borrow::Cow; macro_rules! map_impl { @@ -20,16 +19,10 @@ macro_rules! map_impl { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let subschema = gen.subschema_for::(); - SchemaObject { - instance_type: Some(InstanceType::Object.into()), - object: Some(Box::new(ObjectValidation { - additional_properties: Some(Box::new(subschema)), - ..Default::default() - })), - ..Default::default() - } - .into() + json_schema!({ + "type": "object", + "additionalProperties": gen.subschema_for::(), + }) } } }; diff --git a/schemars/src/json_schema_impls/mod.rs b/schemars/src/json_schema_impls/mod.rs index 0c548eb3..ebcb61b2 100644 --- a/schemars/src/json_schema_impls/mod.rs +++ b/schemars/src/json_schema_impls/mod.rs @@ -21,11 +21,11 @@ macro_rules! forward_impl { <$target>::schema_id() } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { + fn json_schema(gen: &mut $crate::gen::SchemaGenerator) -> $crate::Schema { <$target>::json_schema(gen) } - fn _schemars_private_non_optional_json_schema(gen: &mut SchemaGenerator) -> Schema { + fn _schemars_private_non_optional_json_schema(gen: &mut $crate::gen::SchemaGenerator) -> $crate::Schema { <$target>::_schemars_private_non_optional_json_schema(gen) } @@ -35,55 +35,61 @@ macro_rules! forward_impl { } }; ($ty:ty => $target:ty) => { - forward_impl!((JsonSchema for $ty) => $target); + forward_impl!(($crate::JsonSchema for $ty) => $target); }; } mod array; -#[cfg(feature = "arrayvec05")] -mod arrayvec05; -#[cfg(feature = "arrayvec07")] -mod arrayvec07; -#[cfg(std_atomic)] -mod atomic; -#[cfg(feature = "bytes")] -mod bytes; -#[cfg(feature = "chrono")] -mod chrono; mod core; -#[cfg(any( - feature = "rust_decimal", - feature = "bigdecimal03", - feature = "bigdecimal04" -))] -mod decimal; -#[cfg(feature = "either")] -mod either; -#[cfg(feature = "enumset")] -mod enumset; mod ffi; -#[cfg(feature = "indexmap")] -mod indexmap; -#[cfg(feature = "indexmap2")] -mod indexmap2; mod maps; mod nonzero_signed; mod nonzero_unsigned; mod primitives; -#[cfg(feature = "semver")] -mod semver; mod sequences; mod serdejson; -#[cfg(feature = "smallvec")] -mod smallvec; -#[cfg(feature = "smol_str")] -mod smol_str; mod time; mod tuple; -#[cfg(feature = "url")] -mod url; -#[cfg(feature = "uuid08")] -mod uuid08; +mod wrapper; + +#[cfg(std_atomic)] +mod atomic; + +#[cfg(feature = "arrayvec07")] +mod arrayvec07; + +#[cfg(feature = "bytes1")] +mod bytes1 { + forward_impl!(bytes1::Bytes => Vec); + forward_impl!(bytes1::BytesMut => Vec); +} + +#[cfg(feature = "chrono04")] +mod chrono04; + +#[cfg(any(feature = "rust_decimal1", feature = "bigdecimal04"))] +mod decimal; + +#[cfg(feature = "either1")] +mod either1; + +#[cfg(feature = "enumset1")] +forward_impl!(( crate::JsonSchema for enumset1::EnumSet) => std::collections::BTreeSet); + +#[cfg(feature = "indexmap2")] +mod indexmap2; + +#[cfg(feature = "semver1")] +mod semver1; + +#[cfg(feature = "smallvec1")] +forward_impl!(( crate::JsonSchema for smallvec1::SmallVec where A::Item: crate::JsonSchema) => Vec); + +#[cfg(feature = "smol_str02")] +forward_impl!(smol_str02::SmolStr => String); + +#[cfg(feature = "url2")] +mod url2; + #[cfg(feature = "uuid1")] mod uuid1; -mod wrapper; diff --git a/schemars/src/json_schema_impls/nonzero_signed.rs b/schemars/src/json_schema_impls/nonzero_signed.rs index c2fba2b2..d1b51429 100644 --- a/schemars/src/json_schema_impls/nonzero_signed.rs +++ b/schemars/src/json_schema_impls/nonzero_signed.rs @@ -1,6 +1,5 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{JsonSchema, Schema}; use std::borrow::Cow; use std::num::*; @@ -18,14 +17,12 @@ macro_rules! nonzero_unsigned_impl { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let zero_schema: Schema = SchemaObject { - const_value: Some(0.into()), - ..Default::default() - } - .into(); - let mut schema: SchemaObject = <$primitive>::json_schema(gen).into(); - schema.subschemas().not = Some(Box::from(zero_schema)); - schema.into() + let mut schema = <$primitive>::json_schema(gen); + let object = schema.ensure_object(); + object.insert("not".to_owned(), serde_json::json!({ + "const": 0 + })); + schema } } }; diff --git a/schemars/src/json_schema_impls/nonzero_unsigned.rs b/schemars/src/json_schema_impls/nonzero_unsigned.rs index 1963d56e..e3c6d1d5 100644 --- a/schemars/src/json_schema_impls/nonzero_unsigned.rs +++ b/schemars/src/json_schema_impls/nonzero_unsigned.rs @@ -1,5 +1,5 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; +use crate::Schema; use crate::JsonSchema; use std::borrow::Cow; use std::num::*; @@ -18,9 +18,10 @@ macro_rules! nonzero_unsigned_impl { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema: SchemaObject = <$primitive>::json_schema(gen).into(); - schema.number().minimum = Some(1.0); - schema.into() + let mut schema = <$primitive>::json_schema(gen); + let object = schema.ensure_object(); + object.insert("minimum".to_owned(), 1.into()); + schema } } }; @@ -32,18 +33,3 @@ nonzero_unsigned_impl!(NonZeroU32 => u32); nonzero_unsigned_impl!(NonZeroU64 => u64); nonzero_unsigned_impl!(NonZeroU128 => u128); nonzero_unsigned_impl!(NonZeroUsize => usize); - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::schema_object_for; - use pretty_assertions::assert_eq; - - #[test] - fn schema_for_nonzero_u32() { - let schema = schema_object_for::(); - assert_eq!(schema.number.unwrap().minimum, Some(1.0)); - assert_eq!(schema.instance_type, Some(InstanceType::Integer.into())); - assert_eq!(schema.format, Some("uint32".to_owned())); - } -} diff --git a/schemars/src/json_schema_impls/primitives.rs b/schemars/src/json_schema_impls/primitives.rs index d1829848..63121fe6 100644 --- a/schemars/src/json_schema_impls/primitives.rs +++ b/schemars/src/json_schema_impls/primitives.rs @@ -1,67 +1,77 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use std::borrow::Cow; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; use std::path::{Path, PathBuf}; macro_rules! simple_impl { - ($type:ty => $instance_type:ident) => { - simple_impl!($type => $instance_type, stringify!($instance_type), None); - }; - ($type:ty => $instance_type:ident, $format:literal) => { - simple_impl!($type => $instance_type, $format, Some($format.to_owned())); + ($type:ty => $instance_type:literal) => { + impl JsonSchema for $type { + no_ref_schema!(); + + fn schema_name() -> String { + $instance_type.to_owned() + } + + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed($instance_type) + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": $instance_type + }) + } + } }; - ($type:ty => $instance_type:ident, $name:expr, $format:expr) => { + ($type:ty => $instance_type:literal, $format:literal) => { impl JsonSchema for $type { no_ref_schema!(); fn schema_name() -> String { - $name.to_owned() + $format.to_owned() } fn schema_id() -> Cow<'static, str> { - Cow::Borrowed($name) + Cow::Borrowed($format) } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::$instance_type.into()), - format: $format, - ..Default::default() - } - .into() + json_schema!({ + "type": $instance_type, + "format": $format + }) } } }; } -simple_impl!(str => String); -simple_impl!(String => String); -simple_impl!(bool => Boolean); -simple_impl!(f32 => Number, "float"); -simple_impl!(f64 => Number, "double"); -simple_impl!(i8 => Integer, "int8"); -simple_impl!(i16 => Integer, "int16"); -simple_impl!(i32 => Integer, "int32"); -simple_impl!(i64 => Integer, "int64"); -simple_impl!(i128 => Integer, "int128"); -simple_impl!(isize => Integer, "int"); -simple_impl!(() => Null); - -simple_impl!(Path => String); -simple_impl!(PathBuf => String); - -simple_impl!(Ipv4Addr => String, "ipv4"); -simple_impl!(Ipv6Addr => String, "ipv6"); -simple_impl!(IpAddr => String, "ip"); - -simple_impl!(SocketAddr => String); -simple_impl!(SocketAddrV4 => String); -simple_impl!(SocketAddrV6 => String); +simple_impl!(str => "string"); +simple_impl!(String => "string"); +simple_impl!(bool => "boolean"); +simple_impl!(f32 => "number", "float"); +simple_impl!(f64 => "number", "double"); +simple_impl!(i8 => "integer", "int8"); +simple_impl!(i16 => "integer", "int16"); +simple_impl!(i32 => "integer", "int32"); +simple_impl!(i64 => "integer", "int64"); +simple_impl!(i128 => "integer", "int128"); +simple_impl!(isize => "integer", "int"); +simple_impl!(() => "null"); + +simple_impl!(Path => "string"); +simple_impl!(PathBuf => "string"); + +simple_impl!(Ipv4Addr => "string", "ipv4"); +simple_impl!(Ipv6Addr => "string", "ipv6"); +simple_impl!(IpAddr => "string", "ip"); + +simple_impl!(SocketAddr => "string"); +simple_impl!(SocketAddrV4 => "string"); +simple_impl!(SocketAddrV6 => "string"); macro_rules! unsigned_impl { - ($type:ty => $instance_type:ident, $format:expr) => { + ($type:ty => $instance_type:literal, $format:literal) => { impl JsonSchema for $type { no_ref_schema!(); @@ -74,24 +84,22 @@ macro_rules! unsigned_impl { } fn json_schema(_: &mut SchemaGenerator) -> Schema { - let mut schema = SchemaObject { - instance_type: Some(InstanceType::$instance_type.into()), - format: Some($format.to_owned()), - ..Default::default() - }; - schema.number().minimum = Some(0.0); - schema.into() + json_schema!({ + "type": $instance_type, + "format": $format, + "minimum": 0 + }) } } }; } -unsigned_impl!(u8 => Integer, "uint8"); -unsigned_impl!(u16 => Integer, "uint16"); -unsigned_impl!(u32 => Integer, "uint32"); -unsigned_impl!(u64 => Integer, "uint64"); -unsigned_impl!(u128 => Integer, "uint128"); -unsigned_impl!(usize => Integer, "uint"); +unsigned_impl!(u8 => "integer", "uint8"); +unsigned_impl!(u16 => "integer", "uint16"); +unsigned_impl!(u32 => "integer", "uint32"); +unsigned_impl!(u64 => "integer", "uint64"); +unsigned_impl!(u128 => "integer", "uint128"); +unsigned_impl!(usize => "integer", "uint"); impl JsonSchema for char { no_ref_schema!(); @@ -105,15 +113,10 @@ impl JsonSchema for char { } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - string: Some(Box::new(StringValidation { - min_length: Some(1), - max_length: Some(1), - ..Default::default() - })), - ..Default::default() - } - .into() + json_schema!({ + "type": "string", + "minLength": 1, + "maxLength": 1, + }) } } diff --git a/schemars/src/json_schema_impls/semver.rs b/schemars/src/json_schema_impls/semver.rs deleted file mode 100644 index dd8ed8ec..00000000 --- a/schemars/src/json_schema_impls/semver.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use semver::Version; -use std::borrow::Cow; - -impl JsonSchema for Version { - no_ref_schema!(); - - fn schema_name() -> String { - "Version".to_owned() - } - - fn schema_id() -> Cow<'static, str> { - Cow::Borrowed("semver::Version") - } - - fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - string: Some(Box::new(StringValidation { - // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string - pattern: Some(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$".to_owned()), - ..Default::default() - })), - ..Default::default() - } - .into() - } -} diff --git a/schemars/src/json_schema_impls/semver1.rs b/schemars/src/json_schema_impls/semver1.rs new file mode 100644 index 00000000..fb47f788 --- /dev/null +++ b/schemars/src/json_schema_impls/semver1.rs @@ -0,0 +1,24 @@ +use crate::gen::SchemaGenerator; +use crate::{json_schema, JsonSchema, Schema}; +use semver1::Version; +use std::borrow::Cow; + +impl JsonSchema for Version { + no_ref_schema!(); + + fn schema_name() -> String { + "Version".to_owned() + } + + fn schema_id() -> Cow<'static, str> { + Cow::Borrowed("semver::Version") + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + json_schema!({ + "type": "string", + // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + "pattern": r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" + }) + } +} diff --git a/schemars/src/json_schema_impls/sequences.rs b/schemars/src/json_schema_impls/sequences.rs index 780307fa..d14d4d33 100644 --- a/schemars/src/json_schema_impls/sequences.rs +++ b/schemars/src/json_schema_impls/sequences.rs @@ -1,6 +1,5 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use std::borrow::Cow; macro_rules! seq_impl { @@ -21,15 +20,10 @@ macro_rules! seq_impl { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(gen.subschema_for::().into()), - ..Default::default() - })), - ..Default::default() - } - .into() + json_schema!({ + "type": "array", + "items": gen.subschema_for::(), + }) } } }; @@ -53,16 +47,11 @@ macro_rules! set_impl { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - unique_items: Some(true), - items: Some(gen.subschema_for::().into()), - ..Default::default() - })), - ..Default::default() - } - .into() + json_schema!({ + "type": "array", + "uniqueItems": true, + "items": gen.subschema_for::(), + }) } } }; diff --git a/schemars/src/json_schema_impls/serdejson.rs b/schemars/src/json_schema_impls/serdejson.rs index 41eafd54..5c6b58d4 100644 --- a/schemars/src/json_schema_impls/serdejson.rs +++ b/schemars/src/json_schema_impls/serdejson.rs @@ -1,6 +1,5 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use serde_json::{Map, Number, Value}; use std::borrow::Cow; use std::collections::BTreeMap; @@ -17,7 +16,7 @@ impl JsonSchema for Value { } fn json_schema(_: &mut SchemaGenerator) -> Schema { - Schema::Bool(true) + true.into() } } @@ -35,11 +34,9 @@ impl JsonSchema for Number { } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::Number.into()), - ..Default::default() - } - .into() + json_schema!({ + "type": "number" + }) } } diff --git a/schemars/src/json_schema_impls/smallvec.rs b/schemars/src/json_schema_impls/smallvec.rs deleted file mode 100644 index f7a75e37..00000000 --- a/schemars/src/json_schema_impls/smallvec.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use smallvec::{Array, SmallVec}; - -forward_impl!(( JsonSchema for SmallVec where A::Item: JsonSchema) => Vec); diff --git a/schemars/src/json_schema_impls/smol_str.rs b/schemars/src/json_schema_impls/smol_str.rs deleted file mode 100644 index cbca4a15..00000000 --- a/schemars/src/json_schema_impls/smol_str.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use smol_str::SmolStr; - -forward_impl!(SmolStr => String); diff --git a/schemars/src/json_schema_impls/time.rs b/schemars/src/json_schema_impls/time.rs index 767a1d2a..7317cfdc 100644 --- a/schemars/src/json_schema_impls/time.rs +++ b/schemars/src/json_schema_impls/time.rs @@ -1,6 +1,5 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use std::borrow::Cow; use std::time::{Duration, SystemTime}; @@ -14,18 +13,14 @@ impl JsonSchema for Duration { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = schema.object(); - obj.required.insert("secs".to_owned()); - obj.required.insert("nanos".to_owned()); - obj.properties - .insert("secs".to_owned(), ::json_schema(gen)); - obj.properties - .insert("nanos".to_owned(), ::json_schema(gen)); - schema.into() + json_schema!({ + "type": "object", + "required": ["secs", "nanos"], + "properties": { + "secs": u64::json_schema(gen), + "nanos": u32::json_schema(gen), + } + }) } } @@ -39,17 +34,13 @@ impl JsonSchema for SystemTime { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..Default::default() - }; - let obj = schema.object(); - obj.required.insert("secs_since_epoch".to_owned()); - obj.required.insert("nanos_since_epoch".to_owned()); - obj.properties - .insert("secs_since_epoch".to_owned(), ::json_schema(gen)); - obj.properties - .insert("nanos_since_epoch".to_owned(), ::json_schema(gen)); - schema.into() + json_schema!({ + "type": "object", + "required": ["secs_since_epoch", "nanos_since_epoch"], + "properties": { + "secs_since_epoch": u64::json_schema(gen), + "nanos_since_epoch": u32::json_schema(gen), + } + }) } } diff --git a/schemars/src/json_schema_impls/tuple.rs b/schemars/src/json_schema_impls/tuple.rs index f67f06a6..ab9a36d2 100644 --- a/schemars/src/json_schema_impls/tuple.rs +++ b/schemars/src/json_schema_impls/tuple.rs @@ -1,6 +1,5 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use std::borrow::Cow; macro_rules! tuple_impls { @@ -24,20 +23,14 @@ macro_rules! tuple_impls { } fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let items = vec![ - $(gen.subschema_for::<$name>()),+ - ]; - SchemaObject { - instance_type: Some(InstanceType::Array.into()), - array: Some(Box::new(ArrayValidation { - items: Some(items.into()), - max_items: Some($len), - min_items: Some($len), - ..Default::default() - })), - ..Default::default() - } - .into() + json_schema!({ + "type": "array", + "items": [ + $(gen.subschema_for::<$name>()),+ + ], + "minItems": $len, + "maxItems": $len, + }) } } )+ @@ -62,29 +55,3 @@ tuple_impls! { 15 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14) 16 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14 T15) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::tests::{schema_for, schema_object_for}; - use pretty_assertions::assert_eq; - - #[test] - fn schema_for_map_any_value() { - let schema = schema_object_for::<(i32, bool)>(); - assert_eq!( - schema.instance_type, - Some(SingleOrVec::from(InstanceType::Array)) - ); - let array_validation = schema.array.unwrap(); - assert_eq!( - array_validation.items, - Some(SingleOrVec::Vec(vec![ - schema_for::(), - schema_for::() - ])) - ); - assert_eq!(array_validation.max_items, Some(2)); - assert_eq!(array_validation.min_items, Some(2)); - } -} diff --git a/schemars/src/json_schema_impls/url.rs b/schemars/src/json_schema_impls/url2.rs similarity index 56% rename from schemars/src/json_schema_impls/url.rs rename to schemars/src/json_schema_impls/url2.rs index be186121..2fcfd04d 100644 --- a/schemars/src/json_schema_impls/url.rs +++ b/schemars/src/json_schema_impls/url2.rs @@ -1,8 +1,7 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use std::borrow::Cow; -use url::Url; +use url2::Url; impl JsonSchema for Url { no_ref_schema!(); @@ -16,11 +15,9 @@ impl JsonSchema for Url { } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - format: Some("uri".to_owned()), - ..Default::default() - } - .into() + json_schema!({ + "type": "string", + "format": "uri", + }) } } diff --git a/schemars/src/json_schema_impls/uuid08.rs b/schemars/src/json_schema_impls/uuid08.rs deleted file mode 100644 index b3b18f83..00000000 --- a/schemars/src/json_schema_impls/uuid08.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; -use std::borrow::Cow; -use uuid08::Uuid; - -impl JsonSchema for Uuid { - no_ref_schema!(); - - fn schema_name() -> String { - "Uuid".to_string() - } - - fn schema_id() -> Cow<'static, str> { - Cow::Borrowed("uuid::Uuid") - } - - fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - format: Some("uuid".to_string()), - ..Default::default() - } - .into() - } -} diff --git a/schemars/src/json_schema_impls/uuid1.rs b/schemars/src/json_schema_impls/uuid1.rs index 2e0c6e95..825f7a24 100644 --- a/schemars/src/json_schema_impls/uuid1.rs +++ b/schemars/src/json_schema_impls/uuid1.rs @@ -1,6 +1,5 @@ use crate::gen::SchemaGenerator; -use crate::schema::*; -use crate::JsonSchema; +use crate::{json_schema, JsonSchema, Schema}; use std::borrow::Cow; use uuid1::Uuid; @@ -16,11 +15,9 @@ impl JsonSchema for Uuid { } fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - format: Some("uuid".to_string()), - ..Default::default() - } - .into() + json_schema!({ + "type": "string", + "format": "uuid", + }) } } diff --git a/schemars/src/json_schema_impls/wrapper.rs b/schemars/src/json_schema_impls/wrapper.rs index 243a06b5..e7e233c2 100644 --- a/schemars/src/json_schema_impls/wrapper.rs +++ b/schemars/src/json_schema_impls/wrapper.rs @@ -1,5 +1,3 @@ -use crate::gen::SchemaGenerator; -use crate::schema::Schema; use crate::JsonSchema; macro_rules! wrapper_impl { diff --git a/schemars/src/lib.rs b/schemars/src/lib.rs index f2332e58..fbe5f1db 100644 --- a/schemars/src/lib.rs +++ b/schemars/src/lib.rs @@ -1,32 +1,9 @@ -#![forbid(unsafe_code)] +#![deny(unsafe_code)] #![doc = include_str!("../README.md")] -/// The map type used by schemars types. -/// -/// Currently a `BTreeMap` or `IndexMap` can be used, but this may change to a different implementation -/// with a similar interface in a future version of schemars. -/// The `IndexMap` will be used when the `preserve_order` feature flag is set. -#[cfg(not(feature = "preserve_order"))] -pub type Map = std::collections::BTreeMap; -#[cfg(feature = "preserve_order")] -pub type Map = indexmap::IndexMap; -/// The set type used by schemars types. -/// -/// Currently a `BTreeSet`, but this may change to a different implementation -/// with a similar interface in a future version of schemars. -pub type Set = std::collections::BTreeSet; - -/// A view into a single entry in a map, which may either be vacant or occupied. -// -/// This is constructed from the `entry` method on `BTreeMap` or `IndexMap`, -/// depending on whether the `preserve_order` feature flag is set. -#[cfg(not(feature = "preserve_order"))] -pub type MapEntry<'a, K, V> = std::collections::btree_map::Entry<'a, K, V>; -#[cfg(feature = "preserve_order")] -pub type MapEntry<'a, K, V> = indexmap::map::Entry<'a, K, V>; - mod flatten; mod json_schema_impls; +mod schema; mod ser; #[macro_use] mod macros; @@ -36,7 +13,6 @@ mod macros; #[doc(hidden)] pub mod _private; pub mod gen; -pub mod schema; pub mod visit; #[cfg(feature = "schemars_derive")] @@ -50,7 +26,7 @@ pub use schemars_derive::*; #[doc(hidden)] pub use serde_json as _serde_json; -use schema::Schema; +pub use schema::Schema; /// A type which can be described as a JSON Schema document. /// @@ -75,7 +51,7 @@ use schema::Schema; /// you will need to determine an appropriate name and ID for the type. /// For non-generic types, the type name/path are suitable for this: /// ``` -/// use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; +/// use schemars::{gen::SchemaGenerator, Schema, JsonSchema}; /// use std::borrow::Cow; /// /// struct NonGenericType; @@ -101,7 +77,7 @@ use schema::Schema; /// /// But generic type parameters which may affect the generated schema should typically be included in the name/ID: /// ``` -/// use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; +/// use schemars::{gen::SchemaGenerator, Schema, JsonSchema}; /// use std::{borrow::Cow, marker::PhantomData}; /// /// struct GenericType(PhantomData); @@ -175,24 +151,3 @@ pub trait JsonSchema { false } } - -#[cfg(test)] -pub mod tests { - use super::*; - - pub fn schema_object_for() -> schema::SchemaObject { - schema_object(schema_for::()) - } - - pub fn schema_for() -> schema::Schema { - let mut gen = gen::SchemaGenerator::default(); - T::json_schema(&mut gen) - } - - pub fn schema_object(schema: schema::Schema) -> schema::SchemaObject { - match schema { - schema::Schema::Object(o) => o, - s => panic!("Schema was not an object: {:?}", s), - } - } -} diff --git a/schemars/src/macros.rs b/schemars/src/macros.rs index 18a6810c..baa144d3 100644 --- a/schemars/src/macros.rs +++ b/schemars/src/macros.rs @@ -76,3 +76,19 @@ macro_rules! schema_for_value { .unwrap() }; } + +// TODO doc +#[macro_export] +macro_rules! json_schema { + ( + {$($json_object:tt)*} + ) => { + $crate::Schema::try_from($crate::_serde_json::json!({$($json_object)*})).unwrap() + }; + (true) => { + $crate::Schema::from(true) + }; + (false) => { + $crate::Schema::from(false) + }; +} diff --git a/schemars/src/schema.rs b/schemars/src/schema.rs index 4bef66e0..6a881589 100644 --- a/schemars/src/schema.rs +++ b/schemars/src/schema.rs @@ -2,550 +2,229 @@ 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 ref_cast::ref_cast_custom; +use ref_cast::RefCastCustom; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::ops::Deref; +use serde_json::{Map, Value}; /// A JSON Schema. -#[allow(clippy::large_enum_variant)] -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(untagged)] -pub enum Schema { - /// A trivial boolean JSON Schema. - /// - /// The schema `true` matches everything (always passes validation), whereas the schema `false` - /// matches nothing (always fails validation). - Bool(bool), - /// A JSON Schema object. - Object(SchemaObject), +#[derive(Debug, Clone, PartialEq, RefCastCustom)] +#[repr(transparent)] +pub struct Schema(Value); + +impl<'de> Deserialize<'de> for Schema { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = Value::deserialize(deserializer)?; + Schema::validate(&value)?; + Ok(Schema(value)) + } } -impl Schema { - /// Creates a new `$ref` schema. - /// - /// The given reference string should be a URI reference. This will usually be a JSON Pointer - /// in [URI Fragment representation](https://tools.ietf.org/html/rfc6901#section-6). - pub fn new_ref(reference: String) -> Self { - SchemaObject::new_ref(reference).into() +impl Serialize for Schema { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + ser::OrderedKeywordWrapper(&self.0).serialize(serializer) } +} - /// Returns `true` if `self` is a `$ref` schema. - /// - /// If `self` is a [`SchemaObject`] with `Some` [`reference`](struct.SchemaObject.html#structfield.reference) set, this returns `true`. - /// Otherwise, returns `false`. - pub fn is_ref(&self) -> bool { - match self { - Schema::Object(o) => o.is_ref(), - _ => false, - } +impl Schema { + pub fn new() -> Self { + Self(Value::Object(Map::new())) } - /// Converts the given schema (if it is a boolean schema) into an equivalent schema object. - /// - /// If the given schema is already a schema object, this has no effect. - /// - /// # Example - /// ``` - /// use schemars::schema::{Schema, SchemaObject}; - /// - /// let bool_schema = Schema::Bool(true); - /// - /// assert_eq!(bool_schema.into_object(), SchemaObject::default()); - /// ``` - 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() - }, - } + pub fn new_ref(reference: String) -> Self { + let mut map = Map::new(); + map.insert("$ref".to_owned(), Value::String(reference)); + Self(Value::Object(map)) } -} -impl From for Schema { - fn from(o: SchemaObject) -> Self { - Schema::Object(o) + pub fn as_value(&self) -> &Value { + &self.0 } -} -impl From for Schema { - fn from(b: bool) -> Self { - Schema::Bool(b) + pub fn as_bool(&self) -> Option { + self.0.as_bool() } -} -/// The root object of a JSON Schema document. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct RootSchema { - /// The `$schema` keyword. - /// - /// See [JSON Schema 8.1.1. The "$schema" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.1.1). - #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] - pub meta_schema: Option, - /// The root schema itself. - #[serde(flatten)] - pub schema: SchemaObject, - /// The `definitions` keyword. - /// - /// In JSON Schema draft 2019-09 this was replaced by $defs, but in Schemars this is still - /// serialized as `definitions` for backward-compatibility. - /// - /// See [JSON Schema 8.2.5. Schema Re-Use With "$defs"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.5), - /// and [JSON Schema (draft 07) 9. Schema Re-Use With "definitions"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-9). - #[serde(alias = "$defs", skip_serializing_if = "Map::is_empty")] - pub definitions: Map, -} + pub fn as_object(&self) -> Option<&Map> { + self.0.as_object() + } -/// A JSON Schema object. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct SchemaObject { - /// Properties which annotate the [`SchemaObject`] which typically have no effect when an object is being validated against the schema. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub metadata: Option>, - /// The `type` keyword. - /// - /// See [JSON Schema Validation 6.1.1. "type"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.1) - /// and [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1). - #[serde(rename = "type", skip_serializing_if = "Option::is_none")] - pub instance_type: Option>, - /// The `format` keyword. - /// - /// See [JSON Schema Validation 7. A Vocabulary for Semantic Content With "format"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-7). - #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option, - /// The `enum` keyword. - /// - /// See [JSON Schema Validation 6.1.2. "enum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.2) - #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] - pub enum_values: Option>, - /// The `const` keyword. - /// - /// See [JSON Schema Validation 6.1.3. "const"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.3) - #[serde( - rename = "const", - skip_serializing_if = "Option::is_none", - deserialize_with = "allow_null" - )] - pub const_value: Option, - /// Properties of the [`SchemaObject`] which define validation assertions in terms of other schemas. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub subschemas: Option>, - /// Properties of the [`SchemaObject`] which define validation assertions for numbers. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub number: Option>, - /// Properties of the [`SchemaObject`] which define validation assertions for strings. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub string: Option>, - /// Properties of the [`SchemaObject`] which define validation assertions for arrays. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub array: Option>, - /// Properties of the [`SchemaObject`] which define validation assertions for objects. - #[serde(flatten, deserialize_with = "skip_if_default")] - pub object: Option>, - /// The `$ref` keyword. - /// - /// See [JSON Schema 8.2.4.1. Direct References with "$ref"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.4.1). - #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")] - pub reference: Option, - /// Arbitrary extra properties which are not part of the JSON Schema specification, or which `schemars` does not support. - #[serde(flatten)] - pub extensions: Map, -} + pub fn as_object_mut(&mut self) -> Option<&mut Map> { + self.0.as_object_mut() + } -// Deserializing "null" to `Option` directly results in `None`, -// this function instead makes it deserialize to `Some(Value::Null)`. -fn allow_null<'de, D>(de: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - Value::deserialize(de).map(Option::Some) -} + pub(crate) fn try_to_object(self) -> Result, bool> { + match self.0 { + Value::Object(m) => Ok(m), + Value::Bool(b) => Err(b), + _ => unreachable!(), + } + } -fn skip_if_default<'de, D, T>(deserializer: D) -> Result>, D::Error> -where - D: serde::Deserializer<'de>, - T: Deserialize<'de> + Default + PartialEq, -{ - let value = T::deserialize(deserializer)?; - if value == T::default() { - Ok(None) - } else { - Ok(Some(Box::new(value))) + pub fn to_value(self) -> Value { + self.0 } -} -macro_rules! get_or_insert_default_fn { - ($name:ident, $ret:ty) => { - get_or_insert_default_fn!( - concat!( - "Returns a mutable reference to this schema's [`", - stringify!($ret), - "`](#structfield.", - stringify!($name), - "), creating it if it was `None`." - ), - $name, - $ret - ); - }; - ($doc:expr, $name:ident, $ret:ty) => { - #[doc = $doc] - pub fn $name(&mut self) -> &mut $ret { - self.$name.get_or_insert_with(Default::default) + pub fn ensure_object(&mut self) -> &mut Map { + if let Some(b) = self.as_bool() { + let mut map = Map::new(); + if !b { + map.insert("not".into(), Value::Object(Map::new())); + } + self.0 = Value::Object(map); } - }; -} -impl SchemaObject { - /// Creates a new `$ref` schema. - /// - /// The given reference string should be a URI reference. This will usually be a JSON Pointer - /// in [URI Fragment representation](https://tools.ietf.org/html/rfc6901#section-6). - pub fn new_ref(reference: String) -> Self { - SchemaObject { - reference: Some(reference), - ..Default::default() - } + self.as_object_mut() + .expect("Schema value should be of type Object.") } - /// Returns `true` if `self` is a `$ref` schema. - /// - /// If `self` has `Some` [`reference`](struct.SchemaObject.html#structfield.reference) set, this returns `true`. - /// Otherwise, returns `false`. - pub fn is_ref(&self) -> bool { - self.reference.is_some() + pub(crate) fn has_type(&self, ty: &str) -> bool { + match self.0.get("type") { + Some(Value::Array(values)) => values.iter().any(|v| v.as_str() == Some(ty)), + Some(Value::String(s)) => s == ty, + _ => false, + } } - /// Returns `true` if `self` accepts values of the given type, according to the [`instance_type`](struct.SchemaObject.html#structfield.instance_type) field. - /// - /// This is a basic check that always returns `true` if no `instance_type` is specified on the schema, - /// and does not check any subschemas. Because of this, both `{}` and `{"not": {}}` accept any type according - /// to this method. - pub fn has_type(&self, ty: InstanceType) -> bool { - self.instance_type - .as_ref() - .map_or(true, |x| x.contains(&ty)) + fn validate(value: &Value) -> Result<(), E> { + use serde::de::Unexpected; + let unexpected = match value { + Value::Bool(_) | Value::Object(_) => return Ok(()), + Value::Null => Unexpected::Unit, + Value::Number(n) => { + if let Some(u) = n.as_u64() { + Unexpected::Unsigned(u) + } else if let Some(i) = n.as_i64() { + Unexpected::Signed(i) + } else if let Some(f) = n.as_f64() { + Unexpected::Float(f) + } else { + unreachable!() + } + } + Value::String(s) => Unexpected::Str(s), + Value::Array(_) => Unexpected::Seq, + }; + + Err(E::invalid_type(unexpected, &"object or boolean")) } - get_or_insert_default_fn!(metadata, Metadata); - get_or_insert_default_fn!(subschemas, SubschemaValidation); - get_or_insert_default_fn!(number, NumberValidation); - get_or_insert_default_fn!(string, StringValidation); - get_or_insert_default_fn!(array, ArrayValidation); - get_or_insert_default_fn!(object, ObjectValidation); -} - -impl From for SchemaObject { - fn from(schema: Schema) -> Self { - schema.into_object() - } -} + #[allow(unsafe_code)] + #[ref_cast_custom] + fn ref_cast(value: &Value) -> &Self; -/// Properties which annotate a [`SchemaObject`] which typically have no effect when an object is being validated against the schema. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct Metadata { - /// The `$id` keyword. - /// - /// See [JSON Schema 8.2.2. The "$id" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2). - #[serde(rename = "$id", skip_serializing_if = "Option::is_none")] - pub id: Option, - /// The `title` keyword. - /// - /// See [JSON Schema Validation 9.1. "title" and "description"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - /// The `description` keyword. - /// - /// See [JSON Schema Validation 9.1. "title" and "description"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - /// The `default` keyword. - /// - /// See [JSON Schema Validation 9.2. "default"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.2). - #[serde( - skip_serializing_if = "Option::is_none", - deserialize_with = "allow_null" - )] - pub default: Option, - /// The `deprecated` keyword. - /// - /// See [JSON Schema Validation 9.3. "deprecated"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.3). - #[serde(skip_serializing_if = "is_false")] - pub deprecated: bool, - /// The `readOnly` keyword. - /// - /// See [JSON Schema Validation 9.4. "readOnly" and "writeOnly"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4). - #[serde(skip_serializing_if = "is_false")] - pub read_only: bool, - /// The `writeOnly` keyword. - /// - /// See [JSON Schema Validation 9.4. "readOnly" and "writeOnly"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4). - #[serde(skip_serializing_if = "is_false")] - pub write_only: bool, - /// The `examples` keyword. - /// - /// See [JSON Schema Validation 9.5. "examples"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.5). - #[serde(skip_serializing_if = "Vec::is_empty")] - pub examples: Vec, + #[allow(unsafe_code)] + #[ref_cast_custom] + fn ref_cast_mut(value: &mut Value) -> &mut Self; } -#[allow(clippy::trivially_copy_pass_by_ref)] -fn is_false(b: &bool) -> bool { - !b +impl From for Value { + fn from(v: Schema) -> Value { + v.0 + } } -/// Properties of a [`SchemaObject`] which define validation assertions in terms of other schemas. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct SubschemaValidation { - /// The `allOf` keyword. - /// - /// See [JSON Schema 9.2.1.1. "allOf"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub all_of: Option>, - /// The `anyOf` keyword. - /// - /// See [JSON Schema 9.2.1.2. "anyOf"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub any_of: Option>, - /// The `oneOf` keyword. - /// - /// See [JSON Schema 9.2.1.3. "oneOf"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.3). - #[serde(skip_serializing_if = "Option::is_none")] - pub one_of: Option>, - /// The `not` keyword. - /// - /// See [JSON Schema 9.2.1.4. "not"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.4). - #[serde(skip_serializing_if = "Option::is_none")] - pub not: Option>, - /// The `if` keyword. - /// - /// See [JSON Schema 9.2.2.1. "if"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.1). - #[serde(rename = "if", skip_serializing_if = "Option::is_none")] - pub if_schema: Option>, - /// The `then` keyword. - /// - /// See [JSON Schema 9.2.2.2. "then"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.2). - #[serde(rename = "then", skip_serializing_if = "Option::is_none")] - pub then_schema: Option>, - /// The `else` keyword. - /// - /// See [JSON Schema 9.2.2.3. "else"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.3). - #[serde(rename = "else", skip_serializing_if = "Option::is_none")] - pub else_schema: Option>, -} +impl std::convert::TryFrom for Schema { + type Error = serde_json::Error; -/// Properties of a [`SchemaObject`] which define validation assertions for numbers. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct NumberValidation { - /// The `multipleOf` keyword. - /// - /// See [JSON Schema Validation 6.2.1. "multipleOf"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub multiple_of: Option, - /// The `maximum` keyword. - /// - /// See [JSON Schema Validation 6.2.2. "maximum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub maximum: Option, - /// The `exclusiveMaximum` keyword. - /// - /// See [JSON Schema Validation 6.2.3. "exclusiveMaximum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.3). - #[serde(skip_serializing_if = "Option::is_none")] - pub exclusive_maximum: Option, - /// The `minimum` keyword. - /// - /// See [JSON Schema Validation 6.2.4. "minimum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.4). - #[serde(skip_serializing_if = "Option::is_none")] - pub minimum: Option, - /// The `exclusiveMinimum` keyword. - /// - /// See [JSON Schema Validation 6.2.5. "exclusiveMinimum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.5). - #[serde(skip_serializing_if = "Option::is_none")] - pub exclusive_minimum: Option, + fn try_from(value: Value) -> serde_json::Result { + Schema::validate(&value)?; + Ok(Schema(value)) + } } -/// Properties of a [`SchemaObject`] which define validation assertions for strings. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct StringValidation { - /// The `maxLength` keyword. - /// - /// See [JSON Schema Validation 6.3.1. "maxLength"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub max_length: Option, - /// The `minLength` keyword. - /// - /// See [JSON Schema Validation 6.3.2. "minLength"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub min_length: Option, - /// The `pattern` keyword. - /// - /// See [JSON Schema Validation 6.3.3. "pattern"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.3). - #[serde(skip_serializing_if = "Option::is_none")] - pub pattern: Option, -} +impl<'a> std::convert::TryFrom<&'a Value> for &'a Schema { + type Error = serde_json::Error; -/// Properties of a [`SchemaObject`] which define validation assertions for arrays. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct ArrayValidation { - /// The `items` keyword. - /// - /// See [JSON Schema 9.3.1.1. "items"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub items: Option>, - /// The `additionalItems` keyword. - /// - /// See [JSON Schema 9.3.1.2. "additionalItems"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub additional_items: Option>, - /// The `maxItems` keyword. - /// - /// See [JSON Schema Validation 6.4.1. "maxItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub max_items: Option, - /// The `minItems` keyword. - /// - /// See [JSON Schema Validation 6.4.2. "minItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub min_items: Option, - /// The `uniqueItems` keyword. - /// - /// See [JSON Schema Validation 6.4.3. "uniqueItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.3). - #[serde(skip_serializing_if = "Option::is_none")] - pub unique_items: Option, - /// The `contains` keyword. - /// - /// See [JSON Schema 9.3.1.4. "contains"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.4). - #[serde(skip_serializing_if = "Option::is_none")] - pub contains: Option>, + fn try_from(value: &Value) -> serde_json::Result<&Schema> { + Schema::validate(value)?; + Ok(Schema::ref_cast(value)) + } } -/// Properties of a [`SchemaObject`] which define validation assertions for objects. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", default)] -pub struct ObjectValidation { - /// The `maxProperties` keyword. - /// - /// See [JSON Schema Validation 6.5.1. "maxProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.1). - #[serde(skip_serializing_if = "Option::is_none")] - pub max_properties: Option, - /// The `minProperties` keyword. - /// - /// See [JSON Schema Validation 6.5.2. "minProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.2). - #[serde(skip_serializing_if = "Option::is_none")] - pub min_properties: Option, - /// The `required` keyword. - /// - /// See [JSON Schema Validation 6.5.3. "required"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.3). - #[serde(skip_serializing_if = "Set::is_empty")] - pub required: Set, - /// The `properties` keyword. - /// - /// See [JSON Schema 9.3.2.1. "properties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.1). - #[serde(skip_serializing_if = "Map::is_empty")] - pub properties: Map, - /// The `patternProperties` keyword. - /// - /// See [JSON Schema 9.3.2.2. "patternProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.2). - #[serde(skip_serializing_if = "Map::is_empty")] - pub pattern_properties: Map, - /// The `additionalProperties` keyword. - /// - /// See [JSON Schema 9.3.2.3. "additionalProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.3). - #[serde(skip_serializing_if = "Option::is_none")] - pub additional_properties: Option>, - /// The `propertyNames` keyword. - /// - /// See [JSON Schema 9.3.2.5. "propertyNames"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.5). - #[serde(skip_serializing_if = "Option::is_none")] - pub property_names: Option>, -} +impl<'a> std::convert::TryFrom<&'a mut Value> for &'a mut Schema { + type Error = serde_json::Error; -/// 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). -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase")] -pub enum InstanceType { - Null, - Boolean, - Object, - Array, - Number, - String, - Integer, + fn try_from(value: &mut Value) -> serde_json::Result<&mut Schema> { + Schema::validate(value)?; + Ok(Schema::ref_cast_mut(value)) + } } -/// A type which can be serialized as a single item, or multiple items. -/// -/// In some contexts, a `Single` may be semantically distinct from a `Vec` containing only item. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))] -#[serde(untagged)] -pub enum SingleOrVec { - Single(Box), - Vec(Vec), +impl Default for Schema { + fn default() -> Self { + Self(Value::Object(Map::new())) + } } -impl From for SingleOrVec { - fn from(single: T) -> Self { - SingleOrVec::Single(Box::new(single)) +impl From> for Schema { + fn from(o: Map) -> Self { + Schema(Value::Object(o)) } } -impl From> for SingleOrVec { - fn from(vec: Vec) -> Self { - SingleOrVec::Vec(vec) +impl From for Schema { + fn from(b: bool) -> Self { + Schema(Value::Bool(b)) } } -impl SingleOrVec { - /// Returns `true` if `self` is either a `Single` equal to `x`, or a `Vec` containing `x`. - /// - /// # Examples - /// - /// ``` - /// use schemars::schema::SingleOrVec; - /// - /// let s = SingleOrVec::from(10); - /// assert!(s.contains(&10)); - /// assert!(!s.contains(&20)); - /// - /// let v = SingleOrVec::from(vec![10, 20]); - /// assert!(v.contains(&10)); - /// assert!(v.contains(&20)); - /// assert!(!v.contains(&30)); - /// ``` - pub fn contains(&self, x: &T) -> bool { - match self { - SingleOrVec::Single(s) => s.deref() == x, - SingleOrVec::Vec(v) => v.contains(x), +mod ser { + use serde::ser::{Serialize, SerializeMap, SerializeSeq}; + use serde_json::Value; + + const ORDERED_KEYWORDS_START: [&str; 6] = + ["$id", "$schema", "title", "description", "type", "format"]; + const ORDERED_KEYWORDS_END: [&str; 2] = ["$defs", "definitions"]; + + pub(super) struct OrderedKeywordWrapper<'a>(pub &'a Value); + + impl Serialize for OrderedKeywordWrapper<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self.0 { + Value::Array(array) => { + let mut seq = serializer.serialize_seq(Some(array.len()))?; + for value in array { + seq.serialize_element(&OrderedKeywordWrapper(value))?; + } + seq.end() + } + Value::Object(object) => { + let mut map = serializer.serialize_map(Some(object.len()))?; + + for key in ORDERED_KEYWORDS_START { + if let Some(value) = object.get(key) { + map.serialize_entry(key, &OrderedKeywordWrapper(value))?; + } + } + + for (key, value) in object { + if !ORDERED_KEYWORDS_START.contains(&key.as_str()) + && !ORDERED_KEYWORDS_END.contains(&key.as_str()) + { + map.serialize_entry(key, &OrderedKeywordWrapper(value))?; + } + } + + for key in ORDERED_KEYWORDS_END { + if let Some(value) = object.get(key) { + map.serialize_entry(key, &OrderedKeywordWrapper(value))?; + } + } + + map.end() + } + _ => self.0.serialize(serializer), + } } } } diff --git a/schemars/src/ser.rs b/schemars/src/ser.rs index 3f69bef5..8be7b343 100644 --- a/schemars/src/ser.rs +++ b/schemars/src/ser.rs @@ -1,8 +1,7 @@ -use crate::schema::*; -use crate::JsonSchema; -use crate::{gen::SchemaGenerator, Map}; -use serde_json::{Error, Value}; -use std::{convert::TryInto, fmt::Display}; +use crate::gen::SchemaGenerator; +use crate::{json_schema, JsonSchema, Schema}; +use serde_json::{Error, Map, Value}; +use std::fmt::Display; pub(crate) struct Serializer<'a> { pub(crate) gen: &'a mut SchemaGenerator, @@ -22,7 +21,7 @@ pub(crate) struct SerializeTuple<'a> { pub(crate) struct SerializeMap<'a> { gen: &'a mut SchemaGenerator, - properties: Map, + properties: Map, current_key: Option, title: &'static str, } @@ -36,13 +35,11 @@ macro_rules! forward_to_subschema_for { } macro_rules! return_instance_type { - ($fn:ident, $ty:ty, $instance_type:ident) => { + ($fn:ident, $ty:ty, $instance_type:expr) => { fn $fn(self, _value: $ty) -> Result { - Ok(SchemaObject { - instance_type: Some(InstanceType::$instance_type.into()), - ..Default::default() - } - .into()) + Ok(json_schema!({ + "type": $instance_type + })) } }; } @@ -59,18 +56,18 @@ impl<'a> serde::Serializer for Serializer<'a> { type SerializeStruct = SerializeMap<'a>; type SerializeStructVariant = Self; - return_instance_type!(serialize_i8, i8, Integer); - return_instance_type!(serialize_i16, i16, Integer); - return_instance_type!(serialize_i32, i32, Integer); - return_instance_type!(serialize_i64, i64, Integer); - return_instance_type!(serialize_i128, i128, Integer); - return_instance_type!(serialize_u8, u8, Integer); - return_instance_type!(serialize_u16, u16, Integer); - return_instance_type!(serialize_u32, u32, Integer); - return_instance_type!(serialize_u64, u64, Integer); - return_instance_type!(serialize_u128, u128, Integer); - return_instance_type!(serialize_f32, f32, Number); - return_instance_type!(serialize_f64, f64, Number); + return_instance_type!(serialize_i8, i8, "integer"); + return_instance_type!(serialize_i16, i16, "integer"); + return_instance_type!(serialize_i32, i32, "integer"); + return_instance_type!(serialize_i64, i64, "integer"); + return_instance_type!(serialize_i128, i128, "integer"); + return_instance_type!(serialize_u8, u8, "integer"); + return_instance_type!(serialize_u16, u16, "integer"); + return_instance_type!(serialize_u32, u32, "integer"); + return_instance_type!(serialize_u64, u64, "integer"); + return_instance_type!(serialize_u128, u128, "integer"); + return_instance_type!(serialize_f32, f32, "number"); + return_instance_type!(serialize_f64, f64, "number"); forward_to_subschema_for!(serialize_bool, bool); forward_to_subschema_for!(serialize_char, char); @@ -93,7 +90,7 @@ impl<'a> serde::Serializer for Serializer<'a> { let value_schema = iter .into_iter() .try_fold(None, |acc, (_, v)| { - if acc == Some(Schema::Bool(true)) { + if acc == Some(true.into()) { return Ok(acc); } @@ -103,21 +100,16 @@ impl<'a> serde::Serializer for Serializer<'a> { })?; Ok(match &acc { None => Some(schema), - Some(items) if items != &schema => Some(Schema::Bool(true)), + Some(items) if items != &schema => Some(true.into()), _ => 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()) + .unwrap_or(true.into()); + + Ok(json_schema!({ + "type": "object", + "additionalProperties": value_schema, + })) } fn serialize_none(self) -> Result { @@ -132,52 +124,47 @@ impl<'a> serde::Serializer for Serializer<'a> { 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(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(self.gen)]), - ..Default::default() - })), - ..Default::default() + schema = match schema.try_to_object() { + Ok(mut obj) => { + let value = obj.get_mut("type"); + match value { + Some(Value::Array(array)) => { + let null = Value::from("null"); + if !array.contains(&null) { + array.push(null); + } + obj.into() + } + Some(Value::String(string)) => { + if string != "null" { + *value.unwrap() = + Value::Array(vec![std::mem::take(string).into(), "null".into()]) + } + obj.into() + } + _ => json_schema!({ + "anyOf": [ + obj, + <()>::json_schema(self.gen) + ] + }), + } } - .into(), + Err(true) => true.into(), + Err(false) => <()>::json_schema(self.gen), } } 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); + schema + .ensure_object() + .insert("nullable".into(), true.into()); }; Ok(schema) @@ -193,7 +180,7 @@ impl<'a> serde::Serializer for Serializer<'a> { _variant_index: u32, _variant: &'static str, ) -> Result { - Ok(Schema::Bool(true)) + Ok(true.into()) } fn serialize_newtype_struct( @@ -205,15 +192,13 @@ impl<'a> serde::Serializer for Serializer<'a> { T: serde::Serialize, { let include_title = self.include_title; - let mut result = value.serialize(self); + let mut schema = value.serialize(self)?; - if include_title { - if let Ok(Schema::Object(ref mut object)) = result { - object.metadata().title = Some(name.to_string()); - } + if include_title && !name.is_empty() { + schema.ensure_object().insert("title".into(), name.into()); } - result + Ok(schema) } fn serialize_newtype_variant( @@ -226,7 +211,7 @@ impl<'a> serde::Serializer for Serializer<'a> { where T: serde::Serialize, { - Ok(Schema::Bool(true)) + Ok(true.into()) } fn serialize_seq(self, _len: Option) -> Result { @@ -313,7 +298,7 @@ impl serde::ser::SerializeTupleVariant for Serializer<'_> { } fn end(self) -> Result { - Ok(Schema::Bool(true)) + Ok(true.into()) } } @@ -333,7 +318,7 @@ impl serde::ser::SerializeStructVariant for Serializer<'_> { } fn end(self) -> Result { - Ok(Schema::Bool(true)) + Ok(true.into()) } } @@ -345,7 +330,7 @@ impl serde::ser::SerializeSeq for SerializeSeq<'_> { where T: serde::Serialize, { - if self.items != Some(Schema::Bool(true)) { + if self.items != Some(true.into()) { let schema = value.serialize(Serializer { gen: self.gen, include_title: false, @@ -354,7 +339,7 @@ impl serde::ser::SerializeSeq for SerializeSeq<'_> { None => self.items = Some(schema), Some(items) => { if items != &schema { - self.items = Some(Schema::Bool(true)) + self.items = Some(true.into()) } } } @@ -364,16 +349,12 @@ impl serde::ser::SerializeSeq for SerializeSeq<'_> { } 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()) + let items = self.items.unwrap_or(true.into()); + + Ok(json_schema!({ + "type": "array", + "items": items + })) } } @@ -394,23 +375,21 @@ impl serde::ser::SerializeTuple for SerializeTuple<'_> { } 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() - }; + let len = self.items.len(); + let mut schema = json_schema!({ + "type": "array", + "items": self.items, + "maxItems": len, + "minItems": len, + }); if !self.title.is_empty() { - schema.metadata().title = Some(self.title.to_owned()); + schema + .ensure_object() + .insert("title".into(), self.title.into()); } - Ok(schema.into()) + Ok(schema) } } @@ -459,26 +438,24 @@ impl serde::ser::SerializeMap for SerializeMap<'_> { gen: self.gen, include_title: false, })?; - self.properties.insert(key, schema); + self.properties.insert(key, schema.into()); 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() - }; + let mut schema = json_schema!({ + "type": "object", + "properties": self.properties, + }); if !self.title.is_empty() { - schema.metadata().title = Some(self.title.to_owned()); + schema + .ensure_object() + .insert("title".into(), self.title.into()); } - Ok(schema.into()) + Ok(schema) } } @@ -498,7 +475,7 @@ impl serde::ser::SerializeStruct for SerializeMap<'_> { gen: self.gen, include_title: false, })?; - self.properties.insert(key.to_string(), prop_schema); + self.properties.insert(key.to_string(), prop_schema.into()); Ok(()) } diff --git a/schemars/src/visit.rs b/schemars/src/visit.rs index 182b5722..cfe11dfa 100644 --- a/schemars/src/visit.rs +++ b/schemars/src/visit.rs @@ -7,116 +7,87 @@ All methods of `Visitor` have a default implementation that makes no change but When overriding one of these methods, you will *usually* want to still call this default implementation. # Example -To add a custom property to all schemas: +To add a custom property to all object schemas: ``` -use schemars::schema::SchemaObject; -use schemars::visit::{Visitor, visit_schema_object}; +use schemars::Schema; +use schemars::visit::{Visitor, visit_schema}; pub struct MyVisitor; impl Visitor for MyVisitor { - fn visit_schema_object(&mut self, schema: &mut SchemaObject) { + fn visit_schema(&mut self, schema: &mut Schema) { // First, make our change to this schema - schema.extensions.insert("my_property".to_string(), serde_json::json!("hello world")); + if let Some(obj) = schema.as_object_mut() { + obj.insert("my_property".to_string(), serde_json::json!("hello world")); + } // Then delegate to default implementation to visit any subschemas - visit_schema_object(self, schema); + visit_schema(self, schema); } } ``` */ -use crate::schema::{RootSchema, Schema, SchemaObject, SingleOrVec}; +use serde_json::{json, Value}; + +use crate::Schema; /// Trait used to recursively modify a constructed schema and its subschemas. pub trait Visitor { - /// Override this method to modify a [`RootSchema`] and (optionally) its subschemas. - /// - /// When overriding this method, you will usually want to call the [`visit_root_schema`] function to visit subschemas. - fn visit_root_schema(&mut self, root: &mut RootSchema) { - visit_root_schema(self, root) - } - /// Override this method to modify a [`Schema`] and (optionally) its subschemas. /// /// When overriding this method, you will usually want to call the [`visit_schema`] function to visit subschemas. fn visit_schema(&mut self, schema: &mut Schema) { visit_schema(self, schema) } - - /// Override this method to modify a [`SchemaObject`] and (optionally) its subschemas. - /// - /// When overriding this method, you will usually want to call the [`visit_schema_object`] function to visit subschemas. - fn visit_schema_object(&mut self, schema: &mut SchemaObject) { - visit_schema_object(self, schema) - } -} - -/// Visits all subschemas of the [`RootSchema`]. -pub fn visit_root_schema(v: &mut V, root: &mut RootSchema) { - v.visit_schema_object(&mut root.schema); - visit_map_values(v, &mut root.definitions); } /// Visits all subschemas of the [`Schema`]. pub fn visit_schema(v: &mut V, schema: &mut Schema) { - if let Schema::Object(schema) = schema { - v.visit_schema_object(schema) - } -} - -/// Visits all subschemas of the [`SchemaObject`]. -pub fn visit_schema_object(v: &mut 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: &mut V, target: &mut Option>) { - if let Some(s) = target { - v.visit_schema(s) - } -} - -fn visit_vec(v: &mut V, target: &mut Option>) { - if let Some(vec) = target { - for s in vec { - v.visit_schema(s) - } - } -} - -fn visit_map_values(v: &mut V, target: &mut crate::Map) { - for s in target.values_mut() { - v.visit_schema(s) - } -} - -fn visit_single_or_vec(v: &mut V, target: &mut Option>) { - match target { - None => {} - Some(SingleOrVec::Single(s)) => v.visit_schema(s), - Some(SingleOrVec::Vec(vec)) => { - for s in vec { - v.visit_schema(s) + if let Some(obj) = schema.as_object_mut() { + for (key, value) in obj { + match key.as_str() { + "not" + | "if" + | "then" + | "else" + | "additionalItems" + | "contains" + | "additionalProperties" + | "propertyNames" => { + if let Ok(subschema) = value.try_into() { + v.visit_schema(subschema) + } + } + "allOf" | "anyOf" | "oneOf" => { + if let Some(array) = value.as_array_mut() { + for value in array { + if let Ok(subschema) = value.try_into() { + v.visit_schema(subschema) + } + } + } + } + "items" => { + if let Some(array) = value.as_array_mut() { + for value in array { + if let Ok(subschema) = value.try_into() { + v.visit_schema(subschema) + } + } + } else if let Ok(subschema) = value.try_into() { + v.visit_schema(subschema) + } + } + "properties" | "patternProperties" | "definitions" | "$defs" => { + if let Some(obj) = value.as_object_mut() { + for value in obj.values_mut() { + if let Ok(subschema) = value.try_into() { + v.visit_schema(subschema) + } + } + } + } + _ => {} } } } @@ -133,29 +104,23 @@ pub struct ReplaceBoolSchemas { impl Visitor for ReplaceBoolSchemas { fn visit_schema(&mut self, schema: &mut Schema) { - visit_schema(self, schema); - - if let Schema::Bool(b) = *schema { - *schema = Schema::Bool(b).into_object().into() - } - } + if let Some(obj) = schema.as_object_mut() { + if self.skip_additional_properties { + if let Some((ap_key, ap_value)) = obj.remove_entry("additionalProperties") { + visit_schema(self, schema); - fn visit_schema_object(&mut self, schema: &mut SchemaObject) { - if self.skip_additional_properties { - if let Some(obj) = &mut schema.object { - if let Some(ap) = &obj.additional_properties { - if let Schema::Bool(_) = ap.as_ref() { - let additional_properties = obj.additional_properties.take(); - visit_schema_object(self, schema); - schema.object().additional_properties = additional_properties; - - return; + if let Some(obj) = schema.as_object_mut() { + obj.insert(ap_key, ap_value); } + + return; } } - } - visit_schema_object(self, schema); + visit_schema(self, schema); + } else { + schema.ensure_object(); + } } } @@ -166,18 +131,19 @@ impl Visitor for ReplaceBoolSchemas { pub struct RemoveRefSiblings; impl Visitor for RemoveRefSiblings { - fn visit_schema_object(&mut 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]), + fn visit_schema(&mut self, schema: &mut Schema) { + visit_schema(self, schema); + + if let Some(obj) = schema.as_object_mut() { + if obj.len() > 1 { + if let Some(ref_value) = obj.remove("$ref") { + if let Value::Array(all_of) = + obj.entry("allOf").or_insert(Value::Array(Vec::new())) + { + all_of.push(json!({ + "$ref": ref_value + })); + } } } } @@ -188,25 +154,18 @@ impl Visitor for RemoveRefSiblings { /// /// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `examples` property. #[derive(Debug, Clone)] -pub struct SetSingleExample { - /// When set to `true`, the `examples` property will not be removed, but its first value will still be copied to `example`. - pub retain_examples: bool, -} +pub struct SetSingleExample; impl Visitor for SetSingleExample { - fn visit_schema_object(&mut self, schema: &mut SchemaObject) { - visit_schema_object(self, schema); - - let first_example = schema.metadata.as_mut().and_then(|m| { - if self.retain_examples { - m.examples.first().cloned() - } else { - m.examples.drain(..).next() - } - }); + fn visit_schema(&mut self, schema: &mut Schema) { + visit_schema(self, schema); - if let Some(example) = first_example { - schema.extensions.insert("example".to_owned(), example); + if let Some(obj) = schema.as_object_mut() { + if let Some(Value::Array(examples)) = obj.remove("examples") { + if let Some(first_example) = examples.into_iter().next() { + obj.insert("example".into(), first_example); + } + } } } } diff --git a/schemars/tests/arrayvec.rs b/schemars/tests/arrayvec.rs index b00b3955..e32d1a72 100644 --- a/schemars/tests/arrayvec.rs +++ b/schemars/tests/arrayvec.rs @@ -1,16 +1,6 @@ mod util; use util::*; -#[test] -fn arrayvec05() -> TestResult { - test_default_generated_schema::>("arrayvec") -} - -#[test] -fn arrayvec05_string() -> TestResult { - test_default_generated_schema::>("arrayvec_string") -} - #[test] fn arrayvec07() -> TestResult { test_default_generated_schema::>("arrayvec") diff --git a/schemars/tests/bytes.rs b/schemars/tests/bytes.rs index 688ab214..57974117 100644 --- a/schemars/tests/bytes.rs +++ b/schemars/tests/bytes.rs @@ -1,5 +1,5 @@ mod util; -use bytes::{Bytes, BytesMut}; +use bytes1::{Bytes, BytesMut}; use util::*; #[test] diff --git a/schemars/tests/chrono.rs b/schemars/tests/chrono.rs index 7f518d79..da6f8d4d 100644 --- a/schemars/tests/chrono.rs +++ b/schemars/tests/chrono.rs @@ -1,5 +1,5 @@ mod util; -use chrono::prelude::*; +use chrono04::prelude::*; use schemars::JsonSchema; use util::*; diff --git a/schemars/tests/decimal.rs b/schemars/tests/decimal.rs index d245583b..246e8138 100644 --- a/schemars/tests/decimal.rs +++ b/schemars/tests/decimal.rs @@ -3,12 +3,7 @@ use util::*; #[test] fn rust_decimal() -> TestResult { - test_default_generated_schema::("rust_decimal") -} - -#[test] -fn bigdecimal03() -> TestResult { - test_default_generated_schema::("bigdecimal03") + test_default_generated_schema::("rust_decimal") } #[test] diff --git a/schemars/tests/dereference.rs b/schemars/tests/dereference.rs deleted file mode 100644 index b1abab0f..00000000 --- a/schemars/tests/dereference.rs +++ /dev/null @@ -1,23 +0,0 @@ -use schemars::{gen::SchemaGenerator, JsonSchema}; -use std::ptr; - -#[allow(dead_code)] -#[derive(JsonSchema)] -struct Struct { - foo: i32, - bar: bool, -} - -#[test] -fn dereference_struct() { - let mut gen = SchemaGenerator::default(); - let struct_ref_schema = gen.subschema_for::(); - let struct_schema = gen.definitions().get(&::schema_name()).unwrap(); - - assert!(struct_ref_schema.is_ref()); - assert!(!struct_schema.is_ref()); - - let dereferenced = gen.dereference(&struct_ref_schema); - assert!(dereferenced.is_some()); - assert!(ptr::eq(dereferenced.unwrap(), struct_schema)); -} diff --git a/schemars/tests/either.rs b/schemars/tests/either.rs index 16a2e591..5dcd0796 100644 --- a/schemars/tests/either.rs +++ b/schemars/tests/either.rs @@ -1,5 +1,5 @@ mod util; -use either::Either; +use either1::Either; use util::*; #[test] diff --git a/schemars/tests/enum.rs b/schemars/tests/enum.rs index 1ed60462..70bd9d21 100644 --- a/schemars/tests/enum.rs +++ b/schemars/tests/enum.rs @@ -1,5 +1,7 @@ mod util; -use schemars::{JsonSchema, Map}; +use std::collections::BTreeMap; + +use schemars::JsonSchema; use util::*; // Ensure that schemars_derive uses the full path to std::string::String @@ -20,7 +22,7 @@ struct Struct { #[schemars(rename_all = "camelCase")] enum External { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -29,6 +31,7 @@ enum External { }, UnitTwo, Tuple(i32, bool), + // FIXME this should probably only replace the "payload" of the enum #[schemars(with = "i32")] WithInt, } @@ -43,7 +46,7 @@ fn enum_external_tag() -> TestResult { #[schemars(tag = "typeProperty")] enum Internal { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -51,6 +54,7 @@ enum Internal { bar: bool, }, UnitTwo, + // FIXME this should probably only replace the "payload" of the enum #[schemars(with = "i32")] WithInt, } @@ -65,7 +69,7 @@ fn enum_internal_tag() -> TestResult { #[schemars(untagged)] enum Untagged { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -73,6 +77,7 @@ enum Untagged { bar: bool, }, Tuple(i32, bool), + // FIXME this should probably only replace the "payload" of the enum #[schemars(with = "i32")] WithInt, } @@ -87,7 +92,7 @@ fn enum_untagged() -> TestResult { #[schemars(tag = "t", content = "c")] enum Adjacent { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -96,6 +101,7 @@ enum Adjacent { }, Tuple(i32, bool), UnitTwo, + // FIXME this should probably only replace the "payload" of the enum #[schemars(with = "i32")] WithInt, } diff --git a/schemars/tests/enum_deny_unknown_fields.rs b/schemars/tests/enum_deny_unknown_fields.rs index 62017840..62c1a455 100644 --- a/schemars/tests/enum_deny_unknown_fields.rs +++ b/schemars/tests/enum_deny_unknown_fields.rs @@ -1,5 +1,7 @@ mod util; -use schemars::{JsonSchema, Map}; +use std::collections::BTreeMap; + +use schemars::JsonSchema; use util::*; // Ensure that schemars_derive uses the full path to std::string::String @@ -22,7 +24,7 @@ struct Struct { #[schemars(rename_all = "camelCase", deny_unknown_fields)] enum External { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -31,6 +33,7 @@ enum External { }, UnitTwo, Tuple(i32, bool), + // FIXME this should probably only replace the "payload" of the enum #[schemars(with = "i32")] WithInt, } @@ -46,7 +49,7 @@ fn enum_external_tag() -> TestResult { #[schemars(tag = "typeProperty", deny_unknown_fields)] enum Internal { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -54,6 +57,7 @@ enum Internal { bar: bool, }, UnitTwo, + // FIXME this should only replace the "payload" of the enum (which doesn't even make sense for unit enums!) #[schemars(with = "i32")] WithInt, } @@ -69,7 +73,7 @@ fn enum_internal_tag() -> TestResult { #[schemars(untagged, deny_unknown_fields)] enum Untagged { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -77,6 +81,7 @@ enum Untagged { bar: bool, }, Tuple(i32, bool), + // FIXME this should probably only replace the "payload" of the enum #[schemars(with = "i32")] WithInt, } @@ -92,7 +97,7 @@ fn enum_untagged() -> TestResult { #[schemars(tag = "t", content = "c", deny_unknown_fields)] enum Adjacent { UnitOne, - StringMap(Map<&'static str, &'static str>), + StringMap(BTreeMap<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -101,6 +106,7 @@ enum Adjacent { }, Tuple(i32, bool), UnitTwo, + // FIXME this should probably only replace the "payload" of the enum #[schemars(with = "i32")] WithInt, } diff --git a/schemars/tests/enumset.rs b/schemars/tests/enumset.rs index 27f20307..c04ba0e1 100644 --- a/schemars/tests/enumset.rs +++ b/schemars/tests/enumset.rs @@ -1,8 +1,11 @@ mod util; -use enumset::{EnumSet, EnumSetType}; +use enumset1::{EnumSet, EnumSetType}; use schemars::JsonSchema; use util::*; +// needed to derive EnumSetType when using a crate alias +extern crate enumset1 as enumset; + #[derive(EnumSetType, JsonSchema)] enum Foo { Bar, diff --git a/schemars/tests/expected/arrayvec_string.json b/schemars/tests/expected/arrayvec_string.json index 42f099a3..ad174d81 100644 --- a/schemars/tests/expected/arrayvec_string.json +++ b/schemars/tests/expected/arrayvec_string.json @@ -1,5 +1,5 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "String", + "title": "string", "type": "string" } \ No newline at end of file diff --git a/schemars/tests/expected/bigdecimal03.json b/schemars/tests/expected/bigdecimal03.json deleted file mode 100644 index 855db6f7..00000000 --- a/schemars/tests/expected/bigdecimal03.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Decimal", - "type": "string", - "pattern": "^-?[0-9]+(\\.[0-9]+)?$" -} \ No newline at end of file diff --git a/schemars/tests/expected/either.json b/schemars/tests/expected/either.json index b028057a..807c9c92 100644 --- a/schemars/tests/expected/either.json +++ b/schemars/tests/expected/either.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Either_int32_or_Either_Boolean_or_Null", + "title": "Either_int32_or_Either_boolean_or_null", "anyOf": [ { "type": "integer", diff --git a/schemars/tests/expected/enum-internal-duf.json b/schemars/tests/expected/enum-internal-duf.json index fc36644f..501f13b8 100644 --- a/schemars/tests/expected/enum-internal-duf.json +++ b/schemars/tests/expected/enum-internal-duf.json @@ -127,8 +127,7 @@ "WithInt" ] } - }, - "additionalProperties": false + } } ] } \ No newline at end of file diff --git a/schemars/tests/expected/range.json b/schemars/tests/expected/range.json index a3b14afa..0bb3aeb1 100644 --- a/schemars/tests/expected/range.json +++ b/schemars/tests/expected/range.json @@ -15,7 +15,7 @@ "$ref": "#/definitions/Range_of_double" }, "bound": { - "$ref": "#/definitions/Bound_of_String" + "$ref": "#/definitions/Bound_of_string" } }, "definitions": { @@ -55,7 +55,7 @@ } } }, - "Bound_of_String": { + "Bound_of_string": { "oneOf": [ { "type": "object", diff --git a/schemars/tests/expected/remote_derive_generic.json b/schemars/tests/expected/remote_derive_generic.json index 2fea80a3..5a65f3bc 100644 --- a/schemars/tests/expected/remote_derive_generic.json +++ b/schemars/tests/expected/remote_derive_generic.json @@ -10,10 +10,10 @@ ], "properties": { "byte_or_bool2": { - "$ref": "#/definitions/Or_for_uint8_and_Boolean" + "$ref": "#/definitions/Or_for_uint8_and_boolean" }, "unit_or_t2": { - "$ref": "#/definitions/Or_for_Null_and_int32" + "$ref": "#/definitions/Or_for_null_and_int32" }, "s": { "$ref": "#/definitions/Str" @@ -30,7 +30,7 @@ } }, "definitions": { - "Or_for_uint8_and_Boolean": { + "Or_for_uint8_and_boolean": { "anyOf": [ { "type": "integer", @@ -42,7 +42,7 @@ } ] }, - "Or_for_Null_and_int32": { + "Or_for_null_and_int32": { "anyOf": [ { "type": "null" diff --git a/schemars/tests/expected/result.json b/schemars/tests/expected/result.json index d8d6ec17..2a015736 100644 --- a/schemars/tests/expected/result.json +++ b/schemars/tests/expected/result.json @@ -8,14 +8,14 @@ ], "properties": { "result1": { - "$ref": "#/definitions/Result_of_MyStruct_or_Array_of_String" + "$ref": "#/definitions/Result_of_MyStruct_or_Array_of_string" }, "result2": { - "$ref": "#/definitions/Result_of_Boolean_or_Null" + "$ref": "#/definitions/Result_of_boolean_or_null" } }, "definitions": { - "Result_of_MyStruct_or_Array_of_String": { + "Result_of_MyStruct_or_Array_of_string": { "oneOf": [ { "type": "object", @@ -56,7 +56,7 @@ } } }, - "Result_of_Boolean_or_Null": { + "Result_of_boolean_or_null": { "oneOf": [ { "type": "object", diff --git a/schemars/tests/expected/schema-name-custom.json b/schemars/tests/expected/schema-name-custom.json index 866fb9d3..844699a0 100644 --- a/schemars/tests/expected/schema-name-custom.json +++ b/schemars/tests/expected/schema-name-custom.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "a-new-name-Array_of_String-int32-int32", + "title": "a-new-name-Array_of_string-int32-int32", "type": "object", "required": [ "inner", diff --git a/schemars/tests/expected/schema-name-default.json b/schemars/tests/expected/schema-name-default.json index 31f7f267..39c39a97 100644 --- a/schemars/tests/expected/schema-name-default.json +++ b/schemars/tests/expected/schema-name-default.json @@ -1,15 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String", + "title": "MyStruct_for_int32_and_null_and_boolean_and_Array_of_string", "type": "object", - "required": [ - "inner", - "t", - "u", - "v", - "w" - ], "properties": { + "inner": { + "$ref": "#/definitions/MySimpleStruct" + }, "t": { "type": "integer", "format": "int32" @@ -25,23 +21,27 @@ "items": { "type": "string" } - }, - "inner": { - "$ref": "#/definitions/MySimpleStruct" } }, + "required": [ + "inner", + "t", + "u", + "v", + "w" + ], "definitions": { "MySimpleStruct": { "type": "object", - "required": [ - "foo" - ], "properties": { "foo": { "type": "integer", "format": "int32" } - } + }, + "required": [ + "foo" + ] } } } \ No newline at end of file diff --git a/schemars/tests/expected/schema-name-mixed-generics.json b/schemars/tests/expected/schema-name-mixed-generics.json index 32ac797e..bddd083c 100644 --- a/schemars/tests/expected/schema-name-mixed-generics.json +++ b/schemars/tests/expected/schema-name-mixed-generics.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MixedGenericStruct_for_MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String_and_42_and_z", + "title": "MixedGenericStruct_for_MyStruct_for_int32_and_null_and_boolean_and_Array_of_string_and_42_and_z", "type": "object", "required": [ "foo", @@ -12,7 +12,7 @@ "format": "int32" }, "generic": { - "$ref": "#/definitions/MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String" + "$ref": "#/definitions/MyStruct_for_int32_and_null_and_boolean_and_Array_of_string" } }, "definitions": { @@ -28,7 +28,7 @@ } } }, - "MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String": { + "MyStruct_for_int32_and_null_and_boolean_and_Array_of_string": { "type": "object", "required": [ "inner", diff --git a/schemars/tests/expected/schema_with-enum-internal.json b/schemars/tests/expected/schema_with-enum-internal.json index 7ede7e6e..75b28dcc 100644 --- a/schemars/tests/expected/schema_with-enum-internal.json +++ b/schemars/tests/expected/schema_with-enum-internal.json @@ -22,8 +22,8 @@ }, { "type": [ - "boolean", - "object" + "object", + "boolean" ], "required": [ "typeProperty" @@ -39,8 +39,8 @@ }, { "type": [ - "boolean", - "object" + "object", + "boolean" ], "required": [ "typeProperty" diff --git a/schemars/tests/expected/smallvec.json b/schemars/tests/expected/smallvec.json index eab45c0a..7ce011d4 100644 --- a/schemars/tests/expected/smallvec.json +++ b/schemars/tests/expected/smallvec.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_String", + "title": "Array_of_string", "type": "array", "items": { "type": "string" diff --git a/schemars/tests/expected/smol_str.json b/schemars/tests/expected/smol_str.json index 42f099a3..ad174d81 100644 --- a/schemars/tests/expected/smol_str.json +++ b/schemars/tests/expected/smol_str.json @@ -1,5 +1,5 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "String", + "title": "string", "type": "string" } \ No newline at end of file diff --git a/schemars/tests/indexmap.rs b/schemars/tests/indexmap.rs index 9501b67c..e243f35a 100644 --- a/schemars/tests/indexmap.rs +++ b/schemars/tests/indexmap.rs @@ -1,13 +1,15 @@ mod util; -use indexmap::{IndexMap, IndexSet}; +use std::hash::RandomState; + +use indexmap2::{IndexMap, IndexSet}; use schemars::JsonSchema; use util::*; #[allow(dead_code)] #[derive(JsonSchema)] struct IndexMapTypes { - map: IndexMap, - set: IndexSet, + map: IndexMap, + set: IndexSet, } #[test] diff --git a/schemars/tests/indexmap2.rs b/schemars/tests/indexmap2.rs deleted file mode 100644 index efc77ddf..00000000 --- a/schemars/tests/indexmap2.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod util; -use indexmap2::{IndexMap, IndexSet}; -use schemars::JsonSchema; -use util::*; - -#[allow(dead_code)] -#[derive(JsonSchema)] -struct IndexMapTypes { - map: IndexMap, - set: IndexSet, -} - -#[test] -fn indexmap_types() -> TestResult { - test_default_generated_schema::("indexmap") -} diff --git a/schemars/tests/schema_for_schema.rs b/schemars/tests/schema_for_schema.rs deleted file mode 100644 index d175505c..00000000 --- a/schemars/tests/schema_for_schema.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod util; -use schemars::gen::SchemaSettings; -use schemars::schema::RootSchema; -use util::*; - -#[test] -fn schema_matches_draft07() -> TestResult { - test_generated_schema::("schema", SchemaSettings::draft07()) -} - -#[test] -fn schema_matches_2019_09() -> TestResult { - test_generated_schema::("schema-2019_09", SchemaSettings::draft2019_09()) -} - -#[test] -fn schema_matches_openapi3() -> TestResult { - test_generated_schema::("schema-openapi3", SchemaSettings::openapi3()) -} diff --git a/schemars/tests/schema_with_enum.rs b/schemars/tests/schema_with_enum.rs index a91aa4d5..5a6c575b 100644 --- a/schemars/tests/schema_with_enum.rs +++ b/schemars/tests/schema_with_enum.rs @@ -2,7 +2,7 @@ mod util; use schemars::JsonSchema; use util::*; -fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { +fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema { ::json_schema(gen) } @@ -21,6 +21,7 @@ pub enum External { #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema, i32, ), + // FIXME this should probably only replace the "payload" of the enum #[schemars(schema_with = "schema_fn")] Unit, } @@ -38,6 +39,7 @@ pub enum Internal { foo: DoesntImplementJsonSchema, }, NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema), + // FIXME this should probably only replace the "payload" of the enum #[schemars(schema_with = "schema_fn")] Unit, } @@ -59,6 +61,7 @@ pub enum Untagged { #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema, i32, ), + // FIXME this should probably only replace the "payload" of the enum #[schemars(schema_with = "schema_fn")] Unit, } @@ -80,6 +83,7 @@ pub enum Adjacent { #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema, i32, ), + // FIXME this should probably only replace the "payload" of the enum #[schemars(schema_with = "schema_fn")] Unit, } diff --git a/schemars/tests/schema_with_struct.rs b/schemars/tests/schema_with_struct.rs index 9c87abeb..70ba24d6 100644 --- a/schemars/tests/schema_with_struct.rs +++ b/schemars/tests/schema_with_struct.rs @@ -2,7 +2,7 @@ mod util; use schemars::JsonSchema; use util::*; -fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { +fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema { ::json_schema(gen) } diff --git a/schemars/tests/semver.rs b/schemars/tests/semver.rs index 617e5086..3c351c65 100644 --- a/schemars/tests/semver.rs +++ b/schemars/tests/semver.rs @@ -1,6 +1,6 @@ mod util; use schemars::JsonSchema; -use semver::Version; +use semver1::Version; use util::*; #[allow(dead_code)] diff --git a/schemars/tests/smallvec.rs b/schemars/tests/smallvec.rs index bc9c6ef6..8412a6a2 100644 --- a/schemars/tests/smallvec.rs +++ b/schemars/tests/smallvec.rs @@ -1,5 +1,5 @@ mod util; -use smallvec::SmallVec; +use smallvec1::SmallVec; use util::*; #[test] diff --git a/schemars/tests/smol_str.rs b/schemars/tests/smol_str.rs index 1e481963..43fad304 100644 --- a/schemars/tests/smol_str.rs +++ b/schemars/tests/smol_str.rs @@ -1,5 +1,5 @@ mod util; -use smol_str::SmolStr; +use smol_str02::SmolStr; use util::*; #[test] diff --git a/schemars/tests/url.rs b/schemars/tests/url.rs index 0e9499a2..018a66b5 100644 --- a/schemars/tests/url.rs +++ b/schemars/tests/url.rs @@ -1,6 +1,6 @@ mod util; use schemars::JsonSchema; -use url::Url; +use url2::Url; use util::*; #[allow(dead_code)] diff --git a/schemars/tests/util/mod.rs b/schemars/tests/util/mod.rs index 595d68bf..99cf6775 100644 --- a/schemars/tests/util/mod.rs +++ b/schemars/tests/util/mod.rs @@ -1,5 +1,6 @@ use pretty_assertions::assert_eq; -use schemars::{gen::SchemaSettings, schema::RootSchema, schema_for, JsonSchema}; +use schemars::visit::Visitor; +use schemars::{gen::SchemaSettings, schema_for, JsonSchema, Schema}; use std::error::Error; use std::fs; @@ -17,7 +18,16 @@ pub fn test_default_generated_schema(file: &str) -> TestResult { test_schema(&actual, file) } -pub fn test_schema(actual: &RootSchema, file: &str) -> TestResult { +pub fn test_schema(actual: &Schema, file: &str) -> TestResult { + // TEMP for easier comparison of schemas handling changes that don't actually affect a schema: + // - `required` ordering has changed + // - previously `f64` properties may now be integers + let actual = &{ + let mut actual = actual.clone(); + TempFixupForTests.visit_schema(&mut actual); + actual + }; + let expected_json = match fs::read_to_string(format!("tests/expected/{}.json", file)) { Ok(j) => j, Err(e) => { @@ -35,8 +45,30 @@ pub fn test_schema(actual: &RootSchema, file: &str) -> TestResult { Ok(()) } -fn write_actual_to_file(schema: &RootSchema, file: &str) -> TestResult { +fn write_actual_to_file(schema: &Schema, file: &str) -> TestResult { let actual_json = serde_json::to_string_pretty(&schema)?; fs::write(format!("tests/actual/{}.json", file), actual_json)?; Ok(()) } + +struct TempFixupForTests; + +impl schemars::visit::Visitor for TempFixupForTests { + fn visit_schema(&mut self, schema: &mut Schema) { + schemars::visit::visit_schema(self, schema); + + if let Some(object) = schema.as_object_mut() { + if let Some(serde_json::Value::Array(required)) = object.get_mut("required") { + required.sort_unstable_by(|a, b| a.as_str().cmp(&b.as_str())); + } + + for (key, value) in object { + if key == "multipleOf" || key.ends_with("aximum") || key.ends_with("inimum") { + if let Some(f) = value.as_f64() { + *value = f.into(); + } + } + } + } + } +} diff --git a/schemars/tests/uuid.rs b/schemars/tests/uuid.rs index bd673b51..77e92c2c 100644 --- a/schemars/tests/uuid.rs +++ b/schemars/tests/uuid.rs @@ -1,11 +1,6 @@ mod util; use util::*; -#[test] -fn uuid08() -> TestResult { - test_default_generated_schema::("uuid") -} - #[test] fn uuid1() -> TestResult { test_default_generated_schema::("uuid") diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs index a511ab41..8d90302c 100644 --- a/schemars_derive/src/attr/validation.rs +++ b/schemars_derive/src/attr/validation.rs @@ -325,119 +325,85 @@ impl ValidationAttrs { } pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) { - if let Some(apply_expr) = self.apply_to_schema_expr() { - *schema_expr = quote! { - { - let mut schema = #schema_expr; - #apply_expr - schema - } - } + let setters = self.make_setters(quote!(&mut schema)); + if !setters.is_empty() { + *schema_expr = quote!({ + let mut schema = #schema_expr; + #(#setters)* + schema + }); } } - fn apply_to_schema_expr(&self) -> Option { - let mut array_validation = Vec::new(); - let mut number_validation = Vec::new(); - let mut object_validation = Vec::new(); - let mut string_validation = Vec::new(); + fn make_setters(&self, mut_schema: impl ToTokens) -> Vec { + let mut result = Vec::new(); if let Some(length_min) = self.length_min.as_ref().or(self.length_equal.as_ref()) { - string_validation.push(quote! { - validation.min_length = Some(#length_min as u32); + result.push(quote! { + schemars::_private::insert_validation_property(#mut_schema, "string", "minLength", #length_min); }); - array_validation.push(quote! { - validation.min_items = Some(#length_min as u32); + result.push(quote! { + schemars::_private::insert_validation_property(#mut_schema, "array", "minItems", #length_min); }); } if let Some(length_max) = self.length_max.as_ref().or(self.length_equal.as_ref()) { - string_validation.push(quote! { - validation.max_length = Some(#length_max as u32); + result.push(quote! { + schemars::_private::insert_validation_property(#mut_schema, "string", "maxLength", #length_max); }); - array_validation.push(quote! { - validation.max_items = Some(#length_max as u32); + result.push(quote! { + schemars::_private::insert_validation_property(#mut_schema, "array", "maxItems", #length_max); }); } if let Some(range_min) = &self.range_min { - number_validation.push(quote! { - validation.minimum = Some(#range_min as f64); + result.push(quote! { + schemars::_private::insert_validation_property(#mut_schema, "number", "minimum", #range_min); }); } if let Some(range_max) = &self.range_max { - number_validation.push(quote! { - validation.maximum = Some(#range_max as f64); + result.push(quote! { + schemars::_private::insert_validation_property(#mut_schema, "number", "maximum", #range_max); }); } if let Some(regex) = &self.regex { - string_validation.push(quote! { - validation.pattern = Some(#regex.to_string()); + result.push(quote! { + schemars::_private::insert_validation_property(#mut_schema, "string", "pattern", #regex); }); } if let Some(contains) = &self.contains { - object_validation.push(quote! { - validation.required.insert(#contains.to_string()); + result.push(quote! { + schemars::_private::append_required(#mut_schema, #contains); }); if self.regex.is_none() { let pattern = crate::regex_syntax::escape(contains); - string_validation.push(quote! { - validation.pattern = Some(#pattern.to_string()); + result.push(quote! { + schemars::_private::insert_validation_property(#mut_schema, "string", "pattern", #pattern); }); } } - let format = self.format.as_ref().map(|f| { - let f = f.schema_str(); - quote! { - schema_object.format = Some(#f.to_string()); - } - }); - - let inner_validation = self - .inner - .as_deref() - .and_then(|inner| inner.apply_to_schema_expr()) - .map(|apply_expr| { - quote! { - if schema_object.has_type(schemars::schema::InstanceType::Array) { - if let Some(schemars::schema::SingleOrVec::Single(inner_schema)) = &mut schema_object.array().items { - let mut schema = &mut **inner_schema; - #apply_expr - } - } - } - }); - - let array_validation = wrap_array_validation(array_validation); - let number_validation = wrap_number_validation(number_validation); - let object_validation = wrap_object_validation(object_validation); - let string_validation = wrap_string_validation(string_validation); - - if array_validation.is_some() - || number_validation.is_some() - || object_validation.is_some() - || string_validation.is_some() - || format.is_some() - || inner_validation.is_some() - { - Some(quote! { - if let schemars::schema::Schema::Object(schema_object) = &mut schema { - #array_validation - #number_validation - #object_validation - #string_validation - #format - #inner_validation - } + if let Some(format) = &self.format { + let f = format.schema_str(); + result.push(quote! { + schema.ensure_object().insert("format".to_owned(), #f.into()); }) - } else { - None + }; + + if let Some(inner) = &self.inner { + let inner_setters = inner.make_setters(quote!(schema)); + if !inner_setters.is_empty() { + result.push(quote! { + schemars::_private::apply_inner_validation(#mut_schema, |schema| { #(#inner_setters)* }); + }) + } } + + result } } @@ -456,59 +422,6 @@ fn parse_lit_into_expr_path( }) } -fn wrap_array_validation(v: Vec) -> Option { - if v.is_empty() { - None - } else { - Some(quote! { - if schema_object.has_type(schemars::schema::InstanceType::Array) { - let validation = schema_object.array(); - #(#v)* - } - }) - } -} - -fn wrap_number_validation(v: Vec) -> Option { - if v.is_empty() { - None - } else { - Some(quote! { - if schema_object.has_type(schemars::schema::InstanceType::Integer) - || schema_object.has_type(schemars::schema::InstanceType::Number) { - let validation = schema_object.number(); - #(#v)* - } - }) - } -} - -fn wrap_object_validation(v: Vec) -> Option { - if v.is_empty() { - None - } else { - Some(quote! { - if schema_object.has_type(schemars::schema::InstanceType::Object) { - let validation = schema_object.object(); - #(#v)* - } - }) - } -} - -fn wrap_string_validation(v: Vec) -> Option { - if v.is_empty() { - None - } else { - Some(quote! { - if schema_object.has_type(schemars::schema::InstanceType::String) { - let validation = schema_object.string(); - #(#v)* - } - }) - } -} - fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, expr: Expr) -> Option { // this odd double-parsing is to make `-10` parsed as an Lit instead of an Expr::Unary let lit: Lit = match syn::parse2(expr.to_token_stream()) { diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index ea6c0a65..c890f75c 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -68,11 +68,11 @@ fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> syn::Result::schema_id() } - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema { <#ty as schemars::JsonSchema>::json_schema(gen) } - fn _schemars_private_non_optional_json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + fn _schemars_private_non_optional_json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema { <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(gen) } @@ -182,7 +182,7 @@ fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> syn::Result schemars::schema::Schema { + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema { #schema_expr } }; diff --git a/schemars_derive/src/metadata.rs b/schemars_derive/src/metadata.rs index 3dd77a05..6de68343 100644 --- a/schemars_derive/src/metadata.rs +++ b/schemars_derive/src/metadata.rs @@ -13,32 +13,46 @@ pub struct SchemaMetadata<'a> { impl<'a> SchemaMetadata<'a> { pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) { + let setters = self.make_setters(); + if !setters.is_empty() { + *schema_expr = quote! {{ + let mut schema = #schema_expr; + let obj = schema.ensure_object(); + #(#setters)* + schema + }} + } + } + + fn make_setters(&self) -> Vec { + let mut setters = Vec::::new(); + if let Some(title) = &self.title { - *schema_expr = quote! { - schemars::_private::metadata::add_title(#schema_expr, #title) - }; + setters.push(quote! { + obj.insert("title".to_owned(), #title.into()); + }); } if let Some(description) = &self.description { - *schema_expr = quote! { - schemars::_private::metadata::add_description(#schema_expr, #description) - }; + setters.push(quote! { + obj.insert("description".to_owned(), #description.into()); + }); } if self.deprecated { - *schema_expr = quote! { - schemars::_private::metadata::add_deprecated(#schema_expr, true) - }; + setters.push(quote! { + obj.insert("deprecated".to_owned(), true.into()); + }); } if self.read_only { - *schema_expr = quote! { - schemars::_private::metadata::add_read_only(#schema_expr, true) - }; + setters.push(quote! { + obj.insert("readOnly".to_owned(), true.into()); + }); } if self.write_only { - *schema_expr = quote! { - schemars::_private::metadata::add_write_only(#schema_expr, true) - }; + setters.push(quote! { + obj.insert("writeOnly".to_owned(), true.into()); + }); } if !self.examples.is_empty() { @@ -47,16 +61,19 @@ impl<'a> SchemaMetadata<'a> { schemars::_serde_json::value::to_value(#eg()) } }); - - *schema_expr = quote! { - schemars::_private::metadata::add_examples(#schema_expr, [#(#examples),*].into_iter().flatten()) - }; + setters.push(quote! { + obj.insert("examples".to_owned(), schemars::_serde_json::Value::Array([#(#examples),*].into_iter().flatten().collect())); + }); } if let Some(default) = &self.default { - *schema_expr = quote! { - schemars::_private::metadata::add_default(#schema_expr, #default.and_then(|d| schemars::_schemars_maybe_to_value!(d))) - }; + setters.push(quote! { + if let Some(default) = #default.and_then(|d| schemars::_schemars_maybe_to_value!(d)) { + obj.insert("default".to_owned(), default); + } + }); } + + setters } } diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index b625114b..585184c5 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -49,9 +49,18 @@ pub fn expr_for_repr(cont: &Container) -> Result { let enum_ident = &cont.ident; let variant_idents = variants.iter().map(|v| &v.ident); - let mut schema_expr = schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::Integer.into()), - enum_values: Some(vec![#((#enum_ident::#variant_idents as #repr_type).into()),*]), + let mut schema_expr = quote!({ + let mut map = schemars::_serde_json::Map::new(); + map.insert("type".to_owned(), "integer".into()); + map.insert( + "enum".to_owned(), + schemars::_serde_json::Value::Array({ + let mut enum_values = Vec::new(); + #(enum_values.push((#enum_ident::#variant_idents as #repr_type).into());)* + enum_values + }), + ); + schemars::Schema::from(map) }); cont.attrs.as_metadata().apply_to_schema(&mut schema_expr); @@ -118,7 +127,7 @@ fn type_for_schema(with_attr: &WithAttr) -> (syn::Type, Option) { )) } - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema { #fun(gen) } } @@ -160,9 +169,18 @@ fn expr_for_external_tagged_enum<'a>( }) .partition(|v| v.is_unit() && v.attrs.is_default()); 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()),*]), + let unit_schema = quote!({ + let mut map = schemars::_serde_json::Map::new(); + map.insert("type".to_owned(), "string".into()); + map.insert( + "enum".to_owned(), + schemars::_serde_json::Value::Array({ + let mut enum_values = Vec::new(); + #(enum_values.push((#unit_names).into());)* + enum_values + }), + ); + schemars::Schema::from(map) }); if complex_variants.is_empty() { @@ -276,47 +294,44 @@ fn expr_for_adjacent_tagged_enum<'a>( let (add_content_to_props, add_content_to_required) = content_schema .map(|content_schema| { ( - quote!(props.insert(#content_name.to_owned(), #content_schema);), - quote!(required.insert(#content_name.to_owned());), + quote!(#content_name: (#content_schema),), + quote!(#content_name,), ) }) .unwrap_or_default(); let name = variant.name(); - let tag_schema = schema_object(quote! { - instance_type: Some(schemars::schema::InstanceType::String.into()), - enum_values: Some(vec![#name.into()]), - }); + let tag_schema = quote! { + schemars::json_schema!({ + "type": "string", + "enum": [#name], + }) + }; let set_additional_properties = if deny_unknown_fields { quote! { - additional_properties: Some(Box::new(false.into())), + "additionalProperties": false, } } else { TokenStream::new() }; - let mut outer_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(), #tag_schema); + let mut outer_schema = quote! { + schemars::json_schema!({ + "type": "object", + "properties": { + #tag_name: (#tag_schema), #add_content_to_props - props }, - required: { - let mut required = schemars::Set::new(); - required.insert(#tag_name.to_owned()); + "required": [ + #tag_name, #add_content_to_required - required - }, + ], // As we're creating a "wrapper" object, we can honor the // disposition of deny_unknown_fields. #set_additional_properties - ..Default::default() - })), - }); + }) + }; variant .attrs @@ -333,21 +348,19 @@ fn expr_for_adjacent_tagged_enum<'a>( /// Callers must determine if all subschemas are mutually exclusive. This can /// be done for most tagging regimes by checking that all tag names are unique. fn variant_subschemas(unique: bool, schemas: Vec) -> TokenStream { - if unique { - schema_object(quote! { - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - one_of: Some(vec![#(#schemas),*]), - ..Default::default() - })), - }) - } else { - schema_object(quote! { - subschemas: Some(Box::new(schemars::schema::SubschemaValidation { - any_of: Some(vec![#(#schemas),*]), - ..Default::default() - })), - }) - } + let keyword = if unique { "oneOf" } else { "anyOf" }; + quote!({ + let mut map = schemars::_serde_json::Map::new(); + map.insert( + #keyword.to_owned(), + schemars::_serde_json::Value::Array({ + let mut enum_values = Vec::new(); + #(enum_values.push(#schemas.to_value());)* + enum_values + }), + ); + schemars::Schema::from(map) + }) } fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) -> TokenStream { @@ -412,16 +425,11 @@ fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream { let len = fields.len() as u32; quote! { - schemars::schema::Schema::Object( - schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::Array.into()), - array: Some(Box::new(schemars::schema::ArrayValidation { - items: Some(vec![#(#fields),*].into()), - max_items: Some(#len), - min_items: Some(#len), - ..Default::default() - })), - ..Default::default() + schemars::json_schema!({ + "type": "array", + "items": [#((#fields)),*], + "minItems": #len, + "maxItems": #len, }) } } @@ -477,7 +485,7 @@ fn expr_for_struct( quote! { { #type_def - schemars::_private::insert_object_property::<#ty>(object_validation, #name, #has_default, #required, #schema_expr); + schemars::_private::insert_object_property::<#ty>(&mut schema, #name, #has_default, #required, #schema_expr); } } }) @@ -502,7 +510,7 @@ fn expr_for_struct( let set_additional_properties = if deny_unknown_fields { quote! { - object_validation.additional_properties = Some(Box::new(false.into())); + "additionalProperties": false, } } else { TokenStream::new() @@ -510,15 +518,12 @@ fn expr_for_struct( quote! { { #set_container_default - let mut schema_object = schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::InstanceType::Object.into()), - ..Default::default() - }; - let object_validation = schema_object.object(); - #set_additional_properties + let mut schema = schemars::json_schema!({ + "type": "object", + #set_additional_properties + }); #(#properties)* - schemars::schema::Schema::Object(schema_object) - #(.flatten(#flattens))* + schema #(.flatten(#flattens))* } } } @@ -578,16 +583,6 @@ fn field_default_expr(field: &Field, container_has_default: bool) -> Option TokenStream { - quote! { - schemars::schema::Schema::Object( - schemars::schema::SchemaObject { - #properties - ..Default::default() - }) - } -} - fn prepend_type_def(type_def: Option, schema_expr: &mut TokenStream) { if let Some(type_def) = type_def { *schema_expr = quote! {