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
	
	 Graham Esau
						Graham Esau