diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md index 251a6a10..2ce0b65a 100644 --- a/docs/1.1-attributes.md +++ b/docs/1.1-attributes.md @@ -276,6 +276,20 @@ Set the Rust built-in [`deprecated`](https://doc.rust-lang.org/edition-guide/rus Set the path to the schemars crate instance the generated code should depend on. This is mostly useful for other crates that depend on schemars in their macros. +

+ +`#[schemars(inner(...))]` +

+ +Sets properties specified by [validator attributes](#supported-validator-attributes) on items of an array schema. For example: + +```rs +struct Struct { + #[schemars(inner(url, regex(pattern = "^https://")))] + urls: Vec, +} +``` +

Doc Comments (`#[doc = "..."]`) diff --git a/docs/_includes/examples/schemars_attrs.rs b/docs/_includes/examples/schemars_attrs.rs index cd69b527..4ad2503f 100644 --- a/docs/_includes/examples/schemars_attrs.rs +++ b/docs/_includes/examples/schemars_attrs.rs @@ -10,6 +10,8 @@ pub struct MyStruct { pub my_bool: bool, #[schemars(default)] pub my_nullable_enum: Option, + #[schemars(inner(regex(pattern = "^x$")))] + pub my_vec_str: Vec, } #[derive(Deserialize, Serialize, JsonSchema)] diff --git a/docs/_includes/examples/schemars_attrs.schema.json b/docs/_includes/examples/schemars_attrs.schema.json index 958cb6bb..9a6a22a6 100644 --- a/docs/_includes/examples/schemars_attrs.schema.json +++ b/docs/_includes/examples/schemars_attrs.schema.json @@ -4,7 +4,8 @@ "type": "object", "required": [ "myBool", - "myNumber" + "myNumber", + "myVecStr" ], "properties": { "myBool": { @@ -26,6 +27,13 @@ "format": "int32", "maximum": 10.0, "minimum": 1.0 + }, + "myVecStr": { + "type": "array", + "items": { + "type": "string", + "pattern": "^x$" + } } }, "additionalProperties": false, diff --git a/schemars/examples/schemars_attrs.rs b/schemars/examples/schemars_attrs.rs index cd69b527..4ad2503f 100644 --- a/schemars/examples/schemars_attrs.rs +++ b/schemars/examples/schemars_attrs.rs @@ -10,6 +10,8 @@ pub struct MyStruct { pub my_bool: bool, #[schemars(default)] pub my_nullable_enum: Option, + #[schemars(inner(regex(pattern = "^x$")))] + pub my_vec_str: Vec, } #[derive(Deserialize, Serialize, JsonSchema)] diff --git a/schemars/examples/schemars_attrs.schema.json b/schemars/examples/schemars_attrs.schema.json index 958cb6bb..9a6a22a6 100644 --- a/schemars/examples/schemars_attrs.schema.json +++ b/schemars/examples/schemars_attrs.schema.json @@ -4,7 +4,8 @@ "type": "object", "required": [ "myBool", - "myNumber" + "myNumber", + "myVecStr" ], "properties": { "myBool": { @@ -26,6 +27,13 @@ "format": "int32", "maximum": 10.0, "minimum": 1.0 + }, + "myVecStr": { + "type": "array", + "items": { + "type": "string", + "pattern": "^x$" + } } }, "additionalProperties": false, diff --git a/schemars/tests/expected/validate_inner.json b/schemars/tests/expected/validate_inner.json new file mode 100644 index 00000000..443a9fa6 --- /dev/null +++ b/schemars/tests/expected/validate_inner.json @@ -0,0 +1,74 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Struct", + "type": "object", + "required": [ + "array_str_length", + "slice_str_contains", + "vec_i32_range", + "vec_str_length", + "vec_str_length2", + "vec_str_regex", + "vec_str_url" + ], + "properties": { + "array_str_length": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100, + "minLength": 5 + }, + "maxItems": 2, + "minItems": 2 + }, + "slice_str_contains": { + "type": "array", + "items": { + "type": "string", + "pattern": "substring\\.\\.\\." + } + }, + "vec_i32_range": { + "type": "array", + "items": { + "type": "integer", + "format": "int32", + "maximum": 10.0, + "minimum": -10.0 + } + }, + "vec_str_length": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100, + "minLength": 1 + } + }, + "vec_str_length2": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "maxItems": 3, + "minItems": 1 + }, + "vec_str_regex": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[Hh]ello\\b" + } + }, + "vec_str_url": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/validate_schemars_attrs.json b/schemars/tests/expected/validate_schemars_attrs.json index d4a14e3f..86658ac4 100644 --- a/schemars/tests/expected/validate_schemars_attrs.json +++ b/schemars/tests/expected/validate_schemars_attrs.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Struct", + "title": "Struct2", "type": "object", "required": [ "contains_str1", diff --git a/schemars/tests/validate.rs b/schemars/tests/validate.rs index e3817343..bf28d62c 100644 --- a/schemars/tests/validate.rs +++ b/schemars/tests/validate.rs @@ -101,7 +101,7 @@ pub struct Struct2 { #[test] fn validate_schemars_attrs() -> TestResult { - test_default_generated_schema::("validate_schemars_attrs") + test_default_generated_schema::("validate_schemars_attrs") } #[derive(JsonSchema)] diff --git a/schemars/tests/validate_inner.rs b/schemars/tests/validate_inner.rs new file mode 100644 index 00000000..535410f1 --- /dev/null +++ b/schemars/tests/validate_inner.rs @@ -0,0 +1,31 @@ +mod util; + +use schemars::JsonSchema; +use util::*; + +// In real code, this would typically be a Regex, potentially created in a `lazy_static!`. +static STARTS_WITH_HELLO: &str = r"^[Hh]ello\b"; + +#[allow(dead_code)] +#[derive(JsonSchema)] +pub struct Struct<'a> { + #[schemars(inner(length(min = 5, max = 100)))] + array_str_length: [&'a str; 2], + #[schemars(inner(contains(pattern = "substring...")))] + slice_str_contains: &'a[&'a str], + #[schemars(inner(regex = "STARTS_WITH_HELLO"))] + vec_str_regex: Vec, + #[schemars(inner(length(min = 1, max = 100)))] + vec_str_length: Vec<&'a str>, + #[schemars(length(min = 1, max = 3), inner(length(min = 1, max = 100)))] + vec_str_length2: Vec, + #[schemars(inner(url))] + vec_str_url: Vec, + #[schemars(inner(range(min = -10, max = 10)))] + vec_i32_range: Vec, +} + +#[test] +fn validate_inner() -> TestResult { + test_default_generated_schema::("validate_inner") +} diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index 79b468a2..f95bc485 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -103,11 +103,7 @@ impl Attrs { } }; - for meta_item in attrs - .iter() - .flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors)) - .flatten() - { + for meta_item in get_meta_items(attrs, attr_type, errors, ignore_errors) { match &meta_item { Meta(NameValue(m)) if m.path.is_ident("with") => { if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.lit) { @@ -167,6 +163,12 @@ impl Attrs { _ if ignore_errors => {} + Meta(List(m)) if m.path.is_ident("inner") && attr_type == "schemars" => { + // This will be processed with the validation attributes. + // It's allowed only for the schemars attribute because the + // validator crate doesn't support it yet. + } + Meta(meta_item) => { if !is_known_serde_or_validation_keyword(meta_item) { let path = meta_item @@ -214,30 +216,25 @@ fn is_known_serde_or_validation_keyword(meta: &syn::Meta) -> bool { } fn get_meta_items( - attr: &syn::Attribute, + attrs: &[syn::Attribute], attr_type: &'static str, errors: &Ctxt, ignore_errors: bool, -) -> Result, ()> { - if !attr.path.is_ident(attr_type) { - return Ok(Vec::new()); - } - - match attr.parse_meta() { - Ok(List(meta)) => Ok(meta.nested.into_iter().collect()), - Ok(other) => { - if !ignore_errors { - errors.error_spanned_by(other, format!("expected #[{}(...)]", attr_type)) - } - Err(()) +) -> Vec { + attrs.iter().fold(vec![], |mut acc, attr| { + if !attr.path.is_ident(attr_type) { + return acc; } - Err(err) => { - if !ignore_errors { - errors.error_spanned_by(attr, err) + match attr.parse_meta() { + Ok(List(meta)) => acc.extend(meta.nested), + Ok(other) if !ignore_errors => { + errors.error_spanned_by(other, format!("expected #[{}(...)]", attr_type)) } - Err(()) + Err(err) if !ignore_errors => errors.error_spanned_by(attr, err), + _ => (), } - } + acc + }) } fn get_lit_str<'a>( diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs index a089a1de..ec7cc145 100644 --- a/schemars_derive/src/attr/validation.rs +++ b/schemars_derive/src/attr/validation.rs @@ -43,13 +43,17 @@ pub struct ValidationAttrs { contains: Option, required: bool, format: Option, + inner: Option>, } impl ValidationAttrs { pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self { + let schemars_items = get_meta_items(attrs, "schemars", errors, false); + let validate_items = get_meta_items(attrs, "validate", errors, true); + ValidationAttrs::default() - .populate(attrs, "schemars", false, errors) - .populate(attrs, "validate", true, errors) + .populate(schemars_items, "schemars", false, errors) + .populate(validate_items, "validate", true, errors) } pub fn required(&self) -> bool { @@ -58,7 +62,7 @@ impl ValidationAttrs { fn populate( mut self, - attrs: &[syn::Attribute], + meta_items: Vec, attr_type: &'static str, ignore_errors: bool, errors: &Ctxt, @@ -97,11 +101,7 @@ impl ValidationAttrs { } }; - for meta_item in attrs - .iter() - .flat_map(|attr| get_meta_items(attr, attr_type, errors, ignore_errors)) - .flatten() - { + for meta_item in meta_items { match &meta_item { NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("length") => { for nested in meta_list.nested.iter() { @@ -247,7 +247,8 @@ impl ValidationAttrs { if !ignore_errors { errors.error_spanned_by( meta, - "unknown item in schemars regex attribute".to_string(), + "unknown item in schemars regex attribute" + .to_string(), ); } } @@ -292,7 +293,8 @@ impl ValidationAttrs { if !ignore_errors { errors.error_spanned_by( meta, - "unknown item in schemars contains attribute".to_string(), + "unknown item in schemars contains attribute" + .to_string(), ); } } @@ -302,6 +304,21 @@ impl ValidationAttrs { } } + NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("inner") => { + match self.inner { + Some(_) => duplicate_error(&meta_list.path), + None => { + let inner_attrs = ValidationAttrs::default().populate( + meta_list.nested.clone().into_iter().collect(), + attr_type, + ignore_errors, + errors, + ); + self.inner = Some(Box::new(inner_attrs)); + } + } + } + _ => {} } } @@ -309,16 +326,24 @@ impl ValidationAttrs { } pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) { + if let Some(apply_expr) = self.apply_to_schema_expr() { + *schema_expr = quote! { + { + let mut schema = #schema_expr; + #apply_expr + schema + } + } + } + } + + fn apply_to_schema_expr(&self) -> Option { let mut array_validation = Vec::new(); let mut number_validation = Vec::new(); let mut object_validation = Vec::new(); let mut string_validation = Vec::new(); - if let Some(length_min) = self - .length_min - .as_ref() - .or(self.length_equal.as_ref()) - { + if let Some(length_min) = self.length_min.as_ref().or(self.length_equal.as_ref()) { string_validation.push(quote! { validation.min_length = Some(#length_min as u32); }); @@ -327,11 +352,7 @@ impl ValidationAttrs { }); } - if let Some(length_max) = self - .length_max - .as_ref() - .or(self.length_equal.as_ref()) - { + if let Some(length_max) = self.length_max.as_ref().or(self.length_equal.as_ref()) { string_validation.push(quote! { validation.max_length = Some(#length_max as u32); }); @@ -378,6 +399,21 @@ impl ValidationAttrs { } }); + let inner_validation = self + .inner + .as_deref() + .and_then(|inner| inner.apply_to_schema_expr()) + .map(|apply_expr| { + quote! { + if schema_object.has_type(schemars::schema::InstanceType::Array) { + if let Some(schemars::schema::SingleOrVec::Single(inner_schema)) = &mut schema_object.array().items { + let mut schema = &mut **inner_schema; + #apply_expr + } + } + } + }); + let array_validation = wrap_array_validation(array_validation); let number_validation = wrap_number_validation(number_validation); let object_validation = wrap_object_validation(object_validation); @@ -388,21 +424,20 @@ impl ValidationAttrs { || object_validation.is_some() || string_validation.is_some() || format.is_some() + || inner_validation.is_some() { - *schema_expr = quote! { - { - let mut schema = #schema_expr; - if let schemars::schema::Schema::Object(schema_object) = &mut schema - { - #array_validation - #number_validation - #object_validation - #string_validation - #format - } - schema + Some(quote! { + if let schemars::schema::Schema::Object(schema_object) = &mut schema { + #array_validation + #number_validation + #object_validation + #string_validation + #format + #inner_validation } - } + }) + } else { + None } } }