Read #[garde(...)]
attributes in addition to #[validate(...)]
(#331)
This commit is contained in:
parent
56cdd45c5a
commit
9770301218
16 changed files with 421 additions and 87 deletions
|
@ -173,3 +173,23 @@ fn my_transform2(schema: &mut Schema) {
|
|||
let mut schema = schemars::schema_for!(str);
|
||||
RecursiveTransform(my_transform2).transform(&mut schema);
|
||||
```
|
||||
|
||||
## Changes to `#[validate(...)]` attributes
|
||||
|
||||
Since [adding support for `#[validate(...)]` attributes](https://graham.cool/schemars/v0/deriving/attributes/#supported-validator-attributes), the [Validator](https://github.com/Keats/validator) crate has made several changes to its supported attributes. Accordingly, Schemars 1.0 has updated its handling of `#[validate(...)]` attributes to match the latest version (currently 0.18.1) of the Validator crate - this removes some attributes, and changes the syntax of others:
|
||||
|
||||
- The `#[validate(phone)]`/`#[schemars(phone)]` attribute is removed. If you want the old behaviour of setting the "format" property on the generated schema, you can use `#[schemars(extend("format = "phone"))]` instead.
|
||||
- The `#[validate(required_nested)]`/`#[schemars(required_nested)]` attribute is removed. If you want the old behaviour, you can use `#[schemars(required)]` instead.
|
||||
- The `#[validate(regex = "...")]`/`#[schemars(regex = "...")]` attribute can no longer use `name = "value"` syntax. Instead, you can use:
|
||||
|
||||
- `#[validate(regex(path = ...)]`
|
||||
- `#[schemars(regex(pattern = ...)]`
|
||||
- `#[schemars(pattern(...)]` (Garde-style)
|
||||
|
||||
- Similarly, the `#[validate(contains = "...")]`/`#[schemars(contains = "...")]` attribute can no longer use `name = "value"` syntax. Instead, you can use:
|
||||
|
||||
- `#[validate(contains(pattern = ...))]`
|
||||
- `#[schemars(contains(pattern = ...))]`
|
||||
- `#[schemars(contains(...))]` (Garde-style)
|
||||
|
||||
As an alternative option, Schemars 1.0 also adds support for `#[garde(...)]` attributes used with the [Garde](https://github.com/jprochazk/garde) crate, along with equivalent `#[schemars(...)]` attributes. See [the documentation](https://graham.cool/schemars/deriving/attributes/#supported-validatorgarde-attributes) for a list of all supported attributes.
|
||||
|
|
|
@ -8,6 +8,7 @@ permalink: /deriving/attributes/
|
|||
<style>
|
||||
h3 code {
|
||||
font-weight: bold;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ You can add attributes to your types to customize Schemars's derived `JsonSchema
|
|||
|
||||
[Serde](https://serde.rs/) allows setting `#[serde(...)]` attributes which change how types are serialized, and Schemars will generally respect these attributes to ensure that generated schemas will match how the type is serialized by serde_json. `#[serde(...)]` attributes can be overriden using `#[schemars(...)]` attributes, which behave identically (e.g. `#[schemars(rename_all = "camelCase")]`). You may find this useful if you want to change the generated schema without affecting Serde's behaviour, or if you're just not using Serde.
|
||||
|
||||
[Validator](https://github.com/Keats/validator) allows setting `#[validate(...)]` attributes to restrict valid values of particular fields, many of which will be used by Schemars to generate more accurate schemas. These can also be overridden by `#[schemars(...)]` attributes.
|
||||
[Validator](https://github.com/Keats/validator) and [Garde](https://github.com/jprochazk/garde) allow setting `#[validate(...)]`/`#[garde(...)]` attributes to restrict valid values of particular fields, many of which will be used by Schemars to generate more accurate schemas. These can also be overridden by `#[schemars(...)]` attributes.
|
||||
|
||||
<details open>
|
||||
<summary style="font-weight: bold">
|
||||
|
@ -23,11 +23,11 @@ TABLE OF CONTENTS
|
|||
- [`flatten`](#flatten)
|
||||
- [`with`](#with)
|
||||
- [`bound`](#bound)
|
||||
1. [Supported Validator Attributes](#supported-validator-attributes)
|
||||
- [`email` / `url`](#email-url)
|
||||
1. [Supported Validator/Garde Attributes](#supported-validatorgarde-attributes)
|
||||
- [`email` / `url` / `ip` / `ipv4` / `ipv6`](#formats)
|
||||
- [`length`](#length)
|
||||
- [`range`](#range)
|
||||
- [`regex`](#regex)
|
||||
- [`regex` / `pattern`](#regex)
|
||||
- [`contains`](#contains)
|
||||
- [`required`](#required)
|
||||
1. [Other Attributes](#other-attributes)
|
||||
|
@ -184,25 +184,28 @@ Serde docs: [container](https://serde.rs/container-attrs.html#bound)
|
|||
|
||||
</div>
|
||||
|
||||
## Supported Validator Attributes
|
||||
## Supported Validator/Garde Attributes
|
||||
|
||||
<div class="indented">
|
||||
|
||||
<h3 id="email-url">
|
||||
<h3 id="formats">
|
||||
|
||||
`#[validate(email)]` / `#[schemars(email)]`<br />
|
||||
`#[validate(url)]` / `#[schemars(url)]`
|
||||
`#[validate(email)]` / `#[garde(email)]` / `#[schemars(email)]`<br />
|
||||
`#[validate(url)]` / `#[garde(url)]`/ `#[schemars(url)]`<br />
|
||||
`#[garde(ip)]`/ `#[schemars(ip)]`<br />
|
||||
`#[garde(ipv4)]`/ `#[schemars(ipv4)]`<br />
|
||||
`#[garde(ipv6)]`/ `#[schemars(ip)v6]`<br />
|
||||
|
||||
</h3>
|
||||
|
||||
Sets the schema's `format` to `email`/`uri`, as appropriate. Only one of these attributes may be present on a single field.
|
||||
Sets the schema's `format` to `email`/`uri`/`ip`/`ipv4`/`ipv6`, as appropriate. Only one of these attributes may be present on a single field.
|
||||
|
||||
Validator docs: [email](https://github.com/Keats/validator#email) / [url](https://github.com/Keats/validator#url)
|
||||
|
||||
<h3 id="length">
|
||||
|
||||
`#[validate(length(min = 1, max = 10))]` / `#[schemars(length(min = 1, max = 10))]`<br />
|
||||
`#[validate(length(equal = 10))]` / `#[schemars(length(equal = 10))]`
|
||||
`#[validate(length(min = 1, max = 10))]` / `#[garde(length(min = 1, max = 10))]` / `#[schemars(length(min = 1, max = 10))]`<br />
|
||||
`#[validate(length(equal = 10))]` / `#[garde(length(equal = 10))]` / `#[schemars(length(equal = 10))]`
|
||||
|
||||
</h3>
|
||||
|
||||
|
@ -212,7 +215,7 @@ Validator docs: [length](https://github.com/Keats/validator#length)
|
|||
|
||||
<h3 id="range">
|
||||
|
||||
`#[validate(range(min = 1, max = 10))]` / `#[schemars(range(min = 1, max = 10))]`
|
||||
`#[validate(range(min = 1, max = 10))]` / `#[garde(range(min = 1, max = 10))]` / `#[schemars(range(min = 1, max = 10))]`
|
||||
|
||||
</h3>
|
||||
|
||||
|
@ -223,29 +226,31 @@ Validator docs: [range](https://github.com/Keats/validator#range)
|
|||
<h3 id="regex">
|
||||
|
||||
`#[validate(regex(path = *static_regex)]`<br />
|
||||
`#[schemars(regex(pattern = r"^\d+$"))]` / `#[schemars(regex(pattern = *static_regex))]`
|
||||
`#[schemars(regex(pattern = r"^\d+$"))]` / `#[schemars(regex(pattern = *static_regex))]`<br />
|
||||
`#[garde(pattern(r"^\d+$")]` / `#[schemars(pattern(r"^\d+$")]`/ `#[schemars(pattern(*static_regex)]`
|
||||
|
||||
</h3>
|
||||
|
||||
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.
|
||||
|
||||
`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.
|
||||
`regex(pattern = ...)` is a Schemars extension, and not currently supported by the Validator crate. When using this form (or the Garde-style `pattern` attribute), 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(pattern = "string"))]` / `#[schemars(contains(pattern = "string"))]`
|
||||
`#[validate(contains(pattern = "string"))]` / `#[schemars(contains(pattern = "string"))]`<br />
|
||||
`#[garde(contains("string"))]` / `#[schemars(contains("string"))]`
|
||||
|
||||
</h3>
|
||||
|
||||
For string schemas, sets the `pattern` property to the given value, with any regex special characters escaped. For object schemas (e.g. when the attribute is set on a HashMap field), includes the value in the `required` property, indicating that the map must contain it as a key.
|
||||
For string schemas, sets the `pattern` property to the given value, with any regex special characters escaped.
|
||||
|
||||
Validator docs: [contains](https://github.com/Keats/validator#contains)
|
||||
|
||||
<h3 id="required">
|
||||
|
||||
`#[validate(required)]` / `#[schemars(required)]`<br />
|
||||
`#[validate(required)]` / `#[garde(required)]` / `#[schemars(required)]`<br />
|
||||
|
||||
</h3>
|
||||
|
||||
|
@ -305,7 +310,7 @@ Set the path to the schemars crate instance the generated code should depend on.
|
|||
|
||||
</h3>
|
||||
|
||||
Sets properties specified by [validator attributes](#supported-validator-attributes) on items of an array schema. For example:
|
||||
Sets properties specified by [validator attributes](#supported-validatorgarde-attributes) on items of an array schema. For example:
|
||||
|
||||
```rust
|
||||
struct Struct {
|
||||
|
|
|
@ -199,26 +199,9 @@ pub fn insert_validation_property(
|
|||
}
|
||||
}
|
||||
|
||||
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(contain);
|
||||
if !array.contains(&value) {
|
||||
array.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn must_contain(schema: &mut Schema, substring: &str) {
|
||||
let escaped = regex_syntax::escape(substring);
|
||||
insert_validation_property(schema, "string", "pattern", escaped);
|
||||
}
|
||||
|
||||
pub fn apply_inner_validation(schema: &mut Schema, f: fn(&mut Schema) -> ()) {
|
||||
|
|
74
schemars/tests/expected/garde.json
Normal file
74
schemars/tests/expected/garde.json
Normal file
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Struct",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min_max": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"minimum": 0.01,
|
||||
"maximum": 100
|
||||
},
|
||||
"min_max2": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"minimum": 1,
|
||||
"maximum": 1000
|
||||
},
|
||||
"regex_str1": {
|
||||
"type": "string",
|
||||
"pattern": "^[Hh]ello\\b"
|
||||
},
|
||||
"contains_str1": {
|
||||
"type": "string",
|
||||
"pattern": "substring\\.\\.\\."
|
||||
},
|
||||
"email_address": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"non_empty_str": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"non_empty_str2": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 1000
|
||||
},
|
||||
"pair": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"required_option": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"x": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"min_max",
|
||||
"min_max2",
|
||||
"regex_str1",
|
||||
"contains_str1",
|
||||
"email_address",
|
||||
"homepage",
|
||||
"non_empty_str",
|
||||
"non_empty_str2",
|
||||
"pair",
|
||||
"required_option",
|
||||
"x"
|
||||
]
|
||||
}
|
8
schemars/tests/expected/garde_newtype.json
Normal file
8
schemars/tests/expected/garde_newtype.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "NewType",
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0,
|
||||
"maximum": 10
|
||||
}
|
74
schemars/tests/expected/garde_schemars_attrs.json
Normal file
74
schemars/tests/expected/garde_schemars_attrs.json
Normal file
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Struct2",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min_max": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"minimum": 0.01,
|
||||
"maximum": 100
|
||||
},
|
||||
"min_max2": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"minimum": 1,
|
||||
"maximum": 1000
|
||||
},
|
||||
"regex_str1": {
|
||||
"type": "string",
|
||||
"pattern": "^[Hh]ello\\b"
|
||||
},
|
||||
"contains_str1": {
|
||||
"type": "string",
|
||||
"pattern": "substring\\.\\.\\."
|
||||
},
|
||||
"email_address": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"non_empty_str": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"non_empty_str2": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 1000
|
||||
},
|
||||
"pair": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"required_option": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"x": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"min_max",
|
||||
"min_max2",
|
||||
"regex_str1",
|
||||
"contains_str1",
|
||||
"email_address",
|
||||
"homepage",
|
||||
"non_empty_str",
|
||||
"non_empty_str2",
|
||||
"pair",
|
||||
"required_option",
|
||||
"x"
|
||||
]
|
||||
}
|
18
schemars/tests/expected/garde_tuple.json
Normal file
18
schemars/tests/expected/garde_tuple.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Tuple",
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0,
|
||||
"maximum": 10
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
}
|
|
@ -58,15 +58,6 @@
|
|||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"map_contains": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "null"
|
||||
},
|
||||
"required": [
|
||||
"map_key"
|
||||
]
|
||||
},
|
||||
"required_option": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
@ -87,7 +78,6 @@
|
|||
"non_empty_str",
|
||||
"non_empty_str2",
|
||||
"pair",
|
||||
"map_contains",
|
||||
"required_option",
|
||||
"x"
|
||||
]
|
||||
|
|
|
@ -58,15 +58,6 @@
|
|||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"map_contains": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "null"
|
||||
},
|
||||
"required": [
|
||||
"map_key"
|
||||
]
|
||||
},
|
||||
"required_option": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
@ -87,7 +78,6 @@
|
|||
"non_empty_str",
|
||||
"non_empty_str2",
|
||||
"pair",
|
||||
"map_contains",
|
||||
"required_option",
|
||||
"x"
|
||||
]
|
||||
|
|
99
schemars/tests/garde.rs
Normal file
99
schemars/tests/garde.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
mod util;
|
||||
use schemars::JsonSchema;
|
||||
use util::*;
|
||||
|
||||
const MIN: u32 = 1;
|
||||
const MAX: u32 = 1000;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(JsonSchema)]
|
||||
pub struct Struct {
|
||||
#[garde(range(min = 0.01, max = 100))]
|
||||
min_max: f32,
|
||||
#[garde(range(min = MIN, max = MAX))]
|
||||
min_max2: f32,
|
||||
#[garde(pattern(r"^[Hh]ello\b"))]
|
||||
regex_str1: String,
|
||||
#[garde(contains(concat!("sub","string...")))]
|
||||
contains_str1: String,
|
||||
#[garde(email)]
|
||||
email_address: String,
|
||||
#[garde(url)]
|
||||
homepage: String,
|
||||
#[garde(length(min = 1, max = 100))]
|
||||
non_empty_str: String,
|
||||
#[garde(length(min = MIN, max = MAX))]
|
||||
non_empty_str2: String,
|
||||
#[garde(length(equal = 2))]
|
||||
pair: Vec<i32>,
|
||||
#[garde(required)]
|
||||
required_option: Option<bool>,
|
||||
#[garde(required)]
|
||||
#[serde(flatten)]
|
||||
required_flattened: Option<Inner>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(JsonSchema)]
|
||||
pub struct Inner {
|
||||
x: i32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garde() -> TestResult {
|
||||
test_default_generated_schema::<Struct>("garde")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(JsonSchema)]
|
||||
pub struct Struct2 {
|
||||
#[schemars(range(min = 0.01, max = 100))]
|
||||
min_max: f32,
|
||||
#[schemars(range(min = MIN, max = MAX))]
|
||||
min_max2: f32,
|
||||
#[schemars(pattern(r"^[Hh]ello\b"))]
|
||||
regex_str1: String,
|
||||
#[schemars(contains(concat!("sub","string...")))]
|
||||
contains_str1: String,
|
||||
#[schemars(email)]
|
||||
email_address: String,
|
||||
#[schemars(url)]
|
||||
homepage: String,
|
||||
#[schemars(length(min = 1, max = 100))]
|
||||
non_empty_str: String,
|
||||
#[schemars(length(min = MIN, max = MAX))]
|
||||
non_empty_str2: String,
|
||||
#[schemars(length(equal = 2))]
|
||||
pair: Vec<i32>,
|
||||
#[schemars(required)]
|
||||
required_option: Option<bool>,
|
||||
#[schemars(required)]
|
||||
#[serde(flatten)]
|
||||
required_flattened: Option<Inner>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garde_schemars_attrs() -> TestResult {
|
||||
test_default_generated_schema::<Struct2>("garde_schemars_attrs")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(JsonSchema)]
|
||||
pub struct Tuple(
|
||||
#[garde(range(max = 10))] u8,
|
||||
#[garde(required)] Option<bool>,
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn garde_tuple() -> TestResult {
|
||||
test_default_generated_schema::<Tuple>("garde_tuple")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(JsonSchema)]
|
||||
pub struct NewType(#[garde(range(max = 10))] u8);
|
||||
|
||||
#[test]
|
||||
fn garde_newtype() -> TestResult {
|
||||
test_default_generated_schema::<NewType>("garde_newtype")
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
mod util;
|
||||
use schemars::JsonSchema;
|
||||
use std::collections::BTreeMap;
|
||||
use util::*;
|
||||
|
||||
struct FakeRegex(&'static str);
|
||||
|
@ -42,8 +41,6 @@ pub struct Struct {
|
|||
non_empty_str2: String,
|
||||
#[validate(length(equal = 2))]
|
||||
pair: Vec<i32>,
|
||||
#[validate(contains(pattern = "map_key"))]
|
||||
map_contains: BTreeMap<String, ()>,
|
||||
#[validate(required)]
|
||||
required_option: Option<bool>,
|
||||
#[validate(required)]
|
||||
|
@ -90,8 +87,6 @@ pub struct Struct2 {
|
|||
non_empty_str2: String,
|
||||
#[schemars(length(equal = 2))]
|
||||
pair: Vec<i32>,
|
||||
#[schemars(contains(pattern = "map_key"))]
|
||||
map_contains: BTreeMap<String, ()>,
|
||||
#[schemars(required)]
|
||||
required_option: Option<bool>,
|
||||
#[schemars(required)]
|
||||
|
|
|
@ -210,9 +210,10 @@ impl FieldAttrs {
|
|||
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");
|
||||
let garde_cx = &mut AttrCtxt::new(cx, attrs, "garde");
|
||||
|
||||
self.common.populate(attrs, schemars_cx, serde_cx);
|
||||
self.validation.populate(schemars_cx, validate_cx);
|
||||
self.validation.populate(schemars_cx, validate_cx, garde_cx);
|
||||
self.process_attr(schemars_cx);
|
||||
self.process_attr(serde_cx);
|
||||
}
|
||||
|
@ -277,6 +278,7 @@ impl ContainerAttrs {
|
|||
None => self.crate_name = parse_name_value_lit_str(meta, cx).ok(),
|
||||
},
|
||||
|
||||
// The actual parsing of `rename` is done by serde
|
||||
"rename" => self.is_renamed = true,
|
||||
|
||||
_ => return Some(meta),
|
||||
|
|
|
@ -103,7 +103,7 @@ pub fn parse_extensions(
|
|||
cx: &AttrCtxt,
|
||||
) -> Result<impl IntoIterator<Item = Extension>, ()> {
|
||||
let parser = Punctuated::<Extension, Token![,]>::parse_terminated;
|
||||
parse_meta_list(meta, cx, parser)
|
||||
parse_meta_list_with(&meta, cx, parser)
|
||||
}
|
||||
|
||||
pub fn parse_length_or_range(outer_meta: Meta, cx: &AttrCtxt) -> Result<LengthOrRange, ()> {
|
||||
|
@ -144,6 +144,10 @@ pub fn parse_length_or_range(outer_meta: Meta, cx: &AttrCtxt) -> Result<LengthOr
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn parse_pattern(meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
|
||||
parse_meta_list_with(&meta, cx, Expr::parse)
|
||||
}
|
||||
|
||||
pub fn parse_schemars_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
|
||||
let mut pattern = None;
|
||||
|
||||
|
@ -200,9 +204,47 @@ pub fn parse_validate_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()>
|
|||
}
|
||||
|
||||
pub fn parse_contains(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
|
||||
#[derive(Debug)]
|
||||
enum ContainsFormat {
|
||||
Metas(Punctuated<Meta, Token![,]>),
|
||||
Expr(Expr),
|
||||
}
|
||||
|
||||
impl Parse for ContainsFormat {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
// An imperfect but good-enough heuristic for determining whether it looks more like a
|
||||
// comma-separated meta list (validator-style), or a single expression (garde-style).
|
||||
// This heuristic may not generalise well-enough for attributes other than `contains`!
|
||||
// `foo = bar` => Metas (not Expr::Assign)
|
||||
// `foo, bar` => Metas
|
||||
// `foo` => Expr (not Meta::Path)
|
||||
// `foo(bar)` => Expr (not Meta::List)
|
||||
if input.peek2(Token![,]) || input.peek2(Token![=]) {
|
||||
Punctuated::parse_terminated(input).map(Self::Metas)
|
||||
} else {
|
||||
input.parse().map(Self::Expr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nested_meta_or_expr = match cx.attr_type {
|
||||
"validate" => parse_meta_list_with(&outer_meta, cx, Punctuated::parse_terminated)
|
||||
.map(ContainsFormat::Metas),
|
||||
"garde" => parse_meta_list_with(&outer_meta, cx, Expr::parse).map(ContainsFormat::Expr),
|
||||
"schemars" => parse_meta_list_with(&outer_meta, cx, ContainsFormat::parse),
|
||||
wat => {
|
||||
unreachable!("Unexpected attr type `{wat}` for `contains` item. This is a bug in schemars, please raise an issue!")
|
||||
}
|
||||
}?;
|
||||
|
||||
let nested_metas = match nested_meta_or_expr {
|
||||
ContainsFormat::Expr(expr) => return Ok(expr),
|
||||
ContainsFormat::Metas(m) => m,
|
||||
};
|
||||
|
||||
let mut pattern = None;
|
||||
|
||||
for nested_meta in parse_nested_meta(outer_meta.clone(), cx)? {
|
||||
for nested_meta in nested_metas {
|
||||
match path_str(nested_meta.path()).as_str() {
|
||||
"pattern" => match &pattern {
|
||||
Some(_) => cx.duplicate_error(&nested_meta),
|
||||
|
@ -229,10 +271,10 @@ pub fn parse_contains(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
|
|||
|
||||
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)
|
||||
parse_meta_list_with(&meta, cx, parser)
|
||||
}
|
||||
|
||||
fn parse_meta_list<F: Parser>(meta: Meta, cx: &AttrCtxt, parser: F) -> Result<F::Output, ()> {
|
||||
fn parse_meta_list_with<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(
|
||||
|
|
|
@ -5,8 +5,8 @@ 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,
|
||||
parse_contains, parse_length_or_range, parse_nested_meta, parse_pattern,
|
||||
parse_schemars_regex, parse_validate_regex, require_path_only, LengthOrRange,
|
||||
},
|
||||
AttrCtxt,
|
||||
};
|
||||
|
@ -15,6 +15,9 @@ use super::{
|
|||
pub enum Format {
|
||||
Email,
|
||||
Uri,
|
||||
Ip,
|
||||
Ipv4,
|
||||
Ipv6,
|
||||
}
|
||||
|
||||
impl Format {
|
||||
|
@ -22,6 +25,9 @@ impl Format {
|
|||
match self {
|
||||
Format::Email => "email",
|
||||
Format::Uri => "url",
|
||||
Format::Ip => "ip",
|
||||
Format::Ipv4 => "ipv4",
|
||||
Format::Ipv6 => "ipv6",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +35,9 @@ impl Format {
|
|||
match self {
|
||||
Format::Email => "email",
|
||||
Format::Uri => "uri",
|
||||
Format::Ip => "ip",
|
||||
Format::Ipv4 => "ipv4",
|
||||
Format::Ipv6 => "ipv6",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,6 +45,9 @@ impl Format {
|
|||
Some(match s {
|
||||
"email" => Format::Email,
|
||||
"url" => Format::Uri,
|
||||
"ip" => Format::Ip,
|
||||
"ipv4" => Format::Ipv4,
|
||||
"ipv6" => Format::Ipv6,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
@ -45,6 +57,7 @@ impl Format {
|
|||
pub struct ValidationAttrs {
|
||||
pub length: Option<LengthOrRange>,
|
||||
pub range: Option<LengthOrRange>,
|
||||
pub pattern: Option<Expr>,
|
||||
pub regex: Option<Expr>,
|
||||
pub contains: Option<Expr>,
|
||||
pub required: bool,
|
||||
|
@ -67,7 +80,7 @@ impl ValidationAttrs {
|
|||
Self::add_length_or_range(range, mutators, "number", "imum", mut_ref_schema);
|
||||
}
|
||||
|
||||
if let Some(regex) = &self.regex {
|
||||
if let Some(regex) = self.regex.as_ref().or(self.pattern.as_ref()) {
|
||||
mutators.push(quote! {
|
||||
schemars::_private::insert_validation_property(#mut_ref_schema, "string", "pattern", (#regex).to_string());
|
||||
});
|
||||
|
@ -75,7 +88,7 @@ impl ValidationAttrs {
|
|||
|
||||
if let Some(contains) = &self.contains {
|
||||
mutators.push(quote! {
|
||||
schemars::_private::must_contain(#mut_ref_schema, #contains.to_string());
|
||||
schemars::_private::must_contain(#mut_ref_schema, &#contains.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -120,9 +133,15 @@ impl ValidationAttrs {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn populate(&mut self, schemars_cx: &mut AttrCtxt, validate_cx: &mut AttrCtxt) {
|
||||
pub(super) fn populate(
|
||||
&mut self,
|
||||
schemars_cx: &mut AttrCtxt,
|
||||
validate_cx: &mut AttrCtxt,
|
||||
garde_cx: &mut AttrCtxt,
|
||||
) {
|
||||
self.process_attr(schemars_cx);
|
||||
self.process_attr(validate_cx);
|
||||
self.process_attr(garde_cx);
|
||||
}
|
||||
|
||||
fn process_attr(&mut self, cx: &mut AttrCtxt) {
|
||||
|
@ -153,22 +172,36 @@ impl ValidationAttrs {
|
|||
}
|
||||
}
|
||||
|
||||
"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) => {
|
||||
unreachable!("Unexpected attr type `{wat}` for regex item. This is a bug in schemars, please raise an issue!")
|
||||
"pattern" if cx.attr_type != "validate" => {
|
||||
match (&self.pattern, &self.regex, &self.contains) {
|
||||
(Some(_p), _, _) => cx.duplicate_error(&meta),
|
||||
(_, Some(_r), _) => cx.mutual_exclusive_error(&meta, "regex"),
|
||||
(_, _, Some(_c)) => cx.mutual_exclusive_error(&meta, "contains"),
|
||||
(None, None, None) => self.pattern = parse_pattern(meta, cx).ok(),
|
||||
}
|
||||
},
|
||||
"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(),
|
||||
}
|
||||
"regex" if cx.attr_type != "garde" => {
|
||||
match (&self.pattern, &self.regex, &self.contains) {
|
||||
(Some(_p), _, _) => cx.mutual_exclusive_error(&meta, "pattern"),
|
||||
(_, Some(_r), _) => cx.duplicate_error(&meta),
|
||||
(_, _, Some(_c)) => cx.mutual_exclusive_error(&meta, "contains"),
|
||||
(None, None, None) => {
|
||||
if cx.attr_type == "validate" {
|
||||
self.regex = parse_validate_regex(meta, cx).ok()
|
||||
} else {
|
||||
self.regex = parse_schemars_regex(meta, cx).ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"contains" => match (&self.pattern, &self.regex, &self.contains) {
|
||||
(Some(_p), _, _) => cx.mutual_exclusive_error(&meta, "pattern"),
|
||||
(_, Some(_r), _) => cx.mutual_exclusive_error(&meta, "regex"),
|
||||
(_, _, Some(_c)) => cx.duplicate_error(&meta),
|
||||
(None, None, None) => self.contains = parse_contains(meta, cx).ok(),
|
||||
},
|
||||
|
||||
"inner" => {
|
||||
"inner" if cx.attr_type != "validate" => {
|
||||
if let Ok(nested_meta) = parse_nested_meta(meta, cx) {
|
||||
let inner = self
|
||||
.inner
|
||||
|
|
|
@ -18,7 +18,7 @@ use syn::spanned::Spanned;
|
|||
|
||||
#[doc = "Derive macro for `JsonSchema` trait."]
|
||||
#[cfg_attr(not(doctest), doc = include_str!("../deriving.md"), doc = include_str!("../attributes.md"))]
|
||||
#[proc_macro_derive(JsonSchema, attributes(schemars, serde, validate))]
|
||||
#[proc_macro_derive(JsonSchema, attributes(schemars, serde, validate, garde))]
|
||||
pub fn derive_json_schema_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let input = parse_macro_input!(input as syn::DeriveInput);
|
||||
derive_json_schema(input, false)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue