diff --git a/docs/0-migrating.md b/docs/0-migrating.md index 4ee936a..499f32c 100644 --- a/docs/0-migrating.md +++ b/docs/0-migrating.md @@ -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. diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md index edb1dbd..a2159db 100644 --- a/docs/1.1-attributes.md +++ b/docs/1.1-attributes.md @@ -8,6 +8,7 @@ permalink: /deriving/attributes/ diff --git a/docs/_includes/attributes.md b/docs/_includes/attributes.md index 3661634..5da084d 100644 --- a/docs/_includes/attributes.md +++ b/docs/_includes/attributes.md @@ -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.
@@ -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) -## Supported Validator Attributes +## Supported Validator/Garde Attributes
-

+

-`#[validate(email)]` / `#[schemars(email)]`
-`#[validate(url)]` / `#[schemars(url)]` +`#[validate(email)]` / `#[garde(email)]` / `#[schemars(email)]`
+`#[validate(url)]` / `#[garde(url)]`/ `#[schemars(url)]`
+`#[garde(ip)]`/ `#[schemars(ip)]`
+`#[garde(ipv4)]`/ `#[schemars(ipv4)]`
+`#[garde(ipv6)]`/ `#[schemars(ip)v6]`

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

-`#[validate(length(min = 1, max = 10))]` / `#[schemars(length(min = 1, max = 10))]`
-`#[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))]`
+`#[validate(length(equal = 10))]` / `#[garde(length(equal = 10))]` / `#[schemars(length(equal = 10))]`

@@ -212,7 +215,7 @@ Validator docs: [length](https://github.com/Keats/validator#length)

-`#[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))]`

@@ -223,29 +226,31 @@ Validator docs: [range](https://github.com/Keats/validator#range)

`#[validate(regex(path = *static_regex)]`
-`#[schemars(regex(pattern = r"^\d+$"))]` / `#[schemars(regex(pattern = *static_regex))]` +`#[schemars(regex(pattern = r"^\d+$"))]` / `#[schemars(regex(pattern = *static_regex))]`
+`#[garde(pattern(r"^\d+$")]` / `#[schemars(pattern(r"^\d+$")]`/ `#[schemars(pattern(*static_regex)]`

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)

-`#[validate(contains(pattern = "string"))]` / `#[schemars(contains(pattern = "string"))]` +`#[validate(contains(pattern = "string"))]` / `#[schemars(contains(pattern = "string"))]`
+`#[garde(contains("string"))]` / `#[schemars(contains("string"))]`

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

-`#[validate(required)]` / `#[schemars(required)]`
+`#[validate(required)]` / `#[garde(required)]` / `#[schemars(required)]`

@@ -305,7 +310,7 @@ Set the path to the schemars crate instance the generated code should depend on. -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 { diff --git a/schemars/src/_private/mod.rs b/schemars/src/_private/mod.rs index 75966b8..404d015 100644 --- a/schemars/src/_private/mod.rs +++ b/schemars/src/_private/mod.rs @@ -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) -> ()) { diff --git a/schemars/tests/expected/garde.json b/schemars/tests/expected/garde.json new file mode 100644 index 0000000..1f5d8a6 --- /dev/null +++ b/schemars/tests/expected/garde.json @@ -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" + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/garde_newtype.json b/schemars/tests/expected/garde_newtype.json new file mode 100644 index 0000000..cd835f5 --- /dev/null +++ b/schemars/tests/expected/garde_newtype.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "NewType", + "type": "integer", + "format": "uint8", + "minimum": 0, + "maximum": 10 +} \ No newline at end of file diff --git a/schemars/tests/expected/garde_schemars_attrs.json b/schemars/tests/expected/garde_schemars_attrs.json new file mode 100644 index 0000000..f548706 --- /dev/null +++ b/schemars/tests/expected/garde_schemars_attrs.json @@ -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" + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/garde_tuple.json b/schemars/tests/expected/garde_tuple.json new file mode 100644 index 0000000..fa81224 --- /dev/null +++ b/schemars/tests/expected/garde_tuple.json @@ -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 +} \ No newline at end of file diff --git a/schemars/tests/expected/validate.json b/schemars/tests/expected/validate.json index b2c893a..64b571f 100644 --- a/schemars/tests/expected/validate.json +++ b/schemars/tests/expected/validate.json @@ -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" ] diff --git a/schemars/tests/expected/validate_schemars_attrs.json b/schemars/tests/expected/validate_schemars_attrs.json index e258f18..1f2e3eb 100644 --- a/schemars/tests/expected/validate_schemars_attrs.json +++ b/schemars/tests/expected/validate_schemars_attrs.json @@ -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" ] diff --git a/schemars/tests/garde.rs b/schemars/tests/garde.rs new file mode 100644 index 0000000..8b7ce85 --- /dev/null +++ b/schemars/tests/garde.rs @@ -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, + #[garde(required)] + required_option: Option, + #[garde(required)] + #[serde(flatten)] + required_flattened: Option, +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +pub struct Inner { + x: i32, +} + +#[test] +fn garde() -> TestResult { + test_default_generated_schema::("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, + #[schemars(required)] + required_option: Option, + #[schemars(required)] + #[serde(flatten)] + required_flattened: Option, +} + +#[test] +fn garde_schemars_attrs() -> TestResult { + test_default_generated_schema::("garde_schemars_attrs") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +pub struct Tuple( + #[garde(range(max = 10))] u8, + #[garde(required)] Option, +); + +#[test] +fn garde_tuple() -> TestResult { + test_default_generated_schema::("garde_tuple") +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +pub struct NewType(#[garde(range(max = 10))] u8); + +#[test] +fn garde_newtype() -> TestResult { + test_default_generated_schema::("garde_newtype") +} diff --git a/schemars/tests/validate.rs b/schemars/tests/validate.rs index bd4f3a1..410593d 100644 --- a/schemars/tests/validate.rs +++ b/schemars/tests/validate.rs @@ -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, - #[validate(contains(pattern = "map_key"))] - map_contains: BTreeMap, #[validate(required)] required_option: Option, #[validate(required)] @@ -90,8 +87,6 @@ pub struct Struct2 { non_empty_str2: String, #[schemars(length(equal = 2))] pair: Vec, - #[schemars(contains(pattern = "map_key"))] - map_contains: BTreeMap, #[schemars(required)] required_option: Option, #[schemars(required)] diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index 9698731..28bc192 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -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), diff --git a/schemars_derive/src/attr/parse_meta.rs b/schemars_derive/src/attr/parse_meta.rs index 9259641..94c89ee 100644 --- a/schemars_derive/src/attr/parse_meta.rs +++ b/schemars_derive/src/attr/parse_meta.rs @@ -103,7 +103,7 @@ pub fn parse_extensions( cx: &AttrCtxt, ) -> Result, ()> { let parser = Punctuated::::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 { @@ -144,6 +144,10 @@ pub fn parse_length_or_range(outer_meta: Meta, cx: &AttrCtxt) -> Result Result { + parse_meta_list_with(&meta, cx, Expr::parse) +} + pub fn parse_schemars_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result { let mut pattern = None; @@ -200,9 +204,47 @@ pub fn parse_validate_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result } pub fn parse_contains(outer_meta: Meta, cx: &AttrCtxt) -> Result { + #[derive(Debug)] + enum ContainsFormat { + Metas(Punctuated), + Expr(Expr), + } + + impl Parse for ContainsFormat { + fn parse(input: ParseStream) -> syn::Result { + // 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 { pub fn parse_nested_meta(meta: Meta, cx: &AttrCtxt) -> Result, ()> { let parser = Punctuated::::parse_terminated; - parse_meta_list(meta, cx, parser) + parse_meta_list_with(&meta, cx, parser) } -fn parse_meta_list(meta: Meta, cx: &AttrCtxt, parser: F) -> Result { +fn parse_meta_list_with(meta: &Meta, cx: &AttrCtxt, parser: F) -> Result { let Meta::List(meta_list) = meta else { let name = path_str(meta.path()); cx.error_spanned_by( diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs index 41031db..c8bf880 100644 --- a/schemars_derive/src/attr/validation.rs +++ b/schemars_derive/src/attr/validation.rs @@ -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, pub range: Option, + pub pattern: Option, pub regex: Option, pub contains: Option, 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 diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index 48afe80..578f45c 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -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)