Rewrite attribute handling code (#330)

This commit is contained in:
Graham Esau 2024-08-27 16:50:47 +01:00 committed by GitHub
parent fb6bd6d439
commit d07a1be031
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1195 additions and 1099 deletions

View file

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

View file

@ -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)
<div class="indented">
<h3 id="email-phone-url">
<h3 id="email-url">
`#[validate(email)]` / `#[schemars(email)]`<br />
`#[validate(phone)]` / `#[schemars(phone)]`<br />
`#[validate(url)]` / `#[schemars(url)]`
</h3>
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)
<h3 id="length">
@ -212,20 +211,20 @@ Validator docs: [range](https://github.com/Keats/validator#range)
<h3 id="regex">
`#[validate(regex = "path::to::regex")]` / `#[schemars(regex = "path::to::regex")]`<br />
`#[schemars(regex(pattern = r"^\d+$"))]`
`#[validate(regex(path = *static_regex)]`<br />
`#[schemars(regex(pattern = r"^\d+$"))]` / `#[schemars(regex(pattern = *static_regex))]`
</h3>
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)
<h3 id="contains">
`#[validate(contains = "string")]` / `#[schemars(contains = "string")]`
`#[validate(contains(pattern = "string"))]` / `#[schemars(contains(pattern = "string"))]`
</h3>
@ -236,13 +235,12 @@ Validator docs: [contains](https://github.com/Keats/validator#contains)
<h3 id="required">
`#[validate(required)]` / `#[schemars(required)]`<br />
`#[validate(required_nested)]`
</h3>
When set on an `Option<T>` 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)
</div>

View file

@ -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<f32>,

View file

@ -42,7 +42,7 @@
"anyOf": [
{
"type": "string",
"format": "phone"
"format": "email"
},
{
"type": "object",

View file

@ -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<f32>,

View file

@ -19,7 +19,7 @@
"properties": {
"StringNewType": {
"type": "string",
"format": "phone"
"format": "email"
}
},
"additionalProperties": false,

View file

@ -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<f32>,

View file

@ -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<f32>,

View file

@ -191,7 +191,7 @@ Serde docs: [container](https://serde.rs/container-attrs.html#bound)
<h3 id="email-phone-url">
`#[validate(email)]` / `#[schemars(email)]`<br />
`#[validate(phone)]` / `#[schemars(phone)]`<br />
`#[validate(email)]` / `#[schemars(email)]`<br />
`#[validate(url)]` / `#[schemars(url)]`
</h3>

View file

@ -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<f32>,

View file

@ -42,7 +42,7 @@
"anyOf": [
{
"type": "string",
"format": "phone"
"format": "email"
},
{
"type": "object",

View file

@ -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<f32>,

View file

@ -19,7 +19,7 @@
"properties": {
"StringNewType": {
"type": "string",
"format": "phone"
"format": "email"
}
},
"additionalProperties": false,

View file

@ -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;
@ -140,6 +141,13 @@ pub fn insert_object_property<T: ?Sized + JsonSchema>(
has_default: bool,
required: bool,
sub_schema: Schema,
) {
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
@ -150,7 +158,7 @@ pub fn insert_object_property<T: ?Sized + JsonSchema>(
properties.insert(key.to_owned(), sub_schema.into());
}
if !has_default && (required || !T::_schemars_private_is_option()) {
if !has_default && (required) {
if let Some(req) = obj
.entry("required")
.or_insert(Value::Array(Vec::new()))
@ -161,6 +169,10 @@ pub fn insert_object_property<T: ?Sized + JsonSchema>(
}
}
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<Value>) {
schema.ensure_object().insert(key.to_owned(), value.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);
}

View file

@ -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
//

View file

@ -5,7 +5,8 @@
"prefixItems": [
{
"type": "number",
"format": "float"
"format": "float",
"writeOnly": true
},
{
"type": "null"

View file

@ -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",

View file

@ -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",

View file

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

View file

@ -1,53 +1,95 @@
error: unknown schemars attribute `foo`
--> tests/ui/invalid_validation_attrs.rs:11:38
error: expected validate regex attribute item to be of the form `regex(...)`
--> tests/ui/invalid_validation_attrs.rs:5:31
|
11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
| ^^^
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
error: expected schemars regex attribute item to be of the form `regex(...)`
--> tests/ui/invalid_validation_attrs.rs:9:31
|
11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
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
--> tests/ui/invalid_validation_attrs.rs:9:59
|
11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
| ^^^^^
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
error: unknown item in schemars length attribute: `bar`
--> tests/ui/invalid_validation_attrs.rs:9:70
|
11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
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,
| ^^^^^

View file

@ -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<i32>,
#[validate(contains = "map_key")]
#[validate(contains(pattern = "map_key"))]
map_contains: BTreeMap<String, ()>,
#[validate(required)]
required_option: Option<bool>,
@ -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<i32>,
#[schemars(contains = "map_key")]
#[schemars(contains(pattern = "map_key"))]
map_contains: BTreeMap<String, ()>,
#[schemars(required)]
required_option: Option<bool>,

View file

@ -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<String>,
#[schemars(inner(length(min = 1, max = 100)))]
vec_str_length: Vec<&'a str>,

View file

@ -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),
})
}
}

View file

@ -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<Field<'a>>,
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<TokenStream>) {
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<TokenStream>) {
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<TokenStream>) {
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);
});
}
}
}

View file

@ -1,21 +1,16 @@
use proc_macro2::TokenStream;
use quote::ToTokens;
use quote::TokenStreamExt;
use syn::Attribute;
pub fn get_doc(attrs: &[Attribute]) -> Option<syn::Expr> {
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

View file

@ -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<WithAttr>,
pub title: Option<Expr>,
pub description: Option<Expr>,
pub struct CommonAttrs {
pub doc: Option<Expr>,
pub deprecated: bool,
pub examples: Vec<syn::Path>,
pub repr: Option<syn::Type>,
pub crate_name: Option<syn::Path>,
pub is_renamed: bool,
pub title: Option<Expr>,
pub description: Option<Expr>,
pub examples: Vec<Path>,
pub extensions: Vec<(String, TokenStream)>,
pub transforms: Vec<Expr>,
}
#[derive(Debug, Default)]
pub struct FieldAttrs {
pub common: CommonAttrs,
pub with: Option<WithAttr>,
pub validation: ValidationAttrs,
}
#[derive(Debug, Default)]
pub struct ContainerAttrs {
pub common: CommonAttrs,
pub repr: Option<Type>,
pub crate_name: Option<Path>,
pub is_renamed: bool,
}
#[derive(Debug, Default)]
pub struct VariantAttrs {
pub common: CommonAttrs,
pub with: Option<WithAttr>,
}
#[derive(Debug)]
pub enum WithAttr {
Type(syn::Type),
Function(syn::Path),
}
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);
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());
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,
}
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)
}
};
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)
}
};
&mut self,
attrs: &[Attribute],
schemars_cx: &mut AttrCtxt,
serde_cx: &mut AttrCtxt,
) {
self.process_attr(schemars_cx);
self.process_attr(serde_cx);
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)),
}
}
self.doc = doc::get_doc(attrs);
self.deprecated = attrs.iter().any(|a| a.path().is_ident("deprecated"));
}
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)),
}
}
fn process_attr(&mut self, cx: &mut AttrCtxt) {
cx.parse_meta(|m, n, c| self.process_meta(m, n, c));
}
Meta::NameValue(m) if m.path.is_ident("title") => match self.title {
Some(_) => duplicate_error(m),
None => self.title = Some(m.value.clone()),
fn process_meta(&mut self, meta: Meta, meta_name: &str, cx: &AttrCtxt) -> Option<Meta> {
match meta_name {
"title" => match self.title {
Some(_) => cx.duplicate_error(&meta),
None => self.title = parse_name_value_expr(meta, cx).ok(),
},
Meta::NameValue(m) if m.path.is_ident("description") => match self.description {
Some(_) => duplicate_error(m),
None => self.description = Some(m.value.clone()),
"description" => match self.description {
Some(_) => cx.duplicate_error(&meta),
None => self.description = parse_name_value_expr(meta, cx).ok(),
},
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)
}
"example" => {
self.examples.extend(parse_name_value_lit_str(meta, cx));
}
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)
"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.crate_name = Some(p)
self.extensions.push((ex.key_str, ex.value));
}
}
}
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::<syn::Expr>(lit_str).is_ok() {
errors.error_spanned_by(
&m.value,
format!(
if lit_str.parse::<Expr>().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::<Extension, Token![,]>::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));
self.transforms.push(expr);
}
}
}
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),
);
}
}
}
}
self
_ => return Some(meta),
}
pub fn is_default(&self) -> bool {
matches!(self, Self {
with: None,
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<TokenStream>) {
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
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<Meta> {
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
}
}
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()
.chain(validation::VALIDATION_KEYWORDS);
meta.path()
.get_ident()
.map(|i| known_keywords.any(|k| i == k))
.unwrap_or(false)
.find(|a| a.path().is_ident("repr"))
.and_then(|a| a.parse_args().ok());
}
fn get_meta_items(
attrs: &[syn::Attribute],
attr_type: &'static str,
errors: &Ctxt,
ignore_errors: bool,
) -> Vec<Meta> {
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<Meta> {
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<Meta> {
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<Meta> {
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::<Meta, Token![,]>::parse_terminated)
{
match attr.parse_args_with(Punctuated::<Meta, Token![,]>::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
metas: Vec<Meta>,
}
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),
}
}
pub fn new_nested_meta(&self, metas: Vec<Meta>) -> Self {
Self { metas, ..*self }
}
pub fn parse_meta(&mut self, mut handle: impl FnMut(Meta, &str, &Self) -> Option<Meta>) {
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<A: ToTokens, T: std::fmt::Display>(&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,
),
);
Err(())
}
}
fn parse_lit_into_ty(
cx: &Ctxt,
attr_type: &'static str,
meta_item_name: &'static str,
lit: &syn::Expr,
) -> Result<syn::Type, ()> {
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()
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())
),
)
})
);
}
}
}
fn parse_lit_into_path(
cx: &Ctxt,
attr_type: &'static str,
meta_item_name: &'static str,
expr: &syn::Expr,
) -> Result<syn::Path, ()> {
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()
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 parse_lit_str<T>(s: &syn::LitStr) -> parse::Result<T>
where
T: Parse,
{
let tokens = spanned_tokens(s)?;
syn::parse2(tokens)
}
fn spanned_tokens(s: &syn::LitStr) -> parse::Result<TokenStream> {
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<Self> {
let key = input.parse::<LitStr>()?;
input.parse::<Token![=]>()?;
let mut value = TokenStream::new();
while !input.is_empty() && !input.peek(Token![,]) {
value.extend([input.parse::<TokenTree>()?]);
}
if value.is_empty() {
return Err(syn::Error::new(input.span(), "Expected extension value"));
}
Ok(Extension { key, value })
}
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)
}

View file

@ -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<Expr, ()> {
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<T: Parse>(meta: Meta, cx: &AttrCtxt) -> Result<T, ()> {
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<T: Parse>(lit_str: LitStr, cx: &AttrCtxt) -> Result<T, ()> {
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::<T>()
.rsplit("::")
.next()
.unwrap_or_default()
.to_ascii_lowercase(),
),
);
})
}
pub fn parse_extensions(
meta: Meta,
cx: &AttrCtxt,
) -> Result<impl IntoIterator<Item = Extension>, ()> {
let parser = Punctuated::<Extension, Token![,]>::parse_terminated;
parse_meta_list(meta, cx, parser)
}
pub fn parse_length_or_range(outer_meta: Meta, cx: &AttrCtxt) -> Result<LengthOrRange, ()> {
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<Expr, ()> {
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<Expr, ()> {
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<Expr, ()> {
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<impl IntoIterator<Item = Meta>, ()> {
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
parse_meta_list(meta, cx, parser)
}
fn parse_meta_list<F: Parser>(meta: Meta, cx: &AttrCtxt, parser: F) -> Result<F::Output, ()> {
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<Expr, ()> {
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<Expr>,
pub max: Option<Expr>,
pub equal: Option<Expr>,
}
#[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<Self> {
let key = input.parse::<LitStr>()?;
input.parse::<Token![=]>()?;
let mut value = TokenStream::new();
while !input.is_empty() && !input.peek(Token![,]) {
value.extend([input.parse::<TokenTree>()?]);
}
if value.is_empty() {
return Err(syn::Error::new(input.span(), "Expected extension value"));
}
Ok(Extension {
key_str: key.value(),
key_lit: key,
value,
})
}
}

View file

@ -67,7 +67,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
// 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<Attribute>) {
}
// 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())

View file

@ -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<Expr>,
length_max: Option<Expr>,
length_equal: Option<Expr>,
range_min: Option<Expr>,
range_max: Option<Expr>,
regex: Option<Expr>,
contains: Option<String>,
required: bool,
format: Option<Format>,
inner: Option<Box<ValidationAttrs>>,
pub length: Option<LengthOrRange>,
pub range: Option<LengthOrRange>,
pub regex: Option<Expr>,
pub contains: Option<Expr>,
pub required: bool,
pub format: Option<Format>,
pub inner: Option<Box<ValidationAttrs>>,
}
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<TokenStream>) {
self.add_mutators2(mutators, &quote!(&mut #SCHEMA));
}
pub fn required(&self) -> bool {
self.required
fn add_mutators2(&self, mutators: &mut Vec<TokenStream>, 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);
}
fn populate(
mut self,
meta_items: Vec<Meta>,
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::<syn::Meta, Token![,]>::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<TokenStream> {
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);
});
}
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, &quote!(inner_schema));
if !inner_mutators.is_empty() {
mutators.push(quote! {
schemars::_private::apply_inner_validation(#mut_ref_schema, |inner_schema| { #(#inner_mutators)* });
})
}
}
}
result
fn add_length_or_range(
value: &LengthOrRange,
mutators: &mut Vec<TokenStream>,
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);
});
}
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);
});
}
}
fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, expr: Expr) -> Option<Expr> {
// 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;
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<Meta> {
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),
}
};
match lit {
Lit::Str(s) => parse_lit_str::<ExprPath>(&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
}
}
}
fn parse_regex_expr(cx: &Ctxt, value: Expr) -> Option<Expr> {
match value {
Expr::Lit(ExprLit {
lit: Lit::Str(litstr),
..
}) => parse_lit_str(&litstr).map_err(|e| cx.syn_error(e)).ok(),
v => Some(v),
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))
}
}
}
}
}

View file

@ -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));
}
}

View file

@ -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<To
<#ty as schemars::JsonSchema>::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<To
#schema_id
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
fn json_schema(#GENERATOR: &mut schemars::SchemaGenerator) -> schemars::Schema {
#schema_expr
}
};

View file

@ -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<TokenStream>,
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<TokenStream> {
let mut setters = Vec::<TokenStream>::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
}
}

View file

@ -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<TokenStream>,
/// An expression that produces a `Schema`
creator: TokenStream,
/// Statements (including terminating semicolon) that mutate a var `schema` of type `Schema`
mutators: Vec<TokenStream>,
}
impl From<TokenStream> 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<TokenStream, syn::Error> {
pub fn expr_for_repr(cont: &Container) -> Result<SchemaExpr, syn::Error> {
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<TokenStream, syn::Error> {
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<TokenStream, syn::Error> {
}),
);
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<TokenStream>) {
}
}
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<Item = &'a Variant<'a>>,
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,7 +238,8 @@ 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() {
let mut schema_expr =
SchemaExpr::from(if variant.is_unit() && variant.attrs.with.is_none() {
quote! {
schemars::_private::new_unit_enum_variant(#name)
}
@ -204,12 +248,9 @@ fn expr_for_external_tagged_enum<'a>(
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<Item = &'a Variant<'a>>,
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<Item = &'a Variant<'a>>,
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,8 +349,7 @@ fn expr_for_adjacent_tagged_enum<'a>(
TokenStream::new()
};
let mut outer_schema = quote! {
schemars::json_schema!({
let mut outer_schema = SchemaExpr::from(quote!(schemars::json_schema!({
"type": "object",
"properties": {
#tag_name: (#tag_schema),
@ -325,13 +362,9 @@ fn expr_for_adjacent_tagged_enum<'a>(
// 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>) -> TokenStream {
fn variant_subschemas(unique: bool, schemas: Vec<SchemaExpr>) -> 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>) -> 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<TokenStream> = 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! {
{
// 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);
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<TokenStream> {
@ -527,7 +557,7 @@ fn field_default_expr(field: &Field, container_has_default: bool) -> Option<Toke
let default_expr = match field_default {
SerdeDefault::None => {
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<Toke
default_expr
})
}
fn prepend_type_def(type_def: Option<TokenStream>, schema_expr: &mut TokenStream) {
if let Some(type_def) = type_def {
*schema_expr = quote! {
{
#type_def
#schema_expr
}
}
}
}