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

Issue:
Change `PublicApi::from_rustdoc_json(path, Options::default())` to `public_api::Builder::from_rustdoc_json(path).build()`
Instead of this public API for the `public-api` lib:

```rust
    // Derive the public API from the rustdoc JSON
    let mut options = public_api::Options::default();
    options.omit_blanket_impls = true;
    options.omit_auto_trait_impls = true;
    let public_api = public_api::PublicApi::from_rustdoc_json(path, options).unwrap();
```

I want the public API to be this:

```rust
    // Derive the public API from the rustdoc JSON
    let public_api = public_api::Builder::from_rustdoc_json(path)
        .omit_blanket_impls(true)
        .omit_auto_trait_impls(true)
        .build()
        .unwrap();
```

which has the following benefits:
* nicer and simpler API
* symmetry with the `rustdoc_json::Builder` API

I have made a prototype commit that passed CI: https://github.com/Enselic/cargo-public-api/pull/293

Doing it properly with a proper public API deprecation path is more work, however. And this is not top prio for me right now. So I am creating this issue to make my long-term intention visible to others.

Pull request description:
Introduce `public_api::Builder`, deprecate `PublicApi::from_rustdoc_json()` and `Options`
Instead of this public API:

    // Derive the public API from the rustdoc JSON
    let mut options = public_api::Options::default();
    options.omit_blanket_impls = true;
    options.omit_auto_trait_impls = true;
    let public_api = public_api::PublicApi::from_rustdoc_json(path, options).unwrap();

The public API is now this:

    // Derive the public API from the rustdoc JSON
    let public_api = public_api::Builder::from_rustdoc_json(path)
        .omit_blanket_impls(true)
        .omit_auto_trait_impls(true)
        .build()
        .unwrap();

which has the following benefits:
 * nicer and simpler API
 * symmetry with the `rustdoc_json::Builder` API

Closes #318


Patch:
diff --git a/README.md b/README.md
index f27b730..d891f01 100644
--- a/README.md
+++ b/README.md
@@ -96,9 +96,9 @@ fn public_api() {
         .unwrap();
 
     // Derive the public API from the rustdoc JSON
-    let public_api =
-        public_api::PublicApi::from_rustdoc_json(rustdoc_json, public_api::Options::default())
-            .unwrap();
+    let public_api = public_api::Builder::from_rustdoc_json(rustdoc_json)
+        .build()
+        .unwrap();
 
     // Assert that the public API looks correct
     expect_test::expect_file!["public-api.txt"].assert_eq(&public_api.to_string());
diff --git a/cargo-public-api/src/api_source.rs b/cargo-public-api/src/api_source.rs
index adfc1d9..df4e464 100644
--- a/cargo-public-api/src/api_source.rs
+++ b/cargo-public-api/src/api_source.rs
@@ -4,7 +4,7 @@ use anyhow::{anyhow, Context, Result};
 use rustdoc_json::BuildError;
 use std::path::{Path, PathBuf};
 
-use public_api::{Options, PublicApi, MINIMUM_NIGHTLY_VERSION};
+use public_api::{PublicApi, MINIMUM_NIGHTLY_VERSION};
 
 use crate::{git_utils, Args, Subcommand};
 
@@ -126,16 +126,12 @@ pub fn build_rustdoc_json(builder: rustdoc_json::Builder) -> Result<PathBuf> {
     }
 }
 
-/// Figure out what [`Options`] to pass to
-/// [`public_api::PublicApi::from_rustdoc_json`] based on our
-/// [`Args`]
-fn get_options(args: &Args) -> Options {
-    let mut options = Options::default();
-    options.debug_sorting = args.debug_sorting;
-    options.omit_blanket_impls = args.omit_blanket_impls();
-    options.omit_auto_trait_impls = args.omit_auto_trait_impls();
-    options.omit_auto_derived_impls = args.omit_auto_derived_impls();
-    options
+fn public_api_builder_from_args(rustdoc_json: &Path, args: &Args) -> public_api::Builder {
+    public_api::Builder::from_rustdoc_json(rustdoc_json)
+        .debug_sorting(args.debug_sorting)
+        .omit_blanket_impls(args.omit_blanket_impls())
+        .omit_auto_trait_impls(args.omit_auto_trait_impls())
+        .omit_auto_derived_impls(args.omit_auto_derived_impls())
 }
 
 /// Creates a rustdoc JSON builder based on the args to this program.
@@ -170,13 +166,11 @@ pub fn builder_from_args(args: &Args) -> rustdoc_json::Builder {
 fn public_api_from_rustdoc_json(path: impl AsRef<Path>, args: &Args) -> Result<PublicApi> {
     let json_path = path.as_ref();
 
-    let options = get_options(args);
-
     if args.verbose {
         println!("Processing {json_path:?}");
     }
 
-    let public_api = PublicApi::from_rustdoc_json(json_path, options).with_context(|| {
+    let public_api = public_api_builder_from_args(json_path, args).build().with_context(|| {
         format!(
             "Failed to parse rustdoc JSON at {json_path:?}.\n\
             This version of `cargo public-api` requires at least:\n\n    {MINIMUM_NIGHTLY_VERSION}\n\n\
diff --git a/public-api/CHANGELOG.md b/public-api/CHANGELOG.md
index c9bcd86..535d68f 100644
--- a/public-api/CHANGELOG.md
+++ b/public-api/CHANGELOG.md
@@ -1,5 +1,9 @@
 # `public-api` changelog
 
+## Unreleased v0.27.4
+* Deprecate `PublicApi::from_rustdoc_json()`. Use `public_api::Builder::from_rustdoc_json()` instead.
+* Deprecate `Options`. Use `public_api::Builder` methods instead.
+
 ## v0.27.3
 * Bump deps
 
diff --git a/public-api/examples/diff_public_api.rs b/public-api/examples/diff_public_api.rs
index e22ffec..d70094d 100644
--- a/public-api/examples/diff_public_api.rs
+++ b/public-api/examples/diff_public_api.rs
@@ -1,21 +1,19 @@
 use std::error::Error;
 
-use public_api::{diff::PublicApiDiff, Options, PublicApi};
+use public_api::diff::PublicApiDiff;
 
 fn main() -> Result<(), Box<dyn Error>> {
-    let options = Options::default();
-
     let old_json = rustdoc_json::Builder::default()
         .toolchain("nightly")
         .manifest_path("test-apis/example_api-v0.1.0/Cargo.toml")
         .build()?;
-    let old = PublicApi::from_rustdoc_json(old_json, options)?;
+    let old = public_api::Builder::from_rustdoc_json(old_json).build()?;
 
     let new_json = rustdoc_json::Builder::default()
         .toolchain("nightly")
         .manifest_path("test-apis/example_api-v0.2.0/Cargo.toml")
         .build()?;
-    let new = PublicApi::from_rustdoc_json(new_json, options)?;
+    let new = public_api::Builder::from_rustdoc_json(new_json).build()?;
 
     let diff = PublicApiDiff::between(old, new);
     println!("{diff:#?}");
diff --git a/public-api/examples/list_public_api.rs b/public-api/examples/list_public_api.rs
index dad6625..2e7ac27 100644
--- a/public-api/examples/list_public_api.rs
+++ b/public-api/examples/list_public_api.rs
@@ -1,14 +1,12 @@
 use std::error::Error;
 
-use public_api::{Options, PublicApi};
-
 fn main() -> Result<(), Box<dyn Error>> {
     let json_path = rustdoc_json::Builder::default()
         .toolchain("nightly")
         .manifest_path("test-apis/example_api-v0.2.0/Cargo.toml")
         .build()?;
 
-    let public_api = PublicApi::from_rustdoc_json(json_path, Options::default())?;
+    let public_api = public_api::Builder::from_rustdoc_json(json_path).build()?;
 
     for public_item in public_api.items() {
         println!("{public_item}");
diff --git a/public-api/src/diff.rs b/public-api/src/diff.rs
index f30d3f9..ceca888 100644
--- a/public-api/src/diff.rs
+++ b/public-api/src/diff.rs
@@ -61,7 +61,7 @@ impl PublicApiDiff {
     /// Allows you to diff the public API between two arbitrary versions of a
     /// library, e.g. different releases. The input parameters `old` and `new`
     /// is the output of two different invocations of
-    /// [`crate::PublicApi::from_rustdoc_json`].
+    /// [`crate::Builder::build`].
     #[must_use]
     pub fn between(old: PublicApi, new: PublicApi) -> Self {
         // We must use a HashBag, because with a HashSet we would lose public
diff --git a/public-api/src/item_processor.rs b/public-api/src/item_processor.rs
index 86070ab..568dfb9 100644
--- a/public-api/src/item_processor.rs
+++ b/public-api/src/item_processor.rs
@@ -1,7 +1,7 @@
 use super::intermediate_public_item::NameableItem;
 use crate::{
     crate_wrapper::CrateWrapper, intermediate_public_item::IntermediatePublicItem,
-    public_item::PublicItem, render::RenderingContext, Options, PublicApi,
+    public_item::PublicItem, render::RenderingContext, BuilderOptions as Options, PublicApi,
 };
 use rustdoc_types::{
     Crate, Id, Impl, Import, Item, ItemEnum, Module, Struct, StructKind, VariantKind,
@@ -53,7 +53,7 @@ pub struct ItemProcessor<'c> {
 }
 
 impl<'c> ItemProcessor<'c> {
-    pub fn new(crate_: &'c Crate, options: Options) -> Self {
+    pub(crate) fn new(crate_: &'c Crate, options: Options) -> Self {
         ItemProcessor {
             crate_: CrateWrapper::new(crate_),
             options,
@@ -386,7 +386,7 @@ pub fn impls_for_item(item: &Item) -> Option<&[Id]> {
     }
 }
 
-pub fn public_api_in_crate(crate_: &Crate, options: Options) -> super::PublicApi {
+pub(crate) fn public_api_in_crate(crate_: &Crate, options: Options) -> super::PublicApi {
     let mut item_processor = ItemProcessor::new(crate_, options);
     item_processor.add_to_work_queue(vec![], &crate_.root);
     item_processor.run();
diff --git a/public-api/src/lib.rs b/public-api/src/lib.rs
index 95feb7a..6955c38 100644
--- a/public-api/src/lib.rs
+++ b/public-api/src/lib.rs
@@ -5,20 +5,12 @@
 //! If you want a convenient CLI for this library, you should use [cargo
 //! public-api](https://github.com/Enselic/cargo-public-api).
 //!
-//! As input to the library, a special output format from `cargo doc` is used,
-//! which goes by the name **rustdoc JSON**. Currently, only `cargo doc` from
-//! the Nightly toolchain can produce **rustdoc JSON** for a library. You build
-//! **rustdoc JSON** like this:
+//! As input to the library, a special output format from `rustdoc +nightly` is
+//! used, which goes by the name **rustdoc JSON**. Currently, only the nightly
+//! toolchain can build **rustdoc JSON**.
 //!
-//! ```bash
-//! cargo +nightly rustdoc --lib -- -Z unstable-options --output-format json
-//! ```
-//!
-//! Consider using [`rustdoc_json`](https://crates.io/crates/rustdoc_json)
-//! instead of invoking the above command yourself.
-//!
-//! The main entry point to the library is [`PublicApi::from_rustdoc_json_str`],
-//! so please read its documentation.
+//! You use the [`rustdoc-json`](https://crates.io/crates/rustdoc_json) library
+//! to programmatically build rustdoc JSON. See below for example code.
 //!
 //! # Examples
 //!
@@ -34,10 +26,6 @@
 //! ```no_run
 #![doc = include_str!("../examples/diff_public_api.rs")]
 //! ```
-//!
-//! The most comprehensive example code on how to use the library can be found
-//! in the thin binary wrapper around the library, see
-//! <https://github.com/Enselic/cargo-public-api/blob/main/public-api/src/main.rs>.
 
 // deny in CI, only warn here
 #![warn(clippy::all, clippy::pedantic, missing_docs)]
@@ -52,7 +40,7 @@ pub mod tokens;
 
 pub mod diff;
 
-use std::path::Path;
+use std::path::{Path, PathBuf};
 
 // Documented at the definition site so cargo doc picks it up
 pub use error::{Error, Result};
@@ -74,11 +62,44 @@ pub const MINIMUM_NIGHTLY_VERSION: &str = "nightly-2023-01-04";
 #[deprecated(since = "0.27.0", note = "Use MINIMUM_NIGHTLY_VERSION instead")]
 pub const MINIMUM_RUSTDOC_JSON_VERSION: &str = MINIMUM_NIGHTLY_VERSION;
 
-/// Contains various options that you can pass to [`PublicApi::from_rustdoc_json`].
+/// See [`Builder`] method docs for what each field means.
 #[derive(Copy, Clone, Debug)]
-#[non_exhaustive] // More options are likely to be added in the future
 #[allow(clippy::struct_excessive_bools)]
-pub struct Options {
+struct BuilderOptions {
+    sorted: bool,
+    debug_sorting: bool,
+    omit_blanket_impls: bool,
+    omit_auto_trait_impls: bool,
+    omit_auto_derived_impls: bool,
+}
+
+/// Builds [`PublicApi`]s. See the [top level][`crate`] module docs for example
+/// code.
+#[derive(Debug, Clone)]
+#[allow(clippy::struct_excessive_bools)]
+pub struct Builder {
+    source: BuilderSource,
+    options: BuilderOptions,
+}
+
+impl Builder {
+    /// Create a new [`PublicApi`] builder from a rustdoc JSON file. See the
+    /// [top level][`crate`] module docs for example code.
+    #[must_use]
+    pub fn from_rustdoc_json(path: impl Into<PathBuf>) -> Self {
+        let options = BuilderOptions {
+            sorted: true,
+            debug_sorting: false,
+            omit_blanket_impls: false,
+            omit_auto_trait_impls: false,
+            omit_auto_derived_impls: false,
+        };
+        Self {
+            source: BuilderSource::RustdocJson(path.into()),
+            options,
+        }
+    }
+
     /// If `true`, items will be sorted before being returned. If you will pass
     /// on the return value to [`diff::PublicApiDiff::between`], it is
     /// currently unnecessary to sort first, because the sorting will be
@@ -86,13 +107,21 @@ pub struct Options {
     ///
     /// The default value is `true`, because usually the performance impact is
     /// negligible, and is is generally more practical to work with sorted data.
-    pub sorted: bool,
+    #[must_use]
+    pub fn sorted(mut self, sorted: bool) -> Self {
+        self.options.sorted = sorted;
+        self
+    }
 
     /// If `true`, item paths include the so called "sorting prefix" that makes
     /// them grouped in a nice way. Only intended for debugging this library.
     ///
     /// The default value is `false`
-    pub debug_sorting: bool,
+    #[must_use]
+    pub fn debug_sorting(mut self, debug_sorting: bool) -> Self {
+        self.options.debug_sorting = debug_sorting;
+        self
+    }
 
     /// If `true`, items that belongs to Blanket Implementations are omitted
     /// from the output. This makes the output less noisy, at the cost of not
@@ -103,7 +132,11 @@ pub struct Options {
     ///
     /// The default value is `false` so that the listed public API is complete
     /// by default.
-    pub omit_blanket_impls: bool,
+    #[must_use]
+    pub fn omit_blanket_impls(mut self, omit_blanket_impls: bool) -> Self {
+        self.options.omit_blanket_impls = omit_blanket_impls;
+        self
+    }
 
     /// If `true`, items that belongs to Auto Trait Implementations are omitted
     /// from the output. This makes the output less noisy, at the cost of not
@@ -114,7 +147,11 @@ pub struct Options {
     ///
     /// The default value is `false` so that the listed public API is complete
     /// by default.
-    pub omit_auto_trait_impls: bool,
+    #[must_use]
+    pub fn omit_auto_trait_impls(mut self, omit_auto_trait_impls: bool) -> Self {
+        self.options.omit_auto_trait_impls = omit_auto_trait_impls;
+        self
+    }
 
     /// If `true`, items that belongs to automatically derived implementations
     /// (`Clone`, `Debug`, `Eq`, etc) are omitted from the output. This makes
@@ -123,33 +160,32 @@ pub struct Options {
     ///
     /// The default value is `false` so that the listed public API is complete
     /// by default.
-    pub omit_auto_derived_impls: bool,
-}
+    #[must_use]
+    pub fn omit_auto_derived_impls(mut self, omit_auto_derived_impls: bool) -> Self {
+        self.options.omit_auto_derived_impls = omit_auto_derived_impls;
+        self
+    }
 
-/// Enables options to be set up like this (note that `Options` is marked
-/// `#[non_exhaustive]`):
-///
-/// ```
-/// # use public_api::Options;
-/// let mut options = Options::default();
-/// options.sorted = true;
-/// // ...
-/// ```
-impl Default for Options {
-    fn default() -> Self {
-        Self {
-            sorted: true,
-            debug_sorting: false,
-            omit_blanket_impls: false,
-            omit_auto_trait_impls: false,
-            omit_auto_derived_impls: false,
-        }
+    /// Builds [`PublicApi`]. See the [top level][`crate`] module docs for
+    /// example code.
+    ///
+    /// # Errors
+    ///
+    /// E.g. if the [JSON](Builder::from_rustdoc_json) is invalid or if the file
+    /// can't be read.
+    pub fn build(self) -> Result<PublicApi> {
+        let s = match &self.source {
+            BuilderSource::RustdocJson(path) => std::fs::read_to_string(path)?,
+            BuilderSource::RustdocJsonStr(s) => s.clone(),
+        };
+
+        from_rustdoc_json_str(s, self.options)
     }
 }
 
 /// The public API of a crate
 ///
-/// Create an instance with [`PublicApi::from_rustdoc_json()`].
+/// Create an instance with [`Builder`].
 ///
 /// ## Rendering the items
 ///
@@ -159,12 +195,12 @@ impl Default for Options {
 /// [`rustdoc_json`](https://crates.io/crates/rustdoc_json) or by calling `cargo rustdoc` yourself.
 ///
 /// ```no_run
-/// use public_api::{PublicApi, Options};
+/// use public_api::PublicApi;
+/// use std::path::PathBuf;
 ///
-/// # let rustdoc_json_str: String = todo!();
-/// let options = Options::default();
+/// # let rustdoc_json: PathBuf = todo!();
 /// // Gather the rustdoc content as described in this crates top-level documentation.
-/// let public_api = PublicApi::from_rustdoc_json_str(&rustdoc_json_str, options)?;
+/// let public_api = public_api::Builder::from_rustdoc_json(&rustdoc_json).build()?;
 ///
 /// for public_item in public_api.items() {
 ///     // here we print the items to stdout, we could also write to a string or a file.
@@ -214,9 +250,18 @@ impl PublicApi {
     /// # Errors
     ///
     /// E.g. if the JSON is invalid or if the file can't be read.
+    #[deprecated(
+        since = "0.27.4",
+        note = "Use `public_api::Builder::from_rustdoc_json(path).option1(arg1).option2(arg2)...` instead."
+    )]
+    #[allow(deprecated)]
+    #[allow(clippy::needless_pass_by_value)] // For API backwards compatibility
     pub fn from_rustdoc_json(path: impl AsRef<Path>, options: Options) -> Result<PublicApi> {
-        #[allow(deprecated)]
-        Self::from_rustdoc_json_str(std::fs::read_to_string(path)?, options)
+        Builder {
+            source: BuilderSource::RustdocJson(path.as_ref().to_owned()),
+            options: options.into(),
+        }
+        .build()
     }
 
     /// Same as [`Self::from_rustdoc_json`], but the rustdoc JSON is read from a
@@ -229,11 +274,17 @@ impl PublicApi {
         since = "0.27.1",
         note = "If you need this edge case API, you need to write your JSON to a temporary file and then use `PublicApi::from_rustdoc_json()` instead."
     )]
+    #[allow(deprecated)]
+    #[allow(clippy::needless_pass_by_value)] // For API backwards compatibility
     pub fn from_rustdoc_json_str(
         rustdoc_json_str: impl AsRef<str>,
         options: Options,
     ) -> Result<PublicApi> {
-        from_rustdoc_json_str(rustdoc_json_str, options)
+        Builder {
+            source: BuilderSource::RustdocJsonStr(rustdoc_json_str.as_ref().to_owned()),
+            options: options.into(),
+        }
+        .build()
     }
 
     /// Returns an iterator over all public items in the public API
@@ -272,7 +323,10 @@ impl std::fmt::Display for PublicApi {
     }
 }
 
-fn from_rustdoc_json_str(rustdoc_json_str: impl AsRef<str>, options: Options) -> Result<PublicApi> {
+fn from_rustdoc_json_str(
+    rustdoc_json_str: impl AsRef<str>,
+    options: BuilderOptions,
+) -> Result<PublicApi> {
     let crate_ = deserialize_without_recursion_limit(rustdoc_json_str.as_ref())?;
 
     let mut public_api = item_processor::public_api_in_crate(&crate_, options);
@@ -292,3 +346,57 @@ fn deserialize_without_recursion_limit(rustdoc_json_str: &str) -> Result<rustdoc
     deserializer.disable_recursion_limit();
     Ok(serde::de::Deserialize::deserialize(&mut deserializer)?)
 }
+
+/// Temporary enum until we have removed [`PublicApi::from_rustdoc_json_str`]
+#[derive(Debug, Clone)]
+enum BuilderSource {
+    RustdocJson(PathBuf),
+    RustdocJsonStr(String),
+}
+
+/// Deprecated. Use [`public_api::Builder`](crate::Builder) instead.
+#[derive(Debug, Clone, Copy)]
+#[allow(clippy::struct_excessive_bools)]
+#[non_exhaustive]
+#[deprecated(
+    since = "0.27.4",
+    note = "Use `public_api::Builder::from_rustdoc_json(path).option1(arg1).option2(arg2)...` instead."
+)]
+pub struct Options {
+    /// Deprecated. Use [`crate::Builder::sorted`] instead.
+    pub sorted: bool,
+    /// Deprecated. Use [`crate::Builder::debug_sorting`] instead.
+    pub debug_sorting: bool,
+    /// Deprecated. Use [`crate::Builder::omit_blanket_impls`] instead.
+    pub omit_blanket_impls: bool,
+    /// Deprecated. Use [`crate::Builder::omit_auto_trait_impls`] instead.
+    pub omit_auto_trait_impls: bool,
+    /// Deprecated. Use [`crate::Builder::omit_auto_derived_impls`] instead.
+    pub omit_auto_derived_impls: bool,
+}
+
+#[allow(deprecated)]
+impl Default for Options {
+    fn default() -> Self {
+        Self {
+            sorted: true,
+            debug_sorting: false,
+            omit_blanket_impls: false,
+            omit_auto_trait_impls: false,
+            omit_auto_derived_impls: false,
+        }
+    }
+}
+
+#[allow(deprecated)]
+impl From<Options> for BuilderOptions {
+    fn from(options: Options) -> Self {
+        Self {
+            sorted: options.sorted,
+            debug_sorting: options.debug_sorting,
+            omit_blanket_impls: options.omit_blanket_impls,
+            omit_auto_trait_impls: options.omit_auto_trait_impls,
+            omit_auto_derived_impls: options.omit_auto_derived_impls,
+        }
+    }
+}
diff --git a/public-api/src/main.rs b/public-api/src/main.rs
index 036b9d0..ff2ff70 100644
--- a/public-api/src/main.rs
+++ b/public-api/src/main.rs
@@ -8,7 +8,7 @@ use std::io::{stdout, ErrorKind, Write};
 use std::path::{Path, PathBuf};
 
 use public_api::diff::PublicApiDiff;
-use public_api::{Options, PublicApi, MINIMUM_NIGHTLY_VERSION};
+use public_api::{PublicApi, MINIMUM_NIGHTLY_VERSION};
 
 #[derive(thiserror::Error, Debug)]
 enum Error {
@@ -36,39 +36,32 @@ fn main_() -> Result<()> {
         return Ok(());
     }
 
-    let mut options = Options::default();
-    if args.simplified {
-        options.omit_blanket_impls = true;
-        options.omit_auto_trait_impls = true;
-    }
-    options.sorted = true;
-
-    let files = args.files;
+    let files = &args.files;
     if args.help || files.is_empty() || files.len() > 2 {
         print_usage()?;
     } else if files.len() == 1 {
         let path = &files[0];
-        print_public_api(path, options)?;
+        print_public_api(path, &args)?;
     } else if files.len() == 2 {
         let old = &files[0];
         let new = &files[1];
-        print_public_api_diff(old, new, options)?;
+        print_public_api_diff(old, new, &args)?;
     }
 
     Ok(())
 }
 
-fn print_public_api(path: &Path, options: Options) -> Result<()> {
-    for public_item in PublicApi::from_rustdoc_json(path, options)?.items() {
+fn print_public_api(path: &Path, args: &Args) -> Result<()> {
+    for public_item in public_api_from_args(path, args)?.items() {
         writeln!(std::io::stdout(), "{public_item}")?;
     }
 
     Ok(())
 }
 
-fn print_public_api_diff(old: &Path, new: &Path, options: Options) -> Result<()> {
-    let old = PublicApi::from_rustdoc_json(old, options)?;
-    let new = PublicApi::from_rustdoc_json(new, options)?;
+fn print_public_api_diff(old: &Path, new: &Path, args: &Args) -> Result<()> {
+    let old = public_api_from_args(old, args)?;
+    let new = public_api_from_args(new, args)?;
 
     let diff = PublicApiDiff::between(old, new);
     print_diff_with_headers(&diff, &mut stdout(), "Removed:", "Changed:", "Added:")?;
@@ -175,6 +168,14 @@ fn args() -> Args {
     args
 }
 
+fn public_api_from_args(path: &Path, args: &Args) -> public_api::Result<PublicApi> {
+    public_api::Builder::from_rustdoc_json(path.to_owned())
+        .omit_blanket_impls(args.simplified)
+        .omit_auto_trait_impls(args.simplified)
+        .sorted(true)
+        .build()
+}
+
 /// Wrapper to handle <https://github.com/rust-lang/rust/issues/46016>
 fn main() -> Result<()> {
     match main_() {
diff --git a/public-api/src/render.rs b/public-api/src/render.rs
index 3365607..f6592df 100644
--- a/public-api/src/render.rs
+++ b/public-api/src/render.rs
@@ -1,8 +1,7 @@
 #![allow(clippy::unused_self)]
-use crate::{
-    intermediate_public_item::{IntermediatePublicItem, NameableItem},
-    Options,
-};
+use crate::intermediate_public_item::{IntermediatePublicItem, NameableItem};
+use crate::tokens::Token;
+use crate::BuilderOptions as Options;
 use std::ops::Deref;
 use std::{cmp::Ordering, collections::HashMap, vec};
 
@@ -20,8 +19,6 @@ macro_rules! ws {
     };
 }
 
-use crate::tokens::Token;
-
 /// When we render an item, it might contain references to other parts of the
 /// public API. For such cases, the rendering code can use the fields in this
 /// struct.
@@ -32,7 +29,7 @@ pub struct RenderingContext<'c> {
     /// Given a rustdoc JSON ID, keeps track of what public items that have this Id.
     pub id_to_items: HashMap<&'c Id, Vec<&'c IntermediatePublicItem<'c>>>,
 
-    pub options: Options,
+    pub(crate) options: Options,
 }
 
 impl<'c> RenderingContext<'c> {
@@ -1316,10 +1313,11 @@ mod test {
             external_crates: HashMap::new(),
             format_version: 0,
         };
+        let builder = crate::Builder::from_rustdoc_json("N/A");
         let context = RenderingContext {
             crate_: &crate_,
             id_to_items: HashMap::new(),
-            options: Options::default(),
+            options: builder.options,
         };
 
         let actual = render_fn(context);


Test patch:
diff --git a/cargo-public-api/tests/expected-output/list_public_items.txt b/cargo-public-api/tests/expected-output/list_public_items.txt
index a771491..20b249e 100644
--- a/cargo-public-api/tests/expected-output/list_public_items.txt
+++ b/cargo-public-api/tests/expected-output/list_public_items.txt
@@ -96,6 +96,24 @@ impl !core::panic::unwind_safe::UnwindSafe for public_api::Error
 impl core::marker::Send for public_api::Error
 impl core::marker::Sync for public_api::Error
 impl core::marker::Unpin for public_api::Error
+pub struct public_api::Builder
+impl public_api::Builder
+pub fn public_api::Builder::build(self) -> public_api::Result<public_api::PublicApi>
+pub fn public_api::Builder::debug_sorting(self, debug_sorting: bool) -> Self
+pub fn public_api::Builder::from_rustdoc_json(path: impl core::convert::Into<std::path::PathBuf>) -> Self
+pub fn public_api::Builder::omit_auto_derived_impls(self, omit_auto_derived_impls: bool) -> Self
+pub fn public_api::Builder::omit_auto_trait_impls(self, omit_auto_trait_impls: bool) -> Self
+pub fn public_api::Builder::omit_blanket_impls(self, omit_blanket_impls: bool) -> Self
+pub fn public_api::Builder::sorted(self, sorted: bool) -> Self
+impl core::clone::Clone for public_api::Builder
+pub fn public_api::Builder::clone(&self) -> public_api::Builder
+impl core::fmt::Debug for public_api::Builder
+pub fn public_api::Builder::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::marker::Send for public_api::Builder
+impl core::marker::Sync for public_api::Builder
+impl core::marker::Unpin for public_api::Builder
+impl core::panic::unwind_safe::RefUnwindSafe for public_api::Builder
+impl core::panic::unwind_safe::UnwindSafe for public_api::Builder
 #[non_exhaustive] pub struct public_api::Options
 pub public_api::Options::debug_sorting: bool
 pub public_api::Options::omit_auto_derived_impls: bool
diff --git a/cargo-public-api/tests/expected-output/subcommand_invocation_public_api_arg.txt b/cargo-public-api/tests/expected-output/subcommand_invocation_public_api_arg.txt
index c471e6f..cfb95e4 100644
--- a/cargo-public-api/tests/expected-output/subcommand_invocation_public_api_arg.txt
+++ b/cargo-public-api/tests/expected-output/subcommand_invocation_public_api_arg.txt
@@ -76,6 +76,19 @@ impl core::fmt::Display for public_api::Error
 pub fn public_api::Error::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
 impl core::fmt::Debug for public_api::Error
 pub fn public_api::Error::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct public_api::Builder
+impl public_api::Builder
+pub fn public_api::Builder::build(self) -> public_api::Result<public_api::PublicApi>
+pub fn public_api::Builder::debug_sorting(self, debug_sorting: bool) -> Self
+pub fn public_api::Builder::from_rustdoc_json(path: impl core::convert::Into<std::path::PathBuf>) -> Self
+pub fn public_api::Builder::omit_auto_derived_impls(self, omit_auto_derived_impls: bool) -> Self
+pub fn public_api::Builder::omit_auto_trait_impls(self, omit_auto_trait_impls: bool) -> Self
+pub fn public_api::Builder::omit_blanket_impls(self, omit_blanket_impls: bool) -> Self
+pub fn public_api::Builder::sorted(self, sorted: bool) -> Self
+impl core::clone::Clone for public_api::Builder
+pub fn public_api::Builder::clone(&self) -> public_api::Builder
+impl core::fmt::Debug for public_api::Builder
+pub fn public_api::Builder::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
 #[non_exhaustive] pub struct public_api::Options
 pub public_api::Options::debug_sorting: bool
 pub public_api::Options::omit_auto_derived_impls: bool
diff --git a/public-api/tests/common/mod.rs b/public-api/tests/common/mod.rs
index f8286a8..24cb631 100644
--- a/public-api/tests/common/mod.rs
+++ b/public-api/tests/common/mod.rs
@@ -2,6 +2,25 @@
 
 use std::path::{Path, PathBuf};
 
+pub fn builder_for_crate(
+    test_crate: impl AsRef<Path>,
+    target_dir: impl AsRef<Path>,
+) -> public_api::Builder {
+    let json = rustdoc_json_path_for_crate(test_crate, target_dir);
+    public_api::Builder::from_rustdoc_json(json)
+}
+
+/// Returns a builder for a so called "simplified" API, which is an API without
+/// Auto Trait or Blanket impls, to reduce public item noise.
+pub fn simplified_builder_for_crate(
+    test_crate: impl AsRef<Path>,
+    target_dir: impl AsRef<Path>,
+) -> public_api::Builder {
+    builder_for_crate(test_crate, target_dir)
+        .omit_blanket_impls(true)
+        .omit_auto_trait_impls(true)
+}
+
 /// Builds rustdoc JSON for the given test crate.
 ///
 /// Output of child processes are not captured by the Rust test framework (see
diff --git a/public-api/tests/public-api-lib-tests.rs b/public-api/tests/public-api-lib-tests.rs
index 2262610..0d73267 100644
--- a/public-api/tests/public-api-lib-tests.rs
+++ b/public-api/tests/public-api-lib-tests.rs
@@ -8,12 +8,15 @@ use std::{
 };
 
 use expect_test::expect_file;
-use public_api::{Error, Options, PublicApi};
+use public_api::Error;
 
 use tempfile::{tempdir, NamedTempFile, TempDir};
 
 mod common;
-use common::{rustdoc_json_path_for_crate, rustdoc_json_path_for_temp_crate};
+use common::{
+    builder_for_crate, rustdoc_json_path_for_crate, rustdoc_json_path_for_temp_crate,
+    simplified_builder_for_crate,
+};
 
 #[test]
 fn public_api() -> Result<(), Box<dyn std::error::Error>> {
@@ -21,7 +24,7 @@ fn public_api() -> Result<(), Box<dyn std::error::Error>> {
         .toolchain("nightly")
         .build()?;
 
-    let public_api = PublicApi::from_rustdoc_json(rustdoc_json, Options::default())?;
+    let public_api = public_api::Builder::from_rustdoc_json(rustdoc_json).build()?;
 
     expect_test::expect_file!["public-api.txt"].assert_eq(&public_api.to_string());
 
@@ -34,9 +37,8 @@ fn not_simplified() {
     let build_dir = tempdir().unwrap();
 
     assert_public_api(
-        rustdoc_json_path_for_crate("../test-apis/example_api-v0.2.0", &build_dir),
+        builder_for_crate("../test-apis/example_api-v0.2.0", &build_dir),
         "./expected-output/example_api-v0.2.0-not-simplified.txt",
-        Options::default(),
     );
 }
 
@@ -45,13 +47,12 @@ fn simplified_without_auto_derived_impls() {
     // Create independent build dir so all tests can run in parallel
     let build_dir = tempdir().unwrap();
 
-    let mut options = simplified();
-    options.omit_auto_derived_impls = true;
-
     assert_public_api(
-        rustdoc_json_path_for_crate("../test-apis/example_api-v0.2.0", &build_dir),
+        builder_for_crate("../test-apis/example_api-v0.2.0", &build_dir)
+            .omit_auto_derived_impls(true)
+            .omit_blanket_impls(true)
+            .omit_auto_trait_impls(true),
         "./expected-output/example_api-v0.2.0-simplified_without_auto_derived_impls.txt",
-        options,
     );
 }
 
@@ -60,13 +61,9 @@ fn omit_blanket_impls() {
     // Create independent build dir so all tests can run in parallel
     let build_dir = tempdir().unwrap();
 
-    let mut options = Options::default();
-    options.omit_blanket_impls = true;
-
     assert_public_api(
-        rustdoc_json_path_for_crate("../test-apis/example_api-v0.2.0", &build_dir),
+        builder_for_crate("../test-apis/example_api-v0.2.0", &build_dir).omit_blanket_impls(true),
         "./expected-output/example_api-v0.2.0-omit_blanket_impls.txt",
-        options,
     );
 }
 
@@ -75,13 +72,10 @@ fn omit_auto_trait_impls() {
     // Create independent build dir so all tests can run in parallel
     let build_dir = tempdir().unwrap();
 
-    let mut options = Options::default();
-    options.omit_auto_trait_impls = true;
-
     assert_public_api(
-        rustdoc_json_path_for_crate("../test-apis/example_api-v0.2.0", &build_dir),
+        builder_for_crate("../test-apis/example_api-v0.2.0", &build_dir)
+            .omit_auto_trait_impls(true),
         "./expected-output/example_api-v0.2.0-omit_auto_trait_impls.txt",
-        options,
     );
 }
 
@@ -165,8 +159,8 @@ fn comprehensive_api() {
     // Create independent build dir so all tests can run in parallel
     let build_dir = tempdir().unwrap();
 
-    assert_simplified_public_api(
-        rustdoc_json_path_for_crate("../test-apis/comprehensive_api", &build_dir),
+    assert_public_api(
+        simplified_builder_for_crate("../test-apis/comprehensive_api", &build_dir),
         "./expected-output/comprehensive_api.txt",
     );
 }
@@ -176,8 +170,8 @@ fn comprehensive_api_proc_macro() {
     // Create independent build dir so all tests can run in parallel
     let build_dir = tempdir().unwrap();
 
-    assert_simplified_public_api(
-        rustdoc_json_path_for_crate("../test-apis/comprehensive_api_proc_macro", &build_dir),
+    assert_public_api(
+        simplified_builder_for_crate("../test-apis/comprehensive_api_proc_macro", &build_dir),
         "./expected-output/comprehensive_api_proc_macro.txt",
     );
 }
@@ -190,10 +184,10 @@ fn comprehensive_api_debug_sorting_no_stack_overflow() {
     // Create independent build dir so all tests can run in parallel
     let build_dir = tempdir().unwrap();
 
-    let mut options = Options::default();
-    options.debug_sorting = true;
     let rustdoc_json = rustdoc_json_path_for_crate("../test-apis/comprehensive_api", &build_dir);
-    let _api = PublicApi::from_rustdoc_json(rustdoc_json, options)
+    let _api = public_api::Builder::from_rustdoc_json(rustdoc_json)
+        .debug_sorting(true)
+        .build()
         .unwrap()
         .to_string();
 }
@@ -202,20 +196,10 @@ fn comprehensive_api_debug_sorting_no_stack_overflow() {
 fn invalid_json() {
     let invalid_json = NamedTempFile::new().unwrap();
     write!(invalid_json.as_file(), "}}}}}}}}}}").unwrap();
-    let result = PublicApi::from_rustdoc_json(invalid_json, Options::default());
+    let result = public_api::Builder::from_rustdoc_json(invalid_json.path()).build();
     assert!(matches!(result, Err(Error::SerdeJsonError(_))));
 }
 
-#[test]
-fn options() {
-    let options = Options::default();
-
-    // If we don't do this, we will not have code coverage 100% of functions in
-    // lib.rs, which is more annoying than doing this clone
-    #[allow(clippy::clone_on_copy)]
-    let _ = options.clone();
-}
-
 struct LibWithJson {
     json_path: PathBuf,
 
@@ -252,44 +236,25 @@ fn rustdoc_json_for_lib(lib: &str) -> LibWithJson {
 }
 
 fn assert_public_api_diff(
-    old_json: impl AsRef<Path>,
-    new_json: impl AsRef<Path>,
+    old_json: impl Into<PathBuf>,
+    new_json: impl Into<PathBuf>,
     expected: impl AsRef<Path>,
 ) {
-    let options = Options::default();
-    let old = PublicApi::from_rustdoc_json(old_json, options).unwrap();
-    let new = PublicApi::from_rustdoc_json(new_json, options).unwrap();
+    let old = public_api::Builder::from_rustdoc_json(old_json)
+        .build()
+        .unwrap();
+    let new = public_api::Builder::from_rustdoc_json(new_json)
+        .build()
+        .unwrap();
 
     let diff = public_api::diff::PublicApiDiff::between(old, new);
     expect_file![expected.as_ref()].assert_debug_eq(&diff);
 }
 
-/// Asserts that the public API of the crate in the given rustdoc JSON file
-/// matches the expected output. For brevity, Auto Trait or Blanket impls are
-/// not included.
-fn assert_simplified_public_api(json: impl AsRef<Path>, expected: impl AsRef<Path>) {
-    assert_public_api(json, expected, simplified());
-}
-
 /// Asserts that the public API of the crate in the given rustdoc JSON file
 /// matches the expected output.
-fn assert_public_api(
-    rustdoc_json: impl AsRef<Path>,
-    expected_output: impl AsRef<Path>,
-    options: Options,
-) {
-    let api = PublicApi::from_rustdoc_json(rustdoc_json, options)
-        .unwrap()
-        .to_string();
+fn assert_public_api(builder: public_api::Builder, expected_output: impl AsRef<Path>) {
+    let api = builder.build().unwrap().to_string();
 
     expect_file![expected_output.as_ref()].assert_eq(&api);
 }
-
-/// Returns options for a so called "simplified" API, which is an API without
-/// Auto Trait or Blanket impls, to reduce public item noise.
-fn simplified() -> Options {
-    let mut options = Options::default();
-    options.omit_blanket_impls = true;
-    options.omit_auto_trait_impls = true;
-    options
-}
diff --git a/public-api/tests/public-api.txt b/public-api/tests/public-api.txt
index eae2864..c689966 100644
--- a/public-api/tests/public-api.txt
+++ b/public-api/tests/public-api.txt
@@ -176,6 +176,44 @@ impl<T> core::borrow::BorrowMut<T> for public_api::Error where T: core::marker::
 pub fn public_api::Error::borrow_mut(&mut self) -> &mut T
 impl<T> core::convert::From<T> for public_api::Error
 pub fn public_api::Error::from(t: T) -> T
+pub struct public_api::Builder
+impl public_api::Builder
+pub fn public_api::Builder::build(self) -> public_api::Result<public_api::PublicApi>
+pub fn public_api::Builder::debug_sorting(self, debug_sorting: bool) -> Self
+pub fn public_api::Builder::from_rustdoc_json(path: impl core::convert::Into<std::path::PathBuf>) -> Self
+pub fn public_api::Builder::omit_auto_derived_impls(self, omit_auto_derived_impls: bool) -> Self
+pub fn public_api::Builder::omit_auto_trait_impls(self, omit_auto_trait_impls: bool) -> Self
+pub fn public_api::Builder::omit_blanket_impls(self, omit_blanket_impls: bool) -> Self
+pub fn public_api::Builder::sorted(self, sorted: bool) -> Self
+impl core::clone::Clone for public_api::Builder
+pub fn public_api::Builder::clone(&self) -> public_api::Builder
+impl core::fmt::Debug for public_api::Builder
+pub fn public_api::Builder::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::marker::Send for public_api::Builder
+impl core::marker::Sync for public_api::Builder
+impl core::marker::Unpin for public_api::Builder
+impl core::panic::unwind_safe::RefUnwindSafe for public_api::Builder
+impl core::panic::unwind_safe::UnwindSafe for public_api::Builder
+impl<T, U> core::convert::Into<U> for public_api::Builder where U: core::convert::From<T>
+pub fn public_api::Builder::into(self) -> U
+impl<T, U> core::convert::TryFrom<U> for public_api::Builder where U: core::convert::Into<T>
+pub type public_api::Builder::Error = core::convert::Infallible
+pub fn public_api::Builder::try_from(value: U) -> core::result::Result<T, <T as core::convert::TryFrom<U>>::Error>
+impl<T, U> core::convert::TryInto<U> for public_api::Builder where U: core::convert::TryFrom<T>
+pub type public_api::Builder::Error = <U as core::convert::TryFrom<T>>::Error
+pub fn public_api::Builder::try_into(self) -> core::result::Result<U, <U as core::convert::TryFrom<T>>::Error>
+impl<T> alloc::borrow::ToOwned for public_api::Builder where T: core::clone::Clone
+pub type public_api::Builder::Owned = T
+pub fn public_api::Builder::clone_into(&self, target: &mut T)
+pub fn public_api::Builder::to_owned(&self) -> T
+impl<T> core::any::Any for public_api::Builder where T: 'static + core::marker::Sized
+pub fn public_api::Builder::type_id(&self) -> core::any::TypeId
+impl<T> core::borrow::Borrow<T> for public_api::Builder where T: core::marker::Sized
+pub fn public_api::Builder::borrow(&self) -> &T
+impl<T> core::borrow::BorrowMut<T> for public_api::Builder where T: core::marker::Sized
+pub fn public_api::Builder::borrow_mut(&mut self) -> &mut T
+impl<T> core::convert::From<T> for public_api::Builder
+pub fn public_api::Builder::from(t: T) -> T
 #[non_exhaustive] pub struct public_api::Options
 pub public_api::Options::debug_sorting: bool
 pub public_api::Options::omit_auto_derived_impls: bool
diff --git a/rustdoc-json/tests/rustdoc-json-lib-tests.rs b/rustdoc-json/tests/rustdoc-json-lib-tests.rs
index 9c30412..56db2fd 100644
--- a/rustdoc-json/tests/rustdoc-json-lib-tests.rs
+++ b/rustdoc-json/tests/rustdoc-json-lib-tests.rs
@@ -1,4 +1,3 @@
-use public_api::{Options, PublicApi};
 use rustdoc_json::PackageTarget;
 
 #[test]
@@ -7,7 +6,7 @@ fn public_api() -> Result<(), Box<dyn std::error::Error>> {
         .toolchain("nightly")
         .build()?;
 
-    let public_api = PublicApi::from_rustdoc_json(rustdoc_json, Options::default())?;
+    let public_api = public_api::Builder::from_rustdoc_json(rustdoc_json).build()?;
 
     expect_test::expect_file!["public-api.txt"].assert_eq(&public_api.to_string());
 
diff --git a/rustup-toolchain/tests/rustup-toolchain-lib-tests.rs b/rustup-toolchain/tests/rustup-toolchain-lib-tests.rs
index 8e39781..8245443 100644
--- a/rustup-toolchain/tests/rustup-toolchain-lib-tests.rs
+++ b/rustup-toolchain/tests/rustup-toolchain-lib-tests.rs
@@ -11,9 +11,9 @@ fn public_api() {
         .unwrap();
 
     // Derive the public API from the rustdoc JSON
-    let public_api =
-        public_api::PublicApi::from_rustdoc_json(rustdoc_json, public_api::Options::default())
-            .unwrap();
+    let public_api = public_api::Builder::from_rustdoc_json(rustdoc_json)
+        .build()
+        .unwrap();
 
     // Assert that the public API looks correct
     expect_test::expect_file!["public-api.txt"].assert_eq(&public_api.to_string());