Repository: oli-obk/cargo_metadata
Pull request #258 (2024-01-20 17:04:48)

Converting Target kind and crate_types to enum representation
Converting Target kind and crate_types to enum representation, along with an Unknown(String) as a fallback.

This is in response to the following issue: https://github.com/oli-obk/cargo_metadata/issues/239


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c045c7d..3a6cdf6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
 ### Changed
 
 - Made `parse_stream` more versatile by accepting anything that implements `Read`.
+- Converted `TargetKind` and `CrateType` to an enum representation.
 
 ### Removed
 
diff --git a/src/lib.rs b/src/lib.rs
index 5af9f1e..4c951fa 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -509,7 +509,7 @@ pub struct Target {
     /// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`.
     ///
     /// Other possible values may be added in the future.
-    pub kind: Vec<String>,
+    pub kind: Vec<TargetKind>,
     /// Similar to `kind`, but only reports the
     /// [Cargo crate types](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field):
     /// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`.
@@ -518,7 +518,7 @@ pub struct Target {
     /// Other possible values may be added in the future.
     #[serde(default)]
     #[cfg_attr(feature = "builder", builder(default))]
-    pub crate_types: Vec<String>,
+    pub crate_types: Vec<CrateType>,
 
     #[serde(default)]
     #[cfg_attr(feature = "builder", builder(default))]
@@ -554,43 +554,160 @@ pub struct Target {
 }
 
 impl Target {
-    fn is_kind(&self, name: &str) -> bool {
-        self.kind.iter().any(|kind| kind == name)
+    fn is_kind(&self, name: TargetKind) -> bool {
+        self.kind.iter().any(|kind| kind == &name)
     }
 
     /// Return true if this target is of kind "lib".
     pub fn is_lib(&self) -> bool {
-        self.is_kind("lib")
+        self.is_kind(TargetKind::Lib)
     }
 
     /// Return true if this target is of kind "bin".
     pub fn is_bin(&self) -> bool {
-        self.is_kind("bin")
+        self.is_kind(TargetKind::Bin)
     }
 
     /// Return true if this target is of kind "example".
     pub fn is_example(&self) -> bool {
-        self.is_kind("example")
+        self.is_kind(TargetKind::Example)
     }
 
     /// Return true if this target is of kind "test".
     pub fn is_test(&self) -> bool {
-        self.is_kind("test")
+        self.is_kind(TargetKind::Test)
     }
 
     /// Return true if this target is of kind "bench".
     pub fn is_bench(&self) -> bool {
-        self.is_kind("bench")
+        self.is_kind(TargetKind::Bench)
     }
 
     /// Return true if this target is of kind "custom-build".
     pub fn is_custom_build(&self) -> bool {
-        self.is_kind("custom-build")
+        self.is_kind(TargetKind::CustomBuild)
     }
 
     /// Return true if this target is of kind "proc-macro".
     pub fn is_proc_macro(&self) -> bool {
-        self.is_kind("proc-macro")
+        self.is_kind(TargetKind::ProcMacro)
+    }
+}
+
+/// Kind of target.
+///
+/// The possible values are `example`, `test`, `bench`, `custom-build` and
+/// [Cargo crate types](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field):
+/// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`.
+///
+/// Other possible values may be added in the future.
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum TargetKind {
+    /// `cargo bench` target
+    #[serde(rename = "bench")]
+    Bench,
+    /// Binary executable target
+    #[serde(rename = "bin")]
+    Bin,
+    /// Custom build target
+    #[serde(rename = "custom-build")]
+    CustomBuild,
+    /// Dynamic system library target
+    #[serde(rename = "cdylib")]
+    CDyLib,
+    /// Dynamic Rust library target
+    #[serde(rename = "dylib")]
+    DyLib,
+    /// Example target
+    #[serde(rename = "example")]
+    Example,
+    /// Rust library
+    #[serde(rename = "lib")]
+    Lib,
+    /// Procedural Macro
+    #[serde(rename = "proc-macro")]
+    ProcMacro,
+    /// Rust library for use as an intermediate artifact
+    #[serde(rename = "rlib")]
+    RLib,
+    /// Static system library
+    #[serde(rename = "staticlib")]
+    StaticLib,
+    /// Test target
+    #[serde(rename = "test")]
+    Test,
+    /// Unknown type
+    #[serde(untagged)]
+    Unknown(String),
+}
+
+impl From<&str> for TargetKind {
+    fn from(value: &str) -> Self {
+        match value {
+            "example" => TargetKind::Example,
+            "test" => TargetKind::Test,
+            "bench" => TargetKind::Bench,
+            "custom-build" => TargetKind::CustomBuild,
+            "bin" => TargetKind::Bin,
+            "lib" => TargetKind::Lib,
+            "rlib" => TargetKind::RLib,
+            "dylib" => TargetKind::DyLib,
+            "cdylib" => TargetKind::CDyLib,
+            "staticlib" => TargetKind::StaticLib,
+            "proc-macro" => TargetKind::ProcMacro,
+            x => TargetKind::Unknown(x.to_string()),
+        }
+    }
+}
+
+/// Similar to `kind`, but only reports the
+/// [Cargo crate types](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field):
+/// `bin`, `lib`, `rlib`, `dylib`, `cdylib`, `staticlib`, `proc-macro`.
+/// Everything that's not a proc macro or a library of some kind is reported as "bin".
+///
+/// Other possible values may be added in the future.
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[non_exhaustive]
+pub enum CrateType {
+    /// Binary executable target
+    #[serde(rename = "bin")]
+    Bin,
+    /// Dynamic system library target
+    #[serde(rename = "cdylib")]
+    CDyLib,
+    /// Dynamic Rust library target
+    #[serde(rename = "dylib")]
+    DyLib,
+    /// Rust library
+    #[serde(rename = "lib")]
+    Lib,
+    /// Procedural Macro
+    #[serde(rename = "proc-macro")]
+    ProcMacro,
+    /// Rust library for use as an intermediate artifact
+    #[serde(rename = "rlib")]
+    RLib,
+    /// Static system library
+    #[serde(rename = "staticlib")]
+    StaticLib,
+    /// Unkown type
+    #[serde(untagged)]
+    Unknown(String),
+}
+
+impl From<&str> for CrateType {
+    fn from(value: &str) -> Self {
+        match value {
+            "bin" => CrateType::Bin,
+            "lib" => CrateType::Lib,
+            "rlib" => CrateType::RLib,
+            "dylib" => CrateType::DyLib,
+            "cdylib" => CrateType::CDyLib,
+            "staticlib" => CrateType::StaticLib,
+            "proc-macro" => CrateType::ProcMacro,
+            x => CrateType::Unknown(x.to_string()),
+        }
     }
 }
 


Test patch:
diff --git a/tests/selftest.rs b/tests/selftest.rs
index dbcc67e..e6dadf7 100644
--- a/tests/selftest.rs
+++ b/tests/selftest.rs
@@ -25,13 +25,13 @@ fn metadata() {
         .iter()
         .find(|t| t.name == "cargo_metadata")
         .unwrap();
-    assert_eq!(lib.kind[0], "lib");
-    assert_eq!(lib.crate_types[0], "lib");
+    assert_eq!(lib.kind[0], "lib".into());
+    assert_eq!(lib.crate_types[0], "lib".into());
 
     let selftest = this.targets.iter().find(|t| t.name == "selftest").unwrap();
     assert_eq!(selftest.name, "selftest");
-    assert_eq!(selftest.kind[0], "test");
-    assert_eq!(selftest.crate_types[0], "bin");
+    assert_eq!(selftest.kind[0], "test".into());
+    assert_eq!(selftest.crate_types[0], "bin".into());
 
     let package_metadata = &metadata.packages[0]
         .metadata
@@ -141,13 +141,13 @@ fn metadata_deps() {
         .iter()
         .find(|t| t.name == "cargo_metadata")
         .unwrap();
-    assert_eq!(lib.kind[0], "lib");
-    assert_eq!(lib.crate_types[0], "lib");
+    assert_eq!(lib.kind[0], "lib".into());
+    assert_eq!(lib.crate_types[0], "lib".into());
 
     let selftest = this.targets.iter().find(|t| t.name == "selftest").unwrap();
     assert_eq!(selftest.name, "selftest");
-    assert_eq!(selftest.kind[0], "test");
-    assert_eq!(selftest.crate_types[0], "bin");
+    assert_eq!(selftest.kind[0], "test".into());
+    assert_eq!(selftest.crate_types[0], "bin".into());
 
     let dependencies = &this.dependencies;
 
diff --git a/tests/test_samples.rs b/tests/test_samples.rs
index c834778..2f406c5 100644
--- a/tests/test_samples.rs
+++ b/tests/test_samples.rs
@@ -95,8 +95,8 @@ fn old_minimal() {
     assert_eq!(pkg.targets.len(), 1);
     let target = &pkg.targets[0];
     assert_eq!(target.name, "foo");
-    assert_eq!(target.kind, vec!["bin"]);
-    assert_eq!(target.crate_types, vec!["bin"]);
+    assert_eq!(target.kind, vec!["bin".into()]);
+    assert_eq!(target.crate_types, vec!["bin".into()]);
     assert_eq!(target.required_features.len(), 0);
     assert_eq!(target.src_path, "/foo/src/main.rs");
     assert_eq!(target.edition, Edition::E2015);
@@ -208,7 +208,7 @@ fn all_the_fields() {
         }
     );
     assert_eq!(meta.workspace_members.len(), 1);
-    assert!(meta.workspace_members[0].to_string().starts_with("all"));
+    assert!(meta.workspace_members[0].to_string().contains("all"));
     if ver >= semver::Version::parse("1.71.0").unwrap() {
         assert_eq!(&*meta.workspace_default_members, &meta.workspace_members);
     }
@@ -217,7 +217,7 @@ fn all_the_fields() {
     let all = meta.packages.iter().find(|p| p.name == "all").unwrap();
     assert_eq!(all.version, semver::Version::parse("0.1.0").unwrap());
     assert_eq!(all.authors, vec!["Jane Doe <user@example.com>"]);
-    assert!(all.id.to_string().starts_with("all"));
+    assert!(all.id.to_string().contains("all"));
     assert_eq!(all.description, Some("Package description.".to_string()));
     assert_eq!(all.license, Some("MIT/Apache-2.0".to_string()));
     assert_eq!(all.license_file, Some(Utf8PathBuf::from("LICENSE")));
@@ -309,10 +309,13 @@ fn all_the_fields() {
     assert_eq!(all.targets.len(), 8);
     let lib = get_file_name!("lib.rs");
     assert_eq!(lib.name, "all");
-    assert_eq!(sorted!(lib.kind), vec!["cdylib", "rlib", "staticlib"]);
+    assert_eq!(
+        sorted!(lib.kind),
+        vec!["cdylib".into(), "rlib".into(), "staticlib".into()]
+    );
     assert_eq!(
         sorted!(lib.crate_types),
-        vec!["cdylib", "rlib", "staticlib"]
+        vec!["cdylib".into(), "rlib".into(), "staticlib".into()]
     );
     assert_eq!(lib.required_features.len(), 0);
     assert_eq!(lib.edition, Edition::E2018);
@@ -321,8 +324,8 @@ fn all_the_fields() {
     assert!(lib.doc);
 
     let main = get_file_name!("main.rs");
-    assert_eq!(main.crate_types, vec!["bin"]);
-    assert_eq!(main.kind, vec!["bin"]);
+    assert_eq!(main.crate_types, vec!["bin".into()]);
+    assert_eq!(main.kind, vec!["bin".into()]);
     assert!(!main.doctest);
     assert!(main.test);
     assert!(main.doc);
@@ -335,17 +338,17 @@ fn all_the_fields() {
     assert_eq!(reqfeat.required_features, vec!["feat2"]);
 
     let ex1 = get_file_name!("ex1.rs");
-    assert_eq!(ex1.kind, vec!["example"]);
+    assert_eq!(ex1.kind, vec!["example".into()]);
     assert!(!ex1.test);
 
     let t1 = get_file_name!("t1.rs");
-    assert_eq!(t1.kind, vec!["test"]);
+    assert_eq!(t1.kind, vec!["test".into()]);
 
     let b1 = get_file_name!("b1.rs");
-    assert_eq!(b1.kind, vec!["bench"]);
+    assert_eq!(b1.kind, vec!["bench".into()]);
 
     let build = get_file_name!("build.rs");
-    assert_eq!(build.kind, vec!["custom-build"]);
+    assert_eq!(build.kind, vec!["custom-build".into()]);
 
     if ver >= semver::Version::parse("1.60.0").unwrap() {
         // 1.60 now reports optional dependencies within the features table
@@ -390,18 +393,13 @@ fn all_the_fields() {
     );
 
     let resolve = meta.resolve.as_ref().unwrap();
-    assert!(resolve
-        .root
-        .as_ref()
-        .unwrap()
-        .to_string()
-        .starts_with("all"));
+    assert!(resolve.root.as_ref().unwrap().to_string().contains("all"));
 
     assert_eq!(resolve.nodes.len(), 9);
     let path_dep = resolve
         .nodes
         .iter()
-        .find(|n| n.id.to_string().starts_with("path-dep"))
+        .find(|n| n.id.to_string().contains("path-dep"))
         .unwrap();
     assert_eq!(path_dep.deps.len(), 0);
     assert_eq!(path_dep.dependencies.len(), 0);
@@ -410,29 +408,29 @@ fn all_the_fields() {
     let bitflags = resolve
         .nodes
         .iter()
-        .find(|n| n.id.to_string().starts_with("bitflags"))
+        .find(|n| n.id.to_string().contains("bitflags"))
         .unwrap();
     assert_eq!(bitflags.features, vec!["default"]);
 
     let featdep = resolve
         .nodes
         .iter()
-        .find(|n| n.id.to_string().starts_with("featdep"))
+        .find(|n| n.id.to_string().contains("featdep"))
         .unwrap();
     assert_eq!(featdep.features, vec!["i128"]);
 
     let all = resolve
         .nodes
         .iter()
-        .find(|n| n.id.to_string().starts_with("all"))
+        .find(|n| n.id.to_string().contains("all"))
         .unwrap();
     assert_eq!(all.dependencies.len(), 8);
     assert_eq!(all.deps.len(), 8);
     let newname = all.deps.iter().find(|d| d.name == "newname").unwrap();
-    assert!(newname.pkg.to_string().starts_with("oldname"));
+    assert!(newname.pkg.to_string().contains("oldname"));
     // Note the underscore here.
     let path_dep = all.deps.iter().find(|d| d.name == "path_dep").unwrap();
-    assert!(path_dep.pkg.to_string().starts_with("path-dep"));
+    assert!(path_dep.pkg.to_string().contains("path-dep"));
     assert_eq!(path_dep.dep_kinds.len(), 1);
     let kind = &path_dep.dep_kinds[0];
     assert_eq!(kind.kind, DependencyKind::Normal);
@@ -443,7 +441,7 @@ fn all_the_fields() {
         .iter()
         .find(|d| d.name == "different_name")
         .unwrap();
-    assert!(namedep.pkg.to_string().starts_with("namedep"));
+    assert!(namedep.pkg.to_string().contains("namedep"));
     assert_eq!(sorted!(all.features), vec!["bitflags", "default", "feat1"]);
 
     let bdep = all.deps.iter().find(|d| d.name == "bdep").unwrap();
@@ -596,7 +594,7 @@ fn advanced_feature_configuration() {
         let all = resolve
             .nodes
             .iter()
-            .find(|n| n.id.to_string().starts_with("all"))
+            .find(|n| !n.features.is_empty())
             .unwrap();
 
         all.features.clone()
@@ -725,3 +723,57 @@ fn missing_workspace_default_members() {
     let meta: Metadata = serde_json::from_str(JSON_OLD_MINIMAL).unwrap();
     let _ = &*meta.workspace_default_members;
 }
+
+#[test]
+fn test_unknown_target_kind_and_crate_type() {
+    // Both kind and crate_type set to a type not yet known
+    let json = r#"
+{
+  "packages": [
+    {
+      "name": "alt",
+      "version": "0.1.0",
+      "id": "alt 0.1.0 (path+file:///alt)",
+      "source": null,
+      "dependencies": [],
+      "targets": [
+        {
+          "kind": [
+            "future-kind"
+          ],
+          "crate_types": [
+            "future-type"
+          ],
+          "name": "alt",
+          "src_path": "/alt/src/lib.rs",
+          "edition": "2018"
+        }
+      ],
+      "features": {},
+      "manifest_path": "/alt/Cargo.toml",
+      "metadata": null,
+      "authors": [],
+      "categories": [],
+      "keywords": [],
+      "readme": null,
+      "repository": null,
+      "edition": "2018",
+      "links": null
+    }
+  ],
+  "workspace_members": [
+    "alt 0.1.0 (path+file:///alt)"
+  ],
+  "resolve": null,
+  "target_directory": "/alt/target",
+  "version": 1,
+  "workspace_root": "/alt"
+}
+"#;
+    let meta: Metadata = serde_json::from_str(json).unwrap();
+    assert_eq!(meta.packages.len(), 1);
+    assert_eq!(meta.packages[0].targets.len(), 1);
+    let target = &meta.packages[0].targets[0];
+    assert_eq!(target.kind[0], "future-kind".into());
+    assert_eq!(target.crate_types[0], "future-type".into());
+}