Allow setting validation attributes via #[schemars(...)]

This commit is contained in:
Graham Esau 2021-04-18 22:17:53 +01:00
parent c013052f59
commit 7914593d89
17 changed files with 607 additions and 99 deletions

View file

@ -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() {

View file

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

View 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());
}

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

View file

@ -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() {

View file

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

View 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());
}

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

View file

@ -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": {

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

View file

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

View 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() {}

View 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);
| ^^^

View file

@ -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,

View file

@ -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),
}) })
} }
} }

View file

@ -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,

View file

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