Read #[garde(...)] attributes in addition to #[validate(...)] (#331)

This commit is contained in:
Graham Esau 2024-08-29 17:12:06 +01:00 committed by GitHub
parent 56cdd45c5a
commit 9770301218
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 421 additions and 87 deletions

View file

@ -173,3 +173,23 @@ fn my_transform2(schema: &mut Schema) {
let mut schema = schemars::schema_for!(str); let mut schema = schemars::schema_for!(str);
RecursiveTransform(my_transform2).transform(&mut schema); 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.

View file

@ -8,6 +8,7 @@ permalink: /deriving/attributes/
<style> <style>
h3 code { h3 code {
font-weight: bold; font-weight: bold;
text-wrap: nowrap;
} }
</style> </style>

View file

@ -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. [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> <details open>
<summary style="font-weight: bold"> <summary style="font-weight: bold">
@ -23,11 +23,11 @@ TABLE OF CONTENTS
- [`flatten`](#flatten) - [`flatten`](#flatten)
- [`with`](#with) - [`with`](#with)
- [`bound`](#bound) - [`bound`](#bound)
1. [Supported Validator Attributes](#supported-validator-attributes) 1. [Supported Validator/Garde Attributes](#supported-validatorgarde-attributes)
- [`email` / `url`](#email-url) - [`email` / `url` / `ip` / `ipv4` / `ipv6`](#formats)
- [`length`](#length) - [`length`](#length)
- [`range`](#range) - [`range`](#range)
- [`regex`](#regex) - [`regex` / `pattern`](#regex)
- [`contains`](#contains) - [`contains`](#contains)
- [`required`](#required) - [`required`](#required)
1. [Other Attributes](#other-attributes) 1. [Other Attributes](#other-attributes)
@ -184,25 +184,28 @@ Serde docs: [container](https://serde.rs/container-attrs.html#bound)
</div> </div>
## Supported Validator Attributes ## Supported Validator/Garde Attributes
<div class="indented"> <div class="indented">
<h3 id="email-url"> <h3 id="formats">
`#[validate(email)]` / `#[schemars(email)]`<br /> `#[validate(email)]` / `#[garde(email)]` / `#[schemars(email)]`<br />
`#[validate(url)]` / `#[schemars(url)]` `#[validate(url)]` / `#[garde(url)]`/ `#[schemars(url)]`<br />
`#[garde(ip)]`/ `#[schemars(ip)]`<br />
`#[garde(ipv4)]`/ `#[schemars(ipv4)]`<br />
`#[garde(ipv6)]`/ `#[schemars(ip)v6]`<br />
</h3> </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) Validator docs: [email](https://github.com/Keats/validator#email) / [url](https://github.com/Keats/validator#url)
<h3 id="length"> <h3 id="length">
`#[validate(length(min = 1, max = 10))]` / `#[schemars(length(min = 1, max = 10))]`<br /> `#[validate(length(min = 1, max = 10))]` / `#[garde(length(min = 1, max = 10))]` / `#[schemars(length(min = 1, max = 10))]`<br />
`#[validate(length(equal = 10))]` / `#[schemars(length(equal = 10))]` `#[validate(length(equal = 10))]` / `#[garde(length(equal = 10))]` / `#[schemars(length(equal = 10))]`
</h3> </h3>
@ -212,7 +215,7 @@ Validator docs: [length](https://github.com/Keats/validator#length)
<h3 id="range"> <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> </h3>
@ -223,29 +226,31 @@ Validator docs: [range](https://github.com/Keats/validator#range)
<h3 id="regex"> <h3 id="regex">
`#[validate(regex(path = *static_regex)]`<br /> `#[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> </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. 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) Validator docs: [regex](https://github.com/Keats/validator#regex)
<h3 id="contains"> <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> </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) Validator docs: [contains](https://github.com/Keats/validator#contains)
<h3 id="required"> <h3 id="required">
`#[validate(required)]` / `#[schemars(required)]`<br /> `#[validate(required)]` / `#[garde(required)]` / `#[schemars(required)]`<br />
</h3> </h3>
@ -305,7 +310,7 @@ Set the path to the schemars crate instance the generated code should depend on.
</h3> </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 ```rust
struct Struct { struct Struct {

View file

@ -199,26 +199,9 @@ pub fn insert_validation_property(
} }
} }
pub fn must_contain(schema: &mut Schema, contain: String) { pub fn must_contain(schema: &mut Schema, substring: &str) {
if schema.has_type("string") { let escaped = regex_syntax::escape(substring);
let pattern = regex_syntax::escape(&contain); insert_validation_property(schema, "string", "pattern", escaped);
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 apply_inner_validation(schema: &mut Schema, f: fn(&mut Schema) -> ()) { pub fn apply_inner_validation(schema: &mut Schema, f: fn(&mut Schema) -> ()) {

View 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"
]
}

View file

@ -0,0 +1,8 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NewType",
"type": "integer",
"format": "uint8",
"minimum": 0,
"maximum": 10
}

View 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"
]
}

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

View file

@ -58,15 +58,6 @@
"minItems": 2, "minItems": 2,
"maxItems": 2 "maxItems": 2
}, },
"map_contains": {
"type": "object",
"additionalProperties": {
"type": "null"
},
"required": [
"map_key"
]
},
"required_option": { "required_option": {
"type": "boolean" "type": "boolean"
}, },
@ -87,7 +78,6 @@
"non_empty_str", "non_empty_str",
"non_empty_str2", "non_empty_str2",
"pair", "pair",
"map_contains",
"required_option", "required_option",
"x" "x"
] ]

View file

@ -58,15 +58,6 @@
"minItems": 2, "minItems": 2,
"maxItems": 2 "maxItems": 2
}, },
"map_contains": {
"type": "object",
"additionalProperties": {
"type": "null"
},
"required": [
"map_key"
]
},
"required_option": { "required_option": {
"type": "boolean" "type": "boolean"
}, },
@ -87,7 +78,6 @@
"non_empty_str", "non_empty_str",
"non_empty_str2", "non_empty_str2",
"pair", "pair",
"map_contains",
"required_option", "required_option",
"x" "x"
] ]

99
schemars/tests/garde.rs Normal file
View 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")
}

View file

@ -1,6 +1,5 @@
mod util; mod util;
use schemars::JsonSchema; use schemars::JsonSchema;
use std::collections::BTreeMap;
use util::*; use util::*;
struct FakeRegex(&'static str); struct FakeRegex(&'static str);
@ -42,8 +41,6 @@ pub struct Struct {
non_empty_str2: String, non_empty_str2: String,
#[validate(length(equal = 2))] #[validate(length(equal = 2))]
pair: Vec<i32>, pair: Vec<i32>,
#[validate(contains(pattern = "map_key"))]
map_contains: BTreeMap<String, ()>,
#[validate(required)] #[validate(required)]
required_option: Option<bool>, required_option: Option<bool>,
#[validate(required)] #[validate(required)]
@ -90,8 +87,6 @@ pub struct Struct2 {
non_empty_str2: String, non_empty_str2: String,
#[schemars(length(equal = 2))] #[schemars(length(equal = 2))]
pair: Vec<i32>, pair: Vec<i32>,
#[schemars(contains(pattern = "map_key"))]
map_contains: BTreeMap<String, ()>,
#[schemars(required)] #[schemars(required)]
required_option: Option<bool>, required_option: Option<bool>,
#[schemars(required)] #[schemars(required)]

View file

@ -210,9 +210,10 @@ impl FieldAttrs {
let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars"); let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars");
let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde"); let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde");
let validate_cx = &mut AttrCtxt::new(cx, attrs, "validate"); 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.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(schemars_cx);
self.process_attr(serde_cx); self.process_attr(serde_cx);
} }
@ -277,6 +278,7 @@ impl ContainerAttrs {
None => self.crate_name = parse_name_value_lit_str(meta, cx).ok(), 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, "rename" => self.is_renamed = true,
_ => return Some(meta), _ => return Some(meta),

View file

@ -103,7 +103,7 @@ pub fn parse_extensions(
cx: &AttrCtxt, cx: &AttrCtxt,
) -> Result<impl IntoIterator<Item = Extension>, ()> { ) -> Result<impl IntoIterator<Item = Extension>, ()> {
let parser = Punctuated::<Extension, Token![,]>::parse_terminated; 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, ()> { 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) 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, ()> { pub fn parse_schemars_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
let mut pattern = None; 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, ()> { 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; 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() { match path_str(nested_meta.path()).as_str() {
"pattern" => match &pattern { "pattern" => match &pattern {
Some(_) => cx.duplicate_error(&nested_meta), 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>, ()> { pub fn parse_nested_meta(meta: Meta, cx: &AttrCtxt) -> Result<impl IntoIterator<Item = Meta>, ()> {
let parser = Punctuated::<Meta, Token![,]>::parse_terminated; 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 Meta::List(meta_list) = meta else {
let name = path_str(meta.path()); let name = path_str(meta.path());
cx.error_spanned_by( cx.error_spanned_by(

View file

@ -5,8 +5,8 @@ use crate::idents::SCHEMA;
use super::{ use super::{
parse_meta::{ parse_meta::{
parse_contains, parse_length_or_range, parse_nested_meta, parse_schemars_regex, parse_contains, parse_length_or_range, parse_nested_meta, parse_pattern,
parse_validate_regex, require_path_only, LengthOrRange, parse_schemars_regex, parse_validate_regex, require_path_only, LengthOrRange,
}, },
AttrCtxt, AttrCtxt,
}; };
@ -15,6 +15,9 @@ use super::{
pub enum Format { pub enum Format {
Email, Email,
Uri, Uri,
Ip,
Ipv4,
Ipv6,
} }
impl Format { impl Format {
@ -22,6 +25,9 @@ impl Format {
match self { match self {
Format::Email => "email", Format::Email => "email",
Format::Uri => "url", Format::Uri => "url",
Format::Ip => "ip",
Format::Ipv4 => "ipv4",
Format::Ipv6 => "ipv6",
} }
} }
@ -29,6 +35,9 @@ impl Format {
match self { match self {
Format::Email => "email", Format::Email => "email",
Format::Uri => "uri", Format::Uri => "uri",
Format::Ip => "ip",
Format::Ipv4 => "ipv4",
Format::Ipv6 => "ipv6",
} }
} }
@ -36,6 +45,9 @@ impl Format {
Some(match s { Some(match s {
"email" => Format::Email, "email" => Format::Email,
"url" => Format::Uri, "url" => Format::Uri,
"ip" => Format::Ip,
"ipv4" => Format::Ipv4,
"ipv6" => Format::Ipv6,
_ => return None, _ => return None,
}) })
} }
@ -45,6 +57,7 @@ impl Format {
pub struct ValidationAttrs { pub struct ValidationAttrs {
pub length: Option<LengthOrRange>, pub length: Option<LengthOrRange>,
pub range: Option<LengthOrRange>, pub range: Option<LengthOrRange>,
pub pattern: Option<Expr>,
pub regex: Option<Expr>, pub regex: Option<Expr>,
pub contains: Option<Expr>, pub contains: Option<Expr>,
pub required: bool, pub required: bool,
@ -67,7 +80,7 @@ impl ValidationAttrs {
Self::add_length_or_range(range, mutators, "number", "imum", mut_ref_schema); 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! { mutators.push(quote! {
schemars::_private::insert_validation_property(#mut_ref_schema, "string", "pattern", (#regex).to_string()); 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 { if let Some(contains) = &self.contains {
mutators.push(quote! { 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(schemars_cx);
self.process_attr(validate_cx); self.process_attr(validate_cx);
self.process_attr(garde_cx);
} }
fn process_attr(&mut self, cx: &mut AttrCtxt) { fn process_attr(&mut self, cx: &mut AttrCtxt) {
@ -153,22 +172,36 @@ impl ValidationAttrs {
} }
} }
"regex" => match (&self.regex, &self.contains, cx.attr_type) { "pattern" if cx.attr_type != "validate" => {
(Some(_), _, _) => cx.duplicate_error(&meta), match (&self.pattern, &self.regex, &self.contains) {
(_, Some(_), _) => cx.mutual_exclusive_error(&meta, "contains"), (Some(_p), _, _) => cx.duplicate_error(&meta),
(None, None, "schemars") => self.regex = parse_schemars_regex(meta, cx).ok(), (_, Some(_r), _) => cx.mutual_exclusive_error(&meta, "regex"),
(None, None, "validate") => self.regex = parse_validate_regex(meta, cx).ok(), (_, _, Some(_c)) => cx.mutual_exclusive_error(&meta, "contains"),
(None, None, wat) => { (None, None, None) => self.pattern = parse_pattern(meta, cx).ok(),
unreachable!("Unexpected attr type `{wat}` for regex item. This is a bug in schemars, please raise an issue!")
} }
}, }
"contains" => match (&self.regex, &self.contains) { "regex" if cx.attr_type != "garde" => {
(Some(_), _) => cx.mutual_exclusive_error(&meta, "regex"), match (&self.pattern, &self.regex, &self.contains) {
(_, Some(_)) => cx.duplicate_error(&meta), (Some(_p), _, _) => cx.mutual_exclusive_error(&meta, "pattern"),
(None, None) => self.contains = parse_contains(meta, cx).ok(), (_, 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) { if let Ok(nested_meta) = parse_nested_meta(meta, cx) {
let inner = self let inner = self
.inner .inner

View file

@ -18,7 +18,7 @@ use syn::spanned::Spanned;
#[doc = "Derive macro for `JsonSchema` trait."] #[doc = "Derive macro for `JsonSchema` trait."]
#[cfg_attr(not(doctest), doc = include_str!("../deriving.md"), doc = include_str!("../attributes.md"))] #[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 { pub fn derive_json_schema_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput); let input = parse_macro_input!(input as syn::DeriveInput);
derive_json_schema(input, false) derive_json_schema(input, false)