diff --git a/docs/_includes/examples/schemars_attrs.rs b/docs/_includes/examples/schemars_attrs.rs index f830e9f..cd69b52 100644 --- a/docs/_includes/examples/schemars_attrs.rs +++ b/docs/_includes/examples/schemars_attrs.rs @@ -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 }, + StringNewType(#[schemars(phone)] String), + StructVariant { + #[schemars(length(min = 1, max = 100))] + floats: Vec, + }, } fn main() { diff --git a/docs/_includes/examples/schemars_attrs.schema.json b/docs/_includes/examples/schemars_attrs.schema.json index d044193..958cb6b 100644 --- a/docs/_includes/examples/schemars_attrs.schema.json +++ b/docs/_includes/examples/schemars_attrs.schema.json @@ -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 } } } diff --git a/docs/_includes/examples/validate.rs b/docs/_includes/examples/validate.rs new file mode 100644 index 0000000..4116976 --- /dev/null +++ b/docs/_includes/examples/validate.rs @@ -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, +} + +#[derive(JsonSchema)] +pub enum MyEnum { + StringNewType(#[validate(phone)] String), + StructVariant { + #[validate(length(min = 1, max = 100))] + floats: Vec, + }, +} + +fn main() { + let schema = schema_for!(MyStruct); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/docs/_includes/examples/validate.schema.json b/docs/_includes/examples/validate.schema.json new file mode 100644 index 0000000..e8ed35e --- /dev/null +++ b/docs/_includes/examples/validate.schema.json @@ -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 + } + ] + } + } +} diff --git a/schemars/examples/schemars_attrs.rs b/schemars/examples/schemars_attrs.rs index f830e9f..cd69b52 100644 --- a/schemars/examples/schemars_attrs.rs +++ b/schemars/examples/schemars_attrs.rs @@ -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 }, + StringNewType(#[schemars(phone)] String), + StructVariant { + #[schemars(length(min = 1, max = 100))] + floats: Vec, + }, } fn main() { diff --git a/schemars/examples/schemars_attrs.schema.json b/schemars/examples/schemars_attrs.schema.json index d044193..958cb6b 100644 --- a/schemars/examples/schemars_attrs.schema.json +++ b/schemars/examples/schemars_attrs.schema.json @@ -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 } } } diff --git a/schemars/examples/validate.rs b/schemars/examples/validate.rs new file mode 100644 index 0000000..4116976 --- /dev/null +++ b/schemars/examples/validate.rs @@ -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, +} + +#[derive(JsonSchema)] +pub enum MyEnum { + StringNewType(#[validate(phone)] String), + StructVariant { + #[validate(length(min = 1, max = 100))] + floats: Vec, + }, +} + +fn main() { + let schema = schema_for!(MyStruct); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/schemars/examples/validate.schema.json b/schemars/examples/validate.schema.json new file mode 100644 index 0000000..e8ed35e --- /dev/null +++ b/schemars/examples/validate.schema.json @@ -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 + } + ] + } + } +} diff --git a/schemars/tests/expected/validate.json b/schemars/tests/expected/validate.json index 0878e84..d4a14e3 100644 --- a/schemars/tests/expected/validate.json +++ b/schemars/tests/expected/validate.json @@ -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": { diff --git a/schemars/tests/expected/validate_schemars_attrs.json b/schemars/tests/expected/validate_schemars_attrs.json new file mode 100644 index 0000000..d4a14e3 --- /dev/null +++ b/schemars/tests/expected/validate_schemars_attrs.json @@ -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" + } + } +} \ No newline at end of file diff --git a/schemars/tests/ui/invalid_attrs.stderr b/schemars/tests/ui/invalid_attrs.stderr index fa3a4f4..4823859 100644 --- a/schemars/tests/ui/invalid_attrs.stderr +++ b/schemars/tests/ui/invalid_attrs.stderr @@ -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)] diff --git a/schemars/tests/ui/invalid_validation_attrs.rs b/schemars/tests/ui/invalid_validation_attrs.rs new file mode 100644 index 0000000..144ddaa --- /dev/null +++ b/schemars/tests/ui/invalid_validation_attrs.rs @@ -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() {} diff --git a/schemars/tests/ui/invalid_validation_attrs.stderr b/schemars/tests/ui/invalid_validation_attrs.stderr new file mode 100644 index 0000000..3d68082 --- /dev/null +++ b/schemars/tests/ui/invalid_validation_attrs.stderr @@ -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); + | ^^^ diff --git a/schemars/tests/validate.rs b/schemars/tests/validate.rs index ca704a7..825b558 100644 --- a/schemars/tests/validate.rs +++ b/schemars/tests/validate.rs @@ -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, #[validate(contains = "map_key")] @@ -49,6 +56,48 @@ fn validate() -> TestResult { test_default_generated_schema::("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, + #[schemars(contains = "map_key")] + map_contains: HashMap, + #[schemars(required)] + required_option: Option, + #[schemars(required)] + #[serde(flatten)] + required_flattened: Option, +} + +#[test] +fn validate_schemars_attrs() -> TestResult { + test_default_generated_schema::("validate_schemars_attrs") +} + #[derive(Debug, JsonSchema)] pub struct Tuple( #[validate(range(max = 10))] u8, diff --git a/schemars_derive/src/ast/from_serde.rs b/schemars_derive/src/ast/from_serde.rs index db2e092..83bfcf3 100644 --- a/schemars_derive/src/ast/from_serde.rs +++ b/schemars_derive/src/ast/from_serde.rs @@ -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), }) } } diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index 93f76c5..cc81a92 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -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, diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs index 8594db1..9b807b3 100644 --- a/schemars_derive/src/attr/validation.rs +++ b/schemars_derive/src/attr/validation.rs @@ -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::(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::(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 { + 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) -> Option { if v.is_empty() { None @@ -283,27 +396,22 @@ fn wrap_string_validation(v: Vec) -> Option { } } -fn get_meta_items( - attr: &syn::Attribute, - attr_type: &'static str, -) -> Result, ()> { - 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 { +fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, lit: &Lit) -> Option { match lit { - Lit::Str(s) => parse_lit_str::(s).ok().map(Expr::Path), + Lit::Str(s) => parse_lit_str::(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 + } } }