Repository: GREsau/schemars
Pull request #328 (2024-08-24 16:17:37)

Allow `regex(path = ...)` value to be a non-string expression
Addresses #302


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b1260e7..e46e943 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,16 +14,12 @@ jobs:
           - nightly
         include:
           - rust: 1.65.0
-            test_features: ""
             allow_failure: false
           - rust: stable
-            test_features: "--all-features"
             allow_failure: false
           - rust: beta
-            test_features: "--all-features"
             allow_failure: false
           - rust: nightly
-            test_features: "--all-features"
             allow_failure: true
       fail-fast: false
     steps:
@@ -38,7 +34,7 @@ jobs:
         continue-on-error: ${{ matrix.allow_failure }}
         working-directory: ./schemars
       - name: Run tests
-        run: cargo test --verbose ${{ matrix.test_features }} --no-fail-fast
+        run: cargo test --verbose --all-features --no-fail-fast
         continue-on-error: ${{ matrix.allow_failure }}
         working-directory: ./schemars
       - name: Run derive tests
diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs
index e99e4f2..8e7caab 100644
--- a/schemars_derive/src/attr/mod.rs
+++ b/schemars_derive/src/attr/mod.rs
@@ -190,12 +190,6 @@ 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.
-                }
-
                 _ => {
                     if !is_known_serde_or_validation_keyword(&meta_item) {
                         let path = meta_item
diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs
index 095590a..0d9acf9 100644
--- a/schemars_derive/src/attr/validation.rs
+++ b/schemars_derive/src/attr/validation.rs
@@ -1,13 +1,14 @@
-use super::{expr_as_lit_str, get_meta_items, parse_lit_into_path, parse_lit_str};
+use super::{expr_as_lit_str, get_meta_items, parse_lit_str};
 use proc_macro2::TokenStream;
 use quote::ToTokens;
 use serde_derive_internals::Ctxt;
 use syn::{
-    parse::Parser, punctuated::Punctuated, Expr, ExprPath, Lit, Meta, MetaList, MetaNameValue, Path,
+    parse::Parser, punctuated::Punctuated, Expr, ExprLit, ExprPath, Lit, Meta, MetaList,
+    MetaNameValue, Path,
 };
 
 pub(crate) static VALIDATION_KEYWORDS: &[&str] = &[
-    "range", "regex", "contains", "email", "phone", "url", "length", "required",
+    "range", "regex", "contains", "email", "phone", "url", "length", "required", "inner",
 ];
 
 #[derive(Debug, Clone, Copy, PartialEq)]
@@ -214,8 +215,7 @@ impl ValidationAttrs {
                         (Some(_), _) => duplicate_error(&nv.path),
                         (None, Some(_)) => mutual_exclusive_error(&nv.path, "contains"),
                         (None, None) => {
-                            self.regex =
-                                parse_lit_into_expr_path(errors, attr_type, "regex", &nv.value).ok()
+                            self.regex = parse_regex_expr(errors, nv.value);
                         }
                     }
                 }
@@ -230,10 +230,7 @@ impl ValidationAttrs {
                                     Meta::NameValue(MetaNameValue { path, value, .. })
                                         if path.is_ident("path") =>
                                     {
-                                        self.regex = parse_lit_into_expr_path(
-                                            errors, attr_type, "path", &value,
-                                        )
-                                        .ok()
+                                        self.regex = parse_regex_expr(errors, value);
                                     }
                                     Meta::NameValue(MetaNameValue { path, value, .. })
                                         if path.is_ident("pattern") =>
@@ -242,7 +239,7 @@ impl ValidationAttrs {
                                             expr_as_lit_str(errors, attr_type, "pattern", &value)
                                                 .ok()
                                                 .map(|litstr| {
-                                                    Expr::Lit(syn::ExprLit {
+                                                    Expr::Lit(ExprLit {
                                                         attrs: Vec::new(),
                                                         lit: Lit::Str(litstr.clone()),
                                                     })
@@ -316,7 +313,18 @@ impl ValidationAttrs {
                     }
                 },
 
-                _ => {}
+                _ if ignore_errors => {}
+
+                _ => {
+                    if let Some(ident) = meta_item.path().get_ident() {
+                        if VALIDATION_KEYWORDS.iter().any(|k| ident == k) {
+                            errors.error_spanned_by(
+                                &meta_item,
+                                format!("could not parse `{ident}` item in schemars attribute"),
+                            );
+                        }
+                    }
+                }
             }
         }
         self
@@ -405,21 +413,6 @@ impl ValidationAttrs {
     }
 }
 
-fn parse_lit_into_expr_path(
-    cx: &Ctxt,
-    attr_type: &'static str,
-    meta_item_name: &'static str,
-    lit: &Expr,
-) -> 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 str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, expr: Expr) -> Option<Expr> {
     // this odd double-parsing is to make `-10` parsed as an Lit instead of an Expr::Unary
     let lit: Lit = match syn::parse2(expr.to_token_stream()) {
@@ -445,3 +438,13 @@ fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, expr: Expr) -> Option<Exp
         }
     }
 }
+
+fn parse_regex_expr(cx: &Ctxt, value: Expr) -> Option<Expr> {
+    match value {
+        Expr::Lit(ExprLit {
+            lit: Lit::Str(litstr),
+            ..
+        }) => parse_lit_str(&litstr).map_err(|e| cx.syn_error(e)).ok(),
+        v => Some(v),
+    }
+}


Test patch:
diff --git a/schemars/tests/indexmap.rs b/schemars/tests/indexmap.rs
index e243f35..76fcd56 100644
--- a/schemars/tests/indexmap.rs
+++ b/schemars/tests/indexmap.rs
@@ -1,5 +1,5 @@
 mod util;
-use std::hash::RandomState;
+use std::collections::hash_map::RandomState;
 
 use indexmap2::{IndexMap, IndexSet};
 use schemars::JsonSchema;
diff --git a/schemars/tests/ui/invalid_validation_attrs.rs b/schemars/tests/ui/invalid_validation_attrs.rs
index be84362..79a4735 100644
--- a/schemars/tests/ui/invalid_validation_attrs.rs
+++ b/schemars/tests/ui/invalid_validation_attrs.rs
@@ -1,10 +1,14 @@
 use schemars::JsonSchema;
 
+// FIXME validation attrs like `email` should be disallowed non structs/enums/variants
+
 #[derive(JsonSchema)]
-pub struct Struct1(#[validate(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
+#[validate(email)]
+pub struct Struct1(#[validate(regex, foo, length(min = 1, equal = 2, bar))] String);
 
 #[derive(JsonSchema)]
-pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
+#[schemars(email)]
+pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
 
 #[derive(JsonSchema)]
 pub struct Struct3(
diff --git a/schemars/tests/ui/invalid_validation_attrs.stderr b/schemars/tests/ui/invalid_validation_attrs.stderr
index 933fd66..56d4404 100644
--- a/schemars/tests/ui/invalid_validation_attrs.stderr
+++ b/schemars/tests/ui/invalid_validation_attrs.stderr
@@ -1,59 +1,53 @@
-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);
-  |                                          ^^^
+  --> tests/ui/invalid_validation_attrs.rs:11:38
+   |
+11 | pub struct Struct2(#[schemars(regex, 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: could not parse `regex` item in schemars attribute
+  --> tests/ui/invalid_validation_attrs.rs:11:31
+   |
+11 | pub struct Struct2(#[schemars(regex, 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);
-  |                                                               ^^^^^
+  --> tests/ui/invalid_validation_attrs.rs:11:59
+   |
+11 | pub struct Struct2(#[schemars(regex, 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);
-  |                                                                          ^^^
+  --> tests/ui/invalid_validation_attrs.rs:11:70
+   |
+11 | pub struct Struct2(#[schemars(regex, foo, length(min = 1, equal = 2, bar))] String);
+   |                                                                      ^^^
 
 error: schemars attribute cannot contain both `contains` and `regex`
-  --> $DIR/invalid_validation_attrs.rs:26:9
+  --> tests/ui/invalid_validation_attrs.rs:30:9
    |
-26 |         contains = "bar",
+30 |         contains = "bar",
    |         ^^^^^^^^
 
 error: duplicate schemars attribute `regex`
-  --> $DIR/invalid_validation_attrs.rs:27:9
+  --> tests/ui/invalid_validation_attrs.rs:31:9
    |
-27 |         regex(path = "baz"),
+31 |         regex(path = "baz"),
    |         ^^^^^
 
 error: schemars attribute cannot contain both `phone` and `email`
-  --> $DIR/invalid_validation_attrs.rs:29:9
+  --> tests/ui/invalid_validation_attrs.rs:33:9
    |
-29 |         email,
+33 |         email,
    |         ^^^^^
 
 error: schemars attribute cannot contain both `phone` and `url`
-  --> $DIR/invalid_validation_attrs.rs:30:9
+  --> tests/ui/invalid_validation_attrs.rs:34:9
    |
-30 |         url
+34 |         url
    |         ^^^
 
 error[E0425]: cannot find value `foo` in this scope
-  --> $DIR/invalid_validation_attrs.rs:12:17
+  --> tests/ui/invalid_validation_attrs.rs:16:17
    |
-12 |         regex = "foo",
+16 |         regex = "foo",
    |                 ^^^^^ not found in this scope
diff --git a/schemars/tests/validate.rs b/schemars/tests/validate.rs
index 560fd57..0d0eefb 100644
--- a/schemars/tests/validate.rs
+++ b/schemars/tests/validate.rs
@@ -16,7 +16,7 @@ pub struct Struct {
     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,
     #[validate(regex(path = "STARTS_WITH_HELLO", code = "foo"))]
     regex_str2: String,