From 957013425b5c2758eed8189c920952fd77f6e2fc Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 30 Jan 2024 15:38:12 -0800 Subject: [PATCH] initial commit --- .github/codecov.yml | 8 + .github/renovate.json | 8 + .github/workflows/ci.yml | 58 ++++++ .github/workflows/coverage.yml | 35 ++++ .github/workflows/docs.yml | 33 ++++ .github/workflows/release.yml | 29 +++ .gitignore | 1 + Cargo.lock | 182 ++++++++++++++++++ Cargo.toml | 31 ++++ LICENSE-APACHE | 202 ++++++++++++++++++++ LICENSE-MIT | 25 +++ README.md | 89 +++++++++ README.tpl | 21 +++ scripts/fix-readmes.awk | 27 +++ scripts/regenerate-readmes.sh | 16 ++ src/lib.rs | 330 +++++++++++++++++++++++++++++++++ 16 files changed, 1095 insertions(+) create mode 100644 .github/codecov.yml create mode 100644 .github/renovate.json create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/coverage.yml create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 README.tpl create mode 100644 scripts/fix-readmes.awk create mode 100755 scripts/regenerate-readmes.sh create mode 100644 src/lib.rs diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 000000000..cd0b5a570 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + informational: true + patch: + default: + target: 0% diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 000000000..8c499fec4 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "local>oxidecomputer/renovate-config", + "local>oxidecomputer/renovate-config//rust/autocreate", + "local>oxidecomputer/renovate-config//actions/pin" + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..5c3abdbbc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +on: + push: + branches: + - main + pull_request: + branches: + - main + +name: CI + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + env: + RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@v4.1.1 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2.7.2 + - name: Lint (clippy) + run: cargo clippy --all-features --all-targets + - name: Lint (rustfmt) + run: cargo xfmt --check + - name: Run rustdoc + env: + RUSTC_BOOTSTRAP: 1 # for feature(doc_cfg) + RUSTDOCFLAGS: -Dwarnings --cfg doc_cfg + run: cargo doc --all-features --workspace + - name: Check for differences + run: git diff --exit-code + + build: + name: Build and test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + rust-version: ["1.60", stable] + fail-fast: false + env: + RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust-version }} + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@cargo-hack + - uses: taiki-e/install-action@nextest + - name: Build + run: cargo hack build --feature-powerset + - name: Test + run: cargo hack nextest run --feature-powerset + - name: Run doctests + run: cargo test --doc --all-features diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 000000000..66306fa0a --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,35 @@ +on: + push: + branches: + - main + pull_request: + branches: + - main + +name: Test coverage + +jobs: + coverage: + name: Collect test coverage + runs-on: ubuntu-latest + # nightly rust might break from time to time + continue-on-error: true + env: + RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: dtolnay/rust-toolchain@nightly # Use nightly to get access to coverage --doc + with: + components: llvm-tools-preview + - uses: Swatinem/rust-cache@a95ba195448af2da9b00fb742d14ffaaf3c21f43 # v2 + - name: Install latest nextest release + uses: taiki-e/install-action@nextest + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + + - name: Collect coverage data + run: ./scripts/commands.sh coverage + - name: Upload coverage data to codecov + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3 + with: + files: lcov.info, lcov-doctest.info diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..78a36542b --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,33 @@ +on: + push: + branches: + - main + +name: Docs + +jobs: + docs: + name: Build and deploy documentation + concurrency: ci-${{ github.ref }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Build rustdoc + env: + RUSTC_BOOTSTRAP: 1 # for feature(doc_cfg) + RUSTDOCFLAGS: -Dwarnings --cfg doc_cfg + run: cargo doc --all-features --workspace + - name: Organize + run: | + rm -rf target/gh-pages + mkdir target/gh-pages + mv target/doc target/gh-pages/rustdoc + touch target/gh-pages/.nojekyll + - name: Deploy + uses: JamesIves/github-pages-deploy-action@releases/v4 + with: + branch: gh-pages + folder: target/gh-pages + single-commit: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..f956da1b7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +# adapted from https://github.com/taiki-e/cargo-hack/blob/main/.github/workflows/release.yml + +name: Publish release +on: + push: + tags: + - '*' + +jobs: + create-release: + if: github.repository_owner == 'oxidecomputer' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + - uses: taiki-e/create-gh-release-action@v1 + with: + changelog: CHANGELOG.md + title: cancel-safe-futures $version + branch: main + prefix: cancel-safe-futures + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..ae5b63a07 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,182 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "newtype-uuid" +version = "0.1.0" +dependencies = [ + "schemars", + "serde", + "uuid", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "schemars" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "schemars_derive" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_json" +version = "1.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "uuid" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..063f5b99e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "newtype-uuid" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +repository = "https://github.com/oxidecomputer/newtype-uuid" +description = "Newtype wrapper around UUIDs" +documentation = "https://docs.rs/newtype-uuid" +readme = "README.md" +keywords = ["uuid", "unique", "guid", "newtype"] +categories = ["data-structures", "uuid"] +rust-version = "1.60" +resolver = "2" +exclude = [".cargo/**/*", ".github/**/*", "scripts/**/*"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg=doc_cfg"] + +[dependencies] +serde = { version = "1", optional = true, features = ["derive"] } +schemars = { version = "0.8", features = ["uuid1"], optional = true } +uuid = { version = "1.7.0", default-features = false } + +[features] +default = ["uuid/default", "std"] +std = ["uuid/std", "alloc"] +alloc = [] +v4 = ["uuid/v4"] +serde = ["dep:serde", "uuid/serde"] +schemars08 = ["dep:schemars", "std"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 000000000..9eb0b097f --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 000000000..8f3e7b29d --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2024 Oxide Computer Company + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..1b5be33ef --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# newtype-uuid + +[![newtype-uuid on crates.io](https://img.shields.io/crates/v/newtype-uuid)](https://crates.io/crates/newtype-uuid) +[![Documentation (latest release)](https://img.shields.io/badge/docs-latest%20version-brightgreen.svg)](https://docs.rs/newtype-uuid) +[![Documentation (main)](https://img.shields.io/badge/docs-main-brightgreen)](https://oxidecomputer.github.io/newtype-uuid/rustdoc/newtype_uuid/) +[![License](https://img.shields.io/badge/license-Apache-green.svg)](LICENSE-APACHE) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE-MIT) + +A newtype wrapper around `Uuid`. + +## Motivation + +Many large systems use UUIDs as unique identifiers for various entities. However, the `Uuid` +type does not carry information about the kind of entity it identifies, which can lead to mixing +up different types of UUIDs at runtime. + +This crate provides a wrapper type around `Uuid` that allows you to specify the kind of entity +the UUID identifies. + +## Example + +```rust +use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag}; + +// First, define a type that represents the kind of UUID this is. +enum MyKind {} + +impl TypedUuidKind for MyKind { + fn tag() -> TypedUuidTag { + TypedUuidTag::new("my_kind") + } +} + +// Now, a UUID can be created with this kind. +let uuid: TypedUuid = "dffc3068-1cd6-47d5-b2f3-636b41b07084".parse().unwrap(); + +// The Display (and therefore ToString) impls still show the same value. +assert_eq!(uuid.to_string(), "dffc3068-1cd6-47d5-b2f3-636b41b07084"); + +// The Debug impl will show the tag as well. +assert_eq!(format!("{:?}", uuid), "dffc3068-1cd6-47d5-b2f3-636b41b07084 (my_kind)"); +``` + +## Implementations + +In general, `TypedUuid` uses the same wire and serialization formats as `Uuid`. This means +that data on the wire does not change; `TypedUuid` is intended to be helpful within Rust code, +not across serialization boundaries. + +- The `Display` and `FromStr` impls are forwarded to the underlying `Uuid`. +- If the `serde` feature is enabled, `TypedUuid` will serialize and deserialize using the same + format as `Uuid`. +- If the `schemars08` feature is enabled, `TypedUuid` will implement `JsonSchema` if the + corresponding `TypedUuidKind` implements `JsonSchema`. + +To abstract over typed and untyped UUIDs, the `GenericUuid` trait is provided. This trait also +permits conversions between typed and untyped UUIDs. + +## Dependencies + +- The only required dependency is the `uuid` crate. Optional features may add further + dependencies. + +## Features + +- `default`: Enables default features in the uuid crate. +- `std`: Enables the use of the standard library. *Enabled by default.* +- `serde`: Enables serialization and deserialization support via Serde. *Not enabled by + default.* +- `v4`: Enables the `new_v4` method for generating UUIDs. *Not enabled by default.* +- `schemars08`: Enables support for generating JSON schemas via schemars 0.8. *Not enabled by + default.* + +## Minimum supported Rust version (MSRV) + +The MSRV of this crate is **Rust 1.60.** In general, this crate will follow the MSRV of the +underlying `uuid` crate. + +## 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/README.tpl b/README.tpl new file mode 100644 index 000000000..9c91366cd --- /dev/null +++ b/README.tpl @@ -0,0 +1,21 @@ +# {{crate}} + +[![newtype-uuid on crates.io](https://img.shields.io/crates/v/newtype-uuid)](https://crates.io/crates/newtype-uuid) +[![Documentation (latest release)](https://img.shields.io/badge/docs-latest%20version-brightgreen.svg)](https://docs.rs/newtype-uuid) +[![Documentation (main)](https://img.shields.io/badge/docs-main-brightgreen)](https://oxidecomputer.github.io/newtype-uuid/rustdoc/newtype_uuid/) +[![License](https://img.shields.io/badge/license-Apache-green.svg)](LICENSE-APACHE) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE-MIT) + +{{readme}} + +## 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/scripts/fix-readmes.awk b/scripts/fix-readmes.awk new file mode 100644 index 000000000..ad87b4b7f --- /dev/null +++ b/scripts/fix-readmes.awk @@ -0,0 +1,27 @@ +# Fix up readmes: +# * Replace ## with # in code blocks. +# * Remove [] without a following () from output. + +BEGIN { + true = 1 + false = 0 + in_block = false +} + +{ + if (!in_block && $0 ~ /^```/) { + in_block = true + } else if (in_block && $0 ~ /^```$/) { + in_block = false + } + + if (in_block) { + sub(/## /, "# ") + print $0 + } else { + # Strip [] without a () that immediately follows them from + # the output. + subbed = gensub(/\[([^\[]+)]([^\(]|$)/, "\\1\\2", "g") + print subbed + } +} diff --git a/scripts/regenerate-readmes.sh b/scripts/regenerate-readmes.sh new file mode 100755 index 000000000..4e6980645 --- /dev/null +++ b/scripts/regenerate-readmes.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Copyright (c) The cargo-guppy Contributors +# SPDX-License-Identifier: MIT OR Apache-2.0 + +# Regenerate readme files in this repository. + +set -eo pipefail + +cd "$(git rev-parse --show-toplevel)" +git ls-files | grep README.tpl$ | while read -r readme; do + dir=$(dirname "$readme") + cargo readme --project-root "$dir" > "$dir/README.md.tmp" + gawk -f "scripts/fix-readmes.awk" "$dir/README.md.tmp" > "$dir/README.md" + rm "$dir/README.md.tmp" +done diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..e5ce24331 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,330 @@ +//! A newtype wrapper around [`Uuid`]. +//! +//! # Motivation +//! +//! Many large systems use UUIDs as unique identifiers for various entities. However, the [`Uuid`] +//! type does not carry information about the kind of entity it identifies, which can lead to mixing +//! up different types of UUIDs at runtime. +//! +//! This crate provides a wrapper type around [`Uuid`] that allows you to specify the kind of entity +//! the UUID identifies. +//! +//! # Example +//! +//! ```rust +//! use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag}; +//! +//! // First, define a type that represents the kind of UUID this is. +//! enum MyKind {} +//! +//! impl TypedUuidKind for MyKind { +//! fn tag() -> TypedUuidTag { +//! TypedUuidTag::new("my_kind") +//! } +//! } +//! +//! // Now, a UUID can be created with this kind. +//! let uuid: TypedUuid = "dffc3068-1cd6-47d5-b2f3-636b41b07084".parse().unwrap(); +//! +//! // The Display (and therefore ToString) impls still show the same value. +//! assert_eq!(uuid.to_string(), "dffc3068-1cd6-47d5-b2f3-636b41b07084"); +//! +//! // The Debug impl will show the tag as well. +//! assert_eq!(format!("{:?}", uuid), "dffc3068-1cd6-47d5-b2f3-636b41b07084 (my_kind)"); +//! ``` +//! +//! # Implementations +//! +//! In general, [`TypedUuid`] uses the same wire and serialization formats as [`Uuid`]. This means +//! that data on the wire does not change; [`TypedUuid`] is intended to be helpful within Rust code, +//! not across serialization boundaries. +//! +//! - The `Display` and `FromStr` impls are forwarded to the underlying [`Uuid`]. +//! - If the `serde` feature is enabled, `TypedUuid` will serialize and deserialize using the same +//! format as [`Uuid`]. +//! - If the `schemars08` feature is enabled, [`TypedUuid`] will implement `JsonSchema` if the +//! corresponding [`TypedUuidKind`] implements `JsonSchema`. +//! +//! To abstract over typed and untyped UUIDs, the [`GenericUuid`] trait is provided. This trait also +//! permits conversions between typed and untyped UUIDs. +//! +//! # Dependencies +//! +//! - The only required dependency is the [`uuid`] crate. Optional features may add further +//! dependencies. +//! +//! # Features +//! +//! - `default`: Enables default features in the uuid crate. +//! - `std`: Enables the use of the standard library. *Enabled by default.* +//! - `serde`: Enables serialization and deserialization support via Serde. *Not enabled by +//! default.* +//! - `v4`: Enables the `new_v4` method for generating UUIDs. *Not enabled by default.* +//! - `schemars08`: Enables support for generating JSON schemas via schemars 0.8. *Not enabled by +//! default.* +//! +//! # Minimum supported Rust version (MSRV) +//! +//! The MSRV of this crate is **Rust 1.60.** In general, this crate will follow the MSRV of the +//! underlying `uuid` crate. + +#![warn(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(doc_cfg, feature(doc_cfg, doc_auto_cfg))] + +use core::{ + cmp::Ordering, + fmt, + hash::{Hash, Hasher}, + marker::PhantomData, + str::FromStr, +}; +use uuid::Uuid; + +/// A UUID with type-level information about what it's used for. +/// +/// For more, see [the library documentation](crate). +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent, bound = ""))] +pub struct TypedUuid { + uuid: Uuid, + _phantom: PhantomData, +} + +impl TypedUuid { + /// Creates a new, random UUID v4 of this type. + #[inline] + #[cfg(feature = "v4")] + pub fn new_v4() -> Self { + Self::from_untyped_uuid(Uuid::new_v4()) + } +} + +// --- +// Trait impls +// --- + +impl PartialEq for TypedUuid { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.uuid.eq(&other.uuid) + } +} + +impl Eq for TypedUuid {} + +impl PartialOrd for TypedUuid { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.uuid.cmp(&other.uuid)) + } +} + +impl Ord for TypedUuid { + #[inline] + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.uuid.cmp(&other.uuid) + } +} + +impl Hash for TypedUuid { + #[inline] + fn hash(&self, state: &mut H) { + self.uuid.hash(state); + } +} + +impl fmt::Debug for TypedUuid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.uuid.fmt(f)?; + write!(f, " ({})", T::tag()) + } +} + +impl fmt::Display for TypedUuid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.uuid.fmt(f) + } +} + +impl Clone for TypedUuid { + #[inline] + fn clone(&self) -> Self { + *self + } +} + +impl Copy for TypedUuid {} + +impl FromStr for TypedUuid { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let uuid = Uuid::from_str(s).map_err(|error| ParseError { + error, + tag: T::tag(), + })?; + Ok(Self::from_untyped_uuid(uuid)) + } +} + +#[cfg(feature = "schemars08")] +mod schemars08_imp { + use super::*; + use schemars::JsonSchema; + + /// 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`. + impl JsonSchema for TypedUuid + where + T: TypedUuidKind + JsonSchema, + { + #[inline] + fn schema_name() -> String { + format!("TypedUuidFor{}", T::schema_name()) + } + + #[inline] + fn schema_id() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Owned(format!("newtype_uuid::TypedUuid<{}>", T::schema_id())) + } + + #[inline] + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + Uuid::json_schema(gen) + } + } +} + +/// Represents marker types that can be used as a type parameter for [`TypedUuid`]. +/// +/// Generally, an implementation of this will be a zero-sized type that can never be constructed. An +/// empty struct or enum works well for this. +/// +/// # Implementations +/// +/// If the `schemars08` feature is enabled, and [`JsonSchema`] is implemented for a kind `T`, then +/// [`TypedUuid`]`` will also implement [`JsonSchema`]. +/// +/// [`JsonSchema`]: schemars::JsonSchema +pub trait TypedUuidKind: Send + Sync + 'static { + /// Returns the corresponding tag for this kind. + /// + /// The tag forms a runtime representation of this type. + fn tag() -> TypedUuidTag; +} + +/// Describes what kind of [`TypedUuid`] something is. +/// +/// This is the runtime equivalent of [`TypedUuidKind`]. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TypedUuidTag(&'static str); + +impl TypedUuidTag { + /// Creates a new `TypedUuidTag` from a static string. + pub const fn new(tag: &'static str) -> Self { + Self(tag) + } + + /// Returns the tag as a string. + pub fn as_str(&self) -> &'static str { + self.0 + } +} + +impl fmt::Display for TypedUuidTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.0) + } +} + +impl AsRef for TypedUuidTag { + fn as_ref(&self) -> &str { + &self.0 + } +} + +/// An error that occurred while parsing a [`TypedUuid`]. +#[derive(Clone, Debug)] +pub struct ParseError { + /// The underlying error. + pub error: uuid::Error, + + /// The tag of the UUID that failed to parse. + pub tag: TypedUuidTag, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "error parsing UUID ({})", self.tag) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.error) + } +} + +/// A trait abstracting over typed and untyped UUIDs. +/// +/// This can be used to write code that's generic over [`TypedUuid`], [`Uuid`], and other types that +/// may wrap [`TypedUuid`] (due to e.g. orphan rules). +/// +/// This trait is similar to `From`, but a bit harder to get wrong -- in general, the conversion +/// from and to untyped UUIDs should be careful and explicit. +pub trait GenericUuid { + /// Creates a new instance of `Self` from an untyped [`Uuid`]. + fn from_untyped_uuid(uuid: Uuid) -> Self; + + /// Converts `self` into an untyped [`Uuid`]. + fn to_untyped_uuid(self) -> Uuid; + + /// Returns the inner [`Uuid`]. + /// + /// Generally, [`to_untyped_uuid`](GenericUuid::to_untyped_uuid) should + /// be preferred. However, in some cases it may be necessary to use this + /// method to satisfy lifetime constraints. + fn as_untyped_uuid(&self) -> &Uuid; +} + +impl GenericUuid for Uuid { + #[inline] + fn from_untyped_uuid(uuid: Uuid) -> Self { + uuid + } + + #[inline] + fn to_untyped_uuid(self) -> Uuid { + self + } + + #[inline] + fn as_untyped_uuid(&self) -> &Uuid { + self + } +} + +impl GenericUuid for TypedUuid { + #[inline] + fn from_untyped_uuid(uuid: Uuid) -> Self { + Self { + uuid, + _phantom: PhantomData, + } + } + + #[inline] + fn to_untyped_uuid(self) -> Uuid { + self.uuid + } + + #[inline] + fn as_untyped_uuid(&self) -> &Uuid { + &self.uuid + } +}