Repository: oxidecomputer/typify (rust)

Issue:
Empty type when `patternProperties` is used.
I'm generating types for the [tree-sitter](https://github.com/tree-sitter/tree-sitter) [schema](https://github.com/tree-sitter/tree-sitter/blob/master/cli/src/generate/grammar-schema.json). The `rules` property in that schema is:

```json
"rules": {
  "type": "object",
  "patternProperties": {
    "^[a-zA-Z_]\\w*$": {
      "$ref": "#/definitions/rule"
    }
  },
  "additionalProperties": false
},
```

This generates a type which is just an empty struct, instead of what it probably should be, which is a `HashMap<String, Rule>`.

I'm working on a fix, but wanted to create an issue for tracking.

Edit: The generated type is:
```rust
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TreeSitterGrammarSpecificationRules {}
impl From<&TreeSitterGrammarSpecificationRules> for TreeSitterGrammarSpecificationRules {
    fn from(value: &TreeSitterGrammarSpecificationRules) -> Self {
        value.clone()
    }
}
```

Pull request:
Support patternProperties when used to create a make-like structure
This supersedes #636 and would close #635. Objects with `patternProperties` are now supported by generating a custom `Deserialize` implementation for a newtyped map which checks each key against the pattern during deserialization. `patternProperties` may have more than one pattern (which will be joined together with a regex alternation), but must have the same `Schema` for every pattern.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index eb6ed30..394e393 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -222,9 +222,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.11"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3"
+checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -232,9 +232,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.11"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa"
+checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
 dependencies = [
  "anstream",
  "anstyle",
@@ -244,9 +244,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.11"
+version = "4.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e"
+checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -936,9 +936,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.121"
+version = "1.0.122"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609"
+checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
 dependencies = [
  "itoa",
  "memchr",
@@ -1039,18 +1039,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
 
 [[package]]
 name = "thiserror"
-version = "1.0.62"
+version = "1.0.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
+checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.62"
+version = "1.0.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
+checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1157,9 +1157,9 @@ dependencies = [
 
 [[package]]
 name = "trybuild"
-version = "1.0.98"
+version = "1.0.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b55265878356bdd85c9baa15859c87de93b2bf1f33acf752040a561e4a228f62"
+checksum = "207aa50d36c4be8d8c6ea829478be44a372c6a77669937bb39c698e52f1491e8"
 dependencies = [
  "glob",
  "serde",
diff --git a/cargo-typify/Cargo.toml b/cargo-typify/Cargo.toml
index 40652eb..22d1ca4 100644
--- a/cargo-typify/Cargo.toml
+++ b/cargo-typify/Cargo.toml
@@ -14,12 +14,12 @@ default-run = "cargo-typify"
 [dependencies]
 typify = { version = "0.1.0", path = "../typify" }
 
-clap = { version = "4.5.11", features = ["derive"] }
+clap = { version = "4.5.13", features = ["derive"] }
 color-eyre = "0.6"
 env_logger = "0.10"
 rustfmt-wrapper = "0.2.1"
 semver = "1.0.23"
-serde_json = "1.0.121"
+serde_json = "1.0.122"
 schemars = "0.8.21"
 
 [dev-dependencies]
diff --git a/typify-impl/Cargo.toml b/typify-impl/Cargo.toml
index 017803b..6ddc551 100644
--- a/typify-impl/Cargo.toml
+++ b/typify-impl/Cargo.toml
@@ -16,9 +16,9 @@ regress = "0.10.0"
 schemars = "0.8.21"
 semver = "1.0.23"
 serde = "1.0.204"
-serde_json = "1.0.121"
+serde_json = "1.0.122"
 syn = { version = "2.0.72", features = ["full"] }
-thiserror = "1.0.62"
+thiserror = "1.0.63"
 unicode-ident = "1.0.12"
 
 
diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs
index 553cd96..6f63868 100644
--- a/typify-impl/src/convert.rs
+++ b/typify-impl/src/convert.rs
@@ -1132,6 +1132,49 @@ impl TypeSpace {
         Ok((TypeEntryDetails::Unit.into(), metadata))
     }
 
+    /// Determine whether a schema's property name validation constraints can be handled
+    fn can_handle_pattern_properties(validation: &ObjectValidation) -> bool {
+        if !validation.required.is_empty() {
+            return false;
+        }
+
+        if !validation.properties.is_empty() {
+            return false;
+        }
+
+        // Ensure we have at least one pattern property and all pattern property
+        // schemas are the same
+        let Some(first_schema) = validation.pattern_properties.values().next() else {
+            return false;
+        };
+
+        if !validation
+            .pattern_properties
+            .values()
+            .all(|schema| schema == first_schema)
+        {
+            return false;
+        }
+
+        // Ensure any additional properties are a false or null schema
+        if validation.additional_properties.as_ref().map(AsRef::as_ref) == Some(&Schema::Bool(true))
+            || matches!(
+                validation.additional_properties.as_ref().map(AsRef::as_ref),
+                Some(&Schema::Object(_))
+            )
+        {
+            return false;
+        }
+
+        // Ensure there are no additional property names constraints, to avoid a
+        // collision between different types of constraints interacting unexpectedly
+        if validation.property_names.is_some() {
+            return false;
+        }
+
+        true
+    }
+
     fn convert_object<'a>(
         &mut self,
         type_name: Name,
@@ -1163,6 +1206,44 @@ impl TypeSpace {
                 )?;
                 Ok((type_entry, metadata))
             }
+
+            Some(validation) if Self::can_handle_pattern_properties(validation) => {
+                let pattern = validation
+                    .pattern_properties
+                    .keys()
+                    .cloned()
+                    .collect::<Vec<_>>()
+                    .join("|");
+
+                // Construct a schema to use for property name validation
+                let property_names = Some(Box::new(Schema::Object(SchemaObject {
+                    string: Some(Box::new(StringValidation {
+                        max_length: None,
+                        min_length: None,
+                        pattern: Some(pattern),
+                    })),
+                    ..Default::default()
+                })));
+
+                // Construct schema to use for property value validation
+                let additional_properties = Some(Box::new(
+                    validation
+                        .pattern_properties
+                        .values()
+                        .next()
+                        .cloned()
+                        .unwrap_or_else(|| unreachable!("pattern_properties cannot be empty here")),
+                ));
+
+                let type_entry = self.make_map(
+                    type_name.into_option(),
+                    &property_names,
+                    &additional_properties,
+                )?;
+
+                Ok((type_entry, metadata))
+            }
+
             None => {
                 let type_entry = self.make_map(type_name.into_option(), &None, &None)?;
                 Ok((type_entry, metadata))
diff --git a/typify-macro/Cargo.toml b/typify-macro/Cargo.toml
index bfb124d..263b701 100644
--- a/typify-macro/Cargo.toml
+++ b/typify-macro/Cargo.toml
@@ -16,7 +16,7 @@ quote = "1.0.36"
 schemars = "0.8.21"
 semver = { version = "1.0.23", features = ["serde"] }
 serde = "1.0.204"
-serde_json = "1.0.121"
+serde_json = "1.0.122"
 serde_tokenstream = "0.2.1"
 syn = { version = "2.0", features = ["full", "extra-traits"] }
 typify-impl = { version = "0.1.0", path = "../typify-impl" }
diff --git a/typify/Cargo.toml b/typify/Cargo.toml
index 7b971a2..b800431 100644
--- a/typify/Cargo.toml
+++ b/typify/Cargo.toml
@@ -27,6 +27,6 @@ regress = "0.10.0"
 rustfmt-wrapper = "0.2.1"
 schemars = "0.8.21"
 serde = "1.0.204"
-serde_json = "1.0.121"
-trybuild = "1.0.98"
+serde_json = "1.0.122"
+trybuild = "1.0.99"
 uuid = { version = "1.10.0", features = ["serde"] }


Test patch:
diff --git a/typify-impl/tests/vega.out b/typify-impl/tests/vega.out
index 935ad13..043bf70 100644
--- a/typify-impl/tests/vega.out
+++ b/typify-impl/tests/vega.out
@@ -29966,13 +29966,28 @@ impl ToString for Element {
 #[doc = r" ```"]
 #[doc = r" </details>"]
 #[derive(Clone, Debug, serde :: Deserialize, serde :: Serialize)]
-#[serde(deny_unknown_fields)]
-pub struct Encode {}
+pub struct Encode(pub std::collections::HashMap<EncodeKey, EncodeEntry>);
+impl std::ops::Deref for Encode {
+    type Target = std::collections::HashMap<EncodeKey, EncodeEntry>;
+    fn deref(&self) -> &std::collections::HashMap<EncodeKey, EncodeEntry> {
+        &self.0
+    }
+}
+impl From<Encode> for std::collections::HashMap<EncodeKey, EncodeEntry> {
+    fn from(value: Encode) -> Self {
+        value.0
+    }
+}
 impl From<&Encode> for Encode {
     fn from(value: &Encode) -> Self {
         value.clone()
     }
 }
+impl From<std::collections::HashMap<EncodeKey, EncodeEntry>> for Encode {
+    fn from(value: std::collections::HashMap<EncodeKey, EncodeEntry>) -> Self {
+        Self(value)
+    }
+}
 #[doc = "EncodeEntry"]
 #[doc = r""]
 #[doc = r" <details><summary>JSON schema</summary>"]
@@ -30419,6 +30434,74 @@ impl From<&EncodeEntry> for EncodeEntry {
         value.clone()
     }
 }
+#[doc = "EncodeKey"]
+#[doc = r""]
+#[doc = r" <details><summary>JSON schema</summary>"]
+#[doc = r""]
+#[doc = r" ```json"]
+#[doc = "{"]
+#[doc = "  \"type\": \"string\","]
+#[doc = "  \"pattern\": \"^.+$\""]
+#[doc = "}"]
+#[doc = r" ```"]
+#[doc = r" </details>"]
+#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde :: Serialize)]
+pub struct EncodeKey(String);
+impl std::ops::Deref for EncodeKey {
+    type Target = String;
+    fn deref(&self) -> &String {
+        &self.0
+    }
+}
+impl From<EncodeKey> for String {
+    fn from(value: EncodeKey) -> Self {
+        value.0
+    }
+}
+impl From<&EncodeKey> for EncodeKey {
+    fn from(value: &EncodeKey) -> Self {
+        value.clone()
+    }
+}
+impl std::str::FromStr for EncodeKey {
+    type Err = self::error::ConversionError;
+    fn from_str(value: &str) -> Result<Self, self::error::ConversionError> {
+        if regress::Regex::new("^.+$").unwrap().find(value).is_none() {
+            return Err("doesn't match pattern \"^.+$\"".into());
+        }
+        Ok(Self(value.to_string()))
+    }
+}
+impl std::convert::TryFrom<&str> for EncodeKey {
+    type Error = self::error::ConversionError;
+    fn try_from(value: &str) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<&String> for EncodeKey {
+    type Error = self::error::ConversionError;
+    fn try_from(value: &String) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<String> for EncodeKey {
+    type Error = self::error::ConversionError;
+    fn try_from(value: String) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl<'de> serde::Deserialize<'de> for EncodeKey {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        String::deserialize(deserializer)?
+            .parse()
+            .map_err(|e: self::error::ConversionError| {
+                <D::Error as serde::de::Error>::custom(e.to_string())
+            })
+    }
+}
 #[doc = "Everything"]
 #[doc = r""]
 #[doc = r" <details><summary>JSON schema</summary>"]
@@ -105101,7 +105184,7 @@ impl From<NumberValue> for TitleVariant1Dy {
 #[derive(Clone, Debug, serde :: Deserialize, serde :: Serialize)]
 pub struct TitleVariant1Encode {
     #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
-    pub subtype_0: Option<TitleVariant1EncodeSubtype0>,
+    pub subtype_0: Option<std::collections::HashMap<TitleVariant1EncodeSubtype0Key, EncodeEntry>>,
     #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
     pub subtype_1: Option<TitleVariant1EncodeSubtype1>,
 }
@@ -105110,30 +105193,78 @@ impl From<&TitleVariant1Encode> for TitleVariant1Encode {
         value.clone()
     }
 }
-#[doc = "TitleVariant1EncodeSubtype0"]
+#[doc = "TitleVariant1EncodeSubtype0Key"]
 #[doc = r""]
 #[doc = r" <details><summary>JSON schema</summary>"]
 #[doc = r""]
 #[doc = r" ```json"]
 #[doc = "{"]
-#[doc = "  \"type\": \"object\","]
-#[doc = "  \"patternProperties\": {"]
-#[doc = "    \"^(?!interactive|name|style).+$\": {"]
-#[doc = "      \"$ref\": \"#/definitions/encodeEntry\""]
-#[doc = "    }"]
-#[doc = "  },"]
-#[doc = "  \"additionalProperties\": false"]
+#[doc = "  \"type\": \"string\","]
+#[doc = "  \"pattern\": \"^(?!interactive|name|style).+$\""]
 #[doc = "}"]
 #[doc = r" ```"]
 #[doc = r" </details>"]
-#[derive(Clone, Debug, serde :: Deserialize, serde :: Serialize)]
-#[serde(deny_unknown_fields)]
-pub struct TitleVariant1EncodeSubtype0 {}
-impl From<&TitleVariant1EncodeSubtype0> for TitleVariant1EncodeSubtype0 {
-    fn from(value: &TitleVariant1EncodeSubtype0) -> Self {
+#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde :: Serialize)]
+pub struct TitleVariant1EncodeSubtype0Key(String);
+impl std::ops::Deref for TitleVariant1EncodeSubtype0Key {
+    type Target = String;
+    fn deref(&self) -> &String {
+        &self.0
+    }
+}
+impl From<TitleVariant1EncodeSubtype0Key> for String {
+    fn from(value: TitleVariant1EncodeSubtype0Key) -> Self {
+        value.0
+    }
+}
+impl From<&TitleVariant1EncodeSubtype0Key> for TitleVariant1EncodeSubtype0Key {
+    fn from(value: &TitleVariant1EncodeSubtype0Key) -> Self {
         value.clone()
     }
 }
+impl std::str::FromStr for TitleVariant1EncodeSubtype0Key {
+    type Err = self::error::ConversionError;
+    fn from_str(value: &str) -> Result<Self, self::error::ConversionError> {
+        if regress::Regex::new("^(?!interactive|name|style).+$")
+            .unwrap()
+            .find(value)
+            .is_none()
+        {
+            return Err("doesn't match pattern \"^(?!interactive|name|style).+$\"".into());
+        }
+        Ok(Self(value.to_string()))
+    }
+}
+impl std::convert::TryFrom<&str> for TitleVariant1EncodeSubtype0Key {
+    type Error = self::error::ConversionError;
+    fn try_from(value: &str) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<&String> for TitleVariant1EncodeSubtype0Key {
+    type Error = self::error::ConversionError;
+    fn try_from(value: &String) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<String> for TitleVariant1EncodeSubtype0Key {
+    type Error = self::error::ConversionError;
+    fn try_from(value: String) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl<'de> serde::Deserialize<'de> for TitleVariant1EncodeSubtype0Key {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        String::deserialize(deserializer)?
+            .parse()
+            .map_err(|e: self::error::ConversionError| {
+                <D::Error as serde::de::Error>::custom(e.to_string())
+            })
+    }
+}
 #[doc = "TitleVariant1EncodeSubtype1"]
 #[doc = r""]
 #[doc = r" <details><summary>JSON schema</summary>"]
diff --git a/typify-test/Cargo.toml b/typify-test/Cargo.toml
index 3d6cb41..7313806 100644
--- a/typify-test/Cargo.toml
+++ b/typify-test/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2021"
 [dependencies]
 regress = "0.10.0"
 serde = "1.0.204"
-serde_json = "1.0.121"
+serde_json = "1.0.122"
 
 [build-dependencies]
 ipnetwork = { version = "0.20.0", features = ["schemars"] }
diff --git a/typify/tests/schemas/property-pattern.json b/typify/tests/schemas/property-pattern.json
new file mode 100644
index 0000000..4d54787
--- /dev/null
+++ b/typify/tests/schemas/property-pattern.json
@@ -0,0 +1,20 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "test grammar for pattern properties",
+  "type": "object",
+  "required": [
+    "rules"
+  ],
+  "additionalProperties": false,
+  "properties": {
+    "rules": {
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z_]\\w*$": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/typify/tests/schemas/property-pattern.rs b/typify/tests/schemas/property-pattern.rs
new file mode 100644
index 0000000..f03b982
--- /dev/null
+++ b/typify/tests/schemas/property-pattern.rs
@@ -0,0 +1,135 @@
+#[doc = r" Error types."]
+pub mod error {
+    #[doc = r" Error from a TryFrom or FromStr implementation."]
+    pub struct ConversionError(std::borrow::Cow<'static, str>);
+    impl std::error::Error for ConversionError {}
+    impl std::fmt::Display for ConversionError {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+            std::fmt::Display::fmt(&self.0, f)
+        }
+    }
+    impl std::fmt::Debug for ConversionError {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+            std::fmt::Debug::fmt(&self.0, f)
+        }
+    }
+    impl From<&'static str> for ConversionError {
+        fn from(value: &'static str) -> Self {
+            Self(value.into())
+        }
+    }
+    impl From<String> for ConversionError {
+        fn from(value: String) -> Self {
+            Self(value.into())
+        }
+    }
+}
+#[doc = "TestGrammarForPatternProperties"]
+#[doc = r""]
+#[doc = r" <details><summary>JSON schema</summary>"]
+#[doc = r""]
+#[doc = r" ```json"]
+#[doc = "{"]
+#[doc = "  \"title\": \"test grammar for pattern properties\","]
+#[doc = "  \"type\": \"object\","]
+#[doc = "  \"required\": ["]
+#[doc = "    \"rules\""]
+#[doc = "  ],"]
+#[doc = "  \"properties\": {"]
+#[doc = "    \"rules\": {"]
+#[doc = "      \"type\": \"object\","]
+#[doc = "      \"patternProperties\": {"]
+#[doc = "        \"^[a-zA-Z_]\\\\w*$\": {"]
+#[doc = "          \"type\": \"string\""]
+#[doc = "        }"]
+#[doc = "      },"]
+#[doc = "      \"additionalProperties\": false"]
+#[doc = "    }"]
+#[doc = "  },"]
+#[doc = "  \"additionalProperties\": false"]
+#[doc = "}"]
+#[doc = r" ```"]
+#[doc = r" </details>"]
+#[derive(Clone, Debug, serde :: Deserialize, serde :: Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct TestGrammarForPatternProperties {
+    pub rules: std::collections::HashMap<TestGrammarForPatternPropertiesRulesKey, String>,
+}
+impl From<&TestGrammarForPatternProperties> for TestGrammarForPatternProperties {
+    fn from(value: &TestGrammarForPatternProperties) -> Self {
+        value.clone()
+    }
+}
+#[doc = "TestGrammarForPatternPropertiesRulesKey"]
+#[doc = r""]
+#[doc = r" <details><summary>JSON schema</summary>"]
+#[doc = r""]
+#[doc = r" ```json"]
+#[doc = "{"]
+#[doc = "  \"type\": \"string\","]
+#[doc = "  \"pattern\": \"^[a-zA-Z_]\\\\w*$\""]
+#[doc = "}"]
+#[doc = r" ```"]
+#[doc = r" </details>"]
+#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde :: Serialize)]
+pub struct TestGrammarForPatternPropertiesRulesKey(String);
+impl std::ops::Deref for TestGrammarForPatternPropertiesRulesKey {
+    type Target = String;
+    fn deref(&self) -> &String {
+        &self.0
+    }
+}
+impl From<TestGrammarForPatternPropertiesRulesKey> for String {
+    fn from(value: TestGrammarForPatternPropertiesRulesKey) -> Self {
+        value.0
+    }
+}
+impl From<&TestGrammarForPatternPropertiesRulesKey> for TestGrammarForPatternPropertiesRulesKey {
+    fn from(value: &TestGrammarForPatternPropertiesRulesKey) -> Self {
+        value.clone()
+    }
+}
+impl std::str::FromStr for TestGrammarForPatternPropertiesRulesKey {
+    type Err = self::error::ConversionError;
+    fn from_str(value: &str) -> Result<Self, self::error::ConversionError> {
+        if regress::Regex::new("^[a-zA-Z_]\\w*$")
+            .unwrap()
+            .find(value)
+            .is_none()
+        {
+            return Err("doesn't match pattern \"^[a-zA-Z_]\\w*$\"".into());
+        }
+        Ok(Self(value.to_string()))
+    }
+}
+impl std::convert::TryFrom<&str> for TestGrammarForPatternPropertiesRulesKey {
+    type Error = self::error::ConversionError;
+    fn try_from(value: &str) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<&String> for TestGrammarForPatternPropertiesRulesKey {
+    type Error = self::error::ConversionError;
+    fn try_from(value: &String) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<String> for TestGrammarForPatternPropertiesRulesKey {
+    type Error = self::error::ConversionError;
+    fn try_from(value: String) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl<'de> serde::Deserialize<'de> for TestGrammarForPatternPropertiesRulesKey {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        String::deserialize(deserializer)?
+            .parse()
+            .map_err(|e: self::error::ConversionError| {
+                <D::Error as serde::de::Error>::custom(e.to_string())
+            })
+    }
+}
+fn main() {}