Repository: oxidecomputer/typify
Pull request #651 (2024-08-12 14:01:07)

Bump assert_cmd from 2.0.15 to 2.0.16
Bumps [assert_cmd](https://github.com/assert-rs/assert_cmd) from 2.0.15 to 2.0.16.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/assert-rs/assert_cmd/blob/master/CHANGELOG.md">assert_cmd's changelog</a>.</em></p>
<blockquote>
<h2>[2.0.16] - 2024-08-09</h2>
<h3>Features</h3>
<ul>
<li>Add getters to <code>Command</code> to mirror those added to the standard library</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/assert-rs/assert_cmd/commit/bf1025e94b14483e110c9ffc368f1c8b4985d17e"><code>bf1025e</code></a> chore: Release assert_cmd version 2.0.16</li>
<li><a href="https://github.com/assert-rs/assert_cmd/commit/50e8293c4b84591fa4e5e3dd557b1068de588130"><code>50e8293</code></a> docs: Update changelog</li>
<li><a href="https://github.com/assert-rs/assert_cmd/commit/6539f0af61c3dab0665f030d95680715f11aacee"><code>6539f0a</code></a> Merge pull request <a href="https://redirect.github.com/assert-rs/assert_cmd/issues/213">#213</a> from mcky/feat-add-command-getters</li>
<li><a href="https://github.com/assert-rs/assert_cmd/commit/b037f9668dcccb4d5ac280c209dbb2fccbb397bd"><code>b037f96</code></a> feat: Add getters to mirror those available on std::process::Command</li>
<li><a href="https://github.com/assert-rs/assert_cmd/commit/4d3dbd33651dc8a6cf08679c150a0e2cb8c9e282"><code>4d3dbd3</code></a> chore(deps): Update Rust crate escargot to v0.5.12</li>
<li><a href="https://github.com/assert-rs/assert_cmd/commit/936e4535f7a0bbd714975ad8a16fddb301097837"><code>936e453</code></a> Merge pull request <a href="https://redirect.github.com/assert-rs/assert_cmd/issues/210">#210</a> from epage/min</li>
<li><a href="https://github.com/assert-rs/assert_cmd/commit/26a368c710386041aec1c4eba6bd015bc263d8a7"><code>26a368c</code></a> chore: Clean up minimal deps hack</li>
<li><a href="https://github.com/assert-rs/assert_cmd/commit/322949259ee5d1d6f0dbe114faf480fb632663de"><code>3229492</code></a> chore(deps): Update Rust Stable to v1.80</li>
<li><a href="https://github.com/assert-rs/assert_cmd/commit/7c639177098d1feaa9972aac93618451e02a0ae6"><code>7c63917</code></a> Merge pull request <a href="https://redirect.github.com/assert-rs/assert_cmd/issues/208">#208</a> from epage/template</li>
<li><a href="https://github.com/assert-rs/assert_cmd/commit/3dc2b41ce6789706d2bf85534dcca3125d7e9769"><code>3dc2b41</code></a> fix: Workaround bad minimal-versions</li>
<li>Additional commits viewable in <a href="https://github.com/assert-rs/assert_cmd/compare/v2.0.15...v2.0.16">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=assert_cmd&package-manager=cargo&previous-version=2.0.15&new-version=2.0.16)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 394e393..958bf1a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -75,9 +75,9 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.4"
+version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
+checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
 
 [[package]]
 name = "anstyle-parse"
@@ -109,13 +109,14 @@ dependencies = [
 
 [[package]]
 name = "assert_cmd"
-version = "2.0.15"
+version = "2.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc65048dd435533bb1baf2ed9956b9a278fbfdcf90301b39ee117f06c0199d37"
+checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d"
 dependencies = [
  "anstyle",
  "bstr",
  "doc-comment",
+ "libc",
  "predicates",
  "predicates-core",
  "predicates-tree",
@@ -222,9 +223,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.13"
+version = "4.5.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
+checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -232,9 +233,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.13"
+version = "4.5.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
+checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
 dependencies = [
  "anstream",
  "anstyle",
@@ -990,9 +991,9 @@ checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
 
 [[package]]
 name = "syn"
-version = "2.0.72"
+version = "2.0.74"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
+checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/cargo-typify/Cargo.toml b/cargo-typify/Cargo.toml
index 22d1ca4..d0bd740 100644
--- a/cargo-typify/Cargo.toml
+++ b/cargo-typify/Cargo.toml
@@ -14,7 +14,7 @@ default-run = "cargo-typify"
 [dependencies]
 typify = { version = "0.1.0", path = "../typify" }
 
-clap = { version = "4.5.13", features = ["derive"] }
+clap = { version = "4.5.15", features = ["derive"] }
 color-eyre = "0.6"
 env_logger = "0.10"
 rustfmt-wrapper = "0.2.1"
@@ -23,7 +23,7 @@ serde_json = "1.0.122"
 schemars = "0.8.21"
 
 [dev-dependencies]
-assert_cmd = "2.0.15"
+assert_cmd = "2.0.16"
 expectorate = "1.1.0"
 newline-converter = "0.3.0"
 tempdir = "0.3.7"
diff --git a/typify-impl/Cargo.toml b/typify-impl/Cargo.toml
index 6ddc551..c39ca2c 100644
--- a/typify-impl/Cargo.toml
+++ b/typify-impl/Cargo.toml
@@ -17,7 +17,7 @@ schemars = "0.8.21"
 semver = "1.0.23"
 serde = "1.0.204"
 serde_json = "1.0.122"
-syn = { version = "2.0.72", features = ["full"] }
+syn = { version = "2.0.74", features = ["full"] }
 thiserror = "1.0.63"
 unicode-ident = "1.0.12"
 
@@ -29,5 +29,5 @@ paste = "1.0.15"
 rustfmt-wrapper = "0.2.1"
 schema = "0.1.0"
 schemars = { version = "0.8.21", features = ["uuid1", "impl_json_schema"] }
-syn = { version = "2.0.72", features = ["full", "extra-traits"] }
+syn = { version = "2.0.74", features = ["full", "extra-traits"] }
 uuid = "1.10.0"
diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs
index ffcd692..b9a156c 100644
--- a/typify-impl/src/lib.rs
+++ b/typify-impl/src/lib.rs
@@ -562,7 +562,7 @@ impl TypeSpace {
                     } else {
                         Name::Unknown
                     };
-                    self.convert_ref_type(type_name, schema, type_id)?
+                    self.convert_ref_type(type_name, schema, type_id)?;
                 }
 
                 Some(replace_type) => {
@@ -570,6 +570,7 @@ impl TypeSpace {
                         replace_type.replace_type.clone(),
                         &replace_type.impls.clone(),
                     );
+
                     self.id_to_entry.insert(type_id, type_entry);
                 }
             }
@@ -597,7 +598,7 @@ impl TypeSpace {
             .and_then(|m| m.default.as_ref())
             .cloned()
             .map(WrappedValue::new);
-        let type_entry = match &mut type_entry.details {
+        let mut type_entry = match &mut type_entry.details {
             // The types that are already named are good to go.
             TypeEntryDetails::Enum(details) => {
                 details.default = default;
@@ -645,9 +646,9 @@ impl TypeSpace {
                 )
             }
         };
-        // TODO need a type alias?
-        if let Some(entry_name) = type_entry.name() {
-            self.name_to_id.insert(entry_name.clone(), type_id.clone());
+        if let Some(entry_name) = type_entry.name().cloned() {
+            let name = type_entry.ensure_unique_name(self).unwrap_or(entry_name);
+            self.name_to_id.insert(name, type_id.clone());
         }
         self.id_to_entry.insert(type_id, type_entry);
         Ok(())
@@ -824,10 +825,10 @@ impl TypeSpace {
     /// checking for duplicate type definitions (e.g. to make sure there aren't
     /// two conflicting types of the same name), and deduplicates various
     /// flavors of built-in types.
-    fn assign_type(&mut self, ty: TypeEntry) -> TypeId {
+    fn assign_type(&mut self, mut ty: TypeEntry) -> TypeId {
         if let TypeEntryDetails::Reference(type_id) = ty.details {
             type_id
-        } else if let Some(name) = ty.name() {
+        } else if let Some(name) = ty.name().cloned() {
             // If there's already a type of this name, we make sure it's
             // identical. Note that this covers all user-defined types.
 
@@ -837,7 +838,7 @@ impl TypeSpace {
             // bunch of places and if that were the case we might expect
             // them to be different and resolve that by renaming or scoping
             // them in some way.
-            if let Some(type_id) = self.name_to_id.get(name) {
+            if let Some(type_id) = self.name_to_id.get(&name) {
                 // TODO we'd like to verify that the type is structurally the
                 // same, but the types may not be functionally equal. This is a
                 // consequence of types being "finalized" after each type
@@ -846,7 +847,8 @@ impl TypeSpace {
                 type_id.clone()
             } else {
                 let type_id = self.assign();
-                self.name_to_id.insert(name.clone(), type_id.clone());
+                let name = ty.ensure_unique_name(self).unwrap_or(name);
+                self.name_to_id.insert(name, type_id.clone());
                 self.id_to_entry.insert(type_id.clone(), ty);
                 type_id
             }
diff --git a/typify-impl/src/type_entry.rs b/typify-impl/src/type_entry.rs
index 3f28853..febbf39 100644
--- a/typify-impl/src/type_entry.rs
+++ b/typify-impl/src/type_entry.rs
@@ -514,6 +514,40 @@ impl TypeEntry {
         self.check_defaults(type_space)
     }
 
+    // Ensures that the name is unique within the provided `TypeSpace`.
+    //
+    // This function checks if the current object's name already exists in the
+    // `TypeSpace`. If it does, the function modifies the name by appending
+    // numbers until a unique name is found (Name2, Name3 ...).
+    //
+    // The unique name is then inserted into the `TypeSpace` and the object's
+    // name is updated accordingly.
+    pub(crate) fn ensure_unique_name(&mut self, type_space: &mut TypeSpace) -> Option<String> {
+        if let Some(name) = self.name() {
+            let mut postfix = 2;
+            let mut new_name = name.clone();
+            while type_space.name_to_id.contains_key(&new_name) {
+                new_name = format!("{name}{postfix}");
+                postfix += 1;
+            }
+            self.rename(new_name.clone());
+            Some(new_name)
+        } else {
+            None
+        }
+    }
+
+    pub(crate) fn rename(&mut self, new_name: String) {
+        match &mut self.details {
+            TypeEntryDetails::Enum(TypeEntryEnum { name, .. })
+            | TypeEntryDetails::Struct(TypeEntryStruct { name, .. })
+            | TypeEntryDetails::Newtype(TypeEntryNewtype { name, .. }) => {
+                *name = new_name;
+            }
+            _ => {}
+        }
+    }
+
     pub(crate) fn name(&self) -> Option<&String> {
         match &self.details {
             TypeEntryDetails::Enum(TypeEntryEnum { name, .. })


Test patch:
diff --git a/typify-test/Cargo.toml b/typify-test/Cargo.toml
index 7313806..68fdebe 100644
--- a/typify-test/Cargo.toml
+++ b/typify-test/Cargo.toml
@@ -13,5 +13,5 @@ ipnetwork = { version = "0.20.0", features = ["schemars"] }
 prettyplease = "0.2.20"
 schemars = "0.8.21"
 serde = "1.0.204"
-syn = "2.0.72"
+syn = "2.0.74"
 typify = { path = "../typify" }
diff --git a/typify/tests/schemas/property-name-collision.json b/typify/tests/schemas/property-name-collision.json
new file mode 100644
index 0000000..76bbd25
--- /dev/null
+++ b/typify/tests/schemas/property-name-collision.json
@@ -0,0 +1,26 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "definitions": {
+        "Foo": {
+            "properties": {
+                "Bar": {
+                    "oneOf": [
+                        {
+                            "$ref": "#/definitions/FooBar"
+                        },
+                        {
+                            "$ref": "#/definitions/Baz"
+                        }
+                    ]
+                }
+            },
+            "type": "object"
+        },
+        "FooBar": {
+            "type": "string"
+        },
+        "Baz": {
+            "type": "string"
+        }
+    }
+}
diff --git a/typify/tests/schemas/property-name-collision.rs b/typify/tests/schemas/property-name-collision.rs
new file mode 100644
index 0000000..80e5413
--- /dev/null
+++ b/typify/tests/schemas/property-name-collision.rs
@@ -0,0 +1,227 @@
+#[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 = "Baz"]
+#[doc = r""]
+#[doc = r" <details><summary>JSON schema</summary>"]
+#[doc = r""]
+#[doc = r" ```json"]
+#[doc = "{"]
+#[doc = "  \"type\": \"string\""]
+#[doc = "}"]
+#[doc = r" ```"]
+#[doc = r" </details>"]
+#[derive(
+    Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde :: Deserialize, serde :: Serialize,
+)]
+pub struct Baz(pub String);
+impl std::ops::Deref for Baz {
+    type Target = String;
+    fn deref(&self) -> &String {
+        &self.0
+    }
+}
+impl From<Baz> for String {
+    fn from(value: Baz) -> Self {
+        value.0
+    }
+}
+impl From<&Baz> for Baz {
+    fn from(value: &Baz) -> Self {
+        value.clone()
+    }
+}
+impl From<String> for Baz {
+    fn from(value: String) -> Self {
+        Self(value)
+    }
+}
+impl std::str::FromStr for Baz {
+    type Err = std::convert::Infallible;
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        Ok(Self(value.to_string()))
+    }
+}
+impl ToString for Baz {
+    fn to_string(&self) -> String {
+        self.0.to_string()
+    }
+}
+#[doc = "Foo"]
+#[doc = r""]
+#[doc = r" <details><summary>JSON schema</summary>"]
+#[doc = r""]
+#[doc = r" ```json"]
+#[doc = "{"]
+#[doc = "  \"type\": \"object\","]
+#[doc = "  \"properties\": {"]
+#[doc = "    \"Bar\": {"]
+#[doc = "      \"oneOf\": ["]
+#[doc = "        {"]
+#[doc = "          \"$ref\": \"#/definitions/FooBar\""]
+#[doc = "        },"]
+#[doc = "        {"]
+#[doc = "          \"$ref\": \"#/definitions/Baz\""]
+#[doc = "        }"]
+#[doc = "      ]"]
+#[doc = "    }"]
+#[doc = "  }"]
+#[doc = "}"]
+#[doc = r" ```"]
+#[doc = r" </details>"]
+#[derive(Clone, Debug, serde :: Deserialize, serde :: Serialize)]
+pub struct Foo {
+    #[serde(rename = "Bar", default, skip_serializing_if = "Option::is_none")]
+    pub bar: Option<FooBar>,
+}
+impl From<&Foo> for Foo {
+    fn from(value: &Foo) -> Self {
+        value.clone()
+    }
+}
+#[doc = "FooBar"]
+#[doc = r""]
+#[doc = r" <details><summary>JSON schema</summary>"]
+#[doc = r""]
+#[doc = r" ```json"]
+#[doc = "{"]
+#[doc = "  \"oneOf\": ["]
+#[doc = "    {"]
+#[doc = "      \"$ref\": \"#/definitions/FooBar\""]
+#[doc = "    },"]
+#[doc = "    {"]
+#[doc = "      \"$ref\": \"#/definitions/Baz\""]
+#[doc = "    }"]
+#[doc = "  ]"]
+#[doc = "}"]
+#[doc = r" ```"]
+#[doc = r" </details>"]
+#[derive(Clone, Debug, serde :: Deserialize, serde :: Serialize)]
+#[serde(untagged)]
+pub enum FooBar {
+    FooBar(FooBar2),
+    Baz(Baz),
+}
+impl From<&FooBar> for FooBar {
+    fn from(value: &FooBar) -> Self {
+        value.clone()
+    }
+}
+impl std::str::FromStr for FooBar {
+    type Err = self::error::ConversionError;
+    fn from_str(value: &str) -> Result<Self, self::error::ConversionError> {
+        if let Ok(v) = value.parse() {
+            Ok(Self::FooBar(v))
+        } else if let Ok(v) = value.parse() {
+            Ok(Self::Baz(v))
+        } else {
+            Err("string conversion failed for all variants".into())
+        }
+    }
+}
+impl std::convert::TryFrom<&str> for FooBar {
+    type Error = self::error::ConversionError;
+    fn try_from(value: &str) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<&String> for FooBar {
+    type Error = self::error::ConversionError;
+    fn try_from(value: &String) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<String> for FooBar {
+    type Error = self::error::ConversionError;
+    fn try_from(value: String) -> Result<Self, self::error::ConversionError> {
+        value.parse()
+    }
+}
+impl ToString for FooBar {
+    fn to_string(&self) -> String {
+        match self {
+            Self::FooBar(x) => x.to_string(),
+            Self::Baz(x) => x.to_string(),
+        }
+    }
+}
+impl From<FooBar2> for FooBar {
+    fn from(value: FooBar2) -> Self {
+        Self::FooBar(value)
+    }
+}
+impl From<Baz> for FooBar {
+    fn from(value: Baz) -> Self {
+        Self::Baz(value)
+    }
+}
+#[doc = "FooBar2"]
+#[doc = r""]
+#[doc = r" <details><summary>JSON schema</summary>"]
+#[doc = r""]
+#[doc = r" ```json"]
+#[doc = "{"]
+#[doc = "  \"type\": \"string\""]
+#[doc = "}"]
+#[doc = r" ```"]
+#[doc = r" </details>"]
+#[derive(
+    Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde :: Deserialize, serde :: Serialize,
+)]
+pub struct FooBar2(pub String);
+impl std::ops::Deref for FooBar2 {
+    type Target = String;
+    fn deref(&self) -> &String {
+        &self.0
+    }
+}
+impl From<FooBar2> for String {
+    fn from(value: FooBar2) -> Self {
+        value.0
+    }
+}
+impl From<&FooBar2> for FooBar2 {
+    fn from(value: &FooBar2) -> Self {
+        value.clone()
+    }
+}
+impl From<String> for FooBar2 {
+    fn from(value: String) -> Self {
+        Self(value)
+    }
+}
+impl std::str::FromStr for FooBar2 {
+    type Err = std::convert::Infallible;
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        Ok(Self(value.to_string()))
+    }
+}
+impl ToString for FooBar2 {
+    fn to_string(&self) -> String {
+        self.0.to_string()
+    }
+}
+fn main() {}