Repository: Enselic/cargo-public-api (rust)

Issue:
rustdoc-json: add support for binaries
Currently rustdoc-json uncoditionallly passes `--lib`. 
This causes the following error on binary only crates:
```shell
error: no library targets found in package `...`
```

I'm not sure what a nice interface would look like here, so I though I'd open this issue to discuss :)

One idea I had was to change the `Builder::package` parameter from a `impl AsRef<str>` to a

```rust
enum Package<T: AsRef<str>> {
    Bin(T),
    Lib(T),
}
```

Pull request description:
Add `rustdoc_json::Builder::package_target()` to build for bin, test, bench, etc
I'll see if I can write some tests tomorrow.

One annoyance I've noticed is that this doesn't work:
```rust
//  cannot infer type of the type parameter `T` declared on the enum `PackageTarget`
Builder::default().package_target(PackageTarget::Lib) 
```
Since `Lib` is the default, i think this won't be a problem very often.

Fixes #245

Patch:
diff --git a/.github/workflows/Nightly.yml b/.github/workflows/Nightly.yml
index c50409a..24511df 100644
--- a/.github/workflows/Nightly.yml
+++ b/.github/workflows/Nightly.yml
@@ -42,7 +42,7 @@ jobs:
       - id: latest-nightly
         run: echo "version=nightly-$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
       - run: |
-          [ -z "$(git status --porcelain)" ] || echo "${{ steps.latest-nightly.outputs.version }}" > test-utils/MINIMUM_RUSTDOC_JSON_VERSION_FOR_TESTS
+          [ -z "$(git status --porcelain)" ] || echo "${{ steps.latest-nightly.outputs.version }}" > cargo-public-api/MINIMUM_RUSTDOC_JSON_VERSION_FOR_TESTS
       - uses: peter-evans/create-pull-request@v4
         with:
           title: Bless `${{ steps.latest-nightly.outputs.version }}` output
diff --git a/.gitignore b/.gitignore
index 1bcec75..4070140 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,9 @@
 # Generated by Cargo
 # will have compiled files and executables
-/target/
+target
 
 # These are backup files generated by rustfmt
 **/*.rs.bk
 
-/test-apis/*/target/
 /test-apis/*/Cargo.lock
+/rustdoc-json/tests/test_crate/Cargo.lock
diff --git a/rustdoc-json/CHANGELOG.md b/rustdoc-json/CHANGELOG.md
index cf35f28..5ece303 100644
--- a/rustdoc-json/CHANGELOG.md
+++ b/rustdoc-json/CHANGELOG.md
@@ -1,3 +1,6 @@
+## Unreleased
+* Add `Builder::package_target` and `PackageTarget`
+
 ## v0.7.3
 * Derive `Clone` for `rustdoc_json::Builder`
 
diff --git a/rustdoc-json/public-api.txt b/rustdoc-json/public-api.txt
index 7f7a219..bb5c33d 100644
--- a/rustdoc-json/public-api.txt
+++ b/rustdoc-json/public-api.txt
@@ -42,6 +42,43 @@ pub fn rustdoc_json::BuildError::try_from(value: U) -> core::result::Result<T, <
 impl<T, U> core::convert::TryInto<U> for rustdoc_json::BuildError where U: core::convert::TryFrom<T>
 pub type rustdoc_json::BuildError::Error = <U as core::convert::TryFrom<T>>::Error
 pub fn rustdoc_json::BuildError::try_into(self) -> core::result::Result<U, <U as core::convert::TryFrom<T>>::Error>
+#[non_exhaustive] pub enum rustdoc_json::PackageTarget
+pub enum variant rustdoc_json::PackageTarget::Bench(alloc::string::String)
+pub enum variant rustdoc_json::PackageTarget::Bin(alloc::string::String)
+pub enum variant rustdoc_json::PackageTarget::Example(alloc::string::String)
+pub enum variant rustdoc_json::PackageTarget::Lib
+pub enum variant rustdoc_json::PackageTarget::Test(alloc::string::String)
+impl core::clone::Clone for rustdoc_json::PackageTarget
+pub fn rustdoc_json::PackageTarget::clone(&self) -> rustdoc_json::PackageTarget
+impl core::fmt::Debug for rustdoc_json::PackageTarget
+pub fn rustdoc_json::PackageTarget::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::default::Default for rustdoc_json::PackageTarget
+pub fn rustdoc_json::PackageTarget::default() -> rustdoc_json::PackageTarget
+impl core::panic::unwind_safe::RefUnwindSafe for rustdoc_json::PackageTarget
+impl core::marker::Send for rustdoc_json::PackageTarget
+impl core::marker::Sync for rustdoc_json::PackageTarget
+impl core::marker::Unpin for rustdoc_json::PackageTarget
+impl core::panic::unwind_safe::UnwindSafe for rustdoc_json::PackageTarget
+impl<T> core::any::Any for rustdoc_json::PackageTarget where T: 'static + core::marker::Sized
+pub fn rustdoc_json::PackageTarget::type_id(&self) -> core::any::TypeId
+impl<T> core::borrow::Borrow<T> for rustdoc_json::PackageTarget where T: core::marker::Sized
+pub fn rustdoc_json::PackageTarget::borrow(&self) -> &T
+impl<T> core::borrow::BorrowMut<T> for rustdoc_json::PackageTarget where T: core::marker::Sized
+pub fn rustdoc_json::PackageTarget::borrow_mut(&mut self) -> &mut T
+impl<T> core::convert::From<T> for rustdoc_json::PackageTarget
+pub fn rustdoc_json::PackageTarget::from(t: T) -> T
+impl<T, U> core::convert::Into<U> for rustdoc_json::PackageTarget where U: core::convert::From<T>
+pub fn rustdoc_json::PackageTarget::into(self) -> U
+impl<T> alloc::borrow::ToOwned for rustdoc_json::PackageTarget where T: core::clone::Clone
+pub type rustdoc_json::PackageTarget::Owned = T
+pub fn rustdoc_json::PackageTarget::clone_into(&self, target: &mut T)
+pub fn rustdoc_json::PackageTarget::to_owned(&self) -> T
+impl<T, U> core::convert::TryFrom<U> for rustdoc_json::PackageTarget where U: core::convert::Into<T>
+pub type rustdoc_json::PackageTarget::Error = core::convert::Infallible
+pub fn rustdoc_json::PackageTarget::try_from(value: U) -> core::result::Result<T, <T as core::convert::TryFrom<U>>::Error>
+impl<T, U> core::convert::TryInto<U> for rustdoc_json::PackageTarget where U: core::convert::TryFrom<T>
+pub type rustdoc_json::PackageTarget::Error = <U as core::convert::TryFrom<T>>::Error
+pub fn rustdoc_json::PackageTarget::try_into(self) -> core::result::Result<U, <U as core::convert::TryFrom<T>>::Error>
 pub struct rustdoc_json::Builder
 impl rustdoc_json::Builder
 pub const fn rustdoc_json::Builder::all_features(self, all_features: bool) -> Self
@@ -53,6 +90,7 @@ pub fn rustdoc_json::Builder::features<I: core::iter::traits::collect::IntoItera
 pub fn rustdoc_json::Builder::manifest_path(self, manifest_path: impl core::convert::AsRef<std::path::Path>) -> Self
 pub const fn rustdoc_json::Builder::no_default_features(self, no_default_features: bool) -> Self
 pub fn rustdoc_json::Builder::package(self, package: impl core::convert::AsRef<str>) -> Self
+pub fn rustdoc_json::Builder::package_target(self, package_target: rustdoc_json::PackageTarget) -> Self
 pub const fn rustdoc_json::Builder::quiet(self, quiet: bool) -> Self
 pub fn rustdoc_json::Builder::target(self, target: alloc::string::String) -> Self
 pub fn rustdoc_json::Builder::target_dir(self, target_dir: impl core::convert::AsRef<std::path::Path>) -> Self
diff --git a/rustdoc-json/src/builder.rs b/rustdoc-json/src/builder.rs
index 79f73ff..7648666 100644
--- a/rustdoc-json/src/builder.rs
+++ b/rustdoc-json/src/builder.rs
@@ -18,6 +18,7 @@ pub fn run_cargo_rustdoc(options: Builder) -> Result<PathBuf, BuildError> {
         rustdoc_json_path_for_manifest_path(
             options.manifest_path,
             options.package.as_deref(),
+            &options.package_target,
             options.target_dir.as_deref(),
             options.target.as_deref(),
         )
@@ -47,6 +48,7 @@ fn cargo_rustdoc_command(options: &Builder) -> Command {
         all_features,
         features,
         package,
+        package_target,
         document_private_items,
         cap_lints,
     } = options;
@@ -63,7 +65,13 @@ fn cargo_rustdoc_command(options: &Builder) -> Command {
         );
 
     command.arg("rustdoc");
-    command.arg("--lib");
+    match package_target {
+        PackageTarget::Lib => command.arg("--lib"),
+        PackageTarget::Bin(target) => command.args(["--bin", target]),
+        PackageTarget::Example(target) => command.args(["--example", target]),
+        PackageTarget::Test(target) => command.args(["--test", target]),
+        PackageTarget::Bench(target) => command.args(["--bench", target]),
+    };
     if let Some(target_dir) = target_dir {
         command.arg("--target-dir");
         command.arg(target_dir);
@@ -106,6 +114,7 @@ fn cargo_rustdoc_command(options: &Builder) -> Command {
 fn rustdoc_json_path_for_manifest_path(
     manifest_path: impl AsRef<Path>,
     package: Option<&str>,
+    package_target: &PackageTarget,
     target_dir: Option<&Path>,
     target: Option<&str>,
 ) -> Result<PathBuf, BuildError> {
@@ -113,9 +122,17 @@ fn rustdoc_json_path_for_manifest_path(
         Some(target_dir) => target_dir.to_owned(),
         None => target_directory(&manifest_path)?,
     };
-    let lib_name = package
-        .map(ToOwned::to_owned)
-        .map_or_else(|| package_name(&manifest_path), Ok)?;
+
+    // get the name of the crate/binary/example/test/bench
+    let package_target_name = match package_target {
+        PackageTarget::Lib => package
+            .map(ToOwned::to_owned)
+            .map_or_else(|| package_name(&manifest_path), Ok)?,
+        PackageTarget::Bin(package)
+        | PackageTarget::Example(package)
+        | PackageTarget::Test(package)
+        | PackageTarget::Bench(package) => package.clone(),
+    };
 
     let mut rustdoc_json_path = target_dir;
     // if one has specified a target explicitly then Cargo appends that target triple name as a subfolder
@@ -123,7 +140,7 @@ fn rustdoc_json_path_for_manifest_path(
         rustdoc_json_path.push(target);
     }
     rustdoc_json_path.push("doc");
-    rustdoc_json_path.push(lib_name.replace('-', "_"));
+    rustdoc_json_path.push(package_target_name.replace('-', "_"));
     rustdoc_json_path.set_extension("json");
     Ok(rustdoc_json_path)
 }
@@ -161,6 +178,7 @@ pub struct Builder {
     all_features: bool,
     features: Vec<String>,
     package: Option<String>,
+    package_target: PackageTarget,
     document_private_items: bool,
     cap_lints: Option<String>,
 }
@@ -177,6 +195,7 @@ impl Default for Builder {
             all_features: false,
             features: vec![],
             package: None,
+            package_target: PackageTarget::default(),
             document_private_items: false,
             cap_lints: Some(String::from("warn")),
         }
@@ -270,6 +289,13 @@ impl Builder {
         self
     }
 
+    /// What part of the package to document. Default: `PackageTarget::Lib`
+    #[must_use]
+    pub fn package_target(mut self, package_target: PackageTarget) -> Self {
+        self.package_target = package_target;
+        self
+    }
+
     /// Whether to pass `--document-private-items` to `cargo rustdoc`. Default: `false`
     #[must_use]
     pub fn document_private_items(mut self, document_private_items: bool) -> Self {
@@ -298,6 +324,23 @@ impl Builder {
     }
 }
 
+/// The part of of the package to document
+#[derive(Default, Debug, Clone)]
+#[non_exhaustive]
+pub enum PackageTarget {
+    /// Document the package as a library, i.e. pass `--lib`
+    #[default]
+    Lib,
+    /// Document the given binary, i.e. pass `--bin <name>`
+    Bin(String),
+    /// Document the given binary, i.e. pass `--example <name>`
+    Example(String),
+    /// Document the given binary, i.e. pass `--test <name>`
+    Test(String),
+    /// Document the given binary, i.e. pass `--bench <name>`
+    Bench(String),
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/rustdoc-json/src/lib.rs b/rustdoc-json/src/lib.rs
index 0f78d08..6e02040 100644
--- a/rustdoc-json/src/lib.rs
+++ b/rustdoc-json/src/lib.rs
@@ -23,7 +23,7 @@
 use std::path::PathBuf;
 
 mod builder;
-pub use builder::Builder;
+pub use builder::{Builder, PackageTarget};
 
 /// Represents all errors that can occur when using [`Builder::build()`].
 #[derive(thiserror::Error, Debug)]


Test patch:
diff --git a/rustdoc-json/tests/rustdoc-json-lib-tests.rs b/rustdoc-json/tests/rustdoc-json-lib-tests.rs
index 7ab725a..8c00481 100644
--- a/rustdoc-json/tests/rustdoc-json-lib-tests.rs
+++ b/rustdoc-json/tests/rustdoc-json-lib-tests.rs
@@ -1,4 +1,5 @@
 use public_api::{Options, PublicApi};
+use rustdoc_json::PackageTarget;
 
 #[test]
 fn public_api() -> Result<(), Box<dyn std::error::Error>> {
@@ -12,3 +13,43 @@ fn public_api() -> Result<(), Box<dyn std::error::Error>> {
 
     Ok(())
 }
+
+#[test]
+fn package_target_bin() {
+    test_alternative_package_target(PackageTarget::Bin("test_crate".into()));
+}
+
+#[test]
+fn package_target_bin_2() {
+    test_alternative_package_target(PackageTarget::Bin("main2".into()));
+}
+
+#[test]
+fn package_target_test() {
+    test_alternative_package_target(PackageTarget::Test("test".into()));
+}
+
+#[test]
+fn package_target_example() {
+    test_alternative_package_target(PackageTarget::Example("example".into()));
+}
+
+#[test]
+fn package_target_bench() {
+    test_alternative_package_target(PackageTarget::Bench("bench".into()));
+}
+
+fn test_alternative_package_target(package_target: PackageTarget) {
+    let target_dir = tempfile::tempdir().unwrap();
+
+    let path = rustdoc_json::Builder::default()
+        .toolchain("nightly".to_owned())
+        .manifest_path("tests/test_crate/Cargo.toml")
+        .quiet(true) // Make it less noisy to run tests
+        .package_target(package_target)
+        .target_dir(&target_dir)
+        .build()
+        .unwrap();
+
+    assert!(path.exists());
+}
diff --git a/rustdoc-json/tests/test_crate/Cargo.toml b/rustdoc-json/tests/test_crate/Cargo.toml
new file mode 100644
index 0000000..cba7a8b
--- /dev/null
+++ b/rustdoc-json/tests/test_crate/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+description = "For testing rustdoc-json"
+name = "test_crate"
+version = "0.1.0"
+edition = "2021"
+
+# Exclude this package from the top level workspace by declaring our own empty
+# workspace
+[workspace]
diff --git a/rustdoc-json/tests/test_crate/benches/bench.rs b/rustdoc-json/tests/test_crate/benches/bench.rs
new file mode 100644
index 0000000..f328e4d
--- /dev/null
+++ b/rustdoc-json/tests/test_crate/benches/bench.rs
@@ -0,0 +1,1 @@
+fn main() {}
diff --git a/rustdoc-json/tests/test_crate/examples/example.rs b/rustdoc-json/tests/test_crate/examples/example.rs
new file mode 100644
index 0000000..f328e4d
--- /dev/null
+++ b/rustdoc-json/tests/test_crate/examples/example.rs
@@ -0,0 +1,1 @@
+fn main() {}
diff --git a/rustdoc-json/tests/test_crate/src/bin/main2.rs b/rustdoc-json/tests/test_crate/src/bin/main2.rs
new file mode 100644
index 0000000..f328e4d
--- /dev/null
+++ b/rustdoc-json/tests/test_crate/src/bin/main2.rs
@@ -0,0 +1,1 @@
+fn main() {}
diff --git a/rustdoc-json/tests/test_crate/src/main.rs b/rustdoc-json/tests/test_crate/src/main.rs
new file mode 100644
index 0000000..f328e4d
--- /dev/null
+++ b/rustdoc-json/tests/test_crate/src/main.rs
@@ -0,0 +1,1 @@
+fn main() {}
diff --git a/rustdoc-json/tests/test_crate/tests/test.rs b/rustdoc-json/tests/test_crate/tests/test.rs
new file mode 100644
index 0000000..f328e4d
--- /dev/null
+++ b/rustdoc-json/tests/test_crate/tests/test.rs
@@ -0,0 +1,1 @@
+fn main() {}
diff --git a/scripts/bless-expected-output-for-tests.sh b/scripts/bless-expected-output-for-tests.sh
index ee6492e..6faccf3 100755
--- a/scripts/bless-expected-output-for-tests.sh
+++ b/scripts/bless-expected-output-for-tests.sh
@@ -1,4 +1,8 @@
 #!/usr/bin/env bash
 set -o nounset -o pipefail -o errexit
 
-UPDATE_EXPECT=1 cargo test --test '*' -p public-api -p cargo-public-api
+UPDATE_EXPECT=1 cargo test --test '*' \
+    -p public-api \
+    -p cargo-public-api \
+    -p rustdoc-json \
+    -p rustup-toolchain \