Rewrite attribute handling code (#330)
This commit is contained in:
parent
fb6bd6d439
commit
d07a1be031
33 changed files with 1195 additions and 1099 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "phone"
|
||||
"format": "email"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"properties": {
|
||||
"StringNewType": {
|
||||
"type": "string",
|
||||
"format": "phone"
|
||||
"format": "email"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "phone"
|
||||
"format": "email"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"properties": {
|
||||
"StringNewType": {
|
||||
"type": "string",
|
||||
"format": "phone"
|
||||
"format": "email"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
//
|
|
@ -5,7 +5,8 @@
|
|||
"prefixItems": [
|
||||
{
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
"format": "float",
|
||||
"writeOnly": true
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
| ^^^^^
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
285
schemars_derive/src/attr/parse_meta.rs
Normal file
285
schemars_derive/src/attr/parse_meta.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
|
|
|
@ -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, "e!(&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, "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 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
15
schemars_derive/src/idents.rs
Normal file
15
schemars_derive/src/idents.rs
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue