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)]
|
||||
pub struct MyStruct {
|
||||
#[serde(rename = "thisIsOverridden")]
|
||||
#[schemars(rename = "myNumber")]
|
||||
#[schemars(rename = "myNumber", range(min = 1, max = 10))]
|
||||
pub my_int: i32,
|
||||
pub my_bool: bool,
|
||||
#[schemars(default)]
|
||||
|
@ -15,8 +15,11 @@ pub struct MyStruct {
|
|||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
#[schemars(untagged)]
|
||||
pub enum MyEnum {
|
||||
StringNewType(String),
|
||||
StructVariant { floats: Vec<f32> },
|
||||
StringNewType(#[schemars(phone)] String),
|
||||
StructVariant {
|
||||
#[schemars(length(min = 1, max = 100))]
|
||||
floats: Vec<f32>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
},
|
||||
"myNumber": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"format": "int32",
|
||||
"maximum": 10.0,
|
||||
"minimum": 1.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
@ -31,7 +33,8 @@
|
|||
"MyEnum": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"format": "phone"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -44,7 +47,9 @@
|
|||
"items": {
|
||||
"type": "number",
|
||||
"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)]
|
||||
pub struct MyStruct {
|
||||
#[serde(rename = "thisIsOverridden")]
|
||||
#[schemars(rename = "myNumber")]
|
||||
#[schemars(rename = "myNumber", range(min = 1, max = 10))]
|
||||
pub my_int: i32,
|
||||
pub my_bool: bool,
|
||||
#[schemars(default)]
|
||||
|
@ -15,8 +15,11 @@ pub struct MyStruct {
|
|||
#[derive(Deserialize, Serialize, JsonSchema)]
|
||||
#[schemars(untagged)]
|
||||
pub enum MyEnum {
|
||||
StringNewType(String),
|
||||
StructVariant { floats: Vec<f32> },
|
||||
StringNewType(#[schemars(phone)] String),
|
||||
StructVariant {
|
||||
#[schemars(length(min = 1, max = 100))]
|
||||
floats: Vec<f32>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
},
|
||||
"myNumber": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"format": "int32",
|
||||
"maximum": 10.0,
|
||||
"minimum": 1.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
@ -31,7 +33,8 @@
|
|||
"MyEnum": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"format": "phone"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
|
@ -44,7 +47,9 @@
|
|||
"items": {
|
||||
"type": "number",
|
||||
"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",
|
||||
"map_contains",
|
||||
"min_max",
|
||||
"min_max2",
|
||||
"non_empty_str",
|
||||
"non_empty_str2",
|
||||
"pair",
|
||||
"regex_str1",
|
||||
"regex_str2",
|
||||
|
@ -25,6 +27,12 @@
|
|||
"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"
|
||||
|
@ -62,6 +70,11 @@
|
|||
"maxLength": 100,
|
||||
"minLength": 1
|
||||
},
|
||||
"non_empty_str2": {
|
||||
"type": "string",
|
||||
"maxLength": 1000,
|
||||
"minLength": 1
|
||||
},
|
||||
"pair": {
|
||||
"type": "array",
|
||||
"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)]
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: unknown schemars container attribute `foo`
|
||||
error: unknown schemars attribute `foo`
|
||||
--> $DIR/invalid_attrs.rs:8:25
|
||||
|
|
||||
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!`.
|
||||
static STARTS_WITH_HELLO: &'static str = r"^[Hh]ello\b";
|
||||
|
||||
const MIN: u32 = 1;
|
||||
const MAX: u32 = 1000;
|
||||
|
||||
#[derive(Debug, JsonSchema)]
|
||||
pub struct Struct {
|
||||
#[validate(range(min = 0.01, max = 100))]
|
||||
min_max: f32,
|
||||
#[validate(range(min = "MIN", max = "MAX"))]
|
||||
min_max2: f32,
|
||||
#[validate(regex = "STARTS_WITH_HELLO")]
|
||||
regex_str1: String,
|
||||
#[validate(regex(path = "STARTS_WITH_HELLO", code = "foo"))]
|
||||
|
@ -28,6 +33,8 @@ pub struct Struct {
|
|||
homepage: String,
|
||||
#[validate(length(min = 1, max = 100))]
|
||||
non_empty_str: String,
|
||||
#[validate(length(min = "MIN", max = "MAX"))]
|
||||
non_empty_str2: String,
|
||||
#[validate(length(equal = 2))]
|
||||
pair: Vec<i32>,
|
||||
#[validate(contains = "map_key")]
|
||||
|
@ -49,6 +56,48 @@ fn validate() -> TestResult {
|
|||
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)]
|
||||
pub struct Tuple(
|
||||
#[validate(range(max = 10))] u8,
|
||||
|
|
|
@ -73,7 +73,7 @@ impl<'a> FromSerde for Field<'a> {
|
|||
ty: serde.ty,
|
||||
original: serde.original,
|
||||
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 => {}
|
||||
|
||||
Meta(meta_item) => {
|
||||
let is_known_serde_keyword = schemars_to_serde::SERDE_KEYWORDS
|
||||
.iter()
|
||||
.any(|k| meta_item.path().is_ident(k));
|
||||
if !is_known_serde_keyword {
|
||||
if !is_known_serde_or_validation_keyword(meta_item) {
|
||||
let path = meta_item
|
||||
.path()
|
||||
.into_token_stream()
|
||||
|
@ -176,16 +173,13 @@ impl Attrs {
|
|||
.replace(' ', "");
|
||||
errors.error_spanned_by(
|
||||
meta_item.path(),
|
||||
format!("unknown schemars container attribute `{}`", path),
|
||||
format!("unknown schemars attribute `{}`", path),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Lit(lit) => {
|
||||
errors.error_spanned_by(
|
||||
lit,
|
||||
"unexpected literal in schemars container attribute",
|
||||
);
|
||||
errors.error_spanned_by(lit, "unexpected literal in schemars 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(
|
||||
attr: &syn::Attribute,
|
||||
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 syn::ExprLit;
|
||||
use syn::NestedMeta;
|
||||
use syn::{Expr, Lit, Meta, MetaNameValue};
|
||||
use serde_derive_internals::Ctxt;
|
||||
use syn::{Expr, ExprLit, ExprPath, Lit, Meta, MetaNameValue, NestedMeta};
|
||||
|
||||
pub(crate) static VALIDATION_KEYWORDS: &[&str] = &[
|
||||
"range", "regex", "contains", "email", "phone", "url", "length", "required",
|
||||
];
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ValidationAttrs {
|
||||
|
@ -18,16 +21,43 @@ pub struct 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(...)]
|
||||
ValidationAttrs::default().populate(attrs)
|
||||
ValidationAttrs::default()
|
||||
.populate(attrs, "schemars", false, errors)
|
||||
.populate(attrs, "validate", true, errors)
|
||||
}
|
||||
|
||||
fn populate(mut self, attrs: &[syn::Attribute]) -> Self {
|
||||
// TODO don't silently ignore unparseable attributes
|
||||
fn populate(
|
||||
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
|
||||
.iter()
|
||||
.flat_map(|attr| get_meta_items(attr, "validate"))
|
||||
.flat_map(|attr| get_meta_items(attr, attr_type, errors))
|
||||
.flatten()
|
||||
{
|
||||
match &meta_item {
|
||||
|
@ -35,15 +65,43 @@ impl ValidationAttrs {
|
|||
for nested in meta_list.nested.iter() {
|
||||
match nested {
|
||||
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") => {
|
||||
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") => {
|
||||
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() {
|
||||
match nested {
|
||||
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") => {
|
||||
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)
|
||||
if m.path().is_ident("required") || m.path().is_ident("required_nested") =>
|
||||
NestedMeta::Meta(Meta::Path(m))
|
||||
if m.is_ident("required") || m.is_ident("required_nested") =>
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
NestedMeta::Meta(m) if m.path().is_ident("url") => {
|
||||
NestedMeta::Meta(Meta::Path(m)) if m.is_ident("url") => {
|
||||
self.format = Some("uri");
|
||||
}
|
||||
|
||||
NestedMeta::Meta(m) if m.path().is_ident("phone") => {
|
||||
NestedMeta::Meta(Meta::Path(m)) if m.is_ident("phone") => {
|
||||
self.format = Some("phone");
|
||||
}
|
||||
|
||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||
path,
|
||||
lit: Lit::Str(regex),
|
||||
..
|
||||
})) if path.is_ident("regex") => {
|
||||
self.regex = parse_lit_str::<syn::ExprPath>(regex).ok().map(Expr::Path)
|
||||
// TODO cause compile error if regex/contains are specified more than once
|
||||
// FIXME #[validate(...)] overrides #[schemars(...)] - should be other way around!
|
||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. }))
|
||||
if path.is_ident("regex") =>
|
||||
{
|
||||
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") => {
|
||||
self.regex = meta_list.nested.iter().find_map(|x| match x {
|
||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||
path,
|
||||
lit: Lit::Str(regex),
|
||||
..
|
||||
})) if path.is_ident("path") => {
|
||||
parse_lit_str::<syn::ExprPath>(regex).ok().map(Expr::Path)
|
||||
for x in meta_list.nested.iter() {
|
||||
match x {
|
||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||
path, lit, ..
|
||||
})) if path.is_ident("path") => {
|
||||
self.regex =
|
||||
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 {
|
||||
path,
|
||||
lit: Lit::Str(contains),
|
||||
..
|
||||
})) if path.is_ident("contains") => self.contains = Some(contains.value()),
|
||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. }))
|
||||
if path.is_ident("contains") =>
|
||||
{
|
||||
self.contains = get_lit_str(errors, attr_type, "contains", lit)
|
||||
.ok()
|
||||
.map(|litstr| litstr.value())
|
||||
}
|
||||
|
||||
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("contains") => {
|
||||
self.contains = meta_list.nested.iter().find_map(|x| match x {
|
||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||
path,
|
||||
lit: Lit::Str(contains),
|
||||
..
|
||||
})) if path.is_ident("pattern") => Some(contains.value()),
|
||||
_ => None,
|
||||
});
|
||||
for x in meta_list.nested.iter() {
|
||||
match x {
|
||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||
path, lit, ..
|
||||
})) if path.is_ident("pattern") => {
|
||||
self.contains = get_lit_str(errors, attr_type, "contains", lit)
|
||||
.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> {
|
||||
if v.is_empty() {
|
||||
None
|
||||
|
@ -283,27 +396,22 @@ fn wrap_string_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_meta_items(
|
||||
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> {
|
||||
fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, lit: &Lit) -> Option<Expr> {
|
||||
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 {
|
||||
attrs: Vec::new(),
|
||||
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