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
- }
- }
- }
-}