Repository: GREsau/schemars (rust)

Issue:
Set regex for Array String items
For the following code:

```Rust
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct Team {
    /// # Users
    /// A list of users.
    #[schemars(regex(pattern = r"^(.*)$"))]
    pub users: Vec<String>
}
```

This is the output I currently see for the `users` field:
```json
"users": {
    "title": "Users",
    "description": "A list users.",
    "type": "array",
    "items": {
        "type": "string"
    }
},
```
As you can see, the regex is simply getting omitted entirely.

What I would like to see is:
```json
"users": {
    "title": "Users",
    "description": "A list users.",
    "type": "array",
    "items": {
        "pattern": "^(.*)$",
        "type": "string"
    }
},
```
Since regex doesn't make sense for an `array`, it should bubble down to the contained `String` type.

I would also expect this to work when setting `length(max = 39)` on the `users` field, where it isn't applied to the array, but to the underlying String.

Pull request description:
Add #[schemars(inner(...)] attribute to specify schema for array items
Resolves #110

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 0540418..075df0e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,3 +3,4 @@ members = [
     "schemars",
     "schemars_derive"
 ]
+resolver = "2"
diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md
index 251a6a1..2ce0b65 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.
 
+<h3 id="inner">
+
+`#[schemars(inner(...))]`
+</h3>
+
+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<String>,
+}
+```
+
 <h3 id="doc">
 
 Doc Comments (`#[doc = "..."]`)
diff --git a/docs/_includes/examples/schemars_attrs.rs b/docs/_includes/examples/schemars_attrs.rs
index cd69b52..4ad2503 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<MyEnum>,
+    #[schemars(inner(regex(pattern = "^x$")))]
+    pub my_vec_str: Vec<String>,
 }
 
 #[derive(Deserialize, Serialize, JsonSchema)]
diff --git a/docs/_includes/examples/schemars_attrs.schema.json b/docs/_includes/examples/schemars_attrs.schema.json
index 958cb6b..9a6a22a 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 cd69b52..4ad2503 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<MyEnum>,
+    #[schemars(inner(regex(pattern = "^x$")))]
+    pub my_vec_str: Vec<String>,
 }
 
 #[derive(Deserialize, Serialize, JsonSchema)]
diff --git a/schemars/examples/schemars_attrs.schema.json b/schemars/examples/schemars_attrs.schema.json
index 958cb6b..9a6a22a 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_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs
index 79b468a..f95bc48 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<Vec<syn::NestedMeta>, ()> {
-    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<syn::NestedMeta> {
+    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 a089a1d..ec7cc14 100644
--- a/schemars_derive/src/attr/validation.rs
+++ b/schemars_derive/src/attr/validation.rs
@@ -43,13 +43,17 @@ pub struct ValidationAttrs {
     contains: Option<String>,
     required: bool,
     format: Option<Format>,
+    inner: Option<Box<ValidationAttrs>>,
 }
 
 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<syn::NestedMeta>,
         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<TokenStream> {
         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
         }
     }
 }


Test patch:
diff --git a/schemars/tests/expected/validate_inner.json b/schemars/tests/expected/validate_inner.json
new file mode 100644
index 0000000..443a9fa
--- /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 d4a14e3..86658ac 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 e381734..bf28d62 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::<Struct>("validate_schemars_attrs")
+    test_default_generated_schema::<Struct2>("validate_schemars_attrs")
 }
 
 #[derive(JsonSchema)]
diff --git a/schemars/tests/validate_inner.rs b/schemars/tests/validate_inner.rs
new file mode 100644
index 0000000..535410f
--- /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<String>,
+    #[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<String>,
+    #[schemars(inner(url))]
+    vec_str_url: Vec<String>,
+    #[schemars(inner(range(min = -10, max = 10)))]
+    vec_i32_range: Vec<i32>,
+}
+
+#[test]
+fn validate_inner() -> TestResult {
+    test_default_generated_schema::<Struct>("validate_inner")
+}