Repository: oxidecomputer/typify
Pull request #825 (2025-05-14 21:35:40)

improve regex validation performance.
the regexes are cached in a lazy static variable to avoid reparse on each evaluation. parse is expensive compared to eval. added a benchmark suite to verify the change. local results:

before:
```
long regex              time:   [57.194 µs 57.266 µs 57.372 µs]
short regex             time:   [5.0102 µs 5.0126 µs 5.0150 µs]
```

after:
```
long regex              time:   [297.06 ns 297.50 ns 297.96 ns]
short regex             time:   [114.12 ns 114.41 ns 114.72 ns]
```

the toolchain bump i had to do for local development. I could not bump the toolchain all the way to latest because increased strictness of irrefutable-let warnings regresses several trybuild test cases.

see https://github.com/oxidecomputer/typify/issues/682


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 4846e82..cc2f89f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -576,9 +576,9 @@ dependencies = [
 
 [[package]]
 name = "lazy_static"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
 
 [[package]]
 name = "libc"
@@ -782,9 +782,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.10.2"
+version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -794,9 +794,9 @@ dependencies = [
 
 [[package]]
 name = "regex-automata"
-version = "0.4.3"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -805,9 +805,9 @@ dependencies = [
 
 [[package]]
 name = "regex-syntax"
-version = "0.8.2"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
 [[package]]
 name = "regress"
diff --git a/Cargo.toml b/Cargo.toml
index 4a99a96..54c83d5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,3 +44,4 @@ thiserror = "2.0.12"
 trybuild = "1.0.105"
 unicode-ident = "1.0.18"
 uuid = "1.16.0"
+criterion = "0.5.1"
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
index 7f466bd..bbf217f 100644
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,3 +1,3 @@
 [toolchain]
-channel = "1.80.1"
+channel = "1.81.0"
 profile = "default"
diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs
index 6e877d7..b92f078 100644
--- a/typify-impl/src/convert.rs
+++ b/typify-impl/src/convert.rs
@@ -2067,8 +2067,7 @@ mod tests {
     use serde_json::json;
 
     use crate::{
-        test_util::validate_output, validate_builtin, Error, Name, TypeSpace, TypeSpaceImpl,
-        TypeSpaceSettings,
+        test_util::validate_output, Error, Name, TypeSpace, TypeSpaceImpl, TypeSpaceSettings,
     };
 
     #[track_caller]
@@ -2177,13 +2176,6 @@ mod tests {
         validate_output::<Bar>();
     }
 
-    // TODO we can turn this on once we generate proper sets.
-    #[ignore]
-    #[test]
-    fn test_set() {
-        validate_builtin!(std::collections::BTreeSet<u32>);
-    }
-
     #[test]
     fn test_low_default() {
         let schema = SchemaObject {
diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs
index 93f91dc..0f5f490 100644
--- a/typify-impl/src/lib.rs
+++ b/typify-impl/src/lib.rs
@@ -1176,7 +1176,7 @@ impl<'a> TypeStruct<'a> {
     }
 
     /// Get all information about each struct property.
-    pub fn properties_info(&'a self) -> impl Iterator<Item = TypeStructPropInfo> {
+    pub fn properties_info(&'a self) -> impl Iterator<Item = TypeStructPropInfo<'a>> {
         self.details
             .properties
             .iter()
diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs
index 2a41977..e8077c6 100644
--- a/typify-impl/src/type_entry.rs
+++ b/typify-impl/src/type_entry.rs
@@ -1527,10 +1527,14 @@ impl TypeEntry {
                         }
                     }
                 });
+
                 let pat = pattern.as_ref().map(|p| {
                     let err = format!("doesn't match pattern \"{}\"", p);
                     quote! {
-                        if regress::Regex::new(#p).unwrap().find(value).is_none() {
+                        static PATTERN: ::std::sync::LazyLock<::regress::Regex> = ::std::sync::LazyLock::new(|| {
+                            ::regress::Regex::new(#p).unwrap()
+                        });
+                        if (&*PATTERN).find(value).is_none() {
                             return Err(#err.into());
                         }
                     }


Test patch:
diff --git a/typify-impl/src/test_util.rs b/typify-impl/src/test_util.rs
index 2a9938f..bbc2fd4 100644
--- a/typify-impl/src/test_util.rs
+++ b/typify-impl/src/test_util.rs
@@ -1,4 +1,4 @@
-// Copyright 2022 Oxide Computer Company
+// Copyright 2025 Oxide Computer Company
 
 use std::{any::type_name, collections::HashSet};
 
@@ -12,7 +12,7 @@ use syn::{
     FieldsNamed, FieldsUnnamed, File, Type, TypePath, TypeTuple, Variant,
 };
 
-use crate::{output::OutputSpace, Name, RefKey, TypeId, TypeSpace};
+use crate::{output::OutputSpace, RefKey, TypeId, TypeSpace};
 
 pub(crate) fn get_type<T: JsonSchema>() -> (TypeSpace, TypeId) {
     let schema = schema_for!(T);
@@ -141,40 +141,6 @@ fn decanonicalize_std_types(mut input: DeriveInput) -> DeriveInput {
     input
 }
 
-#[macro_export]
-macro_rules! validate_builtin {
-    ($t:ty) => {
-        $crate::test_util::validate_builtin_impl::<$t>(stringify!($t))
-    };
-}
-
-#[track_caller]
-pub(crate) fn validate_builtin_impl<T: JsonSchema>(name: &str) {
-    let schema = schema_for!(T);
-    let original_schema = schemars::schema::Schema::Object(schema.schema.clone());
-
-    let mut type_space = TypeSpace::default();
-    type_space
-        .add_ref_types(schema.definitions.clone())
-        .unwrap();
-    let (ty, _) = type_space
-        .convert_schema_object(Name::Unknown, &original_schema, &schema.schema)
-        .unwrap();
-
-    let output = ty.type_ident(&type_space, &None);
-
-    let actual = syn::parse2::<syn::Type>(output.clone()).unwrap();
-    let expected = syn::parse_str::<syn::Type>(name).unwrap();
-
-    // Make sure they match.
-    if let Err(err) = expected.syn_cmp(&actual, false) {
-        println!("{:#?}", schema);
-        println!("actual: {}", output);
-        println!("expected: {}", name);
-        panic!("{}", err);
-    }
-}
-
 pub(crate) trait SynCompare {
     fn syn_cmp(&self, other: &Self, ignore_variant_names: bool) -> Result<(), String>;
 }
diff --git a/typify-impl/tests/vega.out b/typify-impl/tests/vega.out
index 946bef1..5fade63 100644
--- a/typify-impl/tests/vega.out
+++ b/typify-impl/tests/vega.out
@@ -22327,11 +22327,9 @@ impl ::std::convert::From<&DataVariant2FormatVariant0Subtype0ParseVariant1ValueV
 impl ::std::str::FromStr for DataVariant2FormatVariant0Subtype0ParseVariant1ValueVariant1 {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("^(date|utc):.*$")
-            .unwrap()
-            .find(value)
-            .is_none()
-        {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"^(date|utc):.*$\"".into());
         }
         Ok(Self(value.to_string()))
@@ -22849,11 +22847,9 @@ impl ::std::convert::From<&DataVariant2FormatVariant0Subtype1ParseVariant1ValueV
 impl ::std::str::FromStr for DataVariant2FormatVariant0Subtype1ParseVariant1ValueVariant1 {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("^(date|utc):.*$")
-            .unwrap()
-            .find(value)
-            .is_none()
-        {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"^(date|utc):.*$\"".into());
         }
         Ok(Self(value.to_string()))
@@ -23430,11 +23426,9 @@ impl ::std::convert::From<&DataVariant2FormatVariant0Subtype2ParseVariant1ValueV
 impl ::std::str::FromStr for DataVariant2FormatVariant0Subtype2ParseVariant1ValueVariant1 {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("^(date|utc):.*$")
-            .unwrap()
-            .find(value)
-            .is_none()
-        {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"^(date|utc):.*$\"".into());
         }
         Ok(Self(value.to_string()))
@@ -24020,11 +24014,9 @@ impl ::std::convert::From<&DataVariant2FormatVariant0Subtype3ParseVariant1ValueV
 impl ::std::str::FromStr for DataVariant2FormatVariant0Subtype3ParseVariant1ValueVariant1 {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("^(date|utc):.*$")
-            .unwrap()
-            .find(value)
-            .is_none()
-        {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"^(date|utc):.*$\"".into());
         }
         Ok(Self(value.to_string()))
@@ -25225,11 +25217,9 @@ impl ::std::convert::From<&DataVariant3FormatVariant0Subtype0ParseVariant1ValueV
 impl ::std::str::FromStr for DataVariant3FormatVariant0Subtype0ParseVariant1ValueVariant1 {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("^(date|utc):.*$")
-            .unwrap()
-            .find(value)
-            .is_none()
-        {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"^(date|utc):.*$\"".into());
         }
         Ok(Self(value.to_string()))
@@ -25747,11 +25737,9 @@ impl ::std::convert::From<&DataVariant3FormatVariant0Subtype1ParseVariant1ValueV
 impl ::std::str::FromStr for DataVariant3FormatVariant0Subtype1ParseVariant1ValueVariant1 {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("^(date|utc):.*$")
-            .unwrap()
-            .find(value)
-            .is_none()
-        {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"^(date|utc):.*$\"".into());
         }
         Ok(Self(value.to_string()))
@@ -26328,11 +26316,9 @@ impl ::std::convert::From<&DataVariant3FormatVariant0Subtype2ParseVariant1ValueV
 impl ::std::str::FromStr for DataVariant3FormatVariant0Subtype2ParseVariant1ValueVariant1 {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("^(date|utc):.*$")
-            .unwrap()
-            .find(value)
-            .is_none()
-        {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"^(date|utc):.*$\"".into());
         }
         Ok(Self(value.to_string()))
@@ -26918,11 +26904,9 @@ impl ::std::convert::From<&DataVariant3FormatVariant0Subtype3ParseVariant1ValueV
 impl ::std::str::FromStr for DataVariant3FormatVariant0Subtype3ParseVariant1ValueVariant1 {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("^(date|utc):.*$")
-            .unwrap()
-            .find(value)
-            .is_none()
-        {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new("^(date|utc):.*$").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"^(date|utc):.*$\"".into());
         }
         Ok(Self(value.to_string()))
@@ -31398,7 +31382,9 @@ impl ::std::convert::From<&EncodeKey> for EncodeKey {
 impl ::std::str::FromStr for EncodeKey {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("^.+$").unwrap().find(value).is_none() {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new("^.+$").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"^.+$\"".into());
         }
         Ok(Self(value.to_string()))
@@ -107885,11 +107871,11 @@ impl ::std::convert::From<&TitleVariant1EncodeSubtype0Key> for TitleVariant1Enco
 impl ::std::str::FromStr for TitleVariant1EncodeSubtype0Key {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("^(?!interactive|name|style).+$")
-            .unwrap()
-            .find(value)
-            .is_none()
-        {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| {
+                ::regress::Regex::new("^(?!interactive|name|style).+$").unwrap()
+            });
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"^(?!interactive|name|style).+$\"".into());
         }
         Ok(Self(value.to_string()))
diff --git a/typify/tests/schemas/id-or-name.rs b/typify/tests/schemas/id-or-name.rs
index 5ef9f55..469b94c 100644
--- a/typify/tests/schemas/id-or-name.rs
+++ b/typify/tests/schemas/id-or-name.rs
@@ -324,7 +324,9 @@ impl ::std::convert::From<&IdOrYoloYolo> for IdOrYoloYolo {
 impl ::std::str::FromStr for IdOrYoloYolo {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new(".*").unwrap().find(value).is_none() {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new(".*").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \".*\"".into());
         }
         Ok(Self(value.to_string()))
@@ -403,7 +405,14 @@ impl ::std::str::FromStr for Name {
         if value.chars().count() > 63usize {
             return Err("longer than 63 characters".into());
         }
-        if regress :: Regex :: new ("^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$") . unwrap () . find (value) . is_none () { return Err ("doesn't match pattern \"^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$\"" . into ()) ; }
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> = ::std::sync::LazyLock::new(
+            || {
+                :: regress :: Regex :: new ("^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$") . unwrap ()
+            },
+        );
+        if (&*PATTERN).find(value).is_none() {
+            return Err ("doesn't match pattern \"^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$\"" . into ()) ;
+        }
         Ok(Self(value.to_string()))
     }
 }
diff --git a/typify/tests/schemas/property-pattern.rs b/typify/tests/schemas/property-pattern.rs
index e38bef1..cfc13bc 100644
--- a/typify/tests/schemas/property-pattern.rs
+++ b/typify/tests/schemas/property-pattern.rs
@@ -102,11 +102,9 @@ impl ::std::convert::From<&TestGrammarForPatternPropertiesRulesKey>
 impl ::std::str::FromStr for TestGrammarForPatternPropertiesRulesKey {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("^[a-zA-Z_]\\w*$")
-            .unwrap()
-            .find(value)
-            .is_none()
-        {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new("^[a-zA-Z_]\\w*$").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"^[a-zA-Z_]\\w*$\"".into());
         }
         Ok(Self(value.to_string()))
diff --git a/typify/tests/schemas/types-with-more-impls.rs b/typify/tests/schemas/types-with-more-impls.rs
index 56791fd..918b759 100644
--- a/typify/tests/schemas/types-with-more-impls.rs
+++ b/typify/tests/schemas/types-with-more-impls.rs
@@ -58,7 +58,9 @@ impl ::std::convert::From<&PatternString> for PatternString {
 impl ::std::str::FromStr for PatternString {
     type Err = self::error::ConversionError;
     fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
-        if regress::Regex::new("xx").unwrap().find(value).is_none() {
+        static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
+            ::std::sync::LazyLock::new(|| ::regress::Regex::new("xx").unwrap());
+        if (&*PATTERN).find(value).is_none() {
             return Err("doesn't match pattern \"xx\"".into());
         }
         Ok(Self(value.to_string()))