Allow setting validation attributes via #[schemars(...)]
This commit is contained in:
parent
c013052f59
commit
7914593d89
17 changed files with 607 additions and 99 deletions
|
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||||
#[schemars(rename_all = "camelCase", deny_unknown_fields)]
|
#[schemars(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
pub struct MyStruct {
|
pub struct MyStruct {
|
||||||
#[serde(rename = "thisIsOverridden")]
|
#[serde(rename = "thisIsOverridden")]
|
||||||
#[schemars(rename = "myNumber")]
|
#[schemars(rename = "myNumber", range(min = 1, max = 10))]
|
||||||
pub my_int: i32,
|
pub my_int: i32,
|
||||||
pub my_bool: bool,
|
pub my_bool: bool,
|
||||||
#[schemars(default)]
|
#[schemars(default)]
|
||||||
|
@ -15,8 +15,11 @@ pub struct MyStruct {
|
||||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||||
#[schemars(untagged)]
|
#[schemars(untagged)]
|
||||||
pub enum MyEnum {
|
pub enum MyEnum {
|
||||||
StringNewType(String),
|
StringNewType(#[schemars(phone)] String),
|
||||||
StructVariant { floats: Vec<f32> },
|
StructVariant {
|
||||||
|
#[schemars(length(min = 1, max = 100))]
|
||||||
|
floats: Vec<f32>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
},
|
},
|
||||||
"myNumber": {
|
"myNumber": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32",
|
||||||
|
"maximum": 10.0,
|
||||||
|
"minimum": 1.0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
@ -31,7 +33,8 @@
|
||||||
"MyEnum": {
|
"MyEnum": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"format": "phone"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -44,7 +47,9 @@
|
||||||
"items": {
|
"items": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "float"
|
"format": "float"
|
||||||
}
|
},
|
||||||
|
"maxItems": 100,
|
||||||
|
"minItems": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
docs/_includes/examples/validate.rs
Normal file
24
docs/_includes/examples/validate.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use schemars::{schema_for, JsonSchema};
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub struct MyStruct {
|
||||||
|
#[validate(range(min = 1, max = 10))]
|
||||||
|
pub my_int: i32,
|
||||||
|
pub my_bool: bool,
|
||||||
|
#[validate(required)]
|
||||||
|
pub my_nullable_enum: Option<MyEnum>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub enum MyEnum {
|
||||||
|
StringNewType(#[validate(phone)] String),
|
||||||
|
StructVariant {
|
||||||
|
#[validate(length(min = 1, max = 100))]
|
||||||
|
floats: Vec<f32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let schema = schema_for!(MyStruct);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||||
|
}
|
64
docs/_includes/examples/validate.schema.json
Normal file
64
docs/_includes/examples/validate.schema.json
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "MyStruct",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"my_bool",
|
||||||
|
"my_int",
|
||||||
|
"my_nullable_enum"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"my_bool": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"my_int": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"maximum": 10.0,
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"my_nullable_enum": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"StringNewType"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"StringNewType": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "phone"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"StructVariant"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"StructVariant": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"floats"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"floats": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"maxItems": 100,
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||||
#[schemars(rename_all = "camelCase", deny_unknown_fields)]
|
#[schemars(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
pub struct MyStruct {
|
pub struct MyStruct {
|
||||||
#[serde(rename = "thisIsOverridden")]
|
#[serde(rename = "thisIsOverridden")]
|
||||||
#[schemars(rename = "myNumber")]
|
#[schemars(rename = "myNumber", range(min = 1, max = 10))]
|
||||||
pub my_int: i32,
|
pub my_int: i32,
|
||||||
pub my_bool: bool,
|
pub my_bool: bool,
|
||||||
#[schemars(default)]
|
#[schemars(default)]
|
||||||
|
@ -15,8 +15,11 @@ pub struct MyStruct {
|
||||||
#[derive(Deserialize, Serialize, JsonSchema)]
|
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||||
#[schemars(untagged)]
|
#[schemars(untagged)]
|
||||||
pub enum MyEnum {
|
pub enum MyEnum {
|
||||||
StringNewType(String),
|
StringNewType(#[schemars(phone)] String),
|
||||||
StructVariant { floats: Vec<f32> },
|
StructVariant {
|
||||||
|
#[schemars(length(min = 1, max = 100))]
|
||||||
|
floats: Vec<f32>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
},
|
},
|
||||||
"myNumber": {
|
"myNumber": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32",
|
||||||
|
"maximum": 10.0,
|
||||||
|
"minimum": 1.0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
@ -31,7 +33,8 @@
|
||||||
"MyEnum": {
|
"MyEnum": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"format": "phone"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -44,7 +47,9 @@
|
||||||
"items": {
|
"items": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "float"
|
"format": "float"
|
||||||
}
|
},
|
||||||
|
"maxItems": 100,
|
||||||
|
"minItems": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
schemars/examples/validate.rs
Normal file
24
schemars/examples/validate.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use schemars::{schema_for, JsonSchema};
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub struct MyStruct {
|
||||||
|
#[validate(range(min = 1, max = 10))]
|
||||||
|
pub my_int: i32,
|
||||||
|
pub my_bool: bool,
|
||||||
|
#[validate(required)]
|
||||||
|
pub my_nullable_enum: Option<MyEnum>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub enum MyEnum {
|
||||||
|
StringNewType(#[validate(phone)] String),
|
||||||
|
StructVariant {
|
||||||
|
#[validate(length(min = 1, max = 100))]
|
||||||
|
floats: Vec<f32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let schema = schema_for!(MyStruct);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||||
|
}
|
64
schemars/examples/validate.schema.json
Normal file
64
schemars/examples/validate.schema.json
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "MyStruct",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"my_bool",
|
||||||
|
"my_int",
|
||||||
|
"my_nullable_enum"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"my_bool": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"my_int": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"maximum": 10.0,
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"my_nullable_enum": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"StringNewType"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"StringNewType": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "phone"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"StructVariant"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"StructVariant": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"floats"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"floats": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"maxItems": 100,
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,9 @@
|
||||||
"homepage",
|
"homepage",
|
||||||
"map_contains",
|
"map_contains",
|
||||||
"min_max",
|
"min_max",
|
||||||
|
"min_max2",
|
||||||
"non_empty_str",
|
"non_empty_str",
|
||||||
|
"non_empty_str2",
|
||||||
"pair",
|
"pair",
|
||||||
"regex_str1",
|
"regex_str1",
|
||||||
"regex_str2",
|
"regex_str2",
|
||||||
|
@ -25,6 +27,12 @@
|
||||||
"maximum": 100.0,
|
"maximum": 100.0,
|
||||||
"minimum": 0.01
|
"minimum": 0.01
|
||||||
},
|
},
|
||||||
|
"min_max2": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"maximum": 1000.0,
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
"regex_str1": {
|
"regex_str1": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[Hh]ello\\b"
|
"pattern": "^[Hh]ello\\b"
|
||||||
|
@ -62,6 +70,11 @@
|
||||||
"maxLength": 100,
|
"maxLength": 100,
|
||||||
"minLength": 1
|
"minLength": 1
|
||||||
},
|
},
|
||||||
|
"non_empty_str2": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 1000,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
"pair": {
|
"pair": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|
104
schemars/tests/expected/validate_schemars_attrs.json
Normal file
104
schemars/tests/expected/validate_schemars_attrs.json
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Struct",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"contains_str1",
|
||||||
|
"contains_str2",
|
||||||
|
"email_address",
|
||||||
|
"homepage",
|
||||||
|
"map_contains",
|
||||||
|
"min_max",
|
||||||
|
"min_max2",
|
||||||
|
"non_empty_str",
|
||||||
|
"non_empty_str2",
|
||||||
|
"pair",
|
||||||
|
"regex_str1",
|
||||||
|
"regex_str2",
|
||||||
|
"regex_str3",
|
||||||
|
"required_option",
|
||||||
|
"tel",
|
||||||
|
"x"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"min_max": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"maximum": 100.0,
|
||||||
|
"minimum": 0.01
|
||||||
|
},
|
||||||
|
"min_max2": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float",
|
||||||
|
"maximum": 1000.0,
|
||||||
|
"minimum": 1.0
|
||||||
|
},
|
||||||
|
"regex_str1": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[Hh]ello\\b"
|
||||||
|
},
|
||||||
|
"regex_str2": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[Hh]ello\\b"
|
||||||
|
},
|
||||||
|
"regex_str3": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d+$"
|
||||||
|
},
|
||||||
|
"contains_str1": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "substring\\.\\.\\."
|
||||||
|
},
|
||||||
|
"contains_str2": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "substring\\.\\.\\."
|
||||||
|
},
|
||||||
|
"email_address": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "email"
|
||||||
|
},
|
||||||
|
"tel": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "phone"
|
||||||
|
},
|
||||||
|
"homepage": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"non_empty_str": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"non_empty_str2": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 1000,
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"pair": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"maxItems": 2,
|
||||||
|
"minItems": 2
|
||||||
|
},
|
||||||
|
"map_contains": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"map_key"
|
||||||
|
],
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required_option": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ error: duplicate serde attribute `deny_unknown_fields`
|
||||||
8 | #[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
|
8 | #[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
|
||||||
| ^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
error: unknown schemars container attribute `foo`
|
error: unknown schemars attribute `foo`
|
||||||
--> $DIR/invalid_attrs.rs:8:25
|
--> $DIR/invalid_attrs.rs:8:25
|
||||||
|
|
|
|
||||||
8 | #[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
|
8 | #[schemars(default = 0, foo, deny_unknown_fields, deny_unknown_fields)]
|
||||||
|
|
9
schemars/tests/ui/invalid_validation_attrs.rs
Normal file
9
schemars/tests/ui/invalid_validation_attrs.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub struct Struct1(#[validate(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
|
||||||
|
#[derive(JsonSchema)]
|
||||||
|
pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
|
||||||
|
fn main() {}
|
29
schemars/tests/ui/invalid_validation_attrs.stderr
Normal file
29
schemars/tests/ui/invalid_validation_attrs.stderr
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
error: expected validate regex attribute to be a string: `regex = "..."`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:4:39
|
||||||
|
|
|
||||||
|
4 | pub struct Struct1(#[validate(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
| ^
|
||||||
|
|
||||||
|
error: unknown schemars attribute `foo`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:7:42
|
||||||
|
|
|
||||||
|
7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
| ^^^
|
||||||
|
|
||||||
|
error: expected schemars regex attribute to be a string: `regex = "..."`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:7:39
|
||||||
|
|
|
||||||
|
7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
| ^
|
||||||
|
|
||||||
|
error: schemars attribute cannot contain both `equal` and `min`
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:7:63
|
||||||
|
|
|
||||||
|
7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
| ^^^^^^^^^
|
||||||
|
|
||||||
|
error: unknown item in schemars length attribute
|
||||||
|
--> $DIR/invalid_validation_attrs.rs:7:74
|
||||||
|
|
|
||||||
|
7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
|
||||||
|
| ^^^
|
|
@ -6,10 +6,15 @@ use util::*;
|
||||||
// In real code, this would typically be a Regex, potentially created in a `lazy_static!`.
|
// In real code, this would typically be a Regex, potentially created in a `lazy_static!`.
|
||||||
static STARTS_WITH_HELLO: &'static str = r"^[Hh]ello\b";
|
static STARTS_WITH_HELLO: &'static str = r"^[Hh]ello\b";
|
||||||
|
|
||||||
|
const MIN: u32 = 1;
|
||||||
|
const MAX: u32 = 1000;
|
||||||
|
|
||||||
#[derive(Debug, JsonSchema)]
|
#[derive(Debug, JsonSchema)]
|
||||||
pub struct Struct {
|
pub struct Struct {
|
||||||
#[validate(range(min = 0.01, max = 100))]
|
#[validate(range(min = 0.01, max = 100))]
|
||||||
min_max: f32,
|
min_max: f32,
|
||||||
|
#[validate(range(min = "MIN", max = "MAX"))]
|
||||||
|
min_max2: f32,
|
||||||
#[validate(regex = "STARTS_WITH_HELLO")]
|
#[validate(regex = "STARTS_WITH_HELLO")]
|
||||||
regex_str1: String,
|
regex_str1: String,
|
||||||
#[validate(regex(path = "STARTS_WITH_HELLO", code = "foo"))]
|
#[validate(regex(path = "STARTS_WITH_HELLO", code = "foo"))]
|
||||||
|
@ -28,6 +33,8 @@ pub struct Struct {
|
||||||
homepage: String,
|
homepage: String,
|
||||||
#[validate(length(min = 1, max = 100))]
|
#[validate(length(min = 1, max = 100))]
|
||||||
non_empty_str: String,
|
non_empty_str: String,
|
||||||
|
#[validate(length(min = "MIN", max = "MAX"))]
|
||||||
|
non_empty_str2: String,
|
||||||
#[validate(length(equal = 2))]
|
#[validate(length(equal = 2))]
|
||||||
pair: Vec<i32>,
|
pair: Vec<i32>,
|
||||||
#[validate(contains = "map_key")]
|
#[validate(contains = "map_key")]
|
||||||
|
@ -49,6 +56,48 @@ fn validate() -> TestResult {
|
||||||
test_default_generated_schema::<Struct>("validate")
|
test_default_generated_schema::<Struct>("validate")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, JsonSchema)]
|
||||||
|
pub struct Struct2 {
|
||||||
|
#[schemars(range(min = 0.01, max = 100))]
|
||||||
|
min_max: f32,
|
||||||
|
#[schemars(range(min = "MIN", max = "MAX"))]
|
||||||
|
min_max2: f32,
|
||||||
|
#[schemars(regex = "STARTS_WITH_HELLO")]
|
||||||
|
regex_str1: String,
|
||||||
|
#[schemars(regex(path = "STARTS_WITH_HELLO"))]
|
||||||
|
regex_str2: String,
|
||||||
|
#[schemars(regex(pattern = r"^\d+$"))]
|
||||||
|
regex_str3: String,
|
||||||
|
#[schemars(contains = "substring...")]
|
||||||
|
contains_str1: String,
|
||||||
|
#[schemars(contains(pattern = "substring..."))]
|
||||||
|
contains_str2: String,
|
||||||
|
#[schemars(email)]
|
||||||
|
email_address: String,
|
||||||
|
#[schemars(phone)]
|
||||||
|
tel: String,
|
||||||
|
#[schemars(url)]
|
||||||
|
homepage: String,
|
||||||
|
#[schemars(length(min = 1, max = 100))]
|
||||||
|
non_empty_str: String,
|
||||||
|
#[schemars(length(min = "MIN", max = "MAX"))]
|
||||||
|
non_empty_str2: String,
|
||||||
|
#[schemars(length(equal = 2))]
|
||||||
|
pair: Vec<i32>,
|
||||||
|
#[schemars(contains = "map_key")]
|
||||||
|
map_contains: HashMap<String, ()>,
|
||||||
|
#[schemars(required)]
|
||||||
|
required_option: Option<bool>,
|
||||||
|
#[schemars(required)]
|
||||||
|
#[serde(flatten)]
|
||||||
|
required_flattened: Option<Inner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_schemars_attrs() -> TestResult {
|
||||||
|
test_default_generated_schema::<Struct>("validate_schemars_attrs")
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, JsonSchema)]
|
#[derive(Debug, JsonSchema)]
|
||||||
pub struct Tuple(
|
pub struct Tuple(
|
||||||
#[validate(range(max = 10))] u8,
|
#[validate(range(max = 10))] u8,
|
||||||
|
|
|
@ -73,7 +73,7 @@ impl<'a> FromSerde for Field<'a> {
|
||||||
ty: serde.ty,
|
ty: serde.ty,
|
||||||
original: serde.original,
|
original: serde.original,
|
||||||
attrs: Attrs::new(&serde.original.attrs, errors),
|
attrs: Attrs::new(&serde.original.attrs, errors),
|
||||||
validation_attrs: ValidationAttrs::new(&serde.original.attrs),
|
validation_attrs: ValidationAttrs::new(&serde.original.attrs, errors),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,10 +165,7 @@ impl Attrs {
|
||||||
_ if ignore_errors => {}
|
_ if ignore_errors => {}
|
||||||
|
|
||||||
Meta(meta_item) => {
|
Meta(meta_item) => {
|
||||||
let is_known_serde_keyword = schemars_to_serde::SERDE_KEYWORDS
|
if !is_known_serde_or_validation_keyword(meta_item) {
|
||||||
.iter()
|
|
||||||
.any(|k| meta_item.path().is_ident(k));
|
|
||||||
if !is_known_serde_keyword {
|
|
||||||
let path = meta_item
|
let path = meta_item
|
||||||
.path()
|
.path()
|
||||||
.into_token_stream()
|
.into_token_stream()
|
||||||
|
@ -176,16 +173,13 @@ impl Attrs {
|
||||||
.replace(' ', "");
|
.replace(' ', "");
|
||||||
errors.error_spanned_by(
|
errors.error_spanned_by(
|
||||||
meta_item.path(),
|
meta_item.path(),
|
||||||
format!("unknown schemars container attribute `{}`", path),
|
format!("unknown schemars attribute `{}`", path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Lit(lit) => {
|
Lit(lit) => {
|
||||||
errors.error_spanned_by(
|
errors.error_spanned_by(lit, "unexpected literal in schemars attribute");
|
||||||
lit,
|
|
||||||
"unexpected literal in schemars container attribute",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,6 +187,16 @@ impl Attrs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_known_serde_or_validation_keyword(meta: &syn::Meta) -> bool {
|
||||||
|
let mut known_keywords = schemars_to_serde::SERDE_KEYWORDS
|
||||||
|
.iter()
|
||||||
|
.chain(validation::VALIDATION_KEYWORDS);
|
||||||
|
meta.path()
|
||||||
|
.get_ident()
|
||||||
|
.map(|i| known_keywords.any(|k| i == k))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_meta_items(
|
fn get_meta_items(
|
||||||
attr: &syn::Attribute,
|
attr: &syn::Attribute,
|
||||||
attr_type: &'static str,
|
attr_type: &'static str,
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use super::parse_lit_str;
|
use super::{get_lit_str, get_meta_items, parse_lit_into_path, parse_lit_str};
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use syn::ExprLit;
|
use serde_derive_internals::Ctxt;
|
||||||
use syn::NestedMeta;
|
use syn::{Expr, ExprLit, ExprPath, Lit, Meta, MetaNameValue, NestedMeta};
|
||||||
use syn::{Expr, Lit, Meta, MetaNameValue};
|
|
||||||
|
pub(crate) static VALIDATION_KEYWORDS: &[&str] = &[
|
||||||
|
"range", "regex", "contains", "email", "phone", "url", "length", "required",
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct ValidationAttrs {
|
pub struct ValidationAttrs {
|
||||||
|
@ -18,16 +21,43 @@ pub struct ValidationAttrs {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValidationAttrs {
|
impl ValidationAttrs {
|
||||||
pub fn new(attrs: &[syn::Attribute]) -> Self {
|
pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self {
|
||||||
// TODO allow setting "validate" attributes through #[schemars(...)]
|
// TODO allow setting "validate" attributes through #[schemars(...)]
|
||||||
ValidationAttrs::default().populate(attrs)
|
ValidationAttrs::default()
|
||||||
|
.populate(attrs, "schemars", false, errors)
|
||||||
|
.populate(attrs, "validate", true, errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn populate(mut self, attrs: &[syn::Attribute]) -> Self {
|
fn populate(
|
||||||
// TODO don't silently ignore unparseable attributes
|
mut self,
|
||||||
|
attrs: &[syn::Attribute],
|
||||||
|
attr_type: &'static str,
|
||||||
|
ignore_errors: bool,
|
||||||
|
errors: &Ctxt,
|
||||||
|
) -> Self {
|
||||||
|
let duplicate_error = |meta: &MetaNameValue| {
|
||||||
|
if !ignore_errors {
|
||||||
|
let msg = format!(
|
||||||
|
"duplicate schemars attribute `{}`",
|
||||||
|
meta.path.get_ident().unwrap()
|
||||||
|
);
|
||||||
|
errors.error_spanned_by(meta, msg)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mutual_exclusive_error = |meta: &MetaNameValue, other: &str| {
|
||||||
|
if !ignore_errors {
|
||||||
|
let msg = format!(
|
||||||
|
"schemars attribute cannot contain both `{}` and `{}`",
|
||||||
|
meta.path.get_ident().unwrap(),
|
||||||
|
other,
|
||||||
|
);
|
||||||
|
errors.error_spanned_by(meta, msg)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for meta_item in attrs
|
for meta_item in attrs
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|attr| get_meta_items(attr, "validate"))
|
.flat_map(|attr| get_meta_items(attr, attr_type, errors))
|
||||||
.flatten()
|
.flatten()
|
||||||
{
|
{
|
||||||
match &meta_item {
|
match &meta_item {
|
||||||
|
@ -35,15 +65,43 @@ impl ValidationAttrs {
|
||||||
for nested in meta_list.nested.iter() {
|
for nested in meta_list.nested.iter() {
|
||||||
match nested {
|
match nested {
|
||||||
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => {
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => {
|
||||||
self.length_min = str_or_num_to_expr(&nv.lit);
|
if self.length_min.is_some() {
|
||||||
|
duplicate_error(nv)
|
||||||
|
} else if self.length_equal.is_some() {
|
||||||
|
mutual_exclusive_error(nv, "equal")
|
||||||
|
} else {
|
||||||
|
self.length_min = str_or_num_to_expr(&errors, "min", &nv.lit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => {
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => {
|
||||||
self.length_max = str_or_num_to_expr(&nv.lit);
|
if self.length_max.is_some() {
|
||||||
|
duplicate_error(nv)
|
||||||
|
} else if self.length_equal.is_some() {
|
||||||
|
mutual_exclusive_error(nv, "equal")
|
||||||
|
} else {
|
||||||
|
self.length_max = str_or_num_to_expr(&errors, "max", &nv.lit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("equal") => {
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("equal") => {
|
||||||
self.length_equal = str_or_num_to_expr(&nv.lit);
|
if self.length_equal.is_some() {
|
||||||
|
duplicate_error(nv)
|
||||||
|
} else if self.length_min.is_some() {
|
||||||
|
mutual_exclusive_error(nv, "min")
|
||||||
|
} else if self.length_max.is_some() {
|
||||||
|
mutual_exclusive_error(nv, "max")
|
||||||
|
} else {
|
||||||
|
self.length_equal =
|
||||||
|
str_or_num_to_expr(&errors, "equal", &nv.lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
meta => {
|
||||||
|
if !ignore_errors {
|
||||||
|
errors.error_spanned_by(
|
||||||
|
meta,
|
||||||
|
format!("unknown item in schemars length attribute"),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,78 +110,118 @@ impl ValidationAttrs {
|
||||||
for nested in meta_list.nested.iter() {
|
for nested in meta_list.nested.iter() {
|
||||||
match nested {
|
match nested {
|
||||||
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => {
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => {
|
||||||
self.range_min = str_or_num_to_expr(&nv.lit);
|
if self.range_min.is_some() {
|
||||||
|
duplicate_error(nv)
|
||||||
|
} else {
|
||||||
|
self.range_min = str_or_num_to_expr(&errors, "min", &nv.lit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => {
|
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => {
|
||||||
self.range_max = str_or_num_to_expr(&nv.lit);
|
if self.range_max.is_some() {
|
||||||
|
duplicate_error(nv)
|
||||||
|
} else {
|
||||||
|
self.range_max = str_or_num_to_expr(&errors, "max", &nv.lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
meta => {
|
||||||
|
if !ignore_errors {
|
||||||
|
errors.error_spanned_by(
|
||||||
|
meta,
|
||||||
|
format!("unknown item in schemars range attribute"),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NestedMeta::Meta(m)
|
NestedMeta::Meta(Meta::Path(m))
|
||||||
if m.path().is_ident("required") || m.path().is_ident("required_nested") =>
|
if m.is_ident("required") || m.is_ident("required_nested") =>
|
||||||
{
|
{
|
||||||
self.required = true;
|
self.required = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
NestedMeta::Meta(m) if m.path().is_ident("email") => {
|
// TODO cause compile error if format is already Some
|
||||||
|
// FIXME #[validate(...)] overrides #[schemars(...)] - should be other way around!
|
||||||
|
NestedMeta::Meta(Meta::Path(m)) if m.is_ident("email") => {
|
||||||
self.format = Some("email");
|
self.format = Some("email");
|
||||||
}
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(m)) if m.is_ident("url") => {
|
||||||
NestedMeta::Meta(m) if m.path().is_ident("url") => {
|
|
||||||
self.format = Some("uri");
|
self.format = Some("uri");
|
||||||
}
|
}
|
||||||
|
NestedMeta::Meta(Meta::Path(m)) if m.is_ident("phone") => {
|
||||||
NestedMeta::Meta(m) if m.path().is_ident("phone") => {
|
|
||||||
self.format = Some("phone");
|
self.format = Some("phone");
|
||||||
}
|
}
|
||||||
|
|
||||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
// TODO cause compile error if regex/contains are specified more than once
|
||||||
path,
|
// FIXME #[validate(...)] overrides #[schemars(...)] - should be other way around!
|
||||||
lit: Lit::Str(regex),
|
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. }))
|
||||||
..
|
if path.is_ident("regex") =>
|
||||||
})) if path.is_ident("regex") => {
|
{
|
||||||
self.regex = parse_lit_str::<syn::ExprPath>(regex).ok().map(Expr::Path)
|
self.regex = parse_lit_into_expr_path(errors, attr_type, "regex", lit).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("regex") => {
|
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("regex") => {
|
||||||
self.regex = meta_list.nested.iter().find_map(|x| match x {
|
for x in meta_list.nested.iter() {
|
||||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
match x {
|
||||||
path,
|
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||||
lit: Lit::Str(regex),
|
path, lit, ..
|
||||||
..
|
})) if path.is_ident("path") => {
|
||||||
})) if path.is_ident("path") => {
|
self.regex =
|
||||||
parse_lit_str::<syn::ExprPath>(regex).ok().map(Expr::Path)
|
parse_lit_into_expr_path(errors, attr_type, "path", lit).ok()
|
||||||
|
}
|
||||||
|
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||||
|
path, lit, ..
|
||||||
|
})) if path.is_ident("pattern") => {
|
||||||
|
self.regex = get_lit_str(errors, attr_type, "pattern", lit)
|
||||||
|
.ok()
|
||||||
|
.map(|litstr| {
|
||||||
|
Expr::Lit(syn::ExprLit {
|
||||||
|
attrs: Vec::new(),
|
||||||
|
lit: Lit::Str(litstr.clone()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
meta => {
|
||||||
|
if !ignore_errors {
|
||||||
|
errors.error_spanned_by(
|
||||||
|
meta,
|
||||||
|
format!("unknown item in schemars regex attribute"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
}
|
||||||
path,
|
|
||||||
lit: Lit::Str(regex),
|
|
||||||
..
|
|
||||||
})) if path.is_ident("pattern") => Some(Expr::Lit(syn::ExprLit {
|
|
||||||
attrs: Vec::new(),
|
|
||||||
lit: Lit::Str(regex.clone()),
|
|
||||||
})),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. }))
|
||||||
path,
|
if path.is_ident("contains") =>
|
||||||
lit: Lit::Str(contains),
|
{
|
||||||
..
|
self.contains = get_lit_str(errors, attr_type, "contains", lit)
|
||||||
})) if path.is_ident("contains") => self.contains = Some(contains.value()),
|
.ok()
|
||||||
|
.map(|litstr| litstr.value())
|
||||||
|
}
|
||||||
|
|
||||||
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("contains") => {
|
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("contains") => {
|
||||||
self.contains = meta_list.nested.iter().find_map(|x| match x {
|
for x in meta_list.nested.iter() {
|
||||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
match x {
|
||||||
path,
|
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||||
lit: Lit::Str(contains),
|
path, lit, ..
|
||||||
..
|
})) if path.is_ident("pattern") => {
|
||||||
})) if path.is_ident("pattern") => Some(contains.value()),
|
self.contains = get_lit_str(errors, attr_type, "contains", lit)
|
||||||
_ => None,
|
.ok()
|
||||||
});
|
.map(|litstr| litstr.value())
|
||||||
|
}
|
||||||
|
meta => {
|
||||||
|
if !ignore_errors {
|
||||||
|
errors.error_spanned_by(
|
||||||
|
meta,
|
||||||
|
format!("unknown item in schemars contains attribute"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -230,6 +328,21 @@ impl ValidationAttrs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_lit_into_expr_path(
|
||||||
|
cx: &Ctxt,
|
||||||
|
attr_type: &'static str,
|
||||||
|
meta_item_name: &'static str,
|
||||||
|
lit: &syn::Lit,
|
||||||
|
) -> Result<Expr, ()> {
|
||||||
|
parse_lit_into_path(cx, attr_type, meta_item_name, lit).map(|path| {
|
||||||
|
Expr::Path(ExprPath {
|
||||||
|
attrs: Vec::new(),
|
||||||
|
qself: None,
|
||||||
|
path,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn wrap_array_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
|
fn wrap_array_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
|
||||||
if v.is_empty() {
|
if v.is_empty() {
|
||||||
None
|
None
|
||||||
|
@ -283,27 +396,22 @@ fn wrap_string_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_meta_items(
|
fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, lit: &Lit) -> Option<Expr> {
|
||||||
attr: &syn::Attribute,
|
|
||||||
attr_type: &'static str,
|
|
||||||
) -> Result<Vec<syn::NestedMeta>, ()> {
|
|
||||||
if !attr.path.is_ident(attr_type) {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
match attr.parse_meta() {
|
|
||||||
Ok(Meta::List(meta)) => Ok(meta.nested.into_iter().collect()),
|
|
||||||
_ => Err(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn str_or_num_to_expr(lit: &Lit) -> Option<Expr> {
|
|
||||||
match lit {
|
match lit {
|
||||||
Lit::Str(s) => parse_lit_str::<syn::ExprPath>(s).ok().map(Expr::Path),
|
Lit::Str(s) => parse_lit_str::<ExprPath>(s).ok().map(Expr::Path),
|
||||||
Lit::Int(_) | Lit::Float(_) => Some(Expr::Lit(ExprLit {
|
Lit::Int(_) | Lit::Float(_) => Some(Expr::Lit(ExprLit {
|
||||||
attrs: Vec::new(),
|
attrs: Vec::new(),
|
||||||
lit: lit.clone(),
|
lit: lit.clone(),
|
||||||
})),
|
})),
|
||||||
_ => None,
|
_ => {
|
||||||
|
cx.error_spanned_by(
|
||||||
|
lit,
|
||||||
|
format!(
|
||||||
|
"expected `{}` to be a string or number literal",
|
||||||
|
meta_item_name
|
||||||
|
),
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue