Repository: oxidecomputer/typify (rust)

Issue:
Add documentation for `cargo-typify` command
We should describe it in the repo README and have a README for crates.io.

I'd like to push people to using this when they file issues that result in less-than-ideal generation as well.

cc: @coreyja 

Pull request:
getting ready for the 0.0.11 release
Closes #223 

Patch:
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index d94c0eb..038f808 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -13,24 +13,33 @@
 
 == Unreleased changes (release date TBD)
 
-Breaking change: this release removes the `ToString` implementation for
-`TypeSpace` and the `rustfmt-wrapper` dependency. This allows users to use a
-pretty printing method of choice. Users can use the `ToTokens` implementation of
-`TypeSpace`:
-
-Before:
-[source,rust]
-----
-let typespace = TypeSpace::default();
-typespace.to_string()
-----
-
-After:
-[source,rust]
-----
-let typespace = TypeSpace::default();
-typespace.to_stream().to_string()
-----
+This is a big update with many, many changes to code generation, and many more
+JSON schema structures well-handled. Among the many changes:
+
+* Generate a `ToString` impl for untagged enums with trivial variants (#145)
+* Allow conversion overrides by specifying a schema (#155)
+* Handle untyped enums that contain nulls (#167)
+* Handle `not` schemas for enumerated values (#168)
+* Improve generated code for FromStr and TryFrom impls (#174)
+* Handle format specifiers for enumerated strings (#188)
+
+=== *Breaking*: The removal of `TypeSpace::to_string()`
+
+Previously all transitive consumers required the presence of `rustfmt`. In this
+version we leave formatting to the consumer. See link:README.md#formatting[the formatting section of the README] for details on formatting.
+
+=== CLI
+
+This version adds the `cargo-typify` crate for stand-alone code generation.
+
+=== Augmented Generation
+
+Consumers can now affect how code is generated in several ways:
+* adding derive macros to all generated types
+* modifying specific types by name to rename them or add derive macros
+* specifying a replacement type by name
+* specifying a replacement type by schema pattern
+
 
 https://github.com/oxidecomputer/typify/compare/v0.0.10\...HEAD[Full list of commits]
 
diff --git a/Cargo.lock b/Cargo.lock
index d5884f4..b4f7644 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -584,9 +584,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.24"
+version = "1.0.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50686e0021c4136d1d453b2dfe059902278681512a34d4248435dc34b6b5c8ec"
+checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
 dependencies = [
  "proc-macro2",
 ]
diff --git a/README.md b/README.md
index 4df6bb8..db1fb9f 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,21 @@
 # Typify
 
 Typify compiles JSON Schema documents into Rust types. It can be used in one of
-three ways:
+several ways:
 
 - via the macro `import_types!("types.json")` to generate Rust types directly in
   your program
 
-- via a builder interface to generate Rust types in `build.rs`
+- via a builder interface to generate Rust types in `build.rs` or `xtask`
 
-- or via the builder functions to generate persistent files e.g. when building
-  API bindings.
+- via the builder functions to generate persistent files e.g. when building
+  API bindings
+
+- using the [`cargo typify`](./cargo-typify/README.md) command
+
+**If generation fails or is lousy**: Please file an issue and include the JSON
+Schema and Rust output (if there is any). Use `cargo typify` command to
+generate code from the command-line.
 
 ## JSON Schema → Rust types
 
@@ -69,24 +75,34 @@ code before writing it to a file.
 The examples below show different ways to convert a `TypeSpace` to a string
 (`typespace` is a `typify::TypeSpace`).
 
-### No formatting
 
-```rust
-typespace.to_stream().to_string()
-```
+### `rustfmt`
 
-### Rustfmt
+Best for generation of code that might be checked in alongside hand-written
+code such as in the case of an `xtask` or stand-alone code generator (list
+`cargo-typify`).
 
 ```rust
 rustfmt_wrapper::rustfmt(typespace.to_stream().to_string())?
 ```
 
-### Prettyplease
+### `prettyplease`
+
+Best for `build.rs` scripts where transitive dependencies might not have
+`rustfmt` installed so should be self-contained.
 
 ```rust
 prettyplease::unparse(&syn::parse2::<syn::File>(typespace.to_stream())?)
 ```
 
+### No formatting
+
+If no human will ever see the code (and this is almost never the case).
+
+```rust
+typespace.to_stream().to_string()
+```
+
 ## WIP
 
 Typify is a work in progress. Changes that affect output will be indicated with
diff --git a/cargo-typify/Cargo.toml b/cargo-typify/Cargo.toml
index 0d8fe89..81fb3e6 100644
--- a/cargo-typify/Cargo.toml
+++ b/cargo-typify/Cargo.toml
@@ -2,6 +2,12 @@
 name = "cargo-typify"
 version = "0.0.11-dev"
 edition = "2021"
+license = "Apache-2.0"
+description = "cargo command to generate Rust code from a JSON Schema"
+repository = "https://github.com/oxidecomputer/typify"
+readme = "README.md"
+keywords = ["json", "schema", "cargo"]
+categories = ["api-bindings", "compilers"]
 
 default-run = "cargo-typify"
 
diff --git a/cargo-typify/README.md b/cargo-typify/README.md
new file mode 100644
index 0000000..313e676
--- /dev/null
+++ b/cargo-typify/README.md
@@ -0,0 +1,207 @@
+# cargo-typify
+
+Once installed, the following command converts a JSON Schema file into Rust
+code:
+
+```console
+$ cargo typify my_types.json
+```
+
+This is a wrapper around the [`typify`](https://crates.io/crates/typify) crate
+for use at the command-line.
+
+## Installation
+
+Install with `cargo install cargo-typify`. This command requires that `rustfmt`
+is installed. Install rustfmt with rustup component add rustfmt
+
+## Example
+
+**`$ cat id-or-name.json`**
+
+```json
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "definitions": {
+    "IdOrName": {
+      "oneOf": [
+        {
+          "title": "Id",
+          "allOf": [
+            {
+              "type": "string",
+              "format": "uuid"
+            }
+          ]
+        },
+        {
+          "title": "Name",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Name"
+            }
+          ]
+        }
+      ]
+    },
+    "Name": {
+      "title": "A name unique within the parent collection",
+      "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.",
+      "type": "string",
+      "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$",
+      "maxLength": 63
+    }
+  }
+}
+```
+
+**`$ cargo typify id-or-name.json && cat id-or-name.rs`**
+
+```rust
+#![allow(clippy::redundant_closure_call)]
+#![allow(clippy::needless_lifetimes)]
+#![allow(clippy::match_single_binding)]
+#![allow(clippy::clone_on_copy)]
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(untagged)]
+pub enum IdOrName {
+    Id(uuid::Uuid),
+    Name(Name),
+}
+impl From<&IdOrName> for IdOrName {
+    fn from(value: &IdOrName) -> Self {
+        value.clone()
+    }
+}
+impl std::str::FromStr for IdOrName {
+    type Err = &'static str;
+    fn from_str(value: &str) -> Result<Self, &'static str> {
+        if let Ok(v) = value.parse() {
+            Ok(Self::Id(v))
+        } else if let Ok(v) = value.parse() {
+            Ok(Self::Name(v))
+        } else {
+            Err("string conversion failed for all variants")
+        }
+    }
+}
+impl std::convert::TryFrom<&str> for IdOrName {
+    type Error = &'static str;
+    fn try_from(value: &str) -> Result<Self, &'static str> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<&String> for IdOrName {
+    type Error = &'static str;
+    fn try_from(value: &String) -> Result<Self, &'static str> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<String> for IdOrName {
+    type Error = &'static str;
+    fn try_from(value: String) -> Result<Self, &'static str> {
+        value.parse()
+    }
+}
+impl ToString for IdOrName {
+    fn to_string(&self) -> String {
+        match self {
+            Self::Id(x) => x.to_string(),
+            Self::Name(x) => x.to_string(),
+        }
+    }
+}
+impl From<uuid::Uuid> for IdOrName {
+    fn from(value: uuid::Uuid) -> Self {
+        Self::Id(value)
+    }
+}
+impl From<Name> for IdOrName {
+    fn from(value: Name) -> Self {
+        Self::Name(value)
+    }
+}
+#[doc = "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID."]
+#[derive(Clone, Debug, Serialize)]
+pub struct Name(String);
+impl std::ops::Deref for Name {
+    type Target = String;
+    fn deref(&self) -> &String {
+        &self.0
+    }
+}
+impl From<Name> for String {
+    fn from(value: Name) -> Self {
+        value.0
+    }
+}
+impl From<&Name> for Name {
+    fn from(value: &Name) -> Self {
+        value.clone()
+    }
+}
+impl std::str::FromStr for Name {
+    type Err = &'static str;
+    fn from_str(value: &str) -> Result<Self, &'static str> {
+        if value.len() > 63usize {
+            return Err("longer than 63 characters");
+        }
+        if regress::Regex::new("^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$")
+            .unwrap()
+            .find(value)
+            .is_none()
+        {
+            return Err("doesn't match pattern \"^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$\"");
+        }
+        Ok(Self(value.to_string()))
+    }
+}
+impl std::convert::TryFrom<&str> for Name {
+    type Error = &'static str;
+    fn try_from(value: &str) -> Result<Self, &'static str> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<&String> for Name {
+    type Error = &'static str;
+    fn try_from(value: &String) -> Result<Self, &'static str> {
+        value.parse()
+    }
+}
+impl std::convert::TryFrom<String> for Name {
+    type Error = &'static str;
+    fn try_from(value: String) -> Result<Self, &'static str> {
+        value.parse()
+    }
+}
+impl<'de> serde::Deserialize<'de> for Name {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        String::deserialize(deserializer)?
+            .parse()
+            .map_err(|e: &'static str| <D::Error as serde::de::Error>::custom(e.to_string()))
+    }
+}
+```
+
+## Options
+
+See *`cargo typify --help`* for a complete list of options.
+
+The `--output` option lets you override the default output file (replacing the
+input file extension with `.rs`). Use `-` for stdout.
+
+Use `--no-builder` to disable struct builder generation (`--builder` is the
+default). Builder output lets you write code like this:
+
+```rust
+let xy: MyStruct = MyStruct::builder().x_coord(x).y_coord(y).try_into();
+```
+
+The `--additional-derive` adds the specified derive macro to all generated
+types. This may be specified more than once.
\ No newline at end of file
diff --git a/cargo-typify/src/lib.rs b/cargo-typify/src/lib.rs
index fcfbd86..1007e94 100644
--- a/cargo-typify/src/lib.rs
+++ b/cargo-typify/src/lib.rs
@@ -10,7 +10,7 @@ use typify::{TypeSpace, TypeSpaceSettings};
 #[command(author, version, about)]
 #[command(group(
     ArgGroup::new("build")
-        .args(["builder", "positional"]),
+        .args(["builder", "no_builder"]),
 ))]
 pub struct CliArgs {
     /// The input file to read from
@@ -21,11 +21,11 @@ pub struct CliArgs {
     pub builder: bool,
 
     /// Inverse of `--builder`. When set the builder-style interface will not be included.
-    #[arg(short, long, default_value = "false", group = "build")]
-    pub positional: bool,
+    #[arg(short = 'B', long, default_value = "false", group = "build")]
+    pub no_builder: bool,
 
     /// Add an additional derive macro to apply to all defined types.
-    #[arg(short, long)]
+    #[arg(short, long = "additional-derive", value_name = "derive")]
     pub additional_derives: Vec<String>,
 
     /// The output file to write to. If not specified, the input file name will be used with a
@@ -55,7 +55,7 @@ impl CliArgs {
     }
 
     pub fn use_builder(&self) -> bool {
-        !self.positional
+        !self.no_builder
     }
 }
 
@@ -117,7 +117,7 @@ mod tests {
             builder: false,
             additional_derives: vec![],
             output: Some(PathBuf::from("-")),
-            positional: false,
+            no_builder: false,
         };
 
         assert_eq!(args.output_path(), None);
@@ -130,7 +130,7 @@ mod tests {
             builder: false,
             additional_derives: vec![],
             output: Some(PathBuf::from("some_file.rs")),
-            positional: false,
+            no_builder: false,
         };
 
         assert_eq!(args.output_path(), Some(PathBuf::from("some_file.rs")));
@@ -143,7 +143,7 @@ mod tests {
             builder: false,
             additional_derives: vec![],
             output: None,
-            positional: false,
+            no_builder: false,
         };
 
         assert_eq!(args.output_path(), Some(PathBuf::from("input.rs")));
@@ -156,20 +156,20 @@ mod tests {
             builder: false,
             additional_derives: vec![],
             output: None,
-            positional: false,
+            no_builder: false,
         };
 
         assert!(args.use_builder());
     }
 
     #[test]
-    fn test_positional_builder() {
+    fn test_no_builder() {
         let args = CliArgs {
             input: PathBuf::from("input.json"),
             builder: false,
             additional_derives: vec![],
             output: None,
-            positional: true,
+            no_builder: true,
         };
 
         assert!(!args.use_builder());
@@ -182,7 +182,7 @@ mod tests {
             builder: true,
             additional_derives: vec![],
             output: None,
-            positional: false,
+            no_builder: false,
         };
 
         assert!(args.use_builder());
diff --git a/cargo-typify/tests/outputs/positional.rs b/cargo-typify/tests/outputs/no-builder.rs
similarity index 100%
rename from cargo-typify/tests/outputs/positional.rs
rename to cargo-typify/tests/outputs/no-builder.rs
diff --git a/example-build/Cargo.toml b/example-build/Cargo.toml
index 72985f4..e884283 100644
--- a/example-build/Cargo.toml
+++ b/example-build/Cargo.toml
@@ -1,7 +1,6 @@
 [package]
 name = "example-build"
 version = "0.0.0"
-authors = ["Adam H. Leventhal <ahl@oxidecomputer.com>"]
 edition = "2021"
 
 [dependencies]
diff --git a/example-macro/Cargo.toml b/example-macro/Cargo.toml
index c96730d..8d90ad1 100644
--- a/example-macro/Cargo.toml
+++ b/example-macro/Cargo.toml
@@ -1,7 +1,6 @@
 [package]
 name = "example-macro"
 version = "0.0.0"
-authors = ["Adam H. Leventhal <ahl@oxidecomputer.com>"]
 edition = "2021"
 
 [dependencies]
diff --git a/typify-impl/Cargo.toml b/typify-impl/Cargo.toml
index 3be0134..d7477bf 100644
--- a/typify-impl/Cargo.toml
+++ b/typify-impl/Cargo.toml
@@ -1,7 +1,6 @@
 [package]
 name = "typify-impl"
 version = "0.0.11-dev"
-authors = ["Adam H. Leventhal <ahl@oxidecomputer.com"]
 edition = "2021"
 license = "Apache-2.0"
 description = "typify backend implementation"
diff --git a/typify-macro/Cargo.toml b/typify-macro/Cargo.toml
index 572b27e..18bd39a 100644
--- a/typify-macro/Cargo.toml
+++ b/typify-macro/Cargo.toml
@@ -1,7 +1,6 @@
 [package]
 name = "typify-macro"
 version = "0.0.11-dev"
-authors = ["Adam H. Leventhal <ahl@oxidecomputer.com"]
 edition = "2021"
 license = "Apache-2.0"
 description = "typify macro implementation"
diff --git a/typify/Cargo.toml b/typify/Cargo.toml
index 96ca2c8..0e21b3e 100644
--- a/typify/Cargo.toml
+++ b/typify/Cargo.toml
@@ -1,7 +1,6 @@
 [package]
 name = "typify"
 version = "0.0.11-dev"
-authors = ["Adam H. Leventhal <ahl@oxidecomputer.com"]
 edition = "2021"
 license = "Apache-2.0"
 description = "JSON schema to rust type code generator"


Test patch:
diff --git a/cargo-typify/tests/integration.rs b/cargo-typify/tests/integration.rs
index 7b31319..1f0332e 100644
--- a/cargo-typify/tests/integration.rs
+++ b/cargo-typify/tests/integration.rs
@@ -44,7 +44,7 @@ fn test_default_output() {
 }
 
 #[test]
-fn test_positional_stdout() {
+fn test_no_builder_stdout() {
     use assert_cmd::Command;
 
     let input = concat!(env!("CARGO_MANIFEST_DIR"), "/../example.json");
@@ -52,7 +52,7 @@ fn test_positional_stdout() {
     let mut cmd = Command::cargo_bin("cargo-typify").unwrap();
 
     let output = cmd
-        .args(["typify", input, "--positional", "--output", "-"])
+        .args(["typify", input, "--no-builder", "--output", "-"])
         .output()
         .unwrap();
 
@@ -60,7 +60,7 @@ fn test_positional_stdout() {
     let actual = dos2unix(&output_stdout);
 
     assert!(output.status.success());
-    assert_contents("tests/outputs/positional.rs", &actual);
+    assert_contents("tests/outputs/no-builder.rs", &actual);
 }
 
 #[test]
@@ -101,8 +101,8 @@ fn test_derive() {
     cmd.args([
         "typify",
         input,
-        "--positional",
-        "--additional-derives",
+        "--no-builder",
+        "--additional-derive",
         "ExtraDerive",
         "--output",
         output_file.to_str().unwrap(),
@@ -128,10 +128,10 @@ fn test_multi_derive() {
     cmd.args([
         "typify",
         input,
-        "--positional",
-        "--additional-derives",
+        "--no-builder",
+        "--additional-derive",
         "ExtraDerive",
-        "--additional-derives",
+        "--additional-derive",
         "AnotherDerive",
         "--output",
         output_file.to_str().unwrap(),
diff --git a/cargo-typify/tests/outputs/help.txt b/cargo-typify/tests/outputs/help.txt
index 0a70146..d02307a 100644
--- a/cargo-typify/tests/outputs/help.txt
+++ b/cargo-typify/tests/outputs/help.txt
@@ -1,4 +1,4 @@
-A CLI for the `typify` crate that converts JSON Schema files to Rust code
+cargo command to generate Rust code from a JSON Schema
 
 Usage: cargo typify [OPTIONS] <INPUT>
 
@@ -10,10 +10,10 @@ Options:
   -b, --builder
           Whether to include a builder-style interface, this is the default
 
-  -p, --positional
+  -B, --no-builder
           Inverse of `--builder`. When set the builder-style interface will not be included
 
-  -a, --additional-derives <ADDITIONAL_DERIVES>
+  -a, --additional-derive <derive>
           Add an additional derive macro to apply to all defined types
 
   -o, --output <OUTPUT>
diff --git a/typify-test/Cargo.toml b/typify-test/Cargo.toml
index 399e9f7..a0dc13a 100644
--- a/typify-test/Cargo.toml
+++ b/typify-test/Cargo.toml
@@ -1,7 +1,6 @@
 [package]
 name = "typify-test"
 version = "0.0.0"
-authors = ["Adam H. Leventhal <ahl@oxidecomputer.com>"]
 edition = "2021"
 
 [dependencies]