diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fc668aba..51677a65f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,8 +35,8 @@ jobs: strategy: matrix: os: [ubuntu-latest] - # 1.67 is the MSRV - rust-version: ["1.67", stable] + # 1.79 is the MSRV + rust-version: ["1.79", stable] fail-fast: false env: RUSTFLAGS: -D warnings @@ -52,7 +52,7 @@ jobs: - name: Build run: just powerset build --all-targets - name: Test - run: just powerset nextest run --all-targets --no-tests=pass + run: just powerset nextest run --all-targets --no-tests=pass -E 'not (test(ui) or test(snapshot))' - name: Run extended tests (only on stable) if: matrix.rust-version == 'stable' run: cargo nextest run --all-targets --all-features diff --git a/Cargo.lock b/Cargo.lock index def071db8..2e5a5c03b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,71 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "atomicwrites" version = "0.4.4" @@ -25,6 +84,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.9.1" @@ -43,12 +117,64 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "camino" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "console" version = "0.15.10" @@ -62,12 +188,68 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "custom-crate-tests" +version = "0.1.0" +dependencies = [ + "datatest-stable", + "integration-tests", + "newtype-uuid", + "newtype-uuid-macros", + "serde_json", + "trybuild", +] + +[[package]] +name = "datatest-stable" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ebbb3c403031a3739980c2864e3b5ee4efca009dd83d2c0f80a31555243981" +dependencies = [ + "camino", + "fancy-regex", + "libtest-mimic", + "walkdir", +] + [[package]] name = "dyn-clone" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "e2e-kinds" +version = "0.1.0" +dependencies = [ + "newtype-uuid", + "newtype-uuid-macros", + "serde", +] + +[[package]] +name = "e2e-schema-consumer" +version = "0.1.0" +dependencies = [ + "e2e-kinds", + "e2e-schema-producer", + "expectorate", + "newtype-uuid", + "serde", + "typify", +] + +[[package]] +name = "e2e-schema-producer" +version = "0.1.0" +dependencies = [ + "e2e-kinds", + "expectorate", + "schemars", + "serde", + "serde_json", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -90,6 +272,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "escape8259" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" + [[package]] name = "expectorate" version = "1.2.0" @@ -102,6 +290,17 @@ dependencies = [ "similar", ] +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -126,6 +325,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "hashbrown" version = "0.15.2" @@ -143,23 +348,46 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "integration-tests" version = "0.1.0" dependencies = [ + "datatest-stable", "expectorate", + "heck", "newtype-uuid", + "newtype-uuid-macros", "prettyplease", + "proc-macro2", "proptest", + "quote", "schemars", "serde", "serde_json", + "serde_tokenstream", "syn", "test-strategy", + "trybuild", "typify", "uuid", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.14" @@ -188,6 +416,18 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libtest-mimic" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -225,12 +465,28 @@ dependencies = [ name = "newtype-uuid" version = "1.2.4" dependencies = [ + "newtype-uuid-macros", "proptest", "schemars", "serde", + "serde_json", "uuid", ] +[[package]] +name = "newtype-uuid-macros" +version = "0.1.0" +dependencies = [ + "heck", + "newtype-uuid", + "proc-macro2", + "quote", + "serde", + "serde_tokenstream", + "static_assertions", + "syn", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -246,6 +502,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -343,6 +605,17 @@ dependencies = [ "rand_core", ] +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.8.5" @@ -397,6 +670,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schemars" version = "0.8.22" @@ -474,6 +756,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_tokenstream" version = "0.2.2" @@ -492,6 +783,18 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "structmeta" version = "0.3.0" @@ -526,6 +829,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "target-triple" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" + [[package]] name = "tempfile" version = "3.20.0" @@ -539,6 +848,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "test-strategy" version = "0.4.1" @@ -571,6 +889,62 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "trybuild" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "typify" version = "0.4.2" @@ -642,6 +1016,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.17.0" @@ -654,6 +1034,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -721,6 +1111,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -803,6 +1202,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index 50db7ddf6..b6e7f4c21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,27 +1,37 @@ [workspace] resolver = "2" -members = [ - "crates/*", -] +members = ["crates/*", "e2e-example/*"] [workspace.package] edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/oxidecomputer/newtype-uuid" -documentation = "https://docs.rs/newtype-uuid" -rust-version = "1.67" +rust-version = "1.79" [workspace.dependencies] +datatest-stable = "0.3.2" expectorate = "1.2.0" -newtype-uuid = { path = "crates/newtype-uuid" } +heck = "0.5" +integration-tests = { path = "crates/integration-tests" } +my-custom-uuid = { path = "crates/newtype-uuid", package = "newtype-uuid" } +newtype-uuid = { version = "1.2.2", path = "crates/newtype-uuid" } +newtype-uuid-macros = { version = "0.1.0", path = "crates/newtype-uuid-macros" } prettyplease = "0.2.35" +proc-macro2 = "1.0" +quote = "1.0" # Ideally we'd let you use no-std proptest, but proptest requires either the std # or the no_std option to be set. It won't compile without one of those two set. proptest = { version = "1.7.0", features = ["std"], default-features = false } schemars = "0.8.17" serde = "1" serde_json = "1.0.140" +serde_tokenstream = "0.2.2" +static_assertions = "1.1.0" syn = "2.0.104" test-strategy = "0.4.1" +trybuild = "1.0" typify = "0.4.2" uuid = { version = "1.17.0", default-features = false } + +[workspace.lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(doc_cfg)'] } diff --git a/Justfile b/Justfile index d3060615a..b4a80a07f 100644 --- a/Justfile +++ b/Justfile @@ -22,7 +22,9 @@ powerset-no-std *args: # Don't need to pass in the "internal-*" features here, since we don't # enable integration-tests which defines these features. excluded_features="{{excluded_features_no_std}}" - NEXTEST_NO_TESTS=pass cargo hack --feature-powerset --workspace --exclude integration-tests --exclude-features "${excluded_features// /,}" "$@" + NEXTEST_NO_TESTS=pass cargo hack --feature-powerset --workspace --exclude integration-tests \ + --exclude custom-crate-tests --exclude e2e-kinds --exclude e2e-schema-consumer --exclude e2e-schema-producer \ + --exclude-features "${excluded_features// /,}" "$@" # Build docs for crates and direct dependencies rustdoc *args: diff --git a/crates/custom-crate-tests/Cargo.toml b/crates/custom-crate-tests/Cargo.toml new file mode 100644 index 000000000..0312a5851 --- /dev/null +++ b/crates/custom-crate-tests/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "custom-crate-tests" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +integration-tests.workspace = true +my-custom-uuid.workspace = true +newtype-uuid-macros.workspace = true +serde_json = { workspace = true, optional = true } + +[dev-dependencies] +datatest-stable.workspace = true +trybuild.workspace = true + +[[test]] +# Snapshot tests use datatest-stable. +name = "snapshot" +harness = false + +[features] +internal-schemars08-tests = [ + "dep:serde_json", + "integration-tests/internal-schemars08-tests", +] diff --git a/crates/custom-crate-tests/README.md b/crates/custom-crate-tests/README.md new file mode 100644 index 000000000..b8b32bbb6 --- /dev/null +++ b/crates/custom-crate-tests/README.md @@ -0,0 +1,5 @@ +# Custom crate tests + +This crate contains integration tests for the `newtype_uuid_crate` parameter of the `impl_typed_uuid_kinds!` macro. + +When `newtype_uuid_crate = my_custom_uuid` is specified, the generated code should use `::my_custom_uuid::TypedUuidKind`, `::my_custom_uuid::TypedUuidTag`, etc. instead of the default `::newtype_uuid::` prefixes. diff --git a/crates/custom-crate-tests/src/lib.rs b/crates/custom-crate-tests/src/lib.rs new file mode 100644 index 000000000..e71fcba7a --- /dev/null +++ b/crates/custom-crate-tests/src/lib.rs @@ -0,0 +1,4 @@ +//! Tests for custom crate name functionality. + +#[cfg(test)] +mod ui; diff --git a/crates/custom-crate-tests/src/ui.rs b/crates/custom-crate-tests/src/ui.rs new file mode 100644 index 000000000..89ba63d10 --- /dev/null +++ b/crates/custom-crate-tests/src/ui.rs @@ -0,0 +1,6 @@ +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.pass("tests/fixtures/valid/*.rs"); + t.compile_fail("tests/fixtures/invalid/*.rs"); +} diff --git a/crates/custom-crate-tests/tests/fixtures/invalid/crate_name_as_string.rs b/crates/custom-crate-tests/tests/fixtures/invalid/crate_name_as_string.rs new file mode 100644 index 000000000..a83c3222c --- /dev/null +++ b/crates/custom-crate-tests/tests/fixtures/invalid/crate_name_as_string.rs @@ -0,0 +1,12 @@ +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + settings = { + newtype_uuid_crate = "my_custom_uuid", + }, + kinds = { + User = {}, + } +} + +fn main() {} diff --git a/crates/custom-crate-tests/tests/fixtures/invalid/crate_name_as_string.stderr b/crates/custom-crate-tests/tests/fixtures/invalid/crate_name_as_string.stderr new file mode 100644 index 000000000..8232993db --- /dev/null +++ b/crates/custom-crate-tests/tests/fixtures/invalid/crate_name_as_string.stderr @@ -0,0 +1,5 @@ +error: expected identifier + --> tests/fixtures/invalid/crate_name_as_string.rs:5:30 + | +5 | newtype_uuid_crate = "my_custom_uuid", + | ^^^^^^^^^^^^^^^^ diff --git a/crates/custom-crate-tests/tests/fixtures/invalid/missing_crate_value.rs b/crates/custom-crate-tests/tests/fixtures/invalid/missing_crate_value.rs new file mode 100644 index 000000000..756f6c097 --- /dev/null +++ b/crates/custom-crate-tests/tests/fixtures/invalid/missing_crate_value.rs @@ -0,0 +1,12 @@ +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + settings = { + newtype_uuid_crate = , + }, + kinds = { + User = {}, + } +} + +fn main() {} diff --git a/crates/custom-crate-tests/tests/fixtures/invalid/missing_crate_value.stderr b/crates/custom-crate-tests/tests/fixtures/invalid/missing_crate_value.stderr new file mode 100644 index 000000000..a6c40c074 --- /dev/null +++ b/crates/custom-crate-tests/tests/fixtures/invalid/missing_crate_value.stderr @@ -0,0 +1,5 @@ +error: expected anything but a ',', '=', or EOF, but found `,` + --> tests/fixtures/invalid/missing_crate_value.rs:5:30 + | +5 | newtype_uuid_crate = , + | ^ diff --git a/crates/custom-crate-tests/tests/fixtures/invalid/nonexistent_crate.rs b/crates/custom-crate-tests/tests/fixtures/invalid/nonexistent_crate.rs new file mode 100644 index 000000000..267ef5e71 --- /dev/null +++ b/crates/custom-crate-tests/tests/fixtures/invalid/nonexistent_crate.rs @@ -0,0 +1,12 @@ +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + settings = { + newtype_uuid_crate = nonexistent_crate, + }, + kinds = { + User = {}, + } +} + +fn main() {} diff --git a/crates/custom-crate-tests/tests/fixtures/invalid/nonexistent_crate.stderr b/crates/custom-crate-tests/tests/fixtures/invalid/nonexistent_crate.stderr new file mode 100644 index 000000000..5cfccbeef --- /dev/null +++ b/crates/custom-crate-tests/tests/fixtures/invalid/nonexistent_crate.stderr @@ -0,0 +1,16 @@ +error[E0433]: failed to resolve: could not find `nonexistent_crate` in the list of imported crates + --> tests/fixtures/invalid/nonexistent_crate.rs:5:30 + | +5 | newtype_uuid_crate = nonexistent_crate, + | ^^^^^^^^^^^^^^^^^ could not find `nonexistent_crate` in the list of imported crates + +error[E0433]: failed to resolve: could not find `nonexistent_crate` in the list of imported crates + --> tests/fixtures/invalid/nonexistent_crate.rs:5:30 + | +5 | newtype_uuid_crate = nonexistent_crate, + | ^^^^^^^^^^^^^^^^^ could not find `nonexistent_crate` in the list of imported crates + | +help: consider importing this struct + | +1 + use my_custom_uuid::TypedUuidTag; + | diff --git a/crates/custom-crate-tests/tests/fixtures/invalid/output/crate_name_as_string.output.rs b/crates/custom-crate-tests/tests/fixtures/invalid/output/crate_name_as_string.output.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/custom-crate-tests/tests/fixtures/invalid/output/missing_crate_value.output.rs b/crates/custom-crate-tests/tests/fixtures/invalid/output/missing_crate_value.output.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/custom-crate-tests/tests/fixtures/invalid/output/nonexistent_crate.output.rs b/crates/custom-crate-tests/tests/fixtures/invalid/output/nonexistent_crate.output.rs new file mode 100644 index 000000000..9abfd33cf --- /dev/null +++ b/crates/custom-crate-tests/tests/fixtures/invalid/output/nonexistent_crate.output.rs @@ -0,0 +1,16 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UserKind {} +impl ::nonexistent_crate::TypedUuidKind for UserKind { + #[inline] + fn tag() -> ::nonexistent_crate::TypedUuidTag { + const TAG: ::nonexistent_crate::TypedUuidTag = ::nonexistent_crate::TypedUuidTag::new( + "user", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(UserUuid)) + } +} +#[allow(unused)] +pub type UserUuid = ::nonexistent_crate::TypedUuid; diff --git a/crates/custom-crate-tests/tests/fixtures/valid/custom_crate_name.rs b/crates/custom-crate-tests/tests/fixtures/valid/custom_crate_name.rs new file mode 100644 index 000000000..ac73452c3 --- /dev/null +++ b/crates/custom-crate-tests/tests/fixtures/valid/custom_crate_name.rs @@ -0,0 +1,30 @@ +use my_custom_uuid::TypedUuidKind; +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + settings = { + newtype_uuid_crate = my_custom_uuid, + }, + kinds = { + User = {}, + Organization = {}, + Product = {}, + } +} + +fn main() { + // Test that the generated types exist and work with custom crate alias + let _user_kind_tag = UserKind::tag(); + let _org_kind_tag = OrganizationKind::tag(); + let _product_kind_tag = ProductKind::tag(); + + // Test type aliases exist + let _user_uuid: UserUuid; + let _org_uuid: OrganizationUuid; + let _product_uuid: ProductUuid; + + // Test that tags are properly snake_cased + assert_eq!(UserKind::tag().as_str(), "user"); + assert_eq!(OrganizationKind::tag().as_str(), "organization"); + assert_eq!(ProductKind::tag().as_str(), "product"); +} diff --git a/crates/custom-crate-tests/tests/fixtures/valid/custom_crate_with_schemars.rs b/crates/custom-crate-tests/tests/fixtures/valid/custom_crate_with_schemars.rs new file mode 100644 index 000000000..87da153f0 --- /dev/null +++ b/crates/custom-crate-tests/tests/fixtures/valid/custom_crate_with_schemars.rs @@ -0,0 +1,38 @@ +use my_custom_uuid::TypedUuidKind; +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + settings = { + newtype_uuid_crate = my_custom_uuid, + schemars08 = { + attrs = [#[cfg(feature = "internal-schemars08-tests")]], + rust_type = { + crate = "my-api-service", + version = "2.1.0", + path = "my_api_service::models", + }, + }, + }, + kinds = { + Account = {}, + Transaction = {}, + ApiKey = {}, + } +} + +fn main() { + // Test that the generated types exist and work with custom crate alias and schemars + let _account_kind_tag = AccountKind::tag(); + let _transaction_kind_tag = TransactionKind::tag(); + let _api_key_kind_tag = ApiKeyKind::tag(); + + // Test type aliases exist + let _account_uuid: AccountUuid; + let _transaction_uuid: TransactionUuid; + let _api_key_uuid: ApiKeyUuid; + + // Test that tags are properly snake_cased + assert_eq!(AccountKind::tag().as_str(), "account"); + assert_eq!(TransactionKind::tag().as_str(), "transaction"); + assert_eq!(ApiKeyKind::tag().as_str(), "api_key"); +} diff --git a/crates/custom-crate-tests/tests/fixtures/valid/output/custom_crate_name.output.rs b/crates/custom-crate-tests/tests/fixtures/valid/output/custom_crate_name.output.rs new file mode 100644 index 000000000..5332c12d9 --- /dev/null +++ b/crates/custom-crate-tests/tests/fixtures/valid/output/custom_crate_name.output.rs @@ -0,0 +1,48 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UserKind {} +impl ::my_custom_uuid::TypedUuidKind for UserKind { + #[inline] + fn tag() -> ::my_custom_uuid::TypedUuidTag { + const TAG: ::my_custom_uuid::TypedUuidTag = ::my_custom_uuid::TypedUuidTag::new( + "user", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(UserUuid)) + } +} +#[allow(unused)] +pub type UserUuid = ::my_custom_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OrganizationKind {} +impl ::my_custom_uuid::TypedUuidKind for OrganizationKind { + #[inline] + fn tag() -> ::my_custom_uuid::TypedUuidTag { + const TAG: ::my_custom_uuid::TypedUuidTag = ::my_custom_uuid::TypedUuidTag::new( + "organization", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(OrganizationUuid)) + } +} +#[allow(unused)] +pub type OrganizationUuid = ::my_custom_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ProductKind {} +impl ::my_custom_uuid::TypedUuidKind for ProductKind { + #[inline] + fn tag() -> ::my_custom_uuid::TypedUuidTag { + const TAG: ::my_custom_uuid::TypedUuidTag = ::my_custom_uuid::TypedUuidTag::new( + "product", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(ProductUuid)) + } +} +#[allow(unused)] +pub type ProductUuid = ::my_custom_uuid::TypedUuid; diff --git a/crates/custom-crate-tests/tests/fixtures/valid/output/custom_crate_with_schemars.output.rs b/crates/custom-crate-tests/tests/fixtures/valid/output/custom_crate_with_schemars.output.rs new file mode 100644 index 000000000..bbec3fefe --- /dev/null +++ b/crates/custom-crate-tests/tests/fixtures/valid/output/custom_crate_with_schemars.output.rs @@ -0,0 +1,141 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AccountKind {} +impl ::my_custom_uuid::TypedUuidKind for AccountKind { + #[inline] + fn tag() -> ::my_custom_uuid::TypedUuidTag { + const TAG: ::my_custom_uuid::TypedUuidTag = ::my_custom_uuid::TypedUuidTag::new( + "account", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(AccountUuid)) + } +} +#[cfg(feature = "internal-schemars08-tests")] +impl ::my_custom_uuid::macro_support::schemars08::JsonSchema for AccountKind { + fn schema_name() -> ::std::string::String { + "AccountKind".to_string() + } + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed("my_api_service::models::AccountKind") + } + fn json_schema( + _gen: &mut ::my_custom_uuid::macro_support::schemars08::gen::SchemaGenerator, + ) -> ::my_custom_uuid::macro_support::schemars08::schema::Schema { + use ::my_custom_uuid::macro_support::schemars08::schema::*; + let mut schema = SchemaObject { + subschemas: ::std::option::Option::Some( + Box::new(SubschemaValidation { + not: ::std::option::Option::Some(Box::new(Schema::Bool(true))), + ..::std::default::Default::default() + }), + ), + ..::std::default::Default::default() + }; + let mut extensions = ::my_custom_uuid::macro_support::schemars08::Map::new(); + let rust_type = ::my_custom_uuid::macro_support::serde_json::json!( + { "crate" : "my-api-service", "version" : "2.1.0", "path" : + "my_api_service::models::AccountKind", } + ); + extensions.insert("x-rust-type".to_string(), rust_type); + schema.extensions = extensions; + Schema::Object(schema) + } +} +#[allow(unused)] +pub type AccountUuid = ::my_custom_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TransactionKind {} +impl ::my_custom_uuid::TypedUuidKind for TransactionKind { + #[inline] + fn tag() -> ::my_custom_uuid::TypedUuidTag { + const TAG: ::my_custom_uuid::TypedUuidTag = ::my_custom_uuid::TypedUuidTag::new( + "transaction", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(TransactionUuid)) + } +} +#[cfg(feature = "internal-schemars08-tests")] +impl ::my_custom_uuid::macro_support::schemars08::JsonSchema for TransactionKind { + fn schema_name() -> ::std::string::String { + "TransactionKind".to_string() + } + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed("my_api_service::models::TransactionKind") + } + fn json_schema( + _gen: &mut ::my_custom_uuid::macro_support::schemars08::gen::SchemaGenerator, + ) -> ::my_custom_uuid::macro_support::schemars08::schema::Schema { + use ::my_custom_uuid::macro_support::schemars08::schema::*; + let mut schema = SchemaObject { + subschemas: ::std::option::Option::Some( + Box::new(SubschemaValidation { + not: ::std::option::Option::Some(Box::new(Schema::Bool(true))), + ..::std::default::Default::default() + }), + ), + ..::std::default::Default::default() + }; + let mut extensions = ::my_custom_uuid::macro_support::schemars08::Map::new(); + let rust_type = ::my_custom_uuid::macro_support::serde_json::json!( + { "crate" : "my-api-service", "version" : "2.1.0", "path" : + "my_api_service::models::TransactionKind", } + ); + extensions.insert("x-rust-type".to_string(), rust_type); + schema.extensions = extensions; + Schema::Object(schema) + } +} +#[allow(unused)] +pub type TransactionUuid = ::my_custom_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ApiKeyKind {} +impl ::my_custom_uuid::TypedUuidKind for ApiKeyKind { + #[inline] + fn tag() -> ::my_custom_uuid::TypedUuidTag { + const TAG: ::my_custom_uuid::TypedUuidTag = ::my_custom_uuid::TypedUuidTag::new( + "api_key", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(ApiKeyUuid)) + } +} +#[cfg(feature = "internal-schemars08-tests")] +impl ::my_custom_uuid::macro_support::schemars08::JsonSchema for ApiKeyKind { + fn schema_name() -> ::std::string::String { + "ApiKeyKind".to_string() + } + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed("my_api_service::models::ApiKeyKind") + } + fn json_schema( + _gen: &mut ::my_custom_uuid::macro_support::schemars08::gen::SchemaGenerator, + ) -> ::my_custom_uuid::macro_support::schemars08::schema::Schema { + use ::my_custom_uuid::macro_support::schemars08::schema::*; + let mut schema = SchemaObject { + subschemas: ::std::option::Option::Some( + Box::new(SubschemaValidation { + not: ::std::option::Option::Some(Box::new(Schema::Bool(true))), + ..::std::default::Default::default() + }), + ), + ..::std::default::Default::default() + }; + let mut extensions = ::my_custom_uuid::macro_support::schemars08::Map::new(); + let rust_type = ::my_custom_uuid::macro_support::serde_json::json!( + { "crate" : "my-api-service", "version" : "2.1.0", "path" : + "my_api_service::models::ApiKeyKind", } + ); + extensions.insert("x-rust-type".to_string(), rust_type); + schema.extensions = extensions; + Schema::Object(schema) + } +} +#[allow(unused)] +pub type ApiKeyUuid = ::my_custom_uuid::TypedUuid; diff --git a/crates/custom-crate-tests/tests/snapshot.rs b/crates/custom-crate-tests/tests/snapshot.rs new file mode 100644 index 000000000..517850476 --- /dev/null +++ b/crates/custom-crate-tests/tests/snapshot.rs @@ -0,0 +1,18 @@ +use datatest_stable::Utf8Path; +use integration_tests::snapshot_utils; + +datatest_stable::harness! { + // The pattern matches all .rs files that aren't .output.rs files. + { test = valid_snapshot, root = "tests/fixtures/valid", pattern = r"^.*(? datatest_stable::Result<()> { + snapshot_utils::valid_snapshot(path, input) +} + +/// Snapshot tests for invalid inputs. +fn invalid_snapshot(path: &Utf8Path, input: String) -> datatest_stable::Result<()> { + snapshot_utils::invalid_snapshot(path, input) +} diff --git a/crates/integration-tests/Cargo.toml b/crates/integration-tests/Cargo.toml index f56f5f51b..8f11cfc29 100644 --- a/crates/integration-tests/Cargo.toml +++ b/crates/integration-tests/Cargo.toml @@ -6,28 +6,33 @@ license = "MIT OR Apache-2.0" publish = false [dependencies] +datatest-stable.workspace = true +expectorate.workspace = true +heck.workspace = true +newtype-uuid-macros.workspace = true newtype-uuid.workspace = true -expectorate = { workspace = true, optional = true } -prettyplease = { workspace = true, optional = true } +prettyplease.workspace = true +proc-macro2.workspace = true proptest = { workspace = true, optional = true } +quote.workspace = true schemars = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +serde.workspace = true serde_json = { workspace = true, optional = true } -syn = { workspace = true, optional = true } +serde_tokenstream.workspace = true +syn = { workspace = true, features = ["full"] } test-strategy = { workspace = true, optional = true } typify = { workspace = true, optional = true } uuid.workspace = true +[dev-dependencies] +trybuild.workspace = true + [features] internal-schemars08-tests = [ "newtype-uuid/schemars08", "newtype-uuid/serde", - "dep:expectorate", - "dep:prettyplease", "dep:schemars", "dep:serde_json", - "dep:serde", - "dep:syn", "dep:typify", ] internal-proptest1-tests = [ @@ -35,3 +40,8 @@ internal-proptest1-tests = [ "dep:proptest", "dep:test-strategy", ] + +[[test]] +# Snapshot tests use datatest-stable. +name = "snapshot" +harness = false diff --git a/crates/integration-tests/outputs/schema-rust-with-replace.rs b/crates/integration-tests/outputs/schema-rust-with-replace.rs index 266291506..126ffac67 100644 --- a/crates/integration-tests/outputs/schema-rust-with-replace.rs +++ b/crates/integration-tests/outputs/schema-rust-with-replace.rs @@ -43,7 +43,7 @@ pub mod error { /// ], /// "properties": { /// "id": { -/// "$ref": "#/definitions/TypedUuidForMyKind" +/// "$ref": "#/definitions/MyUuid" /// } /// } ///} @@ -51,7 +51,7 @@ pub mod error { /// #[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)] pub struct MyPathStruct { - pub id: ::newtype_uuid::TypedUuid<::my_crate::MyKind>, + pub id: ::my_crate::types::MyUuid, } impl ::std::convert::From<&MyPathStruct> for MyPathStruct { fn from(value: &MyPathStruct) -> Self { diff --git a/crates/integration-tests/outputs/typed-uuid-schema.json b/crates/integration-tests/outputs/typed-uuid-schema.json index 5f196ed57..fd21abd6c 100644 --- a/crates/integration-tests/outputs/typed-uuid-schema.json +++ b/crates/integration-tests/outputs/typed-uuid-schema.json @@ -7,13 +7,18 @@ ], "properties": { "id": { - "$ref": "#/definitions/TypedUuidForMyKind" + "$ref": "#/definitions/MyUuid" } }, "definitions": { - "TypedUuidForMyKind": { + "MyUuid": { "type": "string", - "format": "uuid" + "format": "uuid", + "x-rust-type": { + "crate": "my-crate", + "path": "my_crate::types::MyUuid", + "version": "1.0.0" + } } } } \ No newline at end of file diff --git a/crates/integration-tests/src/json_schema.rs b/crates/integration-tests/src/json_schema.rs index a35f294de..012f6f7f4 100644 --- a/crates/integration-tests/src/json_schema.rs +++ b/crates/integration-tests/src/json_schema.rs @@ -1,23 +1,34 @@ //! JSON schema tests for newtype-uuid. -use newtype_uuid::{TypedUuid, TypedUuidKind, TypedUuidTag}; +use newtype_uuid::TypedUuidKind; +use newtype_uuid_macros::impl_typed_uuid_kinds; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use typify::TypeSpaceSettings; +use typify::{CrateVers, TypeSpaceSettings}; -#[derive(Debug, JsonSchema)] -enum MyKind {} - -impl TypedUuidKind for MyKind { - fn tag() -> TypedUuidTag { - const TAG: TypedUuidTag = TypedUuidTag::new("my_kind"); - TAG +impl_typed_uuid_kinds! { + settings = { + schemars08 = { + attrs = [ + #[cfg(feature = "internal-schemars08-tests")], + ], + rust_type = { + crate = "my-crate", + version = "1.0.0", + path = "my_crate::types", + }, + }, + }, + kinds = { + My = {}, + Test = {}, + Another = {}, } } #[derive(Deserialize, Serialize, JsonSchema)] struct MyPathStruct { - id: TypedUuid, + id: MyUuid, } #[test] @@ -27,17 +38,14 @@ fn test_json_schema_snapshot() { println!("{}", std::env::current_dir().unwrap().display()); expectorate::assert_contents("outputs/typed-uuid-schema.json", &schema_json); - // Now attempt to use typify to convert the JSON schema into Rust code. - let output = generate_schema_with(&TypeSpaceSettings::default(), schema.clone()); - expectorate::assert_contents("outputs/schema-rust.rs", &output); - - // Do so, with a replace directive. + // Use typify with crate directives -- this is the intended usage with + // x-rust-type. The x-rust-type extension enables automatic replacement, so + // that we don't try to generate Rust code for either newtype-uuid or + // my-crate. let mut settings = TypeSpaceSettings::default(); - settings.with_replacement( - "TypedUuidForMyKind", - "::newtype_uuid::TypedUuid<::my_crate::MyKind>", - std::iter::empty(), - ); + settings + .with_crate("newtype-uuid", CrateVers::Any, None) + .with_crate("my-crate", CrateVers::Any, None); let output = generate_schema_with(&settings, schema); expectorate::assert_contents("outputs/schema-rust-with-replace.rs", &output); } @@ -54,3 +62,47 @@ fn generate_schema_with( let file: syn::File = syn::parse2(tokens).expect("parsing tokens succeeded"); prettyplease::unparse(&file) } + +#[test] +fn test_schemars_macro_integration() { + // Test that JsonSchema is implemented + use schemars::JsonSchema; + + // Test schema_name + assert_eq!(TestKind::schema_name(), "TestKind"); + assert_eq!(AnotherKind::schema_name(), "AnotherKind"); + + // Test that we can generate schemas. + let mut generator = schemars::r#gen::SchemaGenerator::default(); + let schema = TestKind::json_schema(&mut generator); + + // Verify it's set to "not: true". + let expected_schema = schemars::schema::Schema::Object(schemars::schema::SchemaObject { + instance_type: None, + subschemas: Some(Box::new(schemars::schema::SubschemaValidation { + not: Some(Box::new(schemars::schema::Schema::Bool(true))), + ..Default::default() + })), + extensions: { + let mut extensions = std::collections::BTreeMap::new(); + extensions.insert( + "x-rust-type".to_string(), + serde_json::json!({ + "crate": "my-crate", + "version": "1.0.0", + "path": "my_crate::types::TestKind" + }), + ); + extensions + }, + ..Default::default() + }); + assert_eq!(schema, expected_schema); +} + +#[test] +fn test_macro_generated_tags() { + // Test that the generated kinds have the correct tags + assert_eq!(TestKind::tag().as_str(), "test"); + assert_eq!(AnotherKind::tag().as_str(), "another"); +} diff --git a/crates/integration-tests/src/lib.rs b/crates/integration-tests/src/lib.rs index 032f753bc..9b2047cd0 100644 --- a/crates/integration-tests/src/lib.rs +++ b/crates/integration-tests/src/lib.rs @@ -4,3 +4,6 @@ mod json_schema; #[cfg(all(test, feature = "internal-proptest1-tests"))] mod proptests; +pub mod snapshot_utils; +#[cfg(test)] +mod ui; diff --git a/crates/integration-tests/src/snapshot_utils.rs b/crates/integration-tests/src/snapshot_utils.rs new file mode 100644 index 000000000..bedc511fd --- /dev/null +++ b/crates/integration-tests/src/snapshot_utils.rs @@ -0,0 +1,75 @@ +//! Common utilities for snapshot testing proc macros. + +use datatest_stable::Utf8Path; +use quote::ToTokens; +use syn::parse_quote; + +// We need access to the proc-macro's internals for this test. An alternative +// would be to make this a unit test, but the integration test harness gives us +// automatic discovery of tests in the `fixtures/` directory, along with +// separate reporting for each test. Those are nice benefits. +#[path = "../../newtype-uuid-macros/src/internals/mod.rs"] +mod internals; + +/// Snapshot tests for valid inputs. +pub fn valid_snapshot(path: &Utf8Path, input: String) -> datatest_stable::Result<()> { + let data = syn::parse_str::(&input)?; + + let output = run_macro(&data); + assert_macro_output(path, output); + + Ok(()) +} + +/// Snapshot tests for invalid inputs. +pub fn invalid_snapshot(path: &Utf8Path, input: String) -> datatest_stable::Result<()> { + let data = syn::parse_str::(&input)?; + + let output = run_macro(&data).map(|output| { + // Drop the errors for snapshot tests -- only use the output. + output.out + }); + assert_macro_output(path, output); + + Ok(()) +} + +fn run_macro(data: &syn::File) -> impl Iterator + '_ { + // Look for invocations of impl_typed_uuid_kinds in the input. + let items = data.items.iter().filter_map(|item| match item { + syn::Item::Macro(item) => { + let is_invocation = item + .mac + .path + .segments + .last() + .map(|s| s.ident == "impl_typed_uuid_kinds") + .unwrap_or(false); + is_invocation.then_some(item) + } + _ => None, + }); + + // Run the macro on each item. + items.map(|item| internals::impl_typed_uuid_kinds(item.mac.tokens.clone())) +} + +fn assert_macro_output(path: &Utf8Path, output: impl IntoIterator) { + // Read the output as a `syn::File`. + let output = output.into_iter(); + let file = parse_quote! { + #(#output)* + }; + + // Format the output. + let output = prettyplease::unparse(&file); + + // Compare the output with the snapshot. The new filename is the same as the + // input, but with ".output.rs" at the end. + let mut output_path = path.parent().unwrap().to_owned(); + output_path.push("output"); + output_path.push(path.file_name().unwrap()); + output_path.set_extension("output.rs"); + + expectorate::assert_contents(&output_path, &output); +} diff --git a/crates/integration-tests/src/ui.rs b/crates/integration-tests/src/ui.rs new file mode 100644 index 000000000..79a700cdb --- /dev/null +++ b/crates/integration-tests/src/ui.rs @@ -0,0 +1,6 @@ +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/fixtures/invalid/*.rs"); + t.pass("tests/fixtures/valid/*.rs"); +} diff --git a/crates/integration-tests/tests/fixtures/invalid/duplicate_def.rs b/crates/integration-tests/tests/fixtures/invalid/duplicate_def.rs new file mode 100644 index 000000000..24f0d0a48 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/duplicate_def.rs @@ -0,0 +1,12 @@ +use newtype_uuid_macros::impl_typed_uuid_kinds; + +enum UserKind {} +struct UserUuid; + +impl_typed_uuid_kinds! { + kinds = { + User = {}, + } +} + +fn main() {} diff --git a/crates/integration-tests/tests/fixtures/invalid/duplicate_def.stderr b/crates/integration-tests/tests/fixtures/invalid/duplicate_def.stderr new file mode 100644 index 000000000..35a34372f --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/duplicate_def.stderr @@ -0,0 +1,21 @@ +error[E0428]: the name `UserUuid` is defined multiple times + --> tests/fixtures/invalid/duplicate_def.rs:8:9 + | +4 | struct UserUuid; + | ---------------- previous definition of the type `UserUuid` here +... +8 | User = {}, + | ^^^^ `UserUuid` redefined here + | + = note: `UserUuid` must be defined only once in the type namespace of this module + +error[E0428]: the name `UserKind` is defined multiple times + --> tests/fixtures/invalid/duplicate_def.rs:8:9 + | +3 | enum UserKind {} + | ------------- previous definition of the type `UserKind` here +... +8 | User = {}, + | ^^^^ `UserKind` redefined here + | + = note: `UserKind` must be defined only once in the type namespace of this module diff --git a/crates/integration-tests/tests/fixtures/invalid/empty_macro.rs b/crates/integration-tests/tests/fixtures/invalid/empty_macro.rs new file mode 100644 index 000000000..418b36420 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/empty_macro.rs @@ -0,0 +1,5 @@ +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! {} + +fn main() {} diff --git a/crates/integration-tests/tests/fixtures/invalid/empty_macro.stderr b/crates/integration-tests/tests/fixtures/invalid/empty_macro.stderr new file mode 100644 index 000000000..b9ac5fa50 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/empty_macro.stderr @@ -0,0 +1,7 @@ +error: missing field `kinds` + --> tests/fixtures/invalid/empty_macro.rs:3:1 + | +3 | impl_typed_uuid_kinds! {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `impl_typed_uuid_kinds` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/integration-tests/tests/fixtures/invalid/invalid_kind_name.rs b/crates/integration-tests/tests/fixtures/invalid/invalid_kind_name.rs new file mode 100644 index 000000000..0ced19a54 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/invalid_kind_name.rs @@ -0,0 +1,28 @@ +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + kinds = { + 123Invalid = {}, + NonÅscii = {}, + // User and Custom are valid. + User = {}, + Custom = { tag = "custom" }, + Hello = { tag = "Hellö" }, + Tag = "hi", + Youre = [ "it" ], + Bad = { + tag = "this_is_fine", + type_name = 42, + alias = 13, + }, + "" = {}, + } +} + +fn main() { + // UserUuid and CustomUuid should exist. + let _user = UserUuid::nil(); + let _custom = CustomUuid::nil(); + let _user_kind: UserKind; + let _custom_kind: CustomKind; +} diff --git a/crates/integration-tests/tests/fixtures/invalid/invalid_kind_name.stderr b/crates/integration-tests/tests/fixtures/invalid/invalid_kind_name.stderr new file mode 100644 index 000000000..92a1889d1 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/invalid_kind_name.stderr @@ -0,0 +1,48 @@ +error: expected identifier + --> tests/fixtures/invalid/invalid_kind_name.rs:5:9 + | +5 | 123Invalid = {}, + | ^^^^^^^^^^ + +error: tag name `non_åscii` must consist of ASCII alphanumeric characters or underscores + (hint: tag name `non_åscii` derived from kind name -- specify `tag = "..." for a custom tag name`) + --> tests/fixtures/invalid/invalid_kind_name.rs:6:9 + | +6 | NonÅscii = {}, + | ^^^^^^^^ + +error: tag name `Hellö` must consist of ASCII alphanumeric characters or underscores + --> tests/fixtures/invalid/invalid_kind_name.rs:10:25 + | +10 | Hello = { tag = "Hellö" }, + | ^^^^^^^ + +error: expected `{` + --> tests/fixtures/invalid/invalid_kind_name.rs:11:15 + | +11 | Tag = "hi", + | ^^^^ + +error: expected `{` + --> tests/fixtures/invalid/invalid_kind_name.rs:12:17 + | +12 | Youre = [ "it" ], + | ^^^^^^^^ + +error: expected identifier + --> tests/fixtures/invalid/invalid_kind_name.rs:15:25 + | +15 | type_name = 42, + | ^^ + +error: expected identifier + --> tests/fixtures/invalid/invalid_kind_name.rs:16:21 + | +16 | alias = 13, + | ^^ + +error: expected identifier + --> tests/fixtures/invalid/invalid_kind_name.rs:18:9 + | +18 | "" = {}, + | ^^ diff --git a/crates/integration-tests/tests/fixtures/invalid/invalid_settings.rs b/crates/integration-tests/tests/fixtures/invalid/invalid_settings.rs new file mode 100644 index 000000000..086246055 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/invalid_settings.rs @@ -0,0 +1,19 @@ +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + settings = { + schemars08 = { + attrs = [#[cfg(feature = "schemars08")]], + rust_type = { + crate = "my-service", + version = "1.0.0", + // Missing required 'path' field + }, + }, + }, + kinds = { + User = {}, + } +} + +fn main() {} diff --git a/crates/integration-tests/tests/fixtures/invalid/invalid_settings.stderr b/crates/integration-tests/tests/fixtures/invalid/invalid_settings.stderr new file mode 100644 index 000000000..46e7b5a1f --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/invalid_settings.stderr @@ -0,0 +1,10 @@ +error: missing field `path` + --> tests/fixtures/invalid/invalid_settings.rs:7:25 + | +7 | rust_type = { + | _________________________^ +8 | | crate = "my-service", +9 | | version = "1.0.0", +10 | | // Missing required 'path' field +11 | | }, + | |_____________^ diff --git a/crates/integration-tests/tests/fixtures/invalid/invalid_syntax.rs b/crates/integration-tests/tests/fixtures/invalid/invalid_syntax.rs new file mode 100644 index 000000000..919fd784d --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/invalid_syntax.rs @@ -0,0 +1,9 @@ +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + // this is not valid syntax at all + User => "user", + Organization => "org", +} + +fn main() {} diff --git a/crates/integration-tests/tests/fixtures/invalid/invalid_syntax.stderr b/crates/integration-tests/tests/fixtures/invalid/invalid_syntax.stderr new file mode 100644 index 000000000..98ee803fd --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/invalid_syntax.stderr @@ -0,0 +1,11 @@ +error: unknown field `User`, expected `settings` or `kinds` + --> tests/fixtures/invalid/invalid_syntax.rs:3:1 + | +3 | / impl_typed_uuid_kinds! { +4 | | // this is not valid syntax at all +5 | | User => "user", +6 | | Organization => "org", +7 | | } + | |_^ + | + = note: this error originates in the macro `impl_typed_uuid_kinds` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/integration-tests/tests/fixtures/invalid/missing_kinds.rs b/crates/integration-tests/tests/fixtures/invalid/missing_kinds.rs new file mode 100644 index 000000000..335c49a65 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/missing_kinds.rs @@ -0,0 +1,17 @@ +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + settings = { + schemars08 = { + attrs = [#[cfg(feature = "schemars08")]], + rust_type = { + crate = "my-service", + version = "1.0.0", + path = "my_service::types", + }, + }, + }, + // Missing the required 'kinds' field. +} + +fn main() {} diff --git a/crates/integration-tests/tests/fixtures/invalid/missing_kinds.stderr b/crates/integration-tests/tests/fixtures/invalid/missing_kinds.stderr new file mode 100644 index 000000000..5ab7ea07a --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/missing_kinds.stderr @@ -0,0 +1,12 @@ +error: missing field `kinds` + --> tests/fixtures/invalid/missing_kinds.rs:3:1 + | +3 | / impl_typed_uuid_kinds! { +4 | | settings = { +5 | | schemars08 = { +6 | | attrs = [#[cfg(feature = "schemars08")]], +... | +15 | | } + | |_^ + | + = note: this error originates in the macro `impl_typed_uuid_kinds` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/integration-tests/tests/fixtures/invalid/output/duplicate_def.output.rs b/crates/integration-tests/tests/fixtures/invalid/output/duplicate_def.output.rs new file mode 100644 index 000000000..9a58575c4 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/output/duplicate_def.output.rs @@ -0,0 +1,16 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UserKind {} +impl ::newtype_uuid::TypedUuidKind for UserKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "user", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(UserUuid)) + } +} +#[allow(unused)] +pub type UserUuid = ::newtype_uuid::TypedUuid; diff --git a/crates/integration-tests/tests/fixtures/invalid/output/empty_macro.output.rs b/crates/integration-tests/tests/fixtures/invalid/output/empty_macro.output.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/integration-tests/tests/fixtures/invalid/output/invalid_kind_name.output.rs b/crates/integration-tests/tests/fixtures/invalid/output/invalid_kind_name.output.rs new file mode 100644 index 000000000..c91b43a91 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/invalid/output/invalid_kind_name.output.rs @@ -0,0 +1,32 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UserKind {} +impl ::newtype_uuid::TypedUuidKind for UserKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "user", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(UserUuid)) + } +} +#[allow(unused)] +pub type UserUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CustomKind {} +impl ::newtype_uuid::TypedUuidKind for CustomKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "custom", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(CustomUuid)) + } +} +#[allow(unused)] +pub type CustomUuid = ::newtype_uuid::TypedUuid; diff --git a/crates/integration-tests/tests/fixtures/invalid/output/invalid_settings.output.rs b/crates/integration-tests/tests/fixtures/invalid/output/invalid_settings.output.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/integration-tests/tests/fixtures/invalid/output/invalid_syntax.output.rs b/crates/integration-tests/tests/fixtures/invalid/output/invalid_syntax.output.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/integration-tests/tests/fixtures/invalid/output/missing_kinds.output.rs b/crates/integration-tests/tests/fixtures/invalid/output/missing_kinds.output.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/integration-tests/tests/fixtures/valid/basic.rs b/crates/integration-tests/tests/fixtures/valid/basic.rs new file mode 100644 index 000000000..d393103f0 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/valid/basic.rs @@ -0,0 +1,26 @@ +use newtype_uuid::TypedUuidKind; +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + kinds = { + User = {}, + Organization = {}, + Pröject = { + tag = "project", + type_name = ProjectKind, + alias = ProjectUuid, + }, + } +} + +fn main() { + // Test that the generated types exist and work + let _user_kind_tag = UserKind::tag(); + let _org_kind_tag = OrganizationKind::tag(); + let _project_kind_tag = ProjectKind::tag(); + + // Test type aliases exist + let _user_uuid: UserUuid; + let _org_uuid: OrganizationUuid; + let _project_uuid: ProjectUuid; +} diff --git a/crates/integration-tests/tests/fixtures/valid/complex_names.rs b/crates/integration-tests/tests/fixtures/valid/complex_names.rs new file mode 100644 index 000000000..d5a42fee9 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/valid/complex_names.rs @@ -0,0 +1,39 @@ +use newtype_uuid::TypedUuidKind; +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + kinds = { + HTTPClient = {}, + XMLParser = {}, + APIKey = {}, + IOHandler = {}, + UserAccount = {}, + ProjectTask = {}, + } +} + +fn main() { + // Test that complex names are handled correctly + let _http_client_tag = HTTPClientKind::tag(); + let _xml_parser_tag = XMLParserKind::tag(); + let _api_key_tag = APIKeyKind::tag(); + let _io_handler_tag = IOHandlerKind::tag(); + let _user_account_tag = UserAccountKind::tag(); + let _project_task_tag = ProjectTaskKind::tag(); + + // Test type aliases exist + let _http_client_uuid: HTTPClientUuid; + let _xml_parser_uuid: XMLParserUuid; + let _api_key_uuid: APIKeyUuid; + let _io_handler_uuid: IOHandlerUuid; + let _user_account_uuid: UserAccountUuid; + let _project_task_uuid: ProjectTaskUuid; + + // Test that complex names are properly snake_cased + assert_eq!(HTTPClientKind::tag().as_str(), "http_client"); + assert_eq!(XMLParserKind::tag().as_str(), "xml_parser"); + assert_eq!(APIKeyKind::tag().as_str(), "api_key"); + assert_eq!(IOHandlerKind::tag().as_str(), "io_handler"); + assert_eq!(UserAccountKind::tag().as_str(), "user_account"); + assert_eq!(ProjectTaskKind::tag().as_str(), "project_task"); +} diff --git a/crates/integration-tests/tests/fixtures/valid/empty_kinds.rs b/crates/integration-tests/tests/fixtures/valid/empty_kinds.rs new file mode 100644 index 000000000..3a97c7fb9 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/valid/empty_kinds.rs @@ -0,0 +1,10 @@ +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + kinds = {} +} + +fn main() { + // Test that the macro compiles successfully with no kinds defined + // This should generate no code but not error +} diff --git a/crates/integration-tests/tests/fixtures/valid/output/basic.output.rs b/crates/integration-tests/tests/fixtures/valid/output/basic.output.rs new file mode 100644 index 000000000..b3ddc156c --- /dev/null +++ b/crates/integration-tests/tests/fixtures/valid/output/basic.output.rs @@ -0,0 +1,48 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UserKind {} +impl ::newtype_uuid::TypedUuidKind for UserKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "user", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(UserUuid)) + } +} +#[allow(unused)] +pub type UserUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OrganizationKind {} +impl ::newtype_uuid::TypedUuidKind for OrganizationKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "organization", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(OrganizationUuid)) + } +} +#[allow(unused)] +pub type OrganizationUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ProjectKind {} +impl ::newtype_uuid::TypedUuidKind for ProjectKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "project", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(ProjectUuid)) + } +} +#[allow(unused)] +pub type ProjectUuid = ::newtype_uuid::TypedUuid; diff --git a/crates/integration-tests/tests/fixtures/valid/output/complex_names.output.rs b/crates/integration-tests/tests/fixtures/valid/output/complex_names.output.rs new file mode 100644 index 000000000..2395302d0 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/valid/output/complex_names.output.rs @@ -0,0 +1,96 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HTTPClientKind {} +impl ::newtype_uuid::TypedUuidKind for HTTPClientKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "http_client", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(HTTPClientUuid)) + } +} +#[allow(unused)] +pub type HTTPClientUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum XMLParserKind {} +impl ::newtype_uuid::TypedUuidKind for XMLParserKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "xml_parser", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(XMLParserUuid)) + } +} +#[allow(unused)] +pub type XMLParserUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum APIKeyKind {} +impl ::newtype_uuid::TypedUuidKind for APIKeyKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "api_key", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(APIKeyUuid)) + } +} +#[allow(unused)] +pub type APIKeyUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IOHandlerKind {} +impl ::newtype_uuid::TypedUuidKind for IOHandlerKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "io_handler", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(IOHandlerUuid)) + } +} +#[allow(unused)] +pub type IOHandlerUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UserAccountKind {} +impl ::newtype_uuid::TypedUuidKind for UserAccountKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "user_account", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(UserAccountUuid)) + } +} +#[allow(unused)] +pub type UserAccountUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ProjectTaskKind {} +impl ::newtype_uuid::TypedUuidKind for ProjectTaskKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "project_task", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(ProjectTaskUuid)) + } +} +#[allow(unused)] +pub type ProjectTaskUuid = ::newtype_uuid::TypedUuid; diff --git a/crates/integration-tests/tests/fixtures/valid/output/empty_kinds.output.rs b/crates/integration-tests/tests/fixtures/valid/output/empty_kinds.output.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/integration-tests/tests/fixtures/valid/output/schemars08_unconditional.output.rs b/crates/integration-tests/tests/fixtures/valid/output/schemars08_unconditional.output.rs new file mode 100644 index 000000000..3c80cc38b --- /dev/null +++ b/crates/integration-tests/tests/fixtures/valid/output/schemars08_unconditional.output.rs @@ -0,0 +1,138 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UserKind {} +impl ::newtype_uuid::TypedUuidKind for UserKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "user", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(UserUuid)) + } +} +impl ::newtype_uuid::macro_support::schemars08::JsonSchema for UserKind { + fn schema_name() -> ::std::string::String { + "UserKind".to_string() + } + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed("my_service::types::UserKind") + } + fn json_schema( + _gen: &mut ::newtype_uuid::macro_support::schemars08::gen::SchemaGenerator, + ) -> ::newtype_uuid::macro_support::schemars08::schema::Schema { + use ::newtype_uuid::macro_support::schemars08::schema::*; + let mut schema = SchemaObject { + subschemas: ::std::option::Option::Some( + Box::new(SubschemaValidation { + not: ::std::option::Option::Some(Box::new(Schema::Bool(true))), + ..::std::default::Default::default() + }), + ), + ..::std::default::Default::default() + }; + let mut extensions = ::newtype_uuid::macro_support::schemars08::Map::new(); + let rust_type = ::newtype_uuid::macro_support::serde_json::json!( + { "crate" : "my-service", "version" : "1.0.0", "path" : + "my_service::types::UserKind", } + ); + extensions.insert("x-rust-type".to_string(), rust_type); + schema.extensions = extensions; + Schema::Object(schema) + } +} +#[allow(unused)] +pub type UserUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OrganizationKind {} +impl ::newtype_uuid::TypedUuidKind for OrganizationKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "organization", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(OrganizationUuid)) + } +} +impl ::newtype_uuid::macro_support::schemars08::JsonSchema for OrganizationKind { + fn schema_name() -> ::std::string::String { + "OrganizationKind".to_string() + } + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed("my_service::types::OrganizationKind") + } + fn json_schema( + _gen: &mut ::newtype_uuid::macro_support::schemars08::gen::SchemaGenerator, + ) -> ::newtype_uuid::macro_support::schemars08::schema::Schema { + use ::newtype_uuid::macro_support::schemars08::schema::*; + let mut schema = SchemaObject { + subschemas: ::std::option::Option::Some( + Box::new(SubschemaValidation { + not: ::std::option::Option::Some(Box::new(Schema::Bool(true))), + ..::std::default::Default::default() + }), + ), + ..::std::default::Default::default() + }; + let mut extensions = ::newtype_uuid::macro_support::schemars08::Map::new(); + let rust_type = ::newtype_uuid::macro_support::serde_json::json!( + { "crate" : "my-service", "version" : "1.0.0", "path" : + "my_service::types::OrganizationKind", } + ); + extensions.insert("x-rust-type".to_string(), rust_type); + schema.extensions = extensions; + Schema::Object(schema) + } +} +#[allow(unused)] +pub type OrganizationUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ProjectKind {} +impl ::newtype_uuid::TypedUuidKind for ProjectKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "project", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(ProjectUuid)) + } +} +impl ::newtype_uuid::macro_support::schemars08::JsonSchema for ProjectKind { + fn schema_name() -> ::std::string::String { + "ProjectKind".to_string() + } + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed("my_service::types::ProjectKind") + } + fn json_schema( + _gen: &mut ::newtype_uuid::macro_support::schemars08::gen::SchemaGenerator, + ) -> ::newtype_uuid::macro_support::schemars08::schema::Schema { + use ::newtype_uuid::macro_support::schemars08::schema::*; + let mut schema = SchemaObject { + subschemas: ::std::option::Option::Some( + Box::new(SubschemaValidation { + not: ::std::option::Option::Some(Box::new(Schema::Bool(true))), + ..::std::default::Default::default() + }), + ), + ..::std::default::Default::default() + }; + let mut extensions = ::newtype_uuid::macro_support::schemars08::Map::new(); + let rust_type = ::newtype_uuid::macro_support::serde_json::json!( + { "crate" : "my-service", "version" : "1.0.0", "path" : + "my_service::types::ProjectKind", } + ); + extensions.insert("x-rust-type".to_string(), rust_type); + schema.extensions = extensions; + Schema::Object(schema) + } +} +#[allow(unused)] +pub type ProjectUuid = ::newtype_uuid::TypedUuid; diff --git a/crates/integration-tests/tests/fixtures/valid/output/with_settings.output.rs b/crates/integration-tests/tests/fixtures/valid/output/with_settings.output.rs new file mode 100644 index 000000000..0aeaf0bf1 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/valid/output/with_settings.output.rs @@ -0,0 +1,141 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UserKind {} +impl ::newtype_uuid::TypedUuidKind for UserKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "user", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(UserUuid)) + } +} +#[cfg(feature = "internal-schemars08-tests")] +impl ::newtype_uuid::macro_support::schemars08::JsonSchema for UserKind { + fn schema_name() -> ::std::string::String { + "UserKind".to_string() + } + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed("my_service::types::UserKind") + } + fn json_schema( + _gen: &mut ::newtype_uuid::macro_support::schemars08::gen::SchemaGenerator, + ) -> ::newtype_uuid::macro_support::schemars08::schema::Schema { + use ::newtype_uuid::macro_support::schemars08::schema::*; + let mut schema = SchemaObject { + subschemas: ::std::option::Option::Some( + Box::new(SubschemaValidation { + not: ::std::option::Option::Some(Box::new(Schema::Bool(true))), + ..::std::default::Default::default() + }), + ), + ..::std::default::Default::default() + }; + let mut extensions = ::newtype_uuid::macro_support::schemars08::Map::new(); + let rust_type = ::newtype_uuid::macro_support::serde_json::json!( + { "crate" : "my-service", "version" : "1.0.0", "path" : + "my_service::types::UserKind", } + ); + extensions.insert("x-rust-type".to_string(), rust_type); + schema.extensions = extensions; + Schema::Object(schema) + } +} +#[allow(unused)] +pub type UserUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OrganizationKind {} +impl ::newtype_uuid::TypedUuidKind for OrganizationKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "organization", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(OrganizationUuid)) + } +} +#[cfg(feature = "internal-schemars08-tests")] +impl ::newtype_uuid::macro_support::schemars08::JsonSchema for OrganizationKind { + fn schema_name() -> ::std::string::String { + "OrganizationKind".to_string() + } + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed("my_service::types::OrganizationKind") + } + fn json_schema( + _gen: &mut ::newtype_uuid::macro_support::schemars08::gen::SchemaGenerator, + ) -> ::newtype_uuid::macro_support::schemars08::schema::Schema { + use ::newtype_uuid::macro_support::schemars08::schema::*; + let mut schema = SchemaObject { + subschemas: ::std::option::Option::Some( + Box::new(SubschemaValidation { + not: ::std::option::Option::Some(Box::new(Schema::Bool(true))), + ..::std::default::Default::default() + }), + ), + ..::std::default::Default::default() + }; + let mut extensions = ::newtype_uuid::macro_support::schemars08::Map::new(); + let rust_type = ::newtype_uuid::macro_support::serde_json::json!( + { "crate" : "my-service", "version" : "1.0.0", "path" : + "my_service::types::OrganizationKind", } + ); + extensions.insert("x-rust-type".to_string(), rust_type); + schema.extensions = extensions; + Schema::Object(schema) + } +} +#[allow(unused)] +pub type OrganizationUuid = ::newtype_uuid::TypedUuid; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ProjectKind {} +impl ::newtype_uuid::TypedUuidKind for ProjectKind { + #[inline] + fn tag() -> ::newtype_uuid::TypedUuidTag { + const TAG: ::newtype_uuid::TypedUuidTag = ::newtype_uuid::TypedUuidTag::new( + "project", + ); + TAG + } + fn alias() -> Option<&'static str> { + Some(stringify!(ProjectUuid)) + } +} +#[cfg(feature = "internal-schemars08-tests")] +impl ::newtype_uuid::macro_support::schemars08::JsonSchema for ProjectKind { + fn schema_name() -> ::std::string::String { + "ProjectKind".to_string() + } + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed("my_service::types::ProjectKind") + } + fn json_schema( + _gen: &mut ::newtype_uuid::macro_support::schemars08::gen::SchemaGenerator, + ) -> ::newtype_uuid::macro_support::schemars08::schema::Schema { + use ::newtype_uuid::macro_support::schemars08::schema::*; + let mut schema = SchemaObject { + subschemas: ::std::option::Option::Some( + Box::new(SubschemaValidation { + not: ::std::option::Option::Some(Box::new(Schema::Bool(true))), + ..::std::default::Default::default() + }), + ), + ..::std::default::Default::default() + }; + let mut extensions = ::newtype_uuid::macro_support::schemars08::Map::new(); + let rust_type = ::newtype_uuid::macro_support::serde_json::json!( + { "crate" : "my-service", "version" : "1.0.0", "path" : + "my_service::types::ProjectKind", } + ); + extensions.insert("x-rust-type".to_string(), rust_type); + schema.extensions = extensions; + Schema::Object(schema) + } +} +#[allow(unused)] +pub type ProjectUuid = ::newtype_uuid::TypedUuid; diff --git a/crates/integration-tests/tests/fixtures/valid/schemars08_unconditional.rs b/crates/integration-tests/tests/fixtures/valid/schemars08_unconditional.rs new file mode 100644 index 000000000..552ccb483 --- /dev/null +++ b/crates/integration-tests/tests/fixtures/valid/schemars08_unconditional.rs @@ -0,0 +1,46 @@ +//! Test a situation in which settings.schemars08.feature isn't specified. In +//! this case, we expect the macro itself to be unconditional. + +#[cfg(feature = "internal-schemars08-tests")] +newtype_uuid_macros::impl_typed_uuid_kinds! { + settings = { + schemars08 = { + rust_type = { + crate = "my-service", + version = "1.0.0", + path = "my_service::types", + }, + }, + }, + kinds = { + User = {}, + Organization = {}, + Pröject = { + tag = "project", + type_name = ProjectKind, + alias = ProjectUuid, + }, + } +} + +fn main() { + #[cfg(feature = "internal-schemars08-tests")] + { + use newtype_uuid::TypedUuidKind; + + // Test that the generated types exist and work + let _user_kind_tag = UserKind::tag(); + let _org_kind_tag = OrganizationKind::tag(); + let _project_kind_tag = ProjectKind::tag(); + + // Test type aliases exist + let _user_uuid: UserUuid; + let _org_uuid: OrganizationUuid; + let _project_uuid: ProjectUuid; + + // Test that tags are properly snake_cased + assert_eq!(UserKind::tag().as_str(), "user"); + assert_eq!(OrganizationKind::tag().as_str(), "organization"); + assert_eq!(ProjectKind::tag().as_str(), "project"); + } +} diff --git a/crates/integration-tests/tests/fixtures/valid/with_settings.rs b/crates/integration-tests/tests/fixtures/valid/with_settings.rs new file mode 100644 index 000000000..2f8b6189f --- /dev/null +++ b/crates/integration-tests/tests/fixtures/valid/with_settings.rs @@ -0,0 +1,37 @@ +use newtype_uuid::TypedUuidKind; +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + settings = { + schemars08 = { + attrs = [#[cfg(feature = "internal-schemars08-tests")]], + rust_type = { + crate = "my-service", + version = "1.0.0", + path = "my_service::types", + }, + }, + }, + kinds = { + User = {}, + Organization = {}, + Project = {}, + } +} + +fn main() { + // Test that the generated types exist and work + let _user_kind_tag = UserKind::tag(); + let _org_kind_tag = OrganizationKind::tag(); + let _project_kind_tag = ProjectKind::tag(); + + // Test type aliases exist + let _user_uuid: UserUuid; + let _org_uuid: OrganizationUuid; + let _project_uuid: ProjectUuid; + + // Test that tags are properly snake_cased + assert_eq!(UserKind::tag().as_str(), "user"); + assert_eq!(OrganizationKind::tag().as_str(), "organization"); + assert_eq!(ProjectKind::tag().as_str(), "project"); +} diff --git a/crates/integration-tests/tests/snapshot.rs b/crates/integration-tests/tests/snapshot.rs new file mode 100644 index 000000000..517850476 --- /dev/null +++ b/crates/integration-tests/tests/snapshot.rs @@ -0,0 +1,18 @@ +use datatest_stable::Utf8Path; +use integration_tests::snapshot_utils; + +datatest_stable::harness! { + // The pattern matches all .rs files that aren't .output.rs files. + { test = valid_snapshot, root = "tests/fixtures/valid", pattern = r"^.*(? datatest_stable::Result<()> { + snapshot_utils::valid_snapshot(path, input) +} + +/// Snapshot tests for invalid inputs. +fn invalid_snapshot(path: &Utf8Path, input: String) -> datatest_stable::Result<()> { + snapshot_utils::invalid_snapshot(path, input) +} diff --git a/crates/newtype-uuid-macros/Cargo.toml b/crates/newtype-uuid-macros/Cargo.toml new file mode 100644 index 000000000..d7583c5ba --- /dev/null +++ b/crates/newtype-uuid-macros/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "newtype-uuid-macros" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +documentation = "https://docs.rs/newtype-uuid-macros" +rust-version.workspace = true +description = "Procedural macro for newtype-uuid" +readme = "README.md" +keywords = ["uuid", "unique", "guid", "newtype"] + +[lib] +proc-macro = true + +[dependencies] +heck.workspace = true +proc-macro2.workspace = true +quote.workspace = true +serde.workspace = true +serde_tokenstream.workspace = true +syn = { workspace = true, features = ["full"] } + +[dev-dependencies] +newtype-uuid = { workspace = true, features = ["v4"] } +serde = { workspace = true, features = ["derive"] } +static_assertions.workspace = true + +[package.metadata.cargo-sync-rdme.badge.badges] +license = true +crates-io = true +docs-rs = true +rust-version = true diff --git a/crates/newtype-uuid-macros/README.md b/crates/newtype-uuid-macros/README.md new file mode 100644 index 000000000..d504fc9d3 --- /dev/null +++ b/crates/newtype-uuid-macros/README.md @@ -0,0 +1,51 @@ + +# newtype-uuid-macros + + +![License: MIT OR Apache-2.0](https://img.shields.io/crates/l/newtype-uuid-macros.svg?) +[![crates.io](https://img.shields.io/crates/v/newtype-uuid-macros.svg?logo=rust)](https://crates.io/crates/newtype-uuid-macros) +[![docs.rs](https://img.shields.io/docsrs/newtype-uuid-macros.svg?logo=docs.rs)](https://docs.rs/newtype-uuid-macros) +[![Rust: ^1.79.0](https://img.shields.io/badge/rust-^1.79.0-93450a.svg?logo=rust)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) + + +Procedural macro for [`newtype-uuid`](https://docs.rs/newtype-uuid). + +This crate provides a procedural macro to help with creating +[`newtype-uuid`](https://docs.rs/newtype-uuid) instances. + +For more information, see the documentation for [`impl_typed_uuid_kinds!`](https://docs.rs/newtype-uuid-macros/0.1.0/newtype_uuid_macros/macro.impl_typed_uuid_kinds.html). + +## Examples + +Basic usage: + +````rust +use newtype_uuid::TypedUuidKind; +use newtype_uuid_macros::impl_typed_uuid_kinds; + +impl_typed_uuid_kinds! { + kinds = { + User = {}, + Organization = {}, + }, +} + +// This generates empty UserKind and OrganizationKind enums implementing +// TypedUuidKind, with the tags "user" and "organization" respectively. +// Tags are snake_case versions of type names. +assert_eq!(UserKind::tag().as_str(), "user"); +assert_eq!(OrganizationKind::tag().as_str(), "organization"); + +// The macro also generates UserUuid and OrganizationUuid type aliases. +let user_uuid = UserUuid::new_v4(); +let organization_uuid = OrganizationUuid::new_v4(); +```` + +For more details and examples, see the documentation for +[`impl_typed_uuid_kinds!`](https://docs.rs/newtype-uuid-macros/0.1.0/newtype_uuid_macros/macro.impl_typed_uuid_kinds.html). + + +## License + +This project is available under the terms of either the [Apache 2.0 license](LICENSE-APACHE) or the [MIT +license](LICENSE-MIT). diff --git a/crates/newtype-uuid-macros/examples/basic-macro.rs b/crates/newtype-uuid-macros/examples/basic-macro.rs new file mode 100644 index 000000000..c05f14c20 --- /dev/null +++ b/crates/newtype-uuid-macros/examples/basic-macro.rs @@ -0,0 +1,49 @@ +//! Basic usage example for the `impl_typed_uuid_kinds` macro. +//! +//! This example demonstrates how to use the macro to generate typed UUID kinds +//! and their corresponding type aliases. + +use newtype_uuid::TypedUuidKind; +use newtype_uuid_macros::impl_typed_uuid_kinds; + +// Generate typed UUID kinds for different entities in our application. +impl_typed_uuid_kinds! { + kinds = { + User = {}, + Organization = {}, + Project = {}, + } +} + +// The above macro generates: +// +// * pub enum UserKind {} +// * pub type UserUuid = TypedUuid; +// * pub type OrganizationKind {} +// * pub type OrganizationUuid = TypedUuid; +// * pub type ProjectKind {} +// * pub type ProjectUuid = TypedUuid; + +fn main() { + // Create some UUIDs of different types. + let user_uuid = UserUuid::new_v4(); + let org_uuid = OrganizationUuid::new_v4(); + let project_uuid = ProjectUuid::new_v4(); + + // Print the UUIDs and their tags. + println!("User UUID: {} (tag: {})", user_uuid, UserKind::tag()); + println!( + "Organization UUID: {} (tag: {})", + org_uuid, + OrganizationKind::tag() + ); + println!( + "Project UUID: {} (tag: {})", + project_uuid, + ProjectKind::tag() + ); + + // The compiler ensures type safety -- you can't accidentally mix up types. + // This would be a compile error: + // let _error: UserUuid = typed_org; // Error: mismatched types +} diff --git a/crates/newtype-uuid-macros/src/internals/error_store.rs b/crates/newtype-uuid-macros/src/internals/error_store.rs new file mode 100644 index 000000000..fe1a5e5a5 --- /dev/null +++ b/crates/newtype-uuid-macros/src/internals/error_store.rs @@ -0,0 +1,161 @@ +// Copyright 2025 Oxide Computer Company + +//! Handle lists of errors that occur while generating the proc macro. +//! +//! See the documentation of [`ErrorStore`] for more information. + +use std::cell::RefCell; + +/// Top-level struct that holds all errors encountered during the invocation of +/// a proc macro. +/// +/// This allows for collecting errors from multiple sources, and for tracking +/// errors in a hierarchical fashion. +#[derive(Debug)] +pub(crate) struct ErrorStore { + data: RefCell>, +} + +impl ErrorStore { + /// Create a new `ErrorStore`. + pub(crate) fn new() -> Self { + Self { + data: RefCell::new(ErrorStoreData::default()), + } + } + + /// Obtain the list of errors collected by this store. + /// + /// This consumes the store, and implies that there are no [`ErrorSink`] + /// instances that are still alive. + pub(crate) fn into_inner(self) -> Vec { + std::mem::take(&mut self.data.borrow_mut().errors) + } + + /// Create a new sink for collecting errors. + /// + /// This is a top-level sink, i.e. it has no parent. + pub(crate) fn sink(&mut self) -> ErrorSink<'_, T> { + let new_id = self.data.borrow_mut().register_sink(None); + ErrorSink { + data: &self.data, + id: new_id, + } + } +} + +/// A collector for errors. +/// +/// An `ErrorSink` is a context into which errors can be pushed. It can have +/// child `ErrorSink` instances, and the [`ErrorStore`] from which it is +/// ultimately derived tracks whether any errors were pushed to a given +/// `ErrorSink` or its descendants. +/// +/// The lifetime parameter `'a` is the lifetime of the `ErrorStore` that the +/// `ErrorSink` is ultimately derived from. The parameter ensures that +/// `ErrorSink` instances don't outlive the [`ErrorStore`] -- this means that at +/// the time an [`ErrorStore`] is consumed, there aren't any outstanding +/// `ErrorSink` instances. +#[derive(Debug)] +pub(crate) struct ErrorSink<'a, T> { + // It's a bit weird to use both a lifetime parameter and a RefCell, but it + // makes sense here. With `Rc>`, there's no way to statically + // guarantee that the error collection process is done. The lifetime + // parameter statically guarantees that. + // + // Do we need interior mutability? Because of our nested structure, the only + // other alternatives are some kind of `&mut &mut &mut ... T`, or dynamic + // dispatch. Both seem worse than just doing this. + data: &'a RefCell>, + id: usize, +} + +impl<'a, T> ErrorSink<'a, T> { + pub(crate) fn push_critical(&self, error: T) { + // This is always okay because we only briefly borrow the RefCell at any + // time. + self.data.borrow_mut().push_critical(self.id, error); + } + + #[allow(unused)] + pub(crate) fn push_warning(&self, error: T) { + // This is always okay because we only briefly borrow the RefCell at any + // time. + self.data.borrow_mut().push_warning(error); + } + + pub(crate) fn has_critical_errors(&self) -> bool { + // ErrorStore::push_critical_error propagates `has_critical_errors` up the tree while + // writing errors, so we can just check the current ID while reading + // this information. + self.data.borrow().sinks[self.id].has_critical_errors + } + + pub(crate) fn new_child(&self) -> ErrorSink<'a, T> { + let mut errors = self.data.borrow_mut(); + let new_id = errors.register_sink(Some(self.id)); + Self { + data: self.data, + id: new_id, + } + } +} + +#[derive(Debug)] +struct ErrorStoreData { + errors: Vec, + sinks: Vec, +} + +impl Default for ErrorStoreData { + fn default() -> Self { + Self { + errors: Vec::new(), + sinks: Vec::new(), + } + } +} + +impl ErrorStoreData { + /// Critical errors block progress + fn push_critical(&mut self, id: usize, error: T) { + self.errors.push(error); + self.sinks[id].has_critical_errors = true; + + // Propagate the fact that errors were encountered up the tree. + let mut curr = id; + while let Some(parent) = self.sinks[curr].parent { + self.sinks[parent].has_critical_errors = true; + curr = parent; + } + } + + /// Warning errors do not block progress + fn push_warning(&mut self, error: T) { + self.errors.push(error); + } + + fn register_sink(&mut self, parent: Option) -> usize { + // len is the next ID + let id = self.sinks.len(); + self.sinks.push(ErrorSinkData::new(parent)); + id + } +} + +#[derive(Debug)] +struct ErrorSinkData { + // The parent ID in the map. + parent: Option, + // Whether an error was pushed via this specific context or a descendant. + has_critical_errors: bool, +} + +impl ErrorSinkData { + fn new(parent: Option) -> Self { + Self { + parent, + has_critical_errors: false, + } + } +} diff --git a/crates/newtype-uuid-macros/src/internals/imp.rs b/crates/newtype-uuid-macros/src/internals/imp.rs new file mode 100644 index 000000000..f39b10ccc --- /dev/null +++ b/crates/newtype-uuid-macros/src/internals/imp.rs @@ -0,0 +1,433 @@ +use super::error_store::{ErrorSink, ErrorStore}; +use heck::ToSnakeCase; +use proc_macro2::{Delimiter, Span, TokenStream, TokenTree}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; +use serde::Deserialize; +use serde_tokenstream::{ + from_tokenstream, from_tokenstream_spanned, OrderedMap, ParseWrapper, TokenStreamWrapper, +}; +use syn::spanned::Spanned; + +pub struct ImplKindsOutput { + pub out: Option, + pub errors: Vec, +} + +impl ToTokens for ImplKindsOutput { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.extend(self.out.clone()); + tokens.extend(self.errors.iter().map(|error| error.to_compile_error())); + } +} + +pub fn impl_typed_uuid_kinds(input: TokenStream) -> ImplKindsOutput { + let params: ImplKindsParams = match from_tokenstream(&input) { + Ok(input) => input, + Err(error) => { + let errors = vec![error]; + return ImplKindsOutput { out: None, errors }; + } + }; + + let newtype_uuid_ident = syn::Ident::new("newtype_uuid", input.span()); + let newtype_uuid_crate = params + .settings + .newtype_uuid_crate + .as_ref() + .map_or_else(|| &newtype_uuid_ident, |crate_name| crate_name); + + let mut out = quote! {}; + + let mut error_store = ErrorStore::new(); + let errors = error_store.sink(); + + for (kind_tokens, config_tokens) in params.kinds { + let errors = errors.new_child(); + + let Some((root_ident, config)) = parse_kind( + kind_tokens.into_inner(), + config_tokens.into_inner(), + errors.new_child(), + ) else { + // A critical error occurred for this kind -- can't proceed any + // further. + continue; + }; + let Some(config) = config.validate(errors.new_child()) else { + // The config couldn't be parsed -- can't proceed any further. + continue; + }; + + let name = if let Some(tag) = &config.tag { + KindOrExplicitTag::Tag(tag) + } else { + KindOrExplicitTag::Kind(&root_ident) + }; + + // Validate the tag name here using the same logic as in newtype-uuid. + // Doing so in the proc macro results in better error messages. + validate_tag_name(&name, errors.new_child()); + if errors.has_critical_errors() { + // Don't generate output since it'll panic and lead to worse errors. + continue; + } + + let tag_name = name.tag_name(); + let kind_name_ident = config + .type_name + .unwrap_or_else(|| format_ident!("{}Kind", root_ident)); + let alias_ident = config + .alias + .unwrap_or_else(|| format_ident!("{}Uuid", root_ident)); + + let attrs = config.attrs.as_ref().unwrap_or(¶ms.settings.attrs); + let attrs = attrs.iter().map(|attr| &**attr); + + // Generate JsonSchema implementation if schemars08 settings are provided + let schemars_impl = if let Some(schemars_settings) = ¶ms.settings.schemars08 { + generate_schemars_impl( + &kind_name_ident, + &kind_name_ident.to_string(), + schemars_settings, + newtype_uuid_crate, + ) + } else { + quote! {} + }; + + let expanded = quote_spanned! {root_ident.span() => + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + #(#attrs)* + pub enum #kind_name_ident {} + + impl ::#newtype_uuid_crate::TypedUuidKind for #kind_name_ident { + #[inline] + fn tag() -> ::#newtype_uuid_crate::TypedUuidTag { + // `const` ensures that tags are validated at compile-time. + const TAG: ::#newtype_uuid_crate::TypedUuidTag = ::#newtype_uuid_crate::TypedUuidTag::new(#tag_name); + TAG + } + + fn alias() -> Option<&'static str> { + Some(stringify!(#alias_ident)) + } + } + + #schemars_impl + + #[allow(unused)] + pub type #alias_ident = ::#newtype_uuid_crate::TypedUuid<#kind_name_ident>; + }; + + out.extend(expanded); + } + + let errors = error_store.into_inner(); + ImplKindsOutput { + out: Some(out), + errors, + } +} + +fn parse_kind( + kind_tokens: TokenStream, + kind_config_tokens: TokenTree, + errors: ErrorSink<'_, syn::Error>, +) -> Option<(syn::Ident, KindConfig)> { + let kind_ident = match syn::parse2::(kind_tokens) { + Ok(ident) => Some(ident), + Err(err) => { + // Collect the error. + errors.push_critical(err); + None + } + }; + // tokens is surrounded by {} + let kind_config = match kind_config_tokens { + TokenTree::Group(group) => { + // group.delimiter() must be {} + if group.delimiter() == Delimiter::Brace { + match from_tokenstream_spanned::(&group.delim_span(), &group.stream()) { + Ok(config) => Some(config), + Err(err) => { + // Collect the error. + errors.push_critical(err); + None + } + } + } else { + // Collect the error. + errors.push_critical(syn::Error::new(group.span(), "expected `{`")); + None + } + } + _ => { + // Collect the error. + errors.push_critical(syn::Error::new(kind_config_tokens.span(), "expected `{`")); + None + } + }; + + if errors.has_critical_errors() { + None + } else { + Some(( + kind_ident.expect("no critical errors => kind is guaranteed to be Some"), + kind_config.expect("no critical errors => kind config is guaranteed to be Some"), + )) + } +} + +fn validate_tag_name(name: &KindOrExplicitTag<'_>, errors: ErrorSink<'_, syn::Error>) { + let tag_name = name.tag_name(); + let span = name.span(); + + let mut chars = tag_name.chars(); + let Some(first) = chars.next() else { + errors.push_critical(syn::Error::new( + span, + format!("tag name must not be empty{}", name.hint()), + )); + return; + }; + if !(first.is_ascii_alphabetic() || first == '_') { + errors.push_critical(syn::Error::new( + span, + format!( + "tag name `{tag_name}` must start with an ASCII letter or underscore{}", + name.hint(), + ), + )); + } + + for c in chars { + // Tag names can contain hyphens, but Rust identifiers cannot -- so we + // don't check for that here. (Once we allow setting custom tag names, + // we can check for that functionality.) + if !(c.is_ascii_alphanumeric() || c == '_') { + errors.push_critical(syn::Error::new( + span, + format!( + "tag name `{tag_name}` must consist of ASCII \ + alphanumeric characters or underscores{}", + name.hint() + ), + )); + } + } +} + +enum KindOrExplicitTag<'a> { + /// A kind name was specified and will be converted into the corresponding + /// tag name. + Kind(&'a syn::Ident), + + /// A tag name was specified and will be used as-is. + Tag(&'a syn::LitStr), +} + +impl<'a> KindOrExplicitTag<'a> { + fn tag_name(&self) -> String { + match self { + KindOrExplicitTag::Kind(kind_name) => kind_name.to_string().to_snake_case(), + KindOrExplicitTag::Tag(tag_name) => tag_name.value(), + } + } + + fn span(&self) -> Span { + match self { + KindOrExplicitTag::Kind(kind_name) => kind_name.span(), + KindOrExplicitTag::Tag(tag_name) => tag_name.span(), + } + } + + fn hint(&self) -> String { + match self { + KindOrExplicitTag::Kind(kind_name) => { + format!( + "\n(hint: tag name `{}` derived from kind name -- \ + specify `tag = \"...\" for a custom tag name`)", + kind_name.to_string().to_snake_case(), + ) + } + KindOrExplicitTag::Tag(_) => String::new(), + } + } +} + +/// Input structure for the `impl_typed_uuid_kinds` macro. +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ImplKindsParams { + #[serde(default)] + settings: GlobalSettings, + kinds: OrderedMap>, +} + +/// Global settings for the macro. +#[derive(Deserialize, Default)] +#[serde(deny_unknown_fields)] +struct GlobalSettings { + /// The name of the newtype-uuid crate. + #[serde(default)] + newtype_uuid_crate: Option>, + + /// Attributes to apply to generated types. + #[serde(default)] + attrs: Vec, + + /// Schemars configuration. + #[serde(default)] + schemars08: Option, +} + +/// Settings for schemars08 integration. +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct SchemarsSettings { + #[serde(default)] + attrs: Vec, + rust_type: RustTypeSettings, +} + +/// Settings for the x-rust-type extension. +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct RustTypeSettings { + #[serde(rename = "crate")] + crate_name: String, + version: String, + path: String, +} + +/// Configuration for each kind. +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct KindConfig { + /// The type name for this kind. + #[serde(default)] + type_name: Option, + + /// The type alias for the kind. + alias: Option, + + /// The tag for this kind. Defaults to the snake_case name of the kind. + #[serde(default)] + tag: Option, + + /// Attributes to apply to generated types (e.g. derives). + #[serde(default)] + attrs: Option>, +} + +impl KindConfig { + fn validate(self, errors: ErrorSink<'_, syn::Error>) -> Option { + // Parse the type name as an Ident. + let type_name = match self.type_name { + Some(type_name) => match syn::parse2::(type_name.into_inner()) { + Ok(ident) => Ok(Some(ident)), + Err(error) => { + errors.push_critical(error); + Err(()) + } + }, + None => Ok(None), + }; + // Parse the alias as an Ident. + let alias = match self.alias { + Some(alias) => match syn::parse2::(alias.into_inner()) { + Ok(ident) => Ok(Some(ident)), + Err(error) => { + errors.push_critical(error); + Err(()) + } + }, + None => Ok(None), + }; + // Parse the tag as a LitStr. + let tag = match self.tag { + Some(tag) => match syn::parse2::(tag.into_inner()) { + Ok(lit_str) => Ok(Some(lit_str)), + Err(error) => { + errors.push_critical(error); + Err(()) + } + }, + None => Ok(None), + }; + + if errors.has_critical_errors() { + None + } else { + // All parsed values are valid if present. + Some(ParsedKindConfig { + type_name: type_name.expect("type name is valid"), + alias: alias.expect("alias is valid"), + tag: tag.expect("tag is valid"), + attrs: self.attrs, + }) + } + } +} + +struct ParsedKindConfig { + type_name: Option, + alias: Option, + tag: Option, + attrs: Option>, +} + +/// Generate a hand-written JsonSchema implementation for a kind. +fn generate_schemars_impl( + kind_name_ident: &syn::Ident, + kind_name: &str, + schemars_settings: &SchemarsSettings, + newtype_uuid_crate: &syn::Ident, +) -> proc_macro2::TokenStream { + let attrs = schemars_settings.attrs.iter().map(|attrs| &**attrs); + let crate_name = &schemars_settings.rust_type.crate_name; + let version = &schemars_settings.rust_type.version; + let path_prefix = &schemars_settings.rust_type.path; + + // Construct the full path for this specific kind. + let full_path = format!("{}::{}", path_prefix, kind_name_ident); + + quote! { + #(#attrs)* + impl ::#newtype_uuid_crate::macro_support::schemars08::JsonSchema for #kind_name_ident { + fn schema_name() -> ::std::string::String { + #kind_name.to_string() + } + + fn schema_id() -> ::std::borrow::Cow<'static, str> { + ::std::borrow::Cow::Borrowed(#full_path) + } + + fn json_schema( + _gen: &mut ::#newtype_uuid_crate::macro_support::schemars08::gen::SchemaGenerator, + ) -> ::#newtype_uuid_crate::macro_support::schemars08::schema::Schema { + use ::#newtype_uuid_crate::macro_support::schemars08::schema::*; + + let mut schema = SchemaObject { + subschemas: ::std::option::Option::Some(Box::new(SubschemaValidation { + not: ::std::option::Option::Some(Box::new(Schema::Bool(true))), + ..::std::default::Default::default() + })), + ..::std::default::Default::default() + }; + + // Add the x-rust-type extension. + let mut extensions = ::#newtype_uuid_crate::macro_support::schemars08::Map::new(); + let rust_type = ::#newtype_uuid_crate::macro_support::serde_json::json!({ + "crate": #crate_name, + "version": #version, + "path": #full_path, + }); + extensions.insert("x-rust-type".to_string(), rust_type); + schema.extensions = extensions; + + Schema::Object(schema) + } + } + } +} diff --git a/crates/newtype-uuid-macros/src/internals/mod.rs b/crates/newtype-uuid-macros/src/internals/mod.rs new file mode 100644 index 000000000..40660a2c7 --- /dev/null +++ b/crates/newtype-uuid-macros/src/internals/mod.rs @@ -0,0 +1,8 @@ +//! Internal implementation for newtype-uuid-macros. +//! +//! This module is imported by both the proc macro and by snapshot tests. + +mod error_store; +mod imp; + +pub use imp::*; diff --git a/crates/newtype-uuid-macros/src/lib.rs b/crates/newtype-uuid-macros/src/lib.rs new file mode 100644 index 000000000..a3393e678 --- /dev/null +++ b/crates/newtype-uuid-macros/src/lib.rs @@ -0,0 +1,231 @@ +//! Procedural macro for [`newtype-uuid`](https://docs.rs/newtype-uuid). +//! +//! This crate provides a procedural macro to help with creating +//! [`newtype-uuid`](https://docs.rs/newtype-uuid) instances. +//! +//! For more information, see the documentation for [`impl_typed_uuid_kinds!`]. +//! +//! # Examples +//! +//! Basic usage: +//! +//! ``` +//! use newtype_uuid::TypedUuidKind; +//! use newtype_uuid_macros::impl_typed_uuid_kinds; +//! +//! impl_typed_uuid_kinds! { +//! kinds = { +//! User = {}, +//! Organization = {}, +//! }, +//! } +//! +//! // This generates empty UserKind and OrganizationKind enums implementing +//! // TypedUuidKind, with the tags "user" and "organization" respectively. +//! // Tags are snake_case versions of type names. +//! assert_eq!(UserKind::tag().as_str(), "user"); +//! assert_eq!(OrganizationKind::tag().as_str(), "organization"); +//! +//! // The macro also generates UserUuid and OrganizationUuid type aliases. +//! let user_uuid = UserUuid::new_v4(); +//! let organization_uuid = OrganizationUuid::new_v4(); +//! ``` +//! +//! For more details and examples, see the documentation for +//! [`impl_typed_uuid_kinds!`]. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +mod internals; + +use proc_macro::TokenStream; +use quote::ToTokens; + +/// A function-like procedural macro for implementing typed UUID kinds. +/// +/// This macro generates types that implement `TypedUuidKind` and corresponding +/// type aliases for `TypedUuid`. The macro provides an easy way to generate +/// typed UUID kinds in bulk, and also to implement `JsonSchema` support with +/// schemars 0.8. +/// +/// # Basic usage +/// +/// Invoke the `impl_typed_uuid_kinds!` macro within a path that's publicly +/// visible. A dedicated crate for UUID kinds is recommended. +/// +/// By default, for a kind `Foo`, this macro generates: +/// +/// * A `FooKind` type that implements `TypedUuidKind`: `pub enum FooKind {}`. +/// * A `FooUuid` type alias: `pub type FooUuid = TypedUuid;`. +/// +/// ## Examples +/// +/// ``` +/// use newtype_uuid::TypedUuidKind; +/// use newtype_uuid_macros::impl_typed_uuid_kinds; +/// +/// impl_typed_uuid_kinds! { +/// kinds = { +/// User = {}, +/// BusinessUnit = {}, +/// }, +/// } +/// +/// // This generates empty UserKind and BusinessUnitKind enums implementing +/// // TypedUuidKind, with the tags "user" and "business_unit" respectively. +/// // Tags are snake_case versions of type names. +/// assert_eq!(UserKind::tag().as_str(), "user"); +/// assert_eq!(BusinessUnitKind::tag().as_str(), "business_unit"); +/// +/// // The macro also generates UserUuid and BusinessUnitUuid type aliases. +/// let user_uuid = UserUuid::new_v4(); +/// let business_unit_uuid = BusinessUnitUuid::new_v4(); +/// ``` +/// +/// * The generated `Kind` types always implement `Clone`, `Copy`, `Debug`, +/// `Eq`, and `PartialEq`. +/// * The `Kind` types are all empty enums, also known as *marker* or +/// *uninhabited* enums. This means that values for these types cannot be +/// created. (Using empty enums is the recommended approach for +/// `newtype-uuid`). +/// +/// # Per-kind settings +/// +/// Kinds can be customized with the following settings: +/// +/// - `attrs`: Attributes to apply to the kind enum, such as +/// `#[derive(SomeTrait)]` or `#[cfg(feature = "some-feature")]`. *Optional, +/// defaults to the global `attrs`.* +/// - `tag`: The tag to use for the kind (a string literal). *Optional, defaults +/// to the snake_case version of the type name.* +/// - `type_name`: The name of the type to use for the kind (a Rust identifier). +/// *Optional, defaults to `{Name}Kind`*. +/// - `alias`: The name of the type alias to use for the kind (a Rust +/// identifier). *Optional, defaults to `{Name}Uuid`*. +/// +/// Per-kind customizations should generally be unnecessary; the conventionally +/// generated type names should be sufficient for most use cases. +/// +/// ## Examples +/// +/// In this example, we derive `PartialOrd` and `Ord` for `MyUserKind`. +/// +/// ``` +/// use newtype_uuid::TypedUuidKind; +/// use newtype_uuid_macros::impl_typed_uuid_kinds; +/// +/// impl_typed_uuid_kinds! { +/// kinds = { +/// User = { +/// // This is a list of attributes surrounded by square brackets. +/// attrs = [#[derive(PartialOrd, Ord)]], +/// tag = "user", +/// type_name = MyUserKind, +/// }, +/// Organization = { tag = "org", alias = OrgUuid }, +/// }, +/// } +/// +/// // This generates types with the specified names: +/// assert_eq!(MyUserKind::tag().as_str(), "user"); +/// assert_eq!(OrganizationKind::tag().as_str(), "org"); +/// +/// let user_uuid = UserUuid::new_v4(); +/// let org_uuid = OrgUuid::new_v4(); +/// +/// // MyUserKind also implements `Ord`. +/// static_assertions::assert_impl_all!(MyUserKind: Ord); +/// ``` +/// +/// # Global settings +/// +/// This macro accepts global settings under a top-level `settings` map: +/// +/// - `attrs`: A list of attributes to apply to all generated `Kind` types. +/// Per-kind attributes, if provided, will override these. *Optional, defaults +/// to the empty list.* +/// - `newtype_uuid_crate`: The name of the `newtype-uuid` crate (a Rust +/// identifier). *Optional, defaults to `newtype_uuid`.* +/// - `schemars08`: If defined, generates JSON Schema support for the given +/// types using [`schemars` 0.8]. *Optional.* +/// +/// ## JSON Schema support +/// +/// If the `schemars08` global setting is defined, the macro generates JSON +/// Schema support for the `Kind` instances using [schemars 0.8]. +/// +/// **To enable JSON Schema support, you'll need to enable `newtype-uuid`'s +/// `schemars08` feature.** +/// +/// Within `settings.schemars08`, the options are: +/// +/// - `attrs`: A list of attributes to apply to all generated `JsonSchema` +/// implementations. For example, if `schemars` is an optional dependency +/// for the crate where the macro is being invoked, you might specify something +/// like `[#[cfg(feature = "schemars")]]`. +/// - `rust_type`: If defined, adds the `x-rust-type` extension to the schema, +/// enabling automatic replacement with [`typify`] and other tools that +/// support it. *Optional, defaults to not adding the extension.* +/// +/// Automatic replacement enables an end-to-end workflow where the same UUID +/// kinds can be shared between servers and clients. +/// +/// `rust_type` is a map of the following options: +/// +/// - `crate`: The crate name consumers will use to access these types. +/// *Required.* +/// - `version`: The versions of the crate that automatic replacement is +/// supported for. *Required.* +/// - `path`: The path to the module these types can be accessed from, +/// including the crate name. *Required.* +/// +/// For more about `x-rust-type`, see the [`typify` documentation]. +/// +/// [`schemars` 0.8]: https://docs.rs/schemars/0.8/schemars/ +/// [`typify`]: https://docs.rs/typify +/// [`typify` documentation]: +/// https://github.com/oxidecomputer/typify#rust---schema---rust +/// +/// ## Examples +/// +/// An example with all global settings defined: +/// +/// ``` +/// use newtype_uuid::TypedUuidKind; +/// use newtype_uuid_macros::impl_typed_uuid_kinds; +/// +/// impl_typed_uuid_kinds! { +/// settings = { +/// attrs = [#[derive(PartialOrd, Ord)]], +/// newtype_uuid_crate = newtype_uuid, +/// schemars08 = { +/// attrs = [#[cfg(feature = "schemars")]], +/// rust_type = { +/// crate = "my-crate", +/// version = "0.1.0", +/// path = "my_crate::types", +/// }, +/// }, +/// }, +/// kinds = { +/// User = {}, +/// Organization = {}, +/// Project = {}, +/// }, +/// } +/// +/// let user_uuid = UserUuid::new_v4(); +/// let org_uuid = OrganizationUuid::new_v4(); +/// let project_uuid = ProjectUuid::new_v4(); +/// ``` +/// +/// For a working end-to-end example, see the +/// [`e2e-example`](https://github.com/oxidecomputer/newtype-uuid/tree/main/e2e-example) +/// directory in the newtype-uuid repository. +#[proc_macro] +pub fn impl_typed_uuid_kinds(input: TokenStream) -> TokenStream { + internals::impl_typed_uuid_kinds(input.into()) + .into_token_stream() + .into() +} diff --git a/crates/newtype-uuid-macros/tests/integration.rs b/crates/newtype-uuid-macros/tests/integration.rs new file mode 100644 index 000000000..df3fc52f4 --- /dev/null +++ b/crates/newtype-uuid-macros/tests/integration.rs @@ -0,0 +1,109 @@ +use newtype_uuid::TypedUuidKind; +use newtype_uuid_macros::impl_typed_uuid_kinds; +use static_assertions::{assert_impl_all, assert_not_impl_all}; +use std::{fmt, hash::Hash}; + +impl_typed_uuid_kinds! { + kinds = { + User = {}, + Organization = {}, + Project = {}, + } +} + +#[test] +fn test_generated_kinds() { + // Test that the generated kinds have the correct tags. + assert_eq!(UserKind::tag().as_str(), "user"); + assert_eq!(OrganizationKind::tag().as_str(), "organization"); + assert_eq!(ProjectKind::tag().as_str(), "project"); +} + +#[test] +fn test_single_entry() { + impl_typed_uuid_kinds! { + kinds = { + Single = {}, + } + } + + assert_eq!(SingleKind::tag().as_str(), "single"); + let single_uuid = SingleUuid::new_v4(); + assert_eq!(single_uuid.get_version_num(), 4); +} + +#[test] +fn test_snake_case_conversion() { + impl_typed_uuid_kinds! { + kinds = { + // The Rust convention is to use `HttpClient`, `XmlParser`, etc, but + // ensure that these all-caps variants also have the correct tags. + HTTPClient = {}, + XMLParser = {}, + UserAccount = {}, + IOHandler = {}, + } + } + + assert_eq!(HTTPClientKind::tag().as_str(), "http_client"); + assert_eq!(XMLParserKind::tag().as_str(), "xml_parser"); + assert_eq!(UserAccountKind::tag().as_str(), "user_account"); + assert_eq!(IOHandlerKind::tag().as_str(), "io_handler"); +} + +#[test] +fn test_empty_kinds() { + // Test that we can handle an empty kinds map. + impl_typed_uuid_kinds! { + kinds = {} + } +} + +/// Test that global `attrs` are applied to all generated kinds. +#[test] +fn test_global_attrs_param() { + impl_typed_uuid_kinds! { + settings = { + attrs = [#[derive(Hash)]], + }, + kinds = { + GlobalA = {}, + GlobalB = {}, + } + } + + // The GlobalA and GlobalB kinds should impl Clone + Copy + fmt::Debug + Eq + // by default, and also implement Hash. + assert_impl_all!(GlobalAKind: Clone, Copy, fmt::Debug, Eq, Hash); + assert_impl_all!(GlobalBKind: Clone, Copy, fmt::Debug, Eq, Hash); +} + +/// Test that per-kind `attrs` override global ones and are applied only to the +/// correct kind. +#[test] +fn test_per_kind_attrs_param() { + impl_typed_uuid_kinds! { + settings = { + attrs = [#[derive(Hash)]], + }, + kinds = { + A = {}, + B = { + attrs = [], + }, + C = { + attrs = [#[derive(Ord, PartialOrd)]], + }, + } + } + + // AKind should implement Hash. + assert_impl_all!(AKind: Hash); + + // BKind should not implement Hash. + assert_not_impl_all!(BKind: Hash); + + // CKind should implement Ord and PartialOrd, but not Hash. + assert_impl_all!(CKind: Ord, PartialOrd); + assert_not_impl_all!(CKind: Hash); +} diff --git a/crates/newtype-uuid/Cargo.toml b/crates/newtype-uuid/Cargo.toml index 90b64d71b..39bc289de 100644 --- a/crates/newtype-uuid/Cargo.toml +++ b/crates/newtype-uuid/Cargo.toml @@ -5,13 +5,16 @@ edition.workspace = true license.workspace = true repository.workspace = true description = "Newtype wrapper around UUIDs" -documentation.workspace = true +documentation = "https://docs.rs/newtype-uuid" readme = "README.md" keywords = ["uuid", "unique", "guid", "newtype"] categories = ["data-structures", "no-std"] rust-version.workspace = true exclude = [".cargo/**/*", ".github/**/*", "scripts/**/*"] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg=doc_cfg"] @@ -19,16 +22,20 @@ rustdoc-args = ["--cfg=doc_cfg"] [dependencies] proptest = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"], optional = true } +serde_json = { workspace = true, optional = true } schemars = { workspace = true, features = ["uuid1"], optional = true } uuid.workspace = true +[dev-dependencies] +newtype-uuid-macros.workspace = true + [features] default = ["uuid/default", "std"] std = ["alloc", "uuid/std"] alloc = [] v4 = ["uuid/v4"] serde = ["dep:serde", "uuid/serde"] -schemars08 = ["dep:schemars", "std"] +schemars08 = ["dep:schemars", "dep:serde_json", "std"] proptest1 = ["dep:proptest"] [package.metadata.cargo-sync-rdme.badge.badges] diff --git a/crates/newtype-uuid/README.md b/crates/newtype-uuid/README.md index 43a5f6a18..3159de585 100644 --- a/crates/newtype-uuid/README.md +++ b/crates/newtype-uuid/README.md @@ -5,7 +5,7 @@ ![License: MIT OR Apache-2.0](https://img.shields.io/crates/l/newtype-uuid.svg?) [![crates.io](https://img.shields.io/crates/v/newtype-uuid.svg?logo=rust)](https://crates.io/crates/newtype-uuid) [![docs.rs](https://img.shields.io/docsrs/newtype-uuid.svg?logo=docs.rs)](https://docs.rs/newtype-uuid) -[![Rust: ^1.67.0](https://img.shields.io/badge/rust-^1.67.0-93450a.svg?logo=rust)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) +[![Rust: ^1.79.0](https://img.shields.io/badge/rust-^1.79.0-93450a.svg?logo=rust)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) A newtype wrapper around [`Uuid`](https://docs.rs/uuid/1.17.0/uuid/struct.Uuid.html). @@ -50,11 +50,29 @@ assert_eq!( ); ```` -If you have a large number of UUID kinds, consider defining a macro for your purposes. An -example macro: +If you have a large number of UUID kinds, consider using +[`newtype-uuid-macros`] which comes with several convenience features: ````rust -macro_rules! impl_typed_uuid_kind { +use newtype_uuid_macros::impl_typed_uuid_kinds; + +// Invoke this macro with: +impl_typed_uuid_kinds! { + kinds = { + User = {}, + Project = {}, + // ... + }, +} +```` + +See [`newtype-uuid-macros`] for more information. + +For simpler cases, you can also write your own declarative macro. Use this +template to get started: + +````rust +macro_rules! impl_kinds { ($($kind:ident => $tag:literal),* $(,)?) => { $( pub enum $kind {} @@ -71,25 +89,25 @@ macro_rules! impl_typed_uuid_kind { } // Invoke this macro with: -impl_typed_uuid_kind! { - Kind1 => "kind1", - Kind2 => "kind2", +impl_kinds! { + UserKind => "user", + ProjectKind => "project", } ```` ## Implementations -In general, [`TypedUuid`](https://docs.rs/newtype-uuid/1.2.2/newtype_uuid/struct.TypedUuid.html) uses the same wire and serialization formats as [`Uuid`](https://docs.rs/uuid/1.17.0/uuid/struct.Uuid.html). This means -that persistent representations of [`TypedUuid`](https://docs.rs/newtype-uuid/1.2.2/newtype_uuid/struct.TypedUuid.html) are the same as [`Uuid`](https://docs.rs/uuid/1.17.0/uuid/struct.Uuid.html); [`TypedUuid`](https://docs.rs/newtype-uuid/1.2.2/newtype_uuid/struct.TypedUuid.html) is +In general, [`TypedUuid`](https://docs.rs/newtype-uuid/1.2.4/newtype_uuid/struct.TypedUuid.html) uses the same wire and serialization formats as [`Uuid`](https://docs.rs/uuid/1.17.0/uuid/struct.Uuid.html). This means +that persistent representations of [`TypedUuid`](https://docs.rs/newtype-uuid/1.2.4/newtype_uuid/struct.TypedUuid.html) are the same as [`Uuid`](https://docs.rs/uuid/1.17.0/uuid/struct.Uuid.html); [`TypedUuid`](https://docs.rs/newtype-uuid/1.2.4/newtype_uuid/struct.TypedUuid.html) is intended to be helpful within Rust code, not across serialization boundaries. * The `Display` and `FromStr` impls are forwarded to the underlying [`Uuid`](https://docs.rs/uuid/1.17.0/uuid/struct.Uuid.html). * If the `serde` feature is enabled, `TypedUuid` will serialize and deserialize using the same format as [`Uuid`](https://docs.rs/uuid/1.17.0/uuid/struct.Uuid.html). -* If the `schemars08` feature is enabled, [`TypedUuid`](https://docs.rs/newtype-uuid/1.2.2/newtype_uuid/struct.TypedUuid.html) will implement `JsonSchema` if the - corresponding [`TypedUuidKind`](https://docs.rs/newtype-uuid/1.2.2/newtype_uuid/trait.TypedUuidKind.html) implements `JsonSchema`. +* If the `schemars08` feature is enabled, [`TypedUuid`](https://docs.rs/newtype-uuid/1.2.4/newtype_uuid/struct.TypedUuid.html) will implement `JsonSchema` if the + corresponding [`TypedUuidKind`](https://docs.rs/newtype-uuid/1.2.4/newtype_uuid/trait.TypedUuidKind.html) implements `JsonSchema`. -To abstract over typed and untyped UUIDs, the [`GenericUuid`](https://docs.rs/newtype-uuid/1.2.2/newtype_uuid/trait.GenericUuid.html) trait is provided. This trait also +To abstract over typed and untyped UUIDs, the [`GenericUuid`](https://docs.rs/newtype-uuid/1.2.4/newtype_uuid/trait.GenericUuid.html) trait is provided. This trait also permits conversions between typed and untyped UUIDs. ## Dependencies @@ -111,7 +129,7 @@ permits conversions between typed and untyped UUIDs. ## Minimum supported Rust version (MSRV) -The MSRV of this crate is **Rust 1.67.** In general, this crate will follow the MSRV of the +The MSRV of this crate is **Rust 1.79.** In general, this crate will follow the MSRV of the underlying `uuid` crate or of dependencies, with an aim to be conservative. Within the 1.x series, MSRV updates will be accompanied by a minor version bump. The MSRVs for @@ -120,11 +138,14 @@ each minor version are: * Version **1.0.x**: Rust 1.60. * Version **1.1.x**: Rust 1.61. This permits `TypedUuid` to have `const fn` methods. * Version **1.2.x**: Rust 1.67, required by some dependency updates. +* Unreleased: Rust 1.79, required by some dependency updates. ## Alternatives * [`typed-uuid`](https://crates.io/crates/typed-uuid): generally similar, but with a few design decisions that are different. + +[`newtype-uuid-macros`]: https://docs.rs/newtype-uuid-macros ## License diff --git a/crates/newtype-uuid/build.rs b/crates/newtype-uuid/build.rs deleted file mode 100644 index b42975263..000000000 --- a/crates/newtype-uuid/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -fn main() { - // Ideally this would be in the [lints] table in Cargo.toml to avoid a build script, but sadly - // the MSRV for that is Rust 1.75. - // - // TODO: switch to [lints] configuration once the MSRV moves beyond that`. - println!("cargo:rustc-check-cfg=cfg(doc_cfg)"); -} diff --git a/crates/newtype-uuid/src/lib.rs b/crates/newtype-uuid/src/lib.rs index 62cec831d..dcdfba661 100644 --- a/crates/newtype-uuid/src/lib.rs +++ b/crates/newtype-uuid/src/lib.rs @@ -11,7 +11,7 @@ //! //! # Example //! -//! ```rust +//! ``` //! use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag}; //! //! // First, define a type that represents the kind of UUID this is. @@ -40,12 +40,32 @@ //! ); //! ``` //! -//! If you have a large number of UUID kinds, consider defining a macro for your purposes. An -//! example macro: +//! If you have a large number of UUID kinds, consider using +//! [`newtype-uuid-macros`] which comes with several convenience features. +//! +//! ``` +//! use newtype_uuid_macros::impl_typed_uuid_kinds; +//! +//! // Invoke this macro with: +//! impl_typed_uuid_kinds! { +//! kinds = { +//! User = {}, +//! Project = {}, +//! // ... +//! }, +//! } +//! ``` +//! +//! See [`newtype-uuid-macros`] for more information. +//! +//! [`newtype-uuid-macros`]: https://docs.rs/newtype-uuid-macros +//! +//! For simpler cases, you can also write your own declarative macro. Use this +//! template to get started: //! //! ```rust //! # use newtype_uuid::{TypedUuidKind, TypedUuidTag}; -//! macro_rules! impl_typed_uuid_kind { +//! macro_rules! impl_kinds { //! ($($kind:ident => $tag:literal),* $(,)?) => { //! $( //! pub enum $kind {} @@ -62,9 +82,9 @@ //! } //! //! // Invoke this macro with: -//! impl_typed_uuid_kind! { -//! Kind1 => "kind1", -//! Kind2 => "kind2", +//! impl_kinds! { +//! UserKind => "user", +//! ProjectKind => "project", //! } //! ``` //! @@ -102,7 +122,7 @@ //! //! # Minimum supported Rust version (MSRV) //! -//! The MSRV of this crate is **Rust 1.67.** In general, this crate will follow the MSRV of the +//! The MSRV of this crate is **Rust 1.79.** In general, this crate will follow the MSRV of the //! underlying `uuid` crate or of dependencies, with an aim to be conservative. //! //! Within the 1.x series, MSRV updates will be accompanied by a minor version bump. The MSRVs for @@ -111,6 +131,7 @@ //! * Version **1.0.x**: Rust 1.60. //! * Version **1.1.x**: Rust 1.61. This permits `TypedUuid` to have `const fn` methods. //! * Version **1.2.x**: Rust 1.67, required by some dependency updates. +//! * Unreleased: Rust 1.79, required by some dependency updates. //! //! # Alternatives //! @@ -125,6 +146,19 @@ #[cfg(feature = "alloc")] extern crate alloc; +/// Macro support for [`newtype-uuid-macros`]. +/// +/// This module re-exports types needed for [`newtype-uuid-macros`] to work. +/// +/// [`newtype-uuid-macros`]: https://docs.rs/newtype-uuid-macros +#[doc(hidden)] +pub mod macro_support { + #[cfg(feature = "schemars08")] + pub use schemars as schemars08; + #[cfg(feature = "schemars08")] + pub use serde_json; +} + use core::{ cmp::Ordering, fmt, @@ -468,20 +502,33 @@ impl From> for alloc::vec::Vec { #[cfg(feature = "schemars08")] mod schemars08_imp { use super::*; - use schemars::JsonSchema; + use schemars::{ + schema::{InstanceType, Schema, SchemaObject}, + schema_for, JsonSchema, SchemaGenerator, + }; + + const CRATE_NAME: &str = "newtype-uuid"; + const CRATE_VERSION: &str = "1"; + const CRATE_PATH: &str = "newtype_uuid::TypedUuid"; /// Implements `JsonSchema` for `TypedUuid`, if `T` implements `JsonSchema`. /// /// * `schema_name` is set to `"TypedUuidFor"`, concatenated by the schema name of `T`. /// * `schema_id` is set to `format!("newtype_uuid::TypedUuid<{}>", T::schema_id())`. - /// * `json_schema` is the same as the one for `Uuid`. + /// * `json_schema` is the same as the one for `Uuid`, with the `x-rust-type` extension + /// to allow automatic replacement in typify and progenitor. impl JsonSchema for TypedUuid where T: TypedUuidKind + JsonSchema, { #[inline] fn schema_name() -> String { - format!("TypedUuidFor{}", T::schema_name()) + // Use the alias if available, otherwise generate our own schema name. + if let Some(alias) = T::alias() { + alias.to_owned() + } else { + format!("TypedUuidFor{}", T::schema_name()) + } } #[inline] @@ -490,10 +537,84 @@ mod schemars08_imp { } #[inline] - fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { - Uuid::json_schema(gen) + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + // Look at the schema for `T`. If it has `x-rust-type`, *and* if an + // alias is available, we can lift up the `x-rust-type` into our own schema. + // + // We use a new schema generator for `T` to avoid T's schema being + // added to the list of schemas in `generator` in case the lifting + // is successful. + let t_schema = schema_for!(T); + if let Some(schema) = lift_json_schema(&t_schema.schema, T::alias()) { + return schema.into(); + } + + SchemaObject { + instance_type: Some(InstanceType::String.into()), + format: Some("uuid".to_string()), + extensions: [( + "x-rust-type".to_string(), + serde_json::json!({ + "crate": CRATE_NAME, + "version": CRATE_VERSION, + "path": CRATE_PATH, + "parameters": [generator.subschema_for::()] + }), + )] + .into_iter() + .collect(), + ..Default::default() + } + .into() } } + + // ? on Option is too easy to make mistakes with, so we use `let Some(..) = + // .. else` instead. + #[allow(clippy::question_mark)] + fn lift_json_schema(schema: &SchemaObject, alias: Option<&str>) -> Option { + let Some(alias) = alias else { + return None; + }; + + let Some(v) = schema.extensions.get("x-rust-type") else { + return None; + }; + + // The crate, version and path must all be present. + let Some(crate_) = v.get("crate") else { + return None; + }; + let Some(version) = v.get("version") else { + return None; + }; + let Some(path) = v.get("path").and_then(|p| p.as_str()) else { + return None; + }; + let Some((module_path, _)) = path.rsplit_once("::") else { + return None; + }; + + // The preconditions are all met. We can lift the schema by appending + // the alias to the module path. + let alias_path = format!("{module_path}::{alias}"); + + Some(SchemaObject { + instance_type: Some(InstanceType::String.into()), + format: Some("uuid".to_string()), + extensions: [( + "x-rust-type".to_string(), + serde_json::json!({ + "crate": crate_, + "version": version, + "path": alias_path, + }), + )] + .into_iter() + .collect(), + ..Default::default() + }) + } } #[cfg(feature = "proptest1")] @@ -544,37 +665,25 @@ mod proptest1_imp { /// If the `schemars08` feature is enabled, and [`JsonSchema`] is implemented for a kind `T`, then /// [`TypedUuid`]`` will also implement [`JsonSchema`]. /// -/// # Notes -/// -/// If you have a large number of UUID kinds, it can be repetitive to implement this trait for each -/// kind. Here's a template for a macro that can help: +/// If you have a large number of UUID kinds, consider using +/// [`newtype-uuid-macros`] which comes with several convenience features. /// /// ``` -/// use newtype_uuid::{TypedUuidKind, TypedUuidTag}; -/// -/// macro_rules! impl_typed_uuid_kind { -/// ($($kind:ident => $tag:literal),* $(,)?) => { -/// $( -/// pub enum $kind {} -/// -/// impl TypedUuidKind for $kind { -/// #[inline] -/// fn tag() -> TypedUuidTag { -/// const TAG: TypedUuidTag = TypedUuidTag::new($tag); -/// TAG -/// } -/// } -/// )* -/// }; -/// } +/// use newtype_uuid_macros::impl_typed_uuid_kinds; /// /// // Invoke this macro with: -/// impl_typed_uuid_kind! { -/// Kind1 => "kind1", -/// Kind2 => "kind2", +/// impl_typed_uuid_kinds! { +/// kinds = { +/// User = {}, +/// Project = {}, +/// // ... +/// }, /// } /// ``` /// +/// See [`newtype-uuid-macros`] for more information. +/// +/// [`newtype-uuid-macros`]: https://docs.rs/newtype-uuid-macros /// [`JsonSchema`]: schemars::JsonSchema pub trait TypedUuidKind: Send + Sync + 'static { /// Returns the corresponding tag for this kind. @@ -583,6 +692,21 @@ pub trait TypedUuidKind: Send + Sync + 'static { /// /// The tag is required to be a static string. fn tag() -> TypedUuidTag; + + /// Returns a string that corresponds to a type alias for `TypedUuid`, + /// if one is defined. + /// + /// The type alias must be defined in the same module as `Self`. This + /// function is used by the schemars integration to refer to embed a + /// reference to that alias in the schema, if available. + /// + /// This is usually defined by the [`newtype-uuid-macros`] crate. + /// + /// [`newtype-uuid-macros`]: https://docs.rs/newtype-uuid-macros + #[inline] + fn alias() -> Option<&'static str> { + None + } } /// Describes what kind of [`TypedUuid`] something is. diff --git a/e2e-example/README.md b/e2e-example/README.md new file mode 100644 index 000000000..433d1f48d --- /dev/null +++ b/e2e-example/README.md @@ -0,0 +1,23 @@ +# End-to-end schema example + +This directory contains an end-to-end example of automatic schema generation and replacement across a JSON Schema boundary. + +This example consists of three crates: + +1. [**`e2e-kinds`**](e2e-kinds): The base crate that defines the kinds. Both the producer and the consumer depend on this crate. +2. [**`e2e-schema-producer`**](e2e-schema-producer): This crate generates the schema. In a client-server context, this is typically the server. +3. [**`e2e-schema-consumer`**](e2e-schema-consumer): This crate converts the schema into Rust types. In a client-server context, this is typically the client. + +Here's the dependency graph: + +```mermaid +flowchart TD + subgraph "Rust crates" + A[e2e-schema-producer] + B[e2e-schema-consumer] + A & B --> C[e2e-kinds] + end + schema[JSON schema] + schema -.->|generated by| A + schema -.->|consumed by| B +``` diff --git a/e2e-example/e2e-kinds/Cargo.toml b/e2e-example/e2e-kinds/Cargo.toml new file mode 100644 index 000000000..d58bf239b --- /dev/null +++ b/e2e-example/e2e-kinds/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "e2e-kinds" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +newtype-uuid = { workspace = true, features = ["schemars08", "serde", "v4"] } +newtype-uuid-macros.workspace = true +serde = { workspace = true, features = ["derive"] } + +[features] +schemars08 = ["newtype-uuid/schemars08"] diff --git a/e2e-example/e2e-kinds/README.md b/e2e-example/e2e-kinds/README.md new file mode 100644 index 000000000..819b47d99 --- /dev/null +++ b/e2e-example/e2e-kinds/README.md @@ -0,0 +1,9 @@ + +# e2e-kinds + + +End-to-end UUID kinds definitions. + +This crate defines the UUID kinds used by both the producer and consumer +in the end-to-end JSON schema example. + diff --git a/e2e-example/e2e-kinds/src/lib.rs b/e2e-example/e2e-kinds/src/lib.rs new file mode 100644 index 000000000..edd2fee9d --- /dev/null +++ b/e2e-example/e2e-kinds/src/lib.rs @@ -0,0 +1,36 @@ +//! End-to-end UUID kinds definitions. +//! +//! This crate defines the UUID kinds used by both the producer and consumer +//! in the end-to-end JSON schema example. + +use newtype_uuid_macros::impl_typed_uuid_kinds; + +// Use the newtype-uuid-macros crate to define typed UUID kinds. +impl_typed_uuid_kinds! { + settings = { + // Defining the `schemars08` entry causes the kinds to implement + // `JsonSchema`. + schemars08 = { + // settings.schemars08 accepts an `attrs` field which can be used to + // define attributes like `#[cfg(feature)]` flags. + attrs = [#[cfg(feature = "schemars08")]], + // The `rust_type` setting is optional. Defining it makes the kinds + // suitable for automatic replacement with typify and other tools. + rust_type = { + crate = "e2e-kinds", + // This version should typically match the version of the crate, + // but it can also be "*" to match any version. "*" is + // recommended when the list of kinds is append-only. + version = "*", // or version = "0.1.0" + // This is the path to the crate and module where the kinds are + // defined. Here, the kinds are accessible from the crate root. + path = "e2e_kinds", + }, + }, + }, + // A couple of example UUID kinds. + kinds = { + User = {}, + Organization = {}, + }, +} diff --git a/e2e-example/e2e-schema-consumer/Cargo.toml b/e2e-example/e2e-schema-consumer/Cargo.toml new file mode 100644 index 000000000..1b8991230 --- /dev/null +++ b/e2e-example/e2e-schema-consumer/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "e2e-schema-consumer" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +e2e-kinds = { path = "../e2e-kinds" } +e2e-schema-producer = { path = "../e2e-schema-producer" } +newtype-uuid = { workspace = true, features = ["serde", "v4"] } +serde = { workspace = true, features = ["derive"] } +typify.workspace = true + +[dev-dependencies] +expectorate.workspace = true diff --git a/e2e-example/e2e-schema-consumer/README.md b/e2e-example/e2e-schema-consumer/README.md new file mode 100644 index 000000000..477eb118e --- /dev/null +++ b/e2e-example/e2e-schema-consumer/README.md @@ -0,0 +1,11 @@ + +# e2e-schema-consumer + + +End-to-end schema consumer crate. + +This crate is part of the end-to-end JSON schema use and replacement example +in this repository. See the parent README for more. + +For more about typify, see [its documentation](https://docs.rs/typify/0.4.2/typify/index.html). + diff --git a/e2e-example/e2e-schema-consumer/src/lib.rs b/e2e-example/e2e-schema-consumer/src/lib.rs new file mode 100644 index 000000000..7d826243b --- /dev/null +++ b/e2e-example/e2e-schema-consumer/src/lib.rs @@ -0,0 +1,45 @@ +//! End-to-end schema consumer crate. +//! +//! This crate is part of the end-to-end JSON schema use and replacement example +//! in this repository. See the parent README for more. +//! +//! For more about typify, see [its documentation](typify). + +// Typify has several ways to generate types from JSON schemas. This example +// uses the `import_types` proc macro. +use typify::import_types; + +import_types!( + schema = "../e2e-schema-producer/tests/output/assignment-schema.json", + // This is the magic: a list of crates that contain types suitable for + // automatic replacement. + crates = { + // `crates` is a map from crate name to version. The version can be a + // specfic version or `*` to match any version. As long as this version + // matches what's in the schema, the types will be compatible. + // + // Because of the way automatic replacement works, the only crate that + // needs to be specified is `e2e-kinds`. Each type will automatically be + // replaced with the corresponding type *alias* from `e2e-kinds` + // (typically `SomethingUuid`). + "e2e-kinds" = "*", + } +); + +#[cfg(test)] +mod tests { + #[test] + fn test_generated_types_work() { + // These types are defined in the kinds crate. + let user_id = e2e_kinds::UserUuid::new_v4(); + let organization_id = e2e_kinds::OrganizationUuid::new_v4(); + + // `Assignment` is generated by `import_types!` above, and the fields + // within it have been automatically replaced. + let assignment = crate::Assignment { + user_id, + organization_id, + }; + println!("assignment: {:?}", assignment); + } +} diff --git a/e2e-example/e2e-schema-producer/Cargo.toml b/e2e-example/e2e-schema-producer/Cargo.toml new file mode 100644 index 000000000..4b37190b9 --- /dev/null +++ b/e2e-example/e2e-schema-producer/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "e2e-schema-producer" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +e2e-kinds = { path = "../e2e-kinds", features = ["schemars08"] } +schemars = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true + +[dev-dependencies] +expectorate.workspace = true + +[features] +schemars08 = ["dep:schemars", "e2e-kinds/schemars08"] diff --git a/e2e-example/e2e-schema-producer/README.md b/e2e-example/e2e-schema-producer/README.md new file mode 100644 index 000000000..49b460f9f --- /dev/null +++ b/e2e-example/e2e-schema-producer/README.md @@ -0,0 +1,9 @@ + +# e2e-schema-producer + + +End-to-end schema producer crate. + +This crate is part of the end-to-end JSON Schema generation and replacment +example in this repository. See the parent README for more. + diff --git a/e2e-example/e2e-schema-producer/src/lib.rs b/e2e-example/e2e-schema-producer/src/lib.rs new file mode 100644 index 000000000..43fc1e585 --- /dev/null +++ b/e2e-example/e2e-schema-producer/src/lib.rs @@ -0,0 +1,29 @@ +//! End-to-end schema producer crate. +//! +//! This crate is part of the end-to-end JSON Schema generation and replacment +//! example in this repository. See the parent README for more. + +pub use e2e_kinds::{OrganizationUuid, UserUuid}; +use serde::{Deserialize, Serialize}; + +/// A simple type which uses the `UserUuid` and `OrganizationUuid` types defined +/// in this crate. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars08", derive(schemars::JsonSchema))] +pub struct Assignment { + pub user_id: UserUuid, + pub organization_id: OrganizationUuid, +} + +#[cfg(test)] +mod tests { + /// This test generates the JSON schema checked in at + /// ../tests/output/assignment-schema.json. + #[cfg(feature = "schemars08")] + #[test] + fn test_generate_assignment_schema() { + let schema = schemars::schema_for!(super::Assignment); + let schema_json = serde_json::to_string_pretty(&schema).unwrap(); + expectorate::assert_contents("tests/output/assignment-schema.json", &schema_json); + } +} diff --git a/e2e-example/e2e-schema-producer/tests/output/assignment-schema.json b/e2e-example/e2e-schema-producer/tests/output/assignment-schema.json new file mode 100644 index 000000000..fd3c26358 --- /dev/null +++ b/e2e-example/e2e-schema-producer/tests/output/assignment-schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Assignment", + "description": "A simple type which uses the `UserUuid` and `OrganizationUuid` types defined in this crate.", + "type": "object", + "required": [ + "organization_id", + "user_id" + ], + "properties": { + "organization_id": { + "$ref": "#/definitions/OrganizationUuid" + }, + "user_id": { + "$ref": "#/definitions/UserUuid" + } + }, + "definitions": { + "OrganizationUuid": { + "type": "string", + "format": "uuid", + "x-rust-type": { + "crate": "e2e-kinds", + "path": "e2e_kinds::OrganizationUuid", + "version": "*" + } + }, + "UserUuid": { + "type": "string", + "format": "uuid", + "x-rust-type": { + "crate": "e2e-kinds", + "path": "e2e_kinds::UserUuid", + "version": "*" + } + } + } +} \ No newline at end of file