diff --git a/CHANGELOG.md b/CHANGELOG.md index e359a4e..cd9c0b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ - Allow `regex(path = ...)` value to be a non-string expression (https://github.com/GREsau/schemars/issues/302 / https://github.com/GREsau/schemars/pull/328) +### Changed (_⚠️ possibly-breaking changes ⚠️_) + +- Invalid attributes that were previously silently ignored (e.g. setting `schema_with` on structs) will now cause compile errors +- Validation attribute parsing has been altered to match the latest version of the validator crate: + - Remove the `phone` attribute + - Remove the `required_nested` attribute + - `regex` and `contains` attributes must now be specified in list form `#[validate(regex(path = ...))]` rather than name/value form `#[validate(regex = ...)]` + ## [1.0.0-alpha.11] - 2024-08-24 ### Changed diff --git a/docs/_includes/attributes.md b/docs/_includes/attributes.md index 7449d28..bd82a9a 100644 --- a/docs/_includes/attributes.md +++ b/docs/_includes/attributes.md @@ -23,12 +23,12 @@ TABLE OF CONTENTS - [`with`](#with) - [`bound`](#bound) 1. [Supported Validator Attributes](#supported-validator-attributes) - - [`email` / `phone` / `url`](#email-phone-url) + - [`email` / `url`](#email-url) - [`length`](#length) - [`range`](#range) - [`regex`](#regex) - [`contains`](#contains) - - [`required` / `required_nested`](#required) + - [`required`](#required) 1. [Other Attributes](#other-attributes) - [`schema_with`](#schema_with) - [`title` / `description`](#title-description) @@ -177,17 +177,16 @@ Serde docs: [container](https://serde.rs/container-attrs.html#bound)
-

+

`#[validate(email)]` / `#[schemars(email)]`
-`#[validate(phone)]` / `#[schemars(phone)]`
`#[validate(url)]` / `#[schemars(url)]`

-Sets the schema's `format` to `email`/`phone`/`uri`, as appropriate. Only one of these attributes may be present on a single field. +Sets the schema's `format` to `email`/`uri`, as appropriate. Only one of these attributes may be present on a single field. -Validator docs: [email](https://github.com/Keats/validator#email) / [phone](https://github.com/Keats/validator#phone) / [url](https://github.com/Keats/validator#url) +Validator docs: [email](https://github.com/Keats/validator#email) / [url](https://github.com/Keats/validator#url)

@@ -212,20 +211,20 @@ Validator docs: [range](https://github.com/Keats/validator#range)

-`#[validate(regex = "path::to::regex")]` / `#[schemars(regex = "path::to::regex")]`
-`#[schemars(regex(pattern = r"^\d+$"))]` +`#[validate(regex(path = *static_regex)]`
+`#[schemars(regex(pattern = r"^\d+$"))]` / `#[schemars(regex(pattern = *static_regex))]`

-Sets the `pattern` property for string schemas. The `path::to::regex` will typically refer to a [`Regex`](https://docs.rs/regex/*/regex/struct.Regex.html) instance, but Schemars allows it to be any value with a `to_string()` method. +Sets the `pattern` property for string schemas. The `static_regex` will typically refer to a [`Regex`](https://docs.rs/regex/*/regex/struct.Regex.html) instance, but Schemars allows it to be any value with a `to_string()` method. -Providing an inline regex pattern using `regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form, you may want to use a `r"raw string literal"` so that `\\` characters in the regex pattern are not interpreted as escape sequences in the string. +`regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form, you may want to use a `r"raw string literal"` so that `\\` characters in the regex pattern are not interpreted as escape sequences in the string. Using the `path` form is not allowed in a `#[schemars(...)]` attribute. Validator docs: [regex](https://github.com/Keats/validator#regex)

-`#[validate(contains = "string")]` / `#[schemars(contains = "string")]` +`#[validate(contains(pattern = "string"))]` / `#[schemars(contains(pattern = "string"))]`

@@ -236,13 +235,12 @@ Validator docs: [contains](https://github.com/Keats/validator#contains)

`#[validate(required)]` / `#[schemars(required)]`
-`#[validate(required_nested)]`

When set on an `Option` field, this will create a schemas as though the field were a `T`. -Validator docs: [required](https://github.com/Keats/validator#required) / [required_nested](https://github.com/Keats/validator#required_nested) +Validator docs: [required](https://github.com/Keats/validator#required)
diff --git a/docs/_includes/examples/schemars_attrs.rs b/docs/_includes/examples/schemars_attrs.rs index dc9e495..30418b4 100644 --- a/docs/_includes/examples/schemars_attrs.rs +++ b/docs/_includes/examples/schemars_attrs.rs @@ -17,7 +17,7 @@ pub struct MyStruct { #[derive(Deserialize, Serialize, JsonSchema)] #[schemars(untagged)] pub enum MyEnum { - StringNewType(#[schemars(phone)] String), + StringNewType(#[schemars(email)] String), StructVariant { #[schemars(length(min = 1, max = 100))] floats: Vec, diff --git a/docs/_includes/examples/schemars_attrs.schema.json b/docs/_includes/examples/schemars_attrs.schema.json index 85efffd..19a613c 100644 --- a/docs/_includes/examples/schemars_attrs.schema.json +++ b/docs/_includes/examples/schemars_attrs.schema.json @@ -42,7 +42,7 @@ "anyOf": [ { "type": "string", - "format": "phone" + "format": "email" }, { "type": "object", diff --git a/docs/_includes/examples/validate.rs b/docs/_includes/examples/validate.rs index 4116976..70fa4a2 100644 --- a/docs/_includes/examples/validate.rs +++ b/docs/_includes/examples/validate.rs @@ -11,7 +11,7 @@ pub struct MyStruct { #[derive(JsonSchema)] pub enum MyEnum { - StringNewType(#[validate(phone)] String), + StringNewType(#[validate(email)] String), StructVariant { #[validate(length(min = 1, max = 100))] floats: Vec, diff --git a/docs/_includes/examples/validate.schema.json b/docs/_includes/examples/validate.schema.json index e9f8a1d..c7728b8 100644 --- a/docs/_includes/examples/validate.schema.json +++ b/docs/_includes/examples/validate.schema.json @@ -19,7 +19,7 @@ "properties": { "StringNewType": { "type": "string", - "format": "phone" + "format": "email" } }, "additionalProperties": false, diff --git a/docs/_includes/examples_v0/schemars_attrs.rs b/docs/_includes/examples_v0/schemars_attrs.rs index 4ad2503..c8c1412 100644 --- a/docs/_includes/examples_v0/schemars_attrs.rs +++ b/docs/_includes/examples_v0/schemars_attrs.rs @@ -17,7 +17,7 @@ pub struct MyStruct { #[derive(Deserialize, Serialize, JsonSchema)] #[schemars(untagged)] pub enum MyEnum { - StringNewType(#[schemars(phone)] String), + StringNewType(#[schemars(email)] String), StructVariant { #[schemars(length(min = 1, max = 100))] floats: Vec, diff --git a/docs/_includes/examples_v0/validate.rs b/docs/_includes/examples_v0/validate.rs index 4116976..70fa4a2 100644 --- a/docs/_includes/examples_v0/validate.rs +++ b/docs/_includes/examples_v0/validate.rs @@ -11,7 +11,7 @@ pub struct MyStruct { #[derive(JsonSchema)] pub enum MyEnum { - StringNewType(#[validate(phone)] String), + StringNewType(#[validate(email)] String), StructVariant { #[validate(length(min = 1, max = 100))] floats: Vec, diff --git a/docs/_v0/1.1-attributes.md b/docs/_v0/1.1-attributes.md index b668d7f..ea877bb 100644 --- a/docs/_v0/1.1-attributes.md +++ b/docs/_v0/1.1-attributes.md @@ -191,7 +191,7 @@ Serde docs: [container](https://serde.rs/container-attrs.html#bound)

`#[validate(email)]` / `#[schemars(email)]`
-`#[validate(phone)]` / `#[schemars(phone)]`
+`#[validate(email)]` / `#[schemars(email)]`
`#[validate(url)]` / `#[schemars(url)]`

diff --git a/schemars/examples/schemars_attrs.rs b/schemars/examples/schemars_attrs.rs index dc9e495..30418b4 100644 --- a/schemars/examples/schemars_attrs.rs +++ b/schemars/examples/schemars_attrs.rs @@ -17,7 +17,7 @@ pub struct MyStruct { #[derive(Deserialize, Serialize, JsonSchema)] #[schemars(untagged)] pub enum MyEnum { - StringNewType(#[schemars(phone)] String), + StringNewType(#[schemars(email)] String), StructVariant { #[schemars(length(min = 1, max = 100))] floats: Vec, diff --git a/schemars/examples/schemars_attrs.schema.json b/schemars/examples/schemars_attrs.schema.json index 85efffd..19a613c 100644 --- a/schemars/examples/schemars_attrs.schema.json +++ b/schemars/examples/schemars_attrs.schema.json @@ -42,7 +42,7 @@ "anyOf": [ { "type": "string", - "format": "phone" + "format": "email" }, { "type": "object", diff --git a/schemars/examples/validate.rs b/schemars/examples/validate.rs index 4116976..70fa4a2 100644 --- a/schemars/examples/validate.rs +++ b/schemars/examples/validate.rs @@ -11,7 +11,7 @@ pub struct MyStruct { #[derive(JsonSchema)] pub enum MyEnum { - StringNewType(#[validate(phone)] String), + StringNewType(#[validate(email)] String), StructVariant { #[validate(length(min = 1, max = 100))] floats: Vec, diff --git a/schemars/examples/validate.schema.json b/schemars/examples/validate.schema.json index e9f8a1d..c7728b8 100644 --- a/schemars/examples/validate.schema.json +++ b/schemars/examples/validate.schema.json @@ -19,7 +19,7 @@ "properties": { "StringNewType": { "type": "string", - "format": "phone" + "format": "email" } }, "additionalProperties": false, diff --git a/schemars/src/_private/mod.rs b/schemars/src/_private/mod.rs index ccae4e3..75966b8 100644 --- a/schemars/src/_private/mod.rs +++ b/schemars/src/_private/mod.rs @@ -4,6 +4,7 @@ use crate::{JsonSchema, Schema, SchemaGenerator}; use serde::Serialize; use serde_json::{json, map::Entry, Map, Value}; +mod regex_syntax; mod rustdoc; pub use rustdoc::get_title_and_description; @@ -141,24 +142,35 @@ pub fn insert_object_property( required: bool, sub_schema: 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 !has_default && (required || !T::_schemars_private_is_option()) { - if let Some(req) = obj - .entry("required") - .or_insert(Value::Array(Vec::new())) - .as_array_mut() + fn insert_object_property_impl( + schema: &mut Schema, + key: &str, + has_default: bool, + required: bool, + sub_schema: Schema, + ) { + let obj = schema.ensure_object(); + if let Some(properties) = obj + .entry("properties") + .or_insert(Value::Object(Map::new())) + .as_object_mut() { - req.push(key.into()); + properties.insert(key.to_owned(), sub_schema.into()); + } + + if !has_default && (required) { + if let Some(req) = obj + .entry("required") + .or_insert(Value::Array(Vec::new())) + .as_array_mut() + { + req.push(key.into()); + } } } + + let required = required || !T::_schemars_private_is_option(); + insert_object_property_impl(schema, key, has_default, required, sub_schema); } pub fn insert_metadata_property(schema: &mut Schema, key: &str, value: impl Into) { @@ -187,14 +199,21 @@ pub fn insert_validation_property( } } -pub fn append_required(schema: &mut Schema, key: &str) { +pub fn must_contain(schema: &mut Schema, contain: String) { + if schema.has_type("string") { + let pattern = regex_syntax::escape(&contain); + schema + .ensure_object() + .insert("pattern".to_owned(), pattern.into()); + } + 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); + let value = Value::from(contain); if !array.contains(&value) { array.push(value); } diff --git a/schemars_derive/src/regex_syntax.rs b/schemars/src/_private/regex_syntax.rs similarity index 98% rename from schemars_derive/src/regex_syntax.rs rename to schemars/src/_private/regex_syntax.rs index 11cd638..23b67ca 100644 --- a/schemars_derive/src/regex_syntax.rs +++ b/schemars/src/_private/regex_syntax.rs @@ -1,4 +1,5 @@ #![allow(clippy::all)] +use crate::_alloc_prelude::*; // Copied from regex_syntax crate to avoid pulling in the whole crate just for a utility function // https://github.com/rust-lang/regex/blob/431c4e4867e1eb33eb39b23ed47c9934b2672f8f/regex-syntax/src/lib.rs // diff --git a/schemars/tests/expected/skip_tuple_fields.json b/schemars/tests/expected/skip_tuple_fields.json index c3a4255..e3038bc 100644 --- a/schemars/tests/expected/skip_tuple_fields.json +++ b/schemars/tests/expected/skip_tuple_fields.json @@ -5,7 +5,8 @@ "prefixItems": [ { "type": "number", - "format": "float" + "format": "float", + "writeOnly": true }, { "type": "null" diff --git a/schemars/tests/expected/validate.json b/schemars/tests/expected/validate.json index 57c7f52..b2c893a 100644 --- a/schemars/tests/expected/validate.json +++ b/schemars/tests/expected/validate.json @@ -23,10 +23,6 @@ "type": "string", "pattern": "^[Hh]ello\\b" }, - "regex_str3": { - "type": "string", - "pattern": "^\\d+$" - }, "contains_str1": { "type": "string", "pattern": "substring\\.\\.\\." @@ -39,10 +35,6 @@ "type": "string", "format": "email" }, - "tel": { - "type": "string", - "format": "phone" - }, "homepage": { "type": "string", "format": "uri" @@ -88,11 +80,9 @@ "min_max2", "regex_str1", "regex_str2", - "regex_str3", "contains_str1", "contains_str2", "email_address", - "tel", "homepage", "non_empty_str", "non_empty_str2", diff --git a/schemars/tests/expected/validate_schemars_attrs.json b/schemars/tests/expected/validate_schemars_attrs.json index 5e7d31d..e258f18 100644 --- a/schemars/tests/expected/validate_schemars_attrs.json +++ b/schemars/tests/expected/validate_schemars_attrs.json @@ -20,10 +20,6 @@ "pattern": "^[Hh]ello\\b" }, "regex_str2": { - "type": "string", - "pattern": "^[Hh]ello\\b" - }, - "regex_str3": { "type": "string", "pattern": "^\\d+$" }, @@ -39,10 +35,6 @@ "type": "string", "format": "email" }, - "tel": { - "type": "string", - "format": "phone" - }, "homepage": { "type": "string", "format": "uri" @@ -88,11 +80,9 @@ "min_max2", "regex_str1", "regex_str2", - "regex_str3", "contains_str1", "contains_str2", "email_address", - "tel", "homepage", "non_empty_str", "non_empty_str2", diff --git a/schemars/tests/ui/invalid_validation_attrs.rs b/schemars/tests/ui/invalid_validation_attrs.rs index 79a4735..4914b70 100644 --- a/schemars/tests/ui/invalid_validation_attrs.rs +++ b/schemars/tests/ui/invalid_validation_attrs.rs @@ -1,7 +1,5 @@ use schemars::JsonSchema; -// FIXME validation attrs like `email` should be disallowed non structs/enums/variants - #[derive(JsonSchema)] #[validate(email)] pub struct Struct1(#[validate(regex, foo, length(min = 1, equal = 2, bar))] String); @@ -15,6 +13,7 @@ pub struct Struct3( #[validate( regex = "foo", contains = "bar", + regex(pattern = "baz"), regex(path = "baz"), phone, email, @@ -29,6 +28,7 @@ pub struct Struct4( regex = "foo", contains = "bar", regex(path = "baz"), + regex(pattern = "baz"), phone, email, url diff --git a/schemars/tests/ui/invalid_validation_attrs.stderr b/schemars/tests/ui/invalid_validation_attrs.stderr index 56d4404..684343c 100644 --- a/schemars/tests/ui/invalid_validation_attrs.stderr +++ b/schemars/tests/ui/invalid_validation_attrs.stderr @@ -1,53 +1,95 @@ -error: unknown schemars attribute `foo` - --> tests/ui/invalid_validation_attrs.rs:11:38 - | -11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); - | ^^^ +error: expected validate regex attribute item to be of the form `regex(...)` + --> tests/ui/invalid_validation_attrs.rs:5:31 + | +5 | pub struct Struct1(#[validate(regex, foo, length(min = 1, equal = 2, bar))] String); + | ^^^^^ -error: could not parse `regex` item in schemars attribute - --> tests/ui/invalid_validation_attrs.rs:11:31 - | -11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); - | ^^^^^ +error: expected schemars regex attribute item to be of the form `regex(...)` + --> tests/ui/invalid_validation_attrs.rs:9:31 + | +9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); + | ^^^^^ error: schemars attribute cannot contain both `equal` and `min` - --> tests/ui/invalid_validation_attrs.rs:11:59 - | -11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); - | ^^^^^ + --> tests/ui/invalid_validation_attrs.rs:9:59 + | +9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); + | ^^^^^^^^^ -error: unknown item in schemars length attribute - --> tests/ui/invalid_validation_attrs.rs:11:70 - | -11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); - | ^^^ +error: unknown item in schemars length attribute: `bar` + --> tests/ui/invalid_validation_attrs.rs:9:70 + | +9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); + | ^^^ -error: schemars attribute cannot contain both `contains` and `regex` +error: unknown schemars attribute `foo` + --> tests/ui/invalid_validation_attrs.rs:9:38 + | +9 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String); + | ^^^ + +error: unknown schemars attribute `email` + --> tests/ui/invalid_validation_attrs.rs:8:12 + | +8 | #[schemars(email)] + | ^^^^^ + +error: expected validate regex attribute item to be of the form `regex(...)` + --> tests/ui/invalid_validation_attrs.rs:14:9 + | +14 | regex = "foo", + | ^^^^^^^^^^^^^ + +error: expected validate contains attribute item to be of the form `contains(...)` + --> tests/ui/invalid_validation_attrs.rs:15:9 + | +15 | contains = "bar", + | ^^^^^^^^^^^^^^^^ + +error: `pattern` is not supported in `validate(regex(...))` attribute - use either `validate(regex(path = ...))` or `schemars(regex(pattern = ...))` instead + --> tests/ui/invalid_validation_attrs.rs:16:15 + | +16 | regex(pattern = "baz"), + | ^^^^^^^^^^^^^^^ + +error: `validate(regex(...))` attribute requires `path = ...` + --> tests/ui/invalid_validation_attrs.rs:16:9 + | +16 | regex(pattern = "baz"), + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: expected schemars regex attribute item to be of the form `regex(...)` + --> tests/ui/invalid_validation_attrs.rs:28:9 + | +28 | regex = "foo", + | ^^^^^^^^^^^^^ + +error: expected schemars contains attribute item to be of the form `contains(...)` + --> tests/ui/invalid_validation_attrs.rs:29:9 + | +29 | contains = "bar", + | ^^^^^^^^^^^^^^^^ + +error: `path` is not supported in `schemars(regex(...))` attribute - use `schemars(regex(pattern = ...))` instead + --> tests/ui/invalid_validation_attrs.rs:30:15 + | +30 | regex(path = "baz"), + | ^^^^^^^^^^^^ + +error: `schemars(regex(...))` attribute requires `pattern = ...` --> tests/ui/invalid_validation_attrs.rs:30:9 | -30 | contains = "bar", - | ^^^^^^^^ +30 | regex(path = "baz"), + | ^^^^^^^^^^^^^^^^^^^ -error: duplicate schemars attribute `regex` - --> tests/ui/invalid_validation_attrs.rs:31:9 - | -31 | regex(path = "baz"), - | ^^^^^ - -error: schemars attribute cannot contain both `phone` and `email` - --> tests/ui/invalid_validation_attrs.rs:33:9 - | -33 | email, - | ^^^^^ - -error: schemars attribute cannot contain both `phone` and `url` +error: schemars attribute cannot contain both `url` and `email` --> tests/ui/invalid_validation_attrs.rs:34:9 | 34 | url | ^^^ -error[E0425]: cannot find value `foo` in this scope - --> tests/ui/invalid_validation_attrs.rs:16:17 +error: unknown schemars attribute `phone` + --> tests/ui/invalid_validation_attrs.rs:32:9 | -16 | regex = "foo", - | ^^^^^ not found in this scope +32 | phone, + | ^^^^^ diff --git a/schemars/tests/validate.rs b/schemars/tests/validate.rs index 0d0eefb..9e07fd5 100644 --- a/schemars/tests/validate.rs +++ b/schemars/tests/validate.rs @@ -16,20 +16,16 @@ pub struct Struct { min_max: f32, #[validate(range(min = "MIN", max = "MAX"))] min_max2: f32, - #[validate(regex = &*STARTS_WITH_HELLO)] + #[validate(regex(path = &*STARTS_WITH_HELLO))] regex_str1: String, #[validate(regex(path = "STARTS_WITH_HELLO", code = "foo"))] regex_str2: String, - #[validate(regex(pattern = r"^\d+$"))] - regex_str3: String, - #[validate(contains = "substring...")] + #[validate(contains(pattern = "substring..."))] contains_str1: String, #[validate(contains(pattern = "substring...", message = "bar"))] contains_str2: String, #[validate(email)] email_address: String, - #[validate(phone)] - tel: String, #[validate(url)] homepage: String, #[validate(length(min = 1, max = 100))] @@ -38,7 +34,7 @@ pub struct Struct { non_empty_str2: String, #[validate(length(equal = 2))] pair: Vec, - #[validate(contains = "map_key")] + #[validate(contains(pattern = "map_key"))] map_contains: BTreeMap, #[validate(required)] required_option: Option, @@ -66,22 +62,18 @@ pub struct Struct2 { min_max: f32, #[schemars(range(min = "MIN", max = "MAX"))] min_max2: f32, - #[validate(regex = "overridden")] - #[schemars(regex = "STARTS_WITH_HELLO")] + #[validate(regex(path = overridden))] + #[schemars(regex(pattern = &*STARTS_WITH_HELLO))] regex_str1: String, - #[schemars(regex(path = "STARTS_WITH_HELLO"))] - regex_str2: String, #[schemars(regex(pattern = r"^\d+$"))] - regex_str3: String, - #[validate(regex = "overridden")] - #[schemars(contains = "substring...")] + regex_str2: String, + #[validate(contains(pattern = "overridden"))] + #[schemars(contains(pattern = "substring..."))] contains_str1: String, #[schemars(contains(pattern = "substring..."))] contains_str2: String, #[schemars(email)] email_address: String, - #[schemars(phone)] - tel: String, #[schemars(url)] homepage: String, #[schemars(length(min = 1, max = 100))] @@ -90,7 +82,7 @@ pub struct Struct2 { non_empty_str2: String, #[schemars(length(equal = 2))] pair: Vec, - #[schemars(contains = "map_key")] + #[schemars(contains(pattern = "map_key"))] map_contains: BTreeMap, #[schemars(required)] required_option: Option, diff --git a/schemars/tests/validate_inner.rs b/schemars/tests/validate_inner.rs index 6416671..d3d8d65 100644 --- a/schemars/tests/validate_inner.rs +++ b/schemars/tests/validate_inner.rs @@ -13,7 +13,7 @@ pub struct Struct<'a> { array_str_length: [&'a str; 2], #[schemars(inner(contains(pattern = "substring...")))] slice_str_contains: &'a [&'a str], - #[schemars(inner(regex = "STARTS_WITH_HELLO"))] + #[schemars(inner(regex(pattern = STARTS_WITH_HELLO)))] vec_str_regex: Vec, #[schemars(inner(length(min = 1, max = 100)))] vec_str_length: Vec<&'a str>, diff --git a/schemars_derive/src/ast/from_serde.rs b/schemars_derive/src/ast/from_serde.rs index 8406712..d9ea066 100644 --- a/schemars_derive/src/ast/from_serde.rs +++ b/schemars_derive/src/ast/from_serde.rs @@ -1,5 +1,4 @@ use super::*; -use crate::attr::Attrs; use serde_derive_internals::ast as serde_ast; use serde_derive_internals::Ctxt; @@ -26,8 +25,7 @@ impl<'a> FromSerde for Container<'a> { serde_attrs: serde.attrs, data: Data::from_serde(errors, serde.data)?, generics: serde.generics.clone(), - // FIXME this allows with/schema_with attribute on containers - attrs: Attrs::new(&serde.original.attrs, errors), + attrs: ContainerAttrs::new(&serde.original.attrs, errors), }) } } @@ -57,7 +55,7 @@ impl<'a> FromSerde for Variant<'a> { style: serde.style, fields: Field::vec_from_serde(errors, serde.fields)?, original: serde.original, - attrs: Attrs::new(&serde.original.attrs, errors), + attrs: VariantAttrs::new(&serde.original.attrs, errors), }) } } @@ -71,8 +69,7 @@ impl<'a> FromSerde for Field<'a> { serde_attrs: serde.attrs, ty: serde.ty, original: serde.original, - attrs: Attrs::new(&serde.original.attrs, errors), - validation_attrs: ValidationAttrs::new(&serde.original.attrs, errors), + attrs: FieldAttrs::new(&serde.original.attrs, errors), }) } } diff --git a/schemars_derive/src/ast/mod.rs b/schemars_derive/src/ast/mod.rs index 642721e..0b29ca1 100644 --- a/schemars_derive/src/ast/mod.rs +++ b/schemars_derive/src/ast/mod.rs @@ -1,7 +1,9 @@ mod from_serde; -use crate::attr::{Attrs, ValidationAttrs}; +use crate::attr::{ContainerAttrs, FieldAttrs, VariantAttrs}; +use crate::idents::SCHEMA; use from_serde::FromSerde; +use proc_macro2::TokenStream; use serde_derive_internals::ast as serde_ast; use serde_derive_internals::{Ctxt, Derive}; @@ -10,7 +12,7 @@ pub struct Container<'a> { pub serde_attrs: serde_derive_internals::attr::Container, pub data: Data<'a>, pub generics: syn::Generics, - pub attrs: Attrs, + pub attrs: ContainerAttrs, } pub enum Data<'a> { @@ -24,7 +26,7 @@ pub struct Variant<'a> { pub style: serde_ast::Style, pub fields: Vec>, pub original: &'a syn::Variant, - pub attrs: Attrs, + pub attrs: VariantAttrs, } pub struct Field<'a> { @@ -32,8 +34,7 @@ pub struct Field<'a> { pub serde_attrs: serde_derive_internals::attr::Field, pub ty: &'a syn::Type, pub original: &'a syn::Field, - pub attrs: Attrs, - pub validation_attrs: ValidationAttrs, + pub attrs: FieldAttrs, } impl<'a> Container<'a> { @@ -60,6 +61,10 @@ impl<'a> Container<'a> { None } + + pub fn add_mutators(&self, mutators: &mut Vec) { + self.attrs.common.add_mutators(mutators); + } } impl<'a> Variant<'a> { @@ -70,10 +75,30 @@ impl<'a> Variant<'a> { pub fn is_unit(&self) -> bool { matches!(self.style, serde_ast::Style::Unit) } + + pub fn add_mutators(&self, mutators: &mut Vec) { + self.attrs.common.add_mutators(mutators); + } } impl<'a> Field<'a> { pub fn name(&self) -> &str { self.serde_attrs.name().deserialize_name() } + + pub fn add_mutators(&self, mutators: &mut Vec) { + self.attrs.common.add_mutators(mutators); + self.attrs.validation.add_mutators(mutators); + + if self.serde_attrs.skip_deserializing() { + mutators.push(quote! { + schemars::_private::insert_metadata_property(&mut #SCHEMA, "readOnly", true); + }); + } + if self.serde_attrs.skip_serializing() { + mutators.push(quote! { + schemars::_private::insert_metadata_property(&mut #SCHEMA, "writeOnly", true); + }); + } + } } diff --git a/schemars_derive/src/attr/doc.rs b/schemars_derive/src/attr/doc.rs index fafd546..778d493 100644 --- a/schemars_derive/src/attr/doc.rs +++ b/schemars_derive/src/attr/doc.rs @@ -1,21 +1,16 @@ use proc_macro2::TokenStream; -use quote::ToTokens; +use quote::TokenStreamExt; use syn::Attribute; pub fn get_doc(attrs: &[Attribute]) -> Option { - let joiner = quote! {, "\n",}; let mut macro_args: TokenStream = TokenStream::new(); - for nv in attrs + let lines = attrs .iter() .filter(|a| a.path().is_ident("doc")) - .filter_map(|a| a.meta.require_name_value().ok()) - { - if !macro_args.is_empty() { - macro_args.extend(joiner.clone()); - } - macro_args.extend(nv.value.to_token_stream()); - } + .flat_map(|a| a.meta.require_name_value()) + .map(|m| &m.value); + macro_args.append_separated(lines, quote!(, "\n",)); if macro_args.is_empty() { None diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index 8e7caab..9698731 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -1,371 +1,454 @@ mod doc; +mod parse_meta; mod schemars_to_serde; mod validation; -pub use schemars_to_serde::process_serde_attrs; -pub use validation::ValidationAttrs; - -use crate::metadata::SchemaMetadata; -use proc_macro2::{Group, Span, TokenStream, TokenTree}; +use parse_meta::{parse_extensions, parse_name_value_expr, parse_name_value_lit_str}; +use proc_macro2::TokenStream; use quote::ToTokens; use serde_derive_internals::Ctxt; -use syn::parse::{self, Parse}; -use syn::{Expr, LitStr, Meta, MetaNameValue}; +use syn::Ident; +use syn::{punctuated::Punctuated, Attribute, Expr, ExprLit, Lit, Meta, Path, Type}; +use validation::ValidationAttrs; -// FIXME using the same struct for containers+variants+fields means that -// with/schema_with are accepted (but ignored) on containers, and -// repr/crate_name are accepted (but ignored) on variants and fields etc. +use crate::idents::SCHEMA; + +pub use schemars_to_serde::process_serde_attrs; #[derive(Debug, Default)] -pub struct Attrs { - pub with: Option, - pub title: Option, - pub description: Option, +pub struct CommonAttrs { pub doc: Option, pub deprecated: bool, - pub examples: Vec, - pub repr: Option, - pub crate_name: Option, - pub is_renamed: bool, + pub title: Option, + pub description: Option, + pub examples: Vec, pub extensions: Vec<(String, TokenStream)>, pub transforms: Vec, } -#[derive(Debug)] -pub enum WithAttr { - Type(syn::Type), - Function(syn::Path), +#[derive(Debug, Default)] +pub struct FieldAttrs { + pub common: CommonAttrs, + pub with: Option, + pub validation: ValidationAttrs, } -impl Attrs { - pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self { - let mut result = Attrs::default() - .populate(attrs, "schemars", false, errors) - .populate(attrs, "serde", true, errors); +#[derive(Debug, Default)] +pub struct ContainerAttrs { + pub common: CommonAttrs, + pub repr: Option, + pub crate_name: Option, + pub is_renamed: bool, +} - result.deprecated = attrs.iter().any(|a| a.path().is_ident("deprecated")); - result.repr = attrs - .iter() - .find(|a| a.path().is_ident("repr")) - .and_then(|a| a.parse_args().ok()); +#[derive(Debug, Default)] +pub struct VariantAttrs { + pub common: CommonAttrs, + pub with: Option, +} - result.doc = doc::get_doc(attrs); - result - } - - pub fn as_metadata(&self) -> SchemaMetadata<'_> { - SchemaMetadata { - doc: self.doc.as_ref(), - title: self.title.as_ref(), - description: self.description.as_ref(), - deprecated: self.deprecated, - examples: &self.examples, - extensions: &self.extensions, - transforms: &self.transforms, - read_only: false, - write_only: false, - default: None, - } - } +#[derive(Debug)] +pub enum WithAttr { + Type(Type), + Function(Path), +} +impl CommonAttrs { fn populate( - mut self, - attrs: &[syn::Attribute], - attr_type: &'static str, - ignore_errors: bool, - errors: &Ctxt, - ) -> Self { - let duplicate_error = |meta: &MetaNameValue| { - if !ignore_errors { - let msg = format!( - "duplicate schemars attribute `{}`", - meta.path.get_ident().unwrap() - ); - errors.error_spanned_by(meta, msg) + &mut self, + attrs: &[Attribute], + schemars_cx: &mut AttrCtxt, + serde_cx: &mut AttrCtxt, + ) { + self.process_attr(schemars_cx); + self.process_attr(serde_cx); + + self.doc = doc::get_doc(attrs); + self.deprecated = attrs.iter().any(|a| a.path().is_ident("deprecated")); + } + + fn process_attr(&mut self, cx: &mut AttrCtxt) { + cx.parse_meta(|m, n, c| self.process_meta(m, n, c)); + } + + fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option { + match meta_name { + "title" => match self.title { + Some(_) => cx.duplicate_error(&meta), + None => self.title = parse_name_value_expr(meta, cx).ok(), + }, + + "description" => match self.description { + Some(_) => cx.duplicate_error(&meta), + None => self.description = parse_name_value_expr(meta, cx).ok(), + }, + + "example" => { + self.examples.extend(parse_name_value_lit_str(meta, cx)); } - }; - let mutual_exclusive_error = |meta: &MetaNameValue, other: &str| { - if !ignore_errors { - let msg = format!( - "schemars attribute cannot contain both `{}` and `{}`", - meta.path.get_ident().unwrap(), - other, - ); - errors.error_spanned_by(meta, msg) + + "extend" => { + for ex in parse_extensions(meta, cx).into_iter().flatten() { + // This is O(n^2) but should be fine with the typically small number of extensions. + // If this does become a problem, it can be changed to use IndexMap, or a separate Map with cloned keys. + if self.extensions.iter().any(|e| e.0 == ex.key_str) { + cx.error_spanned_by( + ex.key_lit, + format_args!("Duplicate extension key '{}'", ex.key_str), + ); + } else { + self.extensions.push((ex.key_str, ex.value)); + } + } } - }; - for meta_item in get_meta_items(attrs, attr_type, errors, ignore_errors) { - match &meta_item { - Meta::NameValue(m) if m.path.is_ident("with") => { - if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.value) { - match self.with { - Some(WithAttr::Type(_)) => duplicate_error(m), - Some(WithAttr::Function(_)) => mutual_exclusive_error(m, "schema_with"), - None => self.with = Some(WithAttr::Type(ty)), - } - } - } - - Meta::NameValue(m) if m.path.is_ident("schema_with") => { - if let Ok(fun) = parse_lit_into_path(errors, attr_type, "schema_with", &m.value) - { - match self.with { - Some(WithAttr::Function(_)) => duplicate_error(m), - Some(WithAttr::Type(_)) => mutual_exclusive_error(m, "with"), - None => self.with = Some(WithAttr::Function(fun)), - } - } - } - - Meta::NameValue(m) if m.path.is_ident("title") => match self.title { - Some(_) => duplicate_error(m), - None => self.title = Some(m.value.clone()), - }, - - Meta::NameValue(m) if m.path.is_ident("description") => match self.description { - Some(_) => duplicate_error(m), - None => self.description = Some(m.value.clone()), - }, - - Meta::NameValue(m) if m.path.is_ident("example") => { - if let Ok(fun) = parse_lit_into_path(errors, attr_type, "example", &m.value) { - self.examples.push(fun) - } - } - - Meta::NameValue(m) if m.path.is_ident("rename") => self.is_renamed = true, - - Meta::NameValue(m) if m.path.is_ident("crate") && attr_type == "schemars" => { - if let Ok(p) = parse_lit_into_path(errors, attr_type, "crate", &m.value) { - if self.crate_name.is_some() { - duplicate_error(m) - } else { - self.crate_name = Some(p) - } - } - } - - Meta::NameValue(m) if m.path.is_ident("transform") && attr_type == "schemars" => { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit_str), + "transform" => { + if let Ok(expr) = parse_name_value_expr(meta, cx) { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), .. - }) = &m.value + }) = &expr { - if parse_lit_str::(lit_str).is_ok() { - errors.error_spanned_by( - &m.value, - format!( + if lit_str.parse::().is_ok() { + cx.error_spanned_by( + &expr, + format_args!( "Expected a `fn(&mut Schema)` or other value implementing `schemars::transform::Transform`, found `&str`.\nDid you mean `[schemars(transform = {})]`?", lit_str.value() ), ) } - } - self.transforms.push(m.value.clone()); - } - - Meta::List(m) if m.path.is_ident("extend") && attr_type == "schemars" => { - let parser = - syn::punctuated::Punctuated::::parse_terminated; - match m.parse_args_with(parser) { - Ok(extensions) => { - for extension in extensions { - let key = extension.key.value(); - // This is O(n^2) but should be fine with the typically small number of extensions. - // If this does become a problem, it can be changed to use IndexMap, or a separate Map with cloned keys. - if self.extensions.iter().any(|e| e.0 == key) { - errors.error_spanned_by( - extension.key, - format!("Duplicate extension key '{}'", key), - ); - } else { - self.extensions.push((key, extension.value)); - } - } - } - Err(err) => errors.syn_error(err), - } - } - - _ if ignore_errors => {} - - _ => { - if !is_known_serde_or_validation_keyword(&meta_item) { - let path = meta_item - .path() - .into_token_stream() - .to_string() - .replace(' ', ""); - errors.error_spanned_by( - meta_item.path(), - format!("unknown schemars attribute `{}`", path), - ); + } else { + self.transforms.push(expr); } } } + + _ => return Some(meta), } - self + + None } - pub fn is_default(&self) -> bool { - matches!(self, Self { - with: None, + fn is_default(&self) -> bool { + matches!( + self, + Self { title: None, description: None, doc: None, deprecated: false, examples, - repr: None, - crate_name: None, - is_renamed: _, extensions, - transforms - } if examples.is_empty() && extensions.is_empty() && transforms.is_empty()) + transforms, + } if examples.is_empty() && extensions.is_empty() && transforms.is_empty() + ) + } + + pub fn add_mutators(&self, mutators: &mut Vec) { + let mut title = self.title.as_ref().map(ToTokens::to_token_stream); + let mut description = self.description.as_ref().map(ToTokens::to_token_stream); + if let Some(doc) = &self.doc { + if title.is_none() || description.is_none() { + mutators.push(quote!{ + const title_and_description: (&str, &str) = schemars::_private::get_title_and_description(#doc); + }); + title.get_or_insert_with(|| quote!(title_and_description.0)); + description.get_or_insert_with(|| quote!(title_and_description.1)); + } + } + if let Some(title) = title { + mutators.push(quote! { + schemars::_private::insert_metadata_property_if_nonempty(&mut #SCHEMA, "title", #title); + }); + } + if let Some(description) = description { + mutators.push(quote! { + schemars::_private::insert_metadata_property_if_nonempty(&mut #SCHEMA, "description", #description); + }); + } + + if self.deprecated { + mutators.push(quote! { + schemars::_private::insert_metadata_property(&mut #SCHEMA, "deprecated", true); + }); + } + + if !self.examples.is_empty() { + let examples = self.examples.iter().map(|eg| { + quote! { + schemars::_serde_json::value::to_value(#eg()) + } + }); + mutators.push(quote! { + schemars::_private::insert_metadata_property(&mut #SCHEMA, "examples", schemars::_serde_json::Value::Array([#(#examples),*].into_iter().flatten().collect())); + }); + } + + for (k, v) in &self.extensions { + mutators.push(quote! { + schemars::_private::insert_metadata_property(&mut #SCHEMA, #k, schemars::_serde_json::json!(#v)); + }); + } + + for transform in &self.transforms { + mutators.push(quote! { + schemars::transform::Transform::transform(&mut #transform, &mut #SCHEMA); + }); + } } } -fn is_known_serde_or_validation_keyword(meta: &syn::Meta) -> bool { - let mut known_keywords = schemars_to_serde::SERDE_KEYWORDS - .iter() - .chain(validation::VALIDATION_KEYWORDS); - meta.path() - .get_ident() - .map(|i| known_keywords.any(|k| i == k)) - .unwrap_or(false) +impl FieldAttrs { + pub fn new(attrs: &[Attribute], cx: &Ctxt) -> Self { + let mut result = Self::default(); + result.populate(attrs, cx); + result + } + + fn populate(&mut self, attrs: &[Attribute], cx: &Ctxt) { + let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars"); + let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde"); + let validate_cx = &mut AttrCtxt::new(cx, attrs, "validate"); + + self.common.populate(attrs, schemars_cx, serde_cx); + self.validation.populate(schemars_cx, validate_cx); + self.process_attr(schemars_cx); + self.process_attr(serde_cx); + } + + fn process_attr(&mut self, cx: &mut AttrCtxt) { + cx.parse_meta(|m, n, c| self.process_meta(m, n, c)); + } + + fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option { + match meta_name { + "with" => match self.with { + Some(WithAttr::Type(_)) => cx.duplicate_error(&meta), + Some(WithAttr::Function(_)) => cx.mutual_exclusive_error(&meta, "schema_with"), + None => self.with = parse_name_value_lit_str(meta, cx).ok().map(WithAttr::Type), + }, + "schema_with" if cx.attr_type == "schemars" => match self.with { + Some(WithAttr::Function(_)) => cx.duplicate_error(&meta), + Some(WithAttr::Type(_)) => cx.mutual_exclusive_error(&meta, "with"), + None => { + self.with = parse_name_value_lit_str(meta, cx) + .ok() + .map(WithAttr::Function) + } + }, + + _ => return Some(meta), + } + + None + } } -fn get_meta_items( - attrs: &[syn::Attribute], - attr_type: &'static str, - errors: &Ctxt, - ignore_errors: bool, -) -> Vec { +impl ContainerAttrs { + pub fn new(attrs: &[Attribute], cx: &Ctxt) -> Self { + let mut result = Self::default(); + result.populate(attrs, cx); + result + } + + fn populate(&mut self, attrs: &[Attribute], cx: &Ctxt) { + let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars"); + let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde"); + + self.common.populate(attrs, schemars_cx, serde_cx); + self.process_attr(schemars_cx); + self.process_attr(serde_cx); + + self.repr = attrs + .iter() + .find(|a| a.path().is_ident("repr")) + .and_then(|a| a.parse_args().ok()); + } + + fn process_attr(&mut self, cx: &mut AttrCtxt) { + cx.parse_meta(|m, n, c| self.process_meta(m, n, c)); + } + + fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option { + match meta_name { + "crate" => match self.crate_name { + Some(_) => cx.duplicate_error(&meta), + None => self.crate_name = parse_name_value_lit_str(meta, cx).ok(), + }, + + "rename" => self.is_renamed = true, + + _ => return Some(meta), + }; + + None + } +} + +impl VariantAttrs { + pub fn new(attrs: &[Attribute], cx: &Ctxt) -> Self { + let mut result = Self::default(); + result.populate(attrs, cx); + result + } + + fn populate(&mut self, attrs: &[Attribute], cx: &Ctxt) { + let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars"); + let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde"); + + self.common.populate(attrs, schemars_cx, serde_cx); + self.process_attr(schemars_cx); + self.process_attr(serde_cx); + } + + fn process_attr(&mut self, cx: &mut AttrCtxt) { + cx.parse_meta(|m, n, c| self.process_meta(m, n, c)); + } + + fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option { + match meta_name { + "with" => match self.with { + Some(WithAttr::Type(_)) => cx.duplicate_error(&meta), + Some(WithAttr::Function(_)) => cx.mutual_exclusive_error(&meta, "schema_with"), + None => self.with = parse_name_value_lit_str(meta, cx).ok().map(WithAttr::Type), + }, + "schema_with" if cx.attr_type == "schemars" => match self.with { + Some(WithAttr::Function(_)) => cx.duplicate_error(&meta), + Some(WithAttr::Type(_)) => cx.mutual_exclusive_error(&meta, "with"), + None => { + self.with = parse_name_value_lit_str(meta, cx) + .ok() + .map(WithAttr::Function) + } + }, + + _ => return Some(meta), + } + + None + } + + pub fn is_default(&self) -> bool { + matches!( + self, + Self { + common, + with: None, + } if common.is_default() + ) + } +} + +fn get_meta_items(attrs: &[Attribute], attr_type: &'static str, cx: &Ctxt) -> Vec { let mut result = vec![]; + for attr in attrs.iter().filter(|a| a.path().is_ident(attr_type)) { - match attr.parse_args_with(syn::punctuated::Punctuated::::parse_terminated) - { + match attr.parse_args_with(Punctuated::::parse_terminated) { Ok(list) => result.extend(list), - Err(err) if !ignore_errors => errors.syn_error(err), - Err(_) => {} + Err(err) => { + if attr_type == "schemars" { + cx.syn_error(err) + } + } } } result } -fn expr_as_lit_str<'a>( - cx: &Ctxt, +fn path_str(path: &Path) -> String { + path.get_ident() + .map(Ident::to_string) + .unwrap_or_else(|| path.into_token_stream().to_string().replace(' ', "")) +} + +pub struct AttrCtxt<'a> { + inner: &'a Ctxt, attr_type: &'static str, - meta_item_name: &'static str, - expr: &'a syn::Expr, -) -> Result<&'a syn::LitStr, ()> { - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit_str), - .. - }) = expr - { - Ok(lit_str) - } else { - cx.error_spanned_by( - expr, - format!( - "expected {} {} attribute to be a string: `{} = \"...\"`", - attr_type, meta_item_name, meta_item_name - ), - ); - Err(()) - } + metas: Vec, } -fn parse_lit_into_ty( - cx: &Ctxt, - attr_type: &'static str, - meta_item_name: &'static str, - lit: &syn::Expr, -) -> Result { - let string = expr_as_lit_str(cx, attr_type, meta_item_name, lit)?; - - parse_lit_str(string).map_err(|_| { - cx.error_spanned_by( - lit, - format!( - "failed to parse type: `{} = {:?}`", - meta_item_name, - string.value() - ), - ) - }) -} - -fn parse_lit_into_path( - cx: &Ctxt, - attr_type: &'static str, - meta_item_name: &'static str, - expr: &syn::Expr, -) -> Result { - let lit_str = expr_as_lit_str(cx, attr_type, meta_item_name, expr)?; - - parse_lit_str(lit_str).map_err(|_| { - cx.error_spanned_by( - expr, - format!( - "failed to parse path: `{} = {:?}`", - meta_item_name, - lit_str.value() - ), - ) - }) -} - -fn parse_lit_str(s: &syn::LitStr) -> parse::Result -where - T: Parse, -{ - let tokens = spanned_tokens(s)?; - syn::parse2(tokens) -} - -fn spanned_tokens(s: &syn::LitStr) -> parse::Result { - let stream = syn::parse_str(&s.value())?; - Ok(respan_token_stream(stream, s.span())) -} - -fn respan_token_stream(stream: TokenStream, span: Span) -> TokenStream { - stream - .into_iter() - .map(|token| respan_token_tree(token, span)) - .collect() -} - -fn respan_token_tree(mut token: TokenTree, span: Span) -> TokenTree { - if let TokenTree::Group(g) = &mut token { - *g = Group::new(g.delimiter(), respan_token_stream(g.stream(), span)); - } - token.set_span(span); - token -} - -#[derive(Debug)] -struct Extension { - key: LitStr, - value: TokenStream, -} - -impl Parse for Extension { - fn parse(input: parse::ParseStream) -> syn::Result { - let key = input.parse::()?; - input.parse::()?; - let mut value = TokenStream::new(); - - while !input.is_empty() && !input.peek(Token![,]) { - value.extend([input.parse::()?]); +impl<'a> AttrCtxt<'a> { + pub fn new(inner: &'a Ctxt, attrs: &'a [Attribute], attr_type: &'static str) -> Self { + Self { + inner, + attr_type, + metas: get_meta_items(attrs, attr_type, inner), } + } - if value.is_empty() { - return Err(syn::Error::new(input.span(), "Expected extension value")); + pub fn new_nested_meta(&self, metas: Vec) -> Self { + Self { metas, ..*self } + } + + pub fn parse_meta(&mut self, mut handle: impl FnMut(Meta, &str, &Self) -> Option) { + let metas = std::mem::take(&mut self.metas); + self.metas = metas + .into_iter() + .filter_map(|meta| { + meta.path() + .get_ident() + .map(Ident::to_string) + .and_then(|name| handle(meta, &name, self)) + }) + .collect(); + } + + pub fn error_spanned_by(&self, obj: A, msg: T) { + self.inner.error_spanned_by(obj, msg); + } + + pub fn syn_error(&self, err: syn::Error) { + self.inner.syn_error(err); + } + + pub fn mutual_exclusive_error(&self, meta: &Meta, other_attr: &str) { + if self.attr_type == "schemars" { + self.error_spanned_by( + meta, + format_args!( + "schemars attribute cannot contain both `{}` and `{}`", + path_str(meta.path()), + other_attr, + ), + ); } + } - Ok(Extension { key, value }) + pub fn duplicate_error(&self, meta: &Meta) { + if self.attr_type == "schemars" { + self.error_spanned_by( + meta, + format_args!( + "duplicate schemars attribute item `{}`", + path_str(meta.path()) + ), + ); + } } } + +impl Drop for AttrCtxt<'_> { + fn drop(&mut self) { + if self.attr_type == "schemars" { + for unhandled_meta in self.metas.iter().filter(|m| !is_known_serde_keyword(m)) { + self.error_spanned_by( + unhandled_meta.path(), + format_args!( + "unknown schemars attribute `{}`", + path_str(unhandled_meta.path()) + ), + ); + } + } + } +} + +fn is_known_serde_keyword(meta: &Meta) -> bool { + let known_keywords = schemars_to_serde::SERDE_KEYWORDS; + meta.path() + .get_ident() + .map(|i| known_keywords.contains(&i.to_string().as_str())) + .unwrap_or(false) +} diff --git a/schemars_derive/src/attr/parse_meta.rs b/schemars_derive/src/attr/parse_meta.rs new file mode 100644 index 0000000..f77b732 --- /dev/null +++ b/schemars_derive/src/attr/parse_meta.rs @@ -0,0 +1,285 @@ +use proc_macro2::{TokenStream, TokenTree}; +use syn::{ + parse::{Parse, ParseStream, Parser}, + punctuated::Punctuated, + Expr, ExprLit, Lit, LitStr, Meta, MetaNameValue, +}; + +use super::{path_str, AttrCtxt}; + +pub fn require_path_only(meta: Meta, cx: &AttrCtxt) -> Result<(), ()> { + match meta { + Meta::Path(_) => Ok(()), + _ => { + let name = path_str(meta.path()); + cx.error_spanned_by( + meta, + format_args!( + "unexpected value of {} {} attribute item", + cx.attr_type, name + ), + ); + Err(()) + } + } +} + +pub fn parse_name_value_expr(meta: Meta, cx: &AttrCtxt) -> Result { + match meta { + Meta::NameValue(m) => Ok(m.value), + _ => { + let name = path_str(meta.path()); + cx.error_spanned_by( + meta, + format_args!( + "expected {} {} attribute item to have a value: `{} = ...`", + cx.attr_type, name, name + ), + ); + Err(()) + } + } +} + +pub fn parse_name_value_lit_str(meta: Meta, cx: &AttrCtxt) -> Result { + let Meta::NameValue(MetaNameValue { + value: Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }), + .. + }) = meta + else { + let name = path_str(meta.path()); + cx.error_spanned_by( + meta, + format_args!( + "expected {} {} attribute item to have a string value: `{} = \"...\"`", + cx.attr_type, name, name + ), + ); + return Err(()); + }; + + parse_lit_str(lit_str, cx) +} + +fn parse_lit_str(lit_str: LitStr, cx: &AttrCtxt) -> Result { + lit_str.parse().map_err(|_| { + cx.error_spanned_by( + &lit_str, + format_args!( + "failed to parse \"{}\" as a {}", + lit_str.value(), + std::any::type_name::() + .rsplit("::") + .next() + .unwrap_or_default() + .to_ascii_lowercase(), + ), + ); + }) +} + +pub fn parse_extensions( + meta: Meta, + cx: &AttrCtxt, +) -> Result, ()> { + let parser = Punctuated::::parse_terminated; + parse_meta_list(meta, cx, parser) +} + +pub fn parse_length_or_range(outer_meta: Meta, cx: &AttrCtxt) -> Result { + let outer_name = path_str(outer_meta.path()); + let mut result = LengthOrRange::default(); + + for nested_meta in parse_nested_meta(outer_meta, cx)? { + match path_str(nested_meta.path()).as_str() { + "min" => match (&result.min, &result.equal) { + (Some(_), _) => cx.duplicate_error(&nested_meta), + (_, Some(_)) => cx.mutual_exclusive_error(&nested_meta, "equal"), + _ => result.min = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(), + }, + "max" => match (&result.max, &result.equal) { + (Some(_), _) => cx.duplicate_error(&nested_meta), + (_, Some(_)) => cx.mutual_exclusive_error(&nested_meta, "equal"), + _ => result.max = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(), + }, + "equal" => match (&result.min, &result.max, &result.equal) { + (Some(_), _, _) => cx.mutual_exclusive_error(&nested_meta, "min"), + (_, Some(_), _) => cx.mutual_exclusive_error(&nested_meta, "max"), + (_, _, Some(_)) => cx.duplicate_error(&nested_meta), + _ => result.equal = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(), + }, + unknown => { + if cx.attr_type == "schemars" { + cx.error_spanned_by( + nested_meta, + format_args!( + "unknown item in schemars {outer_name} attribute: `{unknown}`", + ), + ); + } + } + } + } + + Ok(result) +} + +pub fn parse_schemars_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result { + let mut pattern = None; + + for nested_meta in parse_nested_meta(outer_meta.clone(), cx)? { + match path_str(nested_meta.path()).as_str() { + "pattern" => match &pattern { + Some(_) => cx.duplicate_error(&nested_meta), + None => pattern = parse_name_value_expr(nested_meta, cx).ok(), + }, + "path" => { + cx.error_spanned_by(nested_meta, "`path` is not supported in `schemars(regex(...))` attribute - use `schemars(regex(pattern = ...))` instead") + }, + unknown => { + cx.error_spanned_by( + nested_meta, + format_args!("unknown item in schemars `regex` attribute: `{unknown}`"), + ); + } + } + } + + pattern.ok_or_else(|| { + cx.error_spanned_by( + outer_meta, + "`schemars(regex(...))` attribute requires `pattern = ...`", + ) + }) +} + +pub fn parse_validate_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result { + let mut path = None; + + for nested_meta in parse_nested_meta(outer_meta.clone(), cx)? { + match path_str(nested_meta.path()).as_str() { + "path" => match &path{ + Some(_) => cx.duplicate_error(&nested_meta), + None => path = parse_name_value_expr_handle_lit_str(nested_meta, cx).ok(), + }, + "pattern" => { + cx.error_spanned_by(nested_meta, "`pattern` is not supported in `validate(regex(...))` attribute - use either `validate(regex(path = ...))` or `schemars(regex(pattern = ...))` instead") + }, + _ => { + // ignore unknown properties in `validate` attribute + } + } + } + + path.ok_or_else(|| { + cx.error_spanned_by( + outer_meta, + "`validate(regex(...))` attribute requires `path = ...`", + ) + }) +} + +pub fn parse_contains(outer_meta: Meta, cx: &AttrCtxt) -> Result { + let mut pattern = None; + + for nested_meta in parse_nested_meta(outer_meta.clone(), cx)? { + match path_str(nested_meta.path()).as_str() { + "pattern" => match &pattern { + Some(_) => cx.duplicate_error(&nested_meta), + None => pattern = parse_name_value_expr(nested_meta, cx).ok(), + }, + unknown => { + if cx.attr_type == "schemars" { + cx.error_spanned_by( + nested_meta, + format_args!("unknown item in schemars `contains` attribute: `{unknown}`"), + ); + } + } + } + } + + pattern.ok_or_else(|| { + cx.error_spanned_by( + outer_meta, + "`contains` attribute item requires `pattern = ...`", + ) + }) +} + +pub fn parse_nested_meta(meta: Meta, cx: &AttrCtxt) -> Result, ()> { + let parser = Punctuated::::parse_terminated; + parse_meta_list(meta, cx, parser) +} + +fn parse_meta_list(meta: Meta, cx: &AttrCtxt, parser: F) -> Result { + let Meta::List(meta_list) = meta else { + let name = path_str(meta.path()); + cx.error_spanned_by( + meta, + format_args!( + "expected {} {} attribute item to be of the form `{}(...)`", + cx.attr_type, name, name + ), + ); + return Err(()); + }; + + meta_list.parse_args_with(parser).map_err(|err| { + cx.syn_error(err); + }) +} + +// Like `parse_name_value_expr`, but if the result is a string literal, then parse its contents. +pub fn parse_name_value_expr_handle_lit_str(meta: Meta, cx: &AttrCtxt) -> Result { + let expr = parse_name_value_expr(meta, cx)?; + + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit_str), + .. + }) = expr + { + parse_lit_str(lit_str, cx) + } else { + Ok(expr) + } +} + +#[derive(Debug, Default)] +pub struct LengthOrRange { + pub min: Option, + pub max: Option, + pub equal: Option, +} + +#[derive(Debug)] +pub struct Extension { + pub key_str: String, + pub key_lit: LitStr, + pub value: TokenStream, +} + +impl Parse for Extension { + fn parse(input: ParseStream) -> syn::Result { + let key = input.parse::()?; + input.parse::()?; + let mut value = TokenStream::new(); + + while !input.is_empty() && !input.peek(Token![,]) { + value.extend([input.parse::()?]); + } + + if value.is_empty() { + return Err(syn::Error::new(input.span(), "Expected extension value")); + } + + Ok(Extension { + key_str: key.value(), + key_lit: key, + value, + }) + } +} diff --git a/schemars_derive/src/attr/schemars_to_serde.rs b/schemars_derive/src/attr/schemars_to_serde.rs index 5b69942..80bcaf0 100644 --- a/schemars_derive/src/attr/schemars_to_serde.rs +++ b/schemars_derive/src/attr/schemars_to_serde.rs @@ -67,7 +67,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec) { // Copy appropriate #[schemars(...)] attributes to #[serde(...)] attributes let (mut serde_meta, mut schemars_meta_names): (Vec<_>, HashSet<_>) = - get_meta_items(attrs, "schemars", ctxt, false) + get_meta_items(attrs, "schemars", ctxt) .into_iter() .filter_map(|meta| { let keyword = get_meta_ident(&meta)?; @@ -85,7 +85,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec) { } // Re-add #[serde(...)] attributes that weren't overridden by #[schemars(...)] attributes - for meta in get_meta_items(&serde_attrs, "serde", ctxt, false) { + for meta in get_meta_items(&serde_attrs, "serde", ctxt) { if let Some(i) = get_meta_ident(&meta) { if !schemars_meta_names.contains(&i) && SERDE_KEYWORDS.contains(&i.as_ref()) diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs index 0d9acf9..ec26308 100644 --- a/schemars_derive/src/attr/validation.rs +++ b/schemars_derive/src/attr/validation.rs @@ -1,21 +1,20 @@ -use super::{expr_as_lit_str, get_meta_items, parse_lit_str}; use proc_macro2::TokenStream; -use quote::ToTokens; -use serde_derive_internals::Ctxt; -use syn::{ - parse::Parser, punctuated::Punctuated, Expr, ExprLit, ExprPath, Lit, Meta, MetaList, - MetaNameValue, Path, +use syn::{Expr, Meta}; + +use crate::idents::SCHEMA; + +use super::{ + parse_meta::{ + parse_contains, parse_length_or_range, parse_nested_meta, parse_schemars_regex, + parse_validate_regex, require_path_only, LengthOrRange, + }, + AttrCtxt, }; -pub(crate) static VALIDATION_KEYWORDS: &[&str] = &[ - "range", "regex", "contains", "email", "phone", "url", "length", "required", "inner", -]; - #[derive(Debug, Clone, Copy, PartialEq)] -enum Format { +pub enum Format { Email, Uri, - Phone, } impl Format { @@ -23,7 +22,6 @@ impl Format { match self { Format::Email => "email", Format::Uri => "url", - Format::Phone => "phone", } } @@ -31,420 +29,167 @@ impl Format { match self { Format::Email => "email", Format::Uri => "uri", - Format::Phone => "phone", + } + } + + fn from_attr_str(s: &str) -> Self { + match s { + "email" => Format::Email, + "url" => Format::Uri, + _ => panic!("Invalid format attr string `{s}`. This is a bug in schemars, please raise an issue!"), } } } #[derive(Debug, Default)] pub struct ValidationAttrs { - length_min: Option, - length_max: Option, - length_equal: Option, - range_min: Option, - range_max: Option, - regex: Option, - contains: Option, - required: bool, - format: Option, - inner: Option>, + pub length: Option, + pub range: Option, + pub regex: Option, + pub contains: Option, + pub required: bool, + pub format: Option, + pub inner: Option>, } impl ValidationAttrs { - pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self { - let schemars_items = get_meta_items(attrs, "schemars", errors, false); - let validate_items = get_meta_items(attrs, "validate", errors, true); - - ValidationAttrs::default() - .populate(schemars_items, "schemars", false, errors) - .populate(validate_items, "validate", true, errors) + pub fn add_mutators(&self, mutators: &mut Vec) { + self.add_mutators2(mutators, "e!(&mut #SCHEMA)); } - pub fn required(&self) -> bool { - self.required - } - - fn populate( - mut self, - meta_items: Vec, - attr_type: &'static str, - ignore_errors: bool, - errors: &Ctxt, - ) -> Self { - let duplicate_error = |path: &Path| { - if !ignore_errors { - let msg = format!( - "duplicate schemars attribute `{}`", - path.get_ident().unwrap() - ); - errors.error_spanned_by(path, msg) - } - }; - let mutual_exclusive_error = |path: &Path, other: &str| { - if !ignore_errors { - let msg = format!( - "schemars attribute cannot contain both `{}` and `{}`", - path.get_ident().unwrap(), - other, - ); - errors.error_spanned_by(path, msg) - } - }; - let duplicate_format_error = |existing: Format, new: Format, path: &syn::Path| { - if !ignore_errors { - let msg = if existing == new { - format!("duplicate schemars attribute `{}`", existing.attr_str()) - } else { - format!( - "schemars attribute cannot contain both `{}` and `{}`", - existing.attr_str(), - new.attr_str(), - ) - }; - errors.error_spanned_by(path, msg) - } - }; - let parse_nested_meta = |meta_list: MetaList| { - let parser = Punctuated::::parse_terminated; - match parser.parse2(meta_list.tokens) { - Ok(p) => p, - Err(e) => { - if !ignore_errors { - errors.syn_error(e); - } - Default::default() - } - } - }; - - for meta_item in meta_items { - match meta_item { - Meta::List(meta_list) if meta_list.path.is_ident("length") => { - for nested in parse_nested_meta(meta_list) { - match nested { - Meta::NameValue(nv) if nv.path.is_ident("min") => { - if self.length_min.is_some() { - duplicate_error(&nv.path) - } else if self.length_equal.is_some() { - mutual_exclusive_error(&nv.path, "equal") - } else { - self.length_min = str_or_num_to_expr(errors, "min", nv.value); - } - } - Meta::NameValue(nv) if nv.path.is_ident("max") => { - if self.length_max.is_some() { - duplicate_error(&nv.path) - } else if self.length_equal.is_some() { - mutual_exclusive_error(&nv.path, "equal") - } else { - self.length_max = str_or_num_to_expr(errors, "max", nv.value); - } - } - Meta::NameValue(nv) if nv.path.is_ident("equal") => { - if self.length_equal.is_some() { - duplicate_error(&nv.path) - } else if self.length_min.is_some() { - mutual_exclusive_error(&nv.path, "min") - } else if self.length_max.is_some() { - mutual_exclusive_error(&nv.path, "max") - } else { - self.length_equal = - str_or_num_to_expr(errors, "equal", nv.value); - } - } - meta => { - if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars length attribute", - ); - } - } - } - } - } - - Meta::List(meta_list) if meta_list.path.is_ident("range") => { - for nested in parse_nested_meta(meta_list) { - match nested { - Meta::NameValue(nv) if nv.path.is_ident("min") => { - if self.range_min.is_some() { - duplicate_error(&nv.path) - } else { - self.range_min = str_or_num_to_expr(errors, "min", nv.value); - } - } - Meta::NameValue(nv) if nv.path.is_ident("max") => { - if self.range_max.is_some() { - duplicate_error(&nv.path) - } else { - self.range_max = str_or_num_to_expr(errors, "max", nv.value); - } - } - meta => { - if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars range attribute", - ); - } - } - } - } - } - - Meta::Path(m) if m.is_ident("required") || m.is_ident("required_nested") => { - self.required = true; - } - - Meta::Path(p) if p.is_ident(Format::Email.attr_str()) => match self.format { - Some(f) => duplicate_format_error(f, Format::Email, &p), - None => self.format = Some(Format::Email), - }, - Meta::Path(p) if p.is_ident(Format::Uri.attr_str()) => match self.format { - Some(f) => duplicate_format_error(f, Format::Uri, &p), - None => self.format = Some(Format::Uri), - }, - Meta::Path(p) if p.is_ident(Format::Phone.attr_str()) => match self.format { - Some(f) => duplicate_format_error(f, Format::Phone, &p), - None => self.format = Some(Format::Phone), - }, - - Meta::NameValue(nv) if nv.path.is_ident("regex") => { - match (&self.regex, &self.contains) { - (Some(_), _) => duplicate_error(&nv.path), - (None, Some(_)) => mutual_exclusive_error(&nv.path, "contains"), - (None, None) => { - self.regex = parse_regex_expr(errors, nv.value); - } - } - } - - Meta::List(meta_list) if meta_list.path.is_ident("regex") => { - match (&self.regex, &self.contains) { - (Some(_), _) => duplicate_error(&meta_list.path), - (None, Some(_)) => mutual_exclusive_error(&meta_list.path, "contains"), - (None, None) => { - for x in parse_nested_meta(meta_list) { - match x { - Meta::NameValue(MetaNameValue { path, value, .. }) - if path.is_ident("path") => - { - self.regex = parse_regex_expr(errors, value); - } - Meta::NameValue(MetaNameValue { path, value, .. }) - if path.is_ident("pattern") => - { - self.regex = - expr_as_lit_str(errors, attr_type, "pattern", &value) - .ok() - .map(|litstr| { - Expr::Lit(ExprLit { - attrs: Vec::new(), - lit: Lit::Str(litstr.clone()), - }) - }) - } - meta => { - if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars regex attribute", - ); - } - } - } - } - } - } - } - - Meta::NameValue(MetaNameValue { path, value, .. }) if path.is_ident("contains") => { - match (&self.contains, &self.regex) { - (Some(_), _) => duplicate_error(&path), - (None, Some(_)) => mutual_exclusive_error(&path, "regex"), - (None, None) => { - self.contains = expr_as_lit_str(errors, attr_type, "contains", &value) - .map(|litstr| litstr.value()) - .ok() - } - } - } - - Meta::List(meta_list) if meta_list.path.is_ident("contains") => { - match (&self.contains, &self.regex) { - (Some(_), _) => duplicate_error(&meta_list.path), - (None, Some(_)) => mutual_exclusive_error(&meta_list.path, "regex"), - (None, None) => { - for x in parse_nested_meta(meta_list) { - match x { - Meta::NameValue(MetaNameValue { path, value, .. }) - if path.is_ident("pattern") => - { - self.contains = - expr_as_lit_str(errors, attr_type, "contains", &value) - .ok() - .map(|litstr| litstr.value()) - } - meta => { - if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars contains attribute", - ); - } - } - } - } - } - } - } - - Meta::List(meta_list) if meta_list.path.is_ident("inner") => match self.inner { - Some(_) => duplicate_error(&meta_list.path), - None => { - let inner_attrs = ValidationAttrs::default().populate( - parse_nested_meta(meta_list).into_iter().collect(), - attr_type, - ignore_errors, - errors, - ); - self.inner = Some(Box::new(inner_attrs)); - } - }, - - _ if ignore_errors => {} - - _ => { - if let Some(ident) = meta_item.path().get_ident() { - if VALIDATION_KEYWORDS.iter().any(|k| ident == k) { - errors.error_spanned_by( - &meta_item, - format!("could not parse `{ident}` item in schemars attribute"), - ); - } - } - } - } - } - self - } - - pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) { - let setters = self.make_setters(quote!(&mut schema)); - if !setters.is_empty() { - *schema_expr = quote!({ - let mut schema = #schema_expr; - #(#setters)* - schema - }); - } - } - - 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()) { - result.push(quote! { - schemars::_private::insert_validation_property(#mut_schema, "string", "minLength", #length_min); - }); - result.push(quote! { - schemars::_private::insert_validation_property(#mut_schema, "array", "minItems", #length_min); - }); + fn add_mutators2(&self, mutators: &mut Vec, mut_ref_schema: &TokenStream) { + if let Some(length) = &self.length { + Self::add_length_or_range(length, mutators, "string", "Length", mut_ref_schema); + Self::add_length_or_range(length, mutators, "array", "Items", mut_ref_schema); } - if let Some(length_max) = self.length_max.as_ref().or(self.length_equal.as_ref()) { - result.push(quote! { - schemars::_private::insert_validation_property(#mut_schema, "string", "maxLength", #length_max); - }); - result.push(quote! { - schemars::_private::insert_validation_property(#mut_schema, "array", "maxItems", #length_max); - }); - } - - if let Some(range_min) = &self.range_min { - result.push(quote! { - schemars::_private::insert_validation_property(#mut_schema, "number", "minimum", #range_min); - }); - } - - if let Some(range_max) = &self.range_max { - result.push(quote! { - schemars::_private::insert_validation_property(#mut_schema, "number", "maximum", #range_max); - }); + if let Some(range) = &self.range { + Self::add_length_or_range(range, mutators, "number", "imum", mut_ref_schema); } if let Some(regex) = &self.regex { - result.push(quote! { - schemars::_private::insert_validation_property(#mut_schema, "string", "pattern", #regex); + mutators.push(quote! { + schemars::_private::insert_validation_property(#mut_ref_schema, "string", "pattern", #regex.to_string()); }); } if let Some(contains) = &self.contains { - result.push(quote! { - schemars::_private::append_required(#mut_schema, #contains); + mutators.push(quote! { + schemars::_private::must_contain(#mut_ref_schema, #contains.to_string()); }); - - if self.regex.is_none() { - let pattern = crate::regex_syntax::escape(contains); - result.push(quote! { - schemars::_private::insert_validation_property(#mut_schema, "string", "pattern", #pattern); - }); - } } if let Some(format) = &self.format { let f = format.schema_str(); - result.push(quote! { - schema.ensure_object().insert("format".into(), #f.into()); + mutators.push(quote! { + (#mut_ref_schema).ensure_object().insert("format".into(), #f.into()); }) }; 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)* }); + let mut inner_mutators = Vec::new(); + inner.add_mutators2(&mut inner_mutators, "e!(inner_schema)); + + if !inner_mutators.is_empty() { + mutators.push(quote! { + schemars::_private::apply_inner_validation(#mut_ref_schema, |inner_schema| { #(#inner_mutators)* }); }) } } - - result } -} -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()) { - Ok(l) => l, - Err(err) => { - cx.syn_error(err); - return None; + fn add_length_or_range( + value: &LengthOrRange, + mutators: &mut Vec, + required_format: &str, + key_suffix: &str, + mut_ref_schema: &TokenStream, + ) { + if let Some(min) = value.min.as_ref().or(value.equal.as_ref()) { + let key = format!("min{key_suffix}"); + mutators.push(quote!{ + schemars::_private::insert_validation_property(#mut_ref_schema, #required_format, #key, #min); + }); } - }; - match lit { - Lit::Str(s) => parse_lit_str::(&s).ok().map(Expr::Path), - Lit::Int(_) | Lit::Float(_) => Some(expr), - _ => { - cx.error_spanned_by( - &expr, - format!( - "expected `{}` to be a string or number literal, not {:?}", - meta_item_name, &expr - ), - ); - None + if let Some(max) = value.max.as_ref().or(value.equal.as_ref()) { + let key = format!("max{key_suffix}"); + mutators.push(quote!{ + schemars::_private::insert_validation_property(#mut_ref_schema, #required_format, #key, #max); + }); + } + } + + pub(super) fn populate(&mut self, schemars_cx: &mut AttrCtxt, validate_cx: &mut AttrCtxt) { + self.process_attr(schemars_cx); + self.process_attr(validate_cx); + } + + fn process_attr(&mut self, cx: &mut AttrCtxt) { + cx.parse_meta(|m, n, c| self.process_meta(m, n, c)); + } + + fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option { + match meta_name { + "length" => match self.length { + Some(_) => cx.duplicate_error(&meta), + None => self.length = parse_length_or_range(meta, cx).ok(), + }, + + "range" => match self.range { + Some(_) => cx.duplicate_error(&meta), + None => self.range = parse_length_or_range(meta, cx).ok(), + }, + + "email" | "url" => self.handle_format(meta, meta_name, cx), + + "required" => { + if self.required { + cx.duplicate_error(&meta); + } else if require_path_only(meta, cx).is_ok() { + self.required = true; + } + } + + "regex" => match (&self.regex, &self.contains, cx.attr_type) { + (Some(_), _, _) => cx.duplicate_error(&meta), + (_, Some(_), _) => cx.mutual_exclusive_error(&meta, "contains"), + (None, None, "schemars") => self.regex = parse_schemars_regex(meta, cx).ok(), + (None, None, "validate") => self.regex = parse_validate_regex(meta, cx).ok(), + (None, None, wat) => panic!("Unexpected attr type `{wat}` for regex item. This is a bug in schemars, please raise an issue!"), + }, + "contains" => match (&self.regex, &self.contains) { + (Some(_), _) => cx.mutual_exclusive_error(&meta, "regex"), + (_, Some(_)) => cx.duplicate_error(&meta), + (None, None) => self.contains = parse_contains(meta, cx).ok(), + }, + + "inner" => { + if let Ok(nested_meta) = parse_nested_meta(meta, cx) { + let inner = self + .inner + .get_or_insert_with(|| Box::new(ValidationAttrs::default())); + let mut inner_cx = cx.new_nested_meta(nested_meta.into_iter().collect()); + inner.process_attr(&mut inner_cx); + } + } + + _ => return Some(meta), + } + + None + } + + fn handle_format(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) { + match &self.format { + Some(f) if f.attr_str() == meta_name => cx.duplicate_error(&meta), + Some(f) => cx.mutual_exclusive_error(&meta, f.attr_str()), + None => { + // FIXME this is too strict - it may be a MetaList in validator attr (e.g. with message/code items) + if require_path_only(meta, cx).is_ok() { + self.format = Some(Format::from_attr_str(meta_name)) + } + } } } } - -fn parse_regex_expr(cx: &Ctxt, value: Expr) -> Option { - match value { - Expr::Lit(ExprLit { - lit: Lit::Str(litstr), - .. - }) => parse_lit_str(&litstr).map_err(|e| cx.syn_error(e)).ok(), - v => Some(v), - } -} diff --git a/schemars_derive/src/idents.rs b/schemars_derive/src/idents.rs new file mode 100644 index 0000000..d940a62 --- /dev/null +++ b/schemars_derive/src/idents.rs @@ -0,0 +1,15 @@ +use proc_macro2::{Ident, Span, TokenStream, TokenTree}; +use quote::TokenStreamExt; + +pub const GENERATOR: ConstIdent = ConstIdent("generator"); +pub const SCHEMA: ConstIdent = ConstIdent("schema"); +pub const STRUCT_DEFAULT: ConstIdent = ConstIdent("struct_default"); + +pub struct ConstIdent(&'static str); + +impl quote::ToTokens for ConstIdent { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ident = Ident::new(self.0, Span::call_site()); + tokens.append(TokenTree::from(ident)); + } +} diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index 6954519..48afe80 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -8,11 +8,11 @@ extern crate proc_macro; mod ast; mod attr; -mod metadata; -mod regex_syntax; +mod idents; mod schema_exprs; use ast::*; +use idents::GENERATOR; use proc_macro2::TokenStream; use syn::spanned::Spanned; @@ -70,12 +70,12 @@ fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> syn::Result::schema_id() } - fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - <#ty as schemars::JsonSchema>::json_schema(generator) + fn json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema { + <#ty as schemars::JsonSchema>::json_schema(#GENERATOR) } - fn _schemars_private_non_optional_json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(generator) + fn _schemars_private_non_optional_json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema { + <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#GENERATOR) } fn _schemars_private_is_option() -> bool { @@ -188,7 +188,7 @@ fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> syn::Result schemars::Schema { + fn json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema { #schema_expr } }; diff --git a/schemars_derive/src/metadata.rs b/schemars_derive/src/metadata.rs deleted file mode 100644 index 0f390c6..0000000 --- a/schemars_derive/src/metadata.rs +++ /dev/null @@ -1,109 +0,0 @@ -use proc_macro2::TokenStream; -use syn::{spanned::Spanned, Expr}; - -#[derive(Debug, Clone)] -pub struct SchemaMetadata<'a> { - pub title: Option<&'a Expr>, - pub description: Option<&'a Expr>, - pub doc: Option<&'a Expr>, - pub deprecated: bool, - pub read_only: bool, - pub write_only: bool, - pub examples: &'a [syn::Path], - pub default: Option, - pub extensions: &'a [(String, TokenStream)], - pub transforms: &'a [Expr], -} - -impl<'a> SchemaMetadata<'a> { - pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) { - let setters = self.make_setters(); - if !setters.is_empty() || !self.transforms.is_empty() { - let apply_transforms = self.transforms.iter().map(|t| { - quote_spanned! {t.span()=> - schemars::transform::Transform::transform(&mut #t, &mut schema); - } - }); - *schema_expr = quote! {{ - let mut schema = #schema_expr; - #(#setters)* - #(#apply_transforms)* - schema - }} - } - } - - fn make_setters(&self) -> Vec { - let mut setters = Vec::::new(); - - if let Some(doc) = &self.doc { - if self.title.is_none() || self.description.is_none() { - setters.push(quote!{ - const title_and_description: (&str, &str) = schemars::_private::get_title_and_description(#doc); - }); - } - } - if let Some(title) = &self.title { - setters.push(quote! { - schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "title", #title); - }); - } else if self.doc.is_some() { - setters.push(quote! { - schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "title", title_and_description.0); - }); - } - if let Some(description) = &self.description { - setters.push(quote! { - schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "description", #description); - }); - } else if self.doc.is_some() { - setters.push(quote! { - schemars::_private::insert_metadata_property_if_nonempty(&mut schema, "description", title_and_description.1); - }); - } - - if self.deprecated { - setters.push(quote! { - schemars::_private::insert_metadata_property(&mut schema, "deprecated", true); - }); - } - - if self.read_only { - setters.push(quote! { - schemars::_private::insert_metadata_property(&mut schema, "readOnly", true); - }); - } - if self.write_only { - setters.push(quote! { - schemars::_private::insert_metadata_property(&mut schema, "writeOnly", true); - }); - } - - if !self.examples.is_empty() { - let examples = self.examples.iter().map(|eg| { - quote! { - schemars::_serde_json::value::to_value(#eg()) - } - }); - setters.push(quote! { - schemars::_private::insert_metadata_property(&mut schema, "examples", schemars::_serde_json::Value::Array([#(#examples),*].into_iter().flatten().collect())); - }); - } - - if let Some(default) = &self.default { - setters.push(quote! { - if let Some(default) = #default.and_then(|d| schemars::_schemars_maybe_to_value!(d)) { - schemars::_private::insert_metadata_property(&mut schema, "default", default); - } - }); - } - - for (k, v) in self.extensions { - setters.push(quote! { - schemars::_private::insert_metadata_property(&mut schema, #k, schemars::_serde_json::json!(#v)); - }); - } - - setters - } -} diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index 5b1eb97..dac1486 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -1,12 +1,55 @@ -use std::collections::HashSet; - -use crate::{ast::*, attr::WithAttr, metadata::SchemaMetadata}; +use crate::{ast::*, attr::WithAttr, idents::*}; use proc_macro2::{Span, TokenStream}; +use quote::ToTokens; use serde_derive_internals::ast::Style; use serde_derive_internals::attr::{self as serde_attr, Default as SerdeDefault, TagType}; +use std::collections::HashSet; use syn::spanned::Spanned; -pub fn expr_for_container(cont: &Container) -> TokenStream { +pub struct SchemaExpr { + /// Definitions for types or functions that may be used within the creator or mutators + definitions: Vec, + /// An expression that produces a `Schema` + creator: TokenStream, + /// Statements (including terminating semicolon) that mutate a var `schema` of type `Schema` + mutators: Vec, +} + +impl From for SchemaExpr { + fn from(creator: TokenStream) -> Self { + Self { + definitions: Vec::new(), + creator, + mutators: Vec::new(), + } + } +} + +impl ToTokens for SchemaExpr { + fn to_tokens(&self, tokens: &mut TokenStream) { + let Self { + definitions, + creator, + mutators, + } = self; + + tokens.extend(if mutators.is_empty() { + quote!({ + #(#definitions)* + #creator + }) + } else { + quote!({ + #(#definitions)* + let mut #SCHEMA = #creator; + #(#mutators)* + #SCHEMA + }) + }); + } +} + +pub fn expr_for_container(cont: &Container) -> SchemaExpr { let mut schema_expr = match &cont.data { Data::Struct(Style::Unit, _) => expr_for_unit_struct(), Data::Struct(Style::Newtype, fields) => expr_for_newtype_struct(&fields[0]), @@ -19,11 +62,11 @@ pub fn expr_for_container(cont: &Container) -> TokenStream { Data::Enum(variants) => expr_for_enum(variants, &cont.serde_attrs), }; - cont.attrs.as_metadata().apply_to_schema(&mut schema_expr); + cont.add_mutators(&mut schema_expr.mutators); schema_expr } -pub fn expr_for_repr(cont: &Container) -> Result { +pub fn expr_for_repr(cont: &Container) -> Result { let repr_type = cont.attrs.repr.as_ref().ok_or_else(|| { syn::Error::new( Span::call_site(), @@ -49,7 +92,7 @@ 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 = quote!({ + let mut schema_expr = SchemaExpr::from(quote!({ let mut map = schemars::_serde_json::Map::new(); map.insert("type".into(), "integer".into()); map.insert( @@ -61,33 +104,33 @@ pub fn expr_for_repr(cont: &Container) -> Result { }), ); schemars::Schema::from(map) - }); + })); + + cont.add_mutators(&mut schema_expr.mutators); - cont.attrs.as_metadata().apply_to_schema(&mut schema_expr); Ok(schema_expr) } -fn expr_for_field(field: &Field, allow_ref: bool) -> TokenStream { +fn expr_for_field(field: &Field, allow_ref: bool) -> SchemaExpr { let (ty, type_def) = type_for_field_schema(field); let span = field.original.span(); - let generator = quote!(generator); - let mut schema_expr = if field.validation_attrs.required() { + let mut schema_expr = SchemaExpr::from(if field.attrs.validation.required { quote_spanned! {span=> - <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#generator) + <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#GENERATOR) } } else if allow_ref { quote_spanned! {span=> - #generator.subschema_for::<#ty>() + #GENERATOR.subschema_for::<#ty>() } } else { quote_spanned! {span=> - <#ty as schemars::JsonSchema>::json_schema(#generator) + <#ty as schemars::JsonSchema>::json_schema(#GENERATOR) } - }; + }); - prepend_type_def(type_def, &mut schema_expr); - field.validation_attrs.apply_to_schema(&mut schema_expr); + schema_expr.definitions.extend(type_def); + field.add_mutators(&mut schema_expr.mutators); schema_expr } @@ -138,7 +181,7 @@ fn type_for_schema(with_attr: &WithAttr) -> (syn::Type, Option) { } } -fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> TokenStream { +fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> SchemaExpr { let deny_unknown_fields = cattrs.deny_unknown_fields(); let variants = variants .iter() @@ -159,7 +202,7 @@ fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> TokenS fn expr_for_external_tagged_enum<'a>( variants: impl Iterator>, deny_unknown_fields: bool, -) -> TokenStream { +) -> SchemaExpr { let mut unique_names = HashSet::<&str>::new(); let mut count = 0; let (unit_variants, complex_variants): (Vec<_>, Vec<_>) = variants @@ -169,7 +212,7 @@ 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 = quote!({ + let unit_schema = SchemaExpr::from(quote!({ let mut map = schemars::_serde_json::Map::new(); map.insert("type".into(), "string".into()); map.insert( @@ -181,7 +224,7 @@ fn expr_for_external_tagged_enum<'a>( }), ); schemars::Schema::from(map) - }); + })); if complex_variants.is_empty() { return unit_schema; @@ -195,21 +238,19 @@ fn expr_for_external_tagged_enum<'a>( schemas.extend(complex_variants.into_iter().map(|variant| { let name = variant.name(); - let mut schema_expr = if variant.is_unit() && variant.attrs.with.is_none() { - quote! { - schemars::_private::new_unit_enum_variant(#name) - } - } else { - let sub_schema = expr_for_untagged_enum_variant(variant, deny_unknown_fields); - quote! { - schemars::_private::new_externally_tagged_enum_variant(#name, #sub_schema) - } - }; + let mut schema_expr = + SchemaExpr::from(if variant.is_unit() && variant.attrs.with.is_none() { + quote! { + schemars::_private::new_unit_enum_variant(#name) + } + } else { + let sub_schema = expr_for_untagged_enum_variant(variant, deny_unknown_fields); + quote! { + schemars::_private::new_externally_tagged_enum_variant(#name, #sub_schema) + } + }); - variant - .attrs - .as_metadata() - .apply_to_schema(&mut schema_expr); + variant.add_mutators(&mut schema_expr.mutators); schema_expr })); @@ -221,7 +262,7 @@ fn expr_for_internal_tagged_enum<'a>( variants: impl Iterator>, tag_name: &str, deny_unknown_fields: bool, -) -> TokenStream { +) -> SchemaExpr { let mut unique_names = HashSet::new(); let mut count = 0; let variant_schemas = variants @@ -229,15 +270,15 @@ fn expr_for_internal_tagged_enum<'a>( unique_names.insert(variant.name()); count += 1; - let name = variant.name(); - let mut schema_expr = expr_for_internal_tagged_enum_variant(variant, deny_unknown_fields); - schema_expr = quote!({ - let mut schema = #schema_expr; - schemars::_private::apply_internal_enum_variant_tag(&mut schema, #tag_name, #name, #deny_unknown_fields); - schema - }); - variant.attrs.as_metadata().apply_to_schema(&mut schema_expr); + + let name = variant.name(); + schema_expr.mutators.push(quote!( + schemars::_private::apply_internal_enum_variant_tag(&mut #SCHEMA, #tag_name, #name, #deny_unknown_fields); + )); + + variant.add_mutators(&mut schema_expr.mutators); + schema_expr }) .collect(); @@ -248,15 +289,12 @@ fn expr_for_internal_tagged_enum<'a>( fn expr_for_untagged_enum<'a>( variants: impl Iterator>, deny_unknown_fields: bool, -) -> TokenStream { +) -> SchemaExpr { let schemas = variants .map(|variant| { let mut schema_expr = expr_for_untagged_enum_variant(variant, deny_unknown_fields); - variant - .attrs - .as_metadata() - .apply_to_schema(&mut schema_expr); + variant.add_mutators(&mut schema_expr.mutators); schema_expr }) @@ -272,7 +310,7 @@ fn expr_for_adjacent_tagged_enum<'a>( tag_name: &str, content_name: &str, deny_unknown_fields: bool, -) -> TokenStream { +) -> SchemaExpr { let mut unique_names = HashSet::new(); let mut count = 0; let schemas = variants @@ -311,27 +349,22 @@ fn expr_for_adjacent_tagged_enum<'a>( TokenStream::new() }; - let mut outer_schema = quote! { - schemars::json_schema!({ - "type": "object", - "properties": { - #tag_name: (#tag_schema), - #add_content_to_props - }, - "required": [ - #tag_name, - #add_content_to_required - ], - // As we're creating a "wrapper" object, we can honor the - // disposition of deny_unknown_fields. - #set_additional_properties - }) - }; + let mut outer_schema = SchemaExpr::from(quote!(schemars::json_schema!({ + "type": "object", + "properties": { + #tag_name: (#tag_schema), + #add_content_to_props + }, + "required": [ + #tag_name, + #add_content_to_required + ], + // As we're creating a "wrapper" object, we can honor the + // disposition of deny_unknown_fields. + #set_additional_properties + }))); - variant - .attrs - .as_metadata() - .apply_to_schema(&mut outer_schema); + variant.add_mutators(&mut outer_schema.mutators); outer_schema }) @@ -342,7 +375,7 @@ 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 { +fn variant_subschemas(unique: bool, schemas: Vec) -> SchemaExpr { let keyword = if unique { "oneOf" } else { "anyOf" }; quote!({ let mut map = schemars::_serde_json::Map::new(); @@ -356,17 +389,18 @@ fn variant_subschemas(unique: bool, schemas: Vec) -> TokenStream { ); schemars::Schema::from(map) }) + .into() } -fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) -> TokenStream { +fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) -> SchemaExpr { if let Some(with_attr) = &variant.attrs.with { let (ty, type_def) = type_for_schema(with_attr); - let generator = quote!(generator); - let mut schema_expr = quote_spanned! {variant.original.span()=> - #generator.subschema_for::<#ty>() - }; + let mut schema_expr = SchemaExpr::from(quote_spanned! {variant.original.span()=> + #GENERATOR.subschema_for::<#ty>() + }); + + schema_expr.definitions.extend(type_def); - prepend_type_def(type_def, &mut schema_expr); return schema_expr; } @@ -381,15 +415,15 @@ fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) fn expr_for_internal_tagged_enum_variant( variant: &Variant, deny_unknown_fields: bool, -) -> TokenStream { +) -> SchemaExpr { if let Some(with_attr) = &variant.attrs.with { let (ty, type_def) = type_for_schema(with_attr); - let generator = quote!(generator); - let mut schema_expr = quote_spanned! {variant.original.span()=> - <#ty as schemars::JsonSchema>::json_schema(#generator) - }; + let mut schema_expr = SchemaExpr::from(quote_spanned! {variant.original.span()=> + <#ty as schemars::JsonSchema>::json_schema(#GENERATOR) + }); + + schema_expr.definitions.extend(type_def); - prepend_type_def(type_def, &mut schema_expr); return schema_expr; } @@ -401,17 +435,18 @@ fn expr_for_internal_tagged_enum_variant( } } -fn expr_for_unit_struct() -> TokenStream { +fn expr_for_unit_struct() -> SchemaExpr { quote! { - generator.subschema_for::<()>() + #GENERATOR.subschema_for::<()>() } + .into() } -fn expr_for_newtype_struct(field: &Field) -> TokenStream { +fn expr_for_newtype_struct(field: &Field) -> SchemaExpr { expr_for_field(field, true) } -fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream { +fn expr_for_tuple_struct(fields: &[Field]) -> SchemaExpr { let fields: Vec<_> = fields .iter() .filter(|f| !f.serde_attrs.skip_deserializing()) @@ -427,74 +462,70 @@ fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream { "maxItems": #len, }) } + .into() } fn expr_for_struct( fields: &[Field], default: &SerdeDefault, deny_unknown_fields: bool, -) -> TokenStream { +) -> SchemaExpr { let set_container_default = match default { SerdeDefault::None => None, - SerdeDefault::Default => Some(quote!(let container_default = Self::default();)), - SerdeDefault::Path(path) => Some(quote!(let container_default = #path();)), + SerdeDefault::Default => Some(quote!(let #STRUCT_DEFAULT = Self::default();)), + SerdeDefault::Path(path) => Some(quote!(let #STRUCT_DEFAULT = #path();)), }; - let properties: Vec<_> = fields + // a vec of mutators + let properties: Vec = fields .iter() .filter(|f| !f.serde_attrs.skip_deserializing() || !f.serde_attrs.skip_serializing()) .map(|field| { if field.serde_attrs.flatten() { let (ty, type_def) = type_for_field_schema(field); - let required = field.validation_attrs.required(); + let required = field.attrs.validation.required; + let mut schema_expr = SchemaExpr::from(quote_spanned! {ty.span()=> + schemars::_private::json_schema_for_flatten::<#ty>(#GENERATOR, #required) + }); - let args = quote!(generator, #required); - let mut schema_expr = quote_spanned! {ty.span()=> - schemars::_private::json_schema_for_flatten::<#ty>(#args) - }; - - prepend_type_def(type_def, &mut schema_expr); + schema_expr.definitions.extend(type_def); quote! { - schemars::_private::flatten(&mut schema, #schema_expr); + schemars::_private::flatten(&mut #SCHEMA, #schema_expr); } } else { let name = field.name(); - let default = field_default_expr(field, set_container_default.is_some()); - let (ty, type_def) = type_for_field_schema(field); - let has_default = default.is_some(); - let required = field.validation_attrs.required(); + let has_default = set_container_default.is_some() || !field.serde_attrs.default().is_none(); + let required = field.attrs.validation.required; - let metadata = SchemaMetadata { - read_only: field.serde_attrs.skip_deserializing(), - write_only: field.serde_attrs.skip_serializing(), - default, - ..field.attrs.as_metadata() - }; - - let generator = quote!(generator); - let mut schema_expr = if field.validation_attrs.required() { + let mut schema_expr = SchemaExpr::from(if field.attrs.validation.required { quote_spanned! {ty.span()=> - <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#generator) + <#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#GENERATOR) } } else { quote_spanned! {ty.span()=> - #generator.subschema_for::<#ty>() + #GENERATOR.subschema_for::<#ty>() } - }; + }); - metadata.apply_to_schema(&mut schema_expr); - field.validation_attrs.apply_to_schema(&mut schema_expr); + field.add_mutators(&mut schema_expr.mutators); + if let Some(default) = field_default_expr(field, set_container_default.is_some()) { + schema_expr.mutators.push(quote! { + #default.and_then(|d| schemars::_schemars_maybe_to_value!(d)) + .map(|d| schemars::_private::insert_metadata_property(&mut #SCHEMA, "default", d)); + }) + } - quote! { - { - #type_def - schemars::_private::insert_object_property::<#ty>(&mut schema, #name, #has_default, #required, #schema_expr); - } - }} + // embed `#type_def` outside of `#schema_expr`, because it's used as the type param + // (i.e. `#type_def` is the definition of `#ty`) + quote!({ + #type_def + schemars::_private::insert_object_property::<#ty>(&mut #SCHEMA, #name, #has_default, #required, #schema_expr); + }) + } }) .collect(); @@ -506,15 +537,14 @@ fn expr_for_struct( TokenStream::new() }; - quote! ({ - #set_container_default - let mut schema = schemars::json_schema!({ + SchemaExpr { + definitions: set_container_default.into_iter().collect(), + creator: quote!(schemars::json_schema!({ "type": "object", #set_additional_properties - }); - #(#properties)* - schema - }) + })), + mutators: properties, + } } fn field_default_expr(field: &Field, container_has_default: bool) -> Option { @@ -527,7 +557,7 @@ fn field_default_expr(field: &Field, container_has_default: bool) -> Option { let member = &field.member; - quote!(container_default.#member) + quote!(#STRUCT_DEFAULT.#member) } SerdeDefault::Default => quote!(<#ty>::default()), SerdeDefault::Path(path) => quote!(#path()), @@ -571,14 +601,3 @@ fn field_default_expr(field: &Field, container_has_default: bool) -> Option, schema_expr: &mut TokenStream) { - if let Some(type_def) = type_def { - *schema_expr = quote! { - { - #type_def - #schema_expr - } - } - } -}