diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..7615fec
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,25 @@
+# see: https://docs.github.com/code-security/dependabot/dependabot-version-updates
+version: 2
+
+updates:
+ - package-ecosystem: cargo
+ directory: /
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+ # workspace crates share dependencies - one PR per batch keeps CI runs and
+ # changelog noise down. commit messages match this repo's convention:
+ # workspace-level changes carry no crate-name prefix
+ groups:
+ cargo-dependencies:
+ patterns:
+ - "*"
+
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index d7d8ee8..ffc1c15 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -6,10 +6,7 @@ on:
- '**'
tags-ignore:
- '**'
- paths:
- - ".github/workflows/**.yml"
- - "**/Cargo.toml"
- - "**.rs"
+ pull_request:
workflow_dispatch:
jobs:
@@ -20,7 +17,7 @@ jobs:
tags: ${{ steps.info.outputs.tags }}
steps:
- name: Checkout Source Code
- uses: actions/checkout@v3
+ uses: actions/checkout@v7
with:
fetch-tags: true
- name: Set Info
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 1e57119..5ba249f 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -15,11 +15,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
- uses: actions/checkout@v3
+ uses: actions/checkout@v7
with:
fetch-tags: true
- name: Install Rust Toolchain (stable)
uses: dtolnay/rust-toolchain@stable
+ # NOTE: publishing needs none of these, but `cargo xtask setup` installs
+ # them and `cargo install` always builds from source. fetching the
+ # prebuilt binaries makes `setup` below a no-op instead - see the same
+ # step in `test.yaml` for details
+ - name: Install Prebuilt Tooling
+ uses: taiki-e/install-action@v2
+ with:
+ tool: cargo-llvm-cov,cargo-deny,cargo-semver-checks,typos
- name: Setup Project
run: cargo xtask setup
- name: Publish to Crates.io
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 489793a..f3796f7 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -11,22 +11,80 @@ jobs:
strategy:
# fail-fast: false
matrix:
- os:
- - ubuntu-latest
- - macos-latest
- - windows-latest
- architecture:
- - x64
+ include:
+ - os: ubuntu-latest
+ architecture: x64
+ - os: macos-latest
+ architecture: arm64
+ - os: windows-latest
+ architecture: x64
steps:
- name: Git Symlink Setup for Windows
if: matrix.os == 'windows-latest'
run: git config --global core.symlinks true
- name: Checkout Source Code
- uses: actions/checkout@v3
+ uses: actions/checkout@v7
- name: Install Rust Toolchain (stable)
uses: dtolnay/rust-toolchain@stable
+ # NOTE: `cargo install` always builds from source - crates.io ships no
+ # binaries. these tools publish prebuilt binaries via GitHub Releases,
+ # which this action fetches in seconds rather than minutes. installing
+ # them here makes the `setup` step below a no-op, since it skips any
+ # tool that is already available
+ # NOTE: these are `install-action` tool names, not crate names - they
+ # match one-to-one here except `typos`, whose crate is `typos-cli` (the
+ # name `cargo xtask setup` installs) and whose binary is `typos`
+ # NOTE: `cargo-semver-checks` is listed even though no job here runs it -
+ # `setup` installs it for `crate:release`, and fetching the prebuilt
+ # binary is far cheaper than letting `setup` build it from source
+ - name: Install Prebuilt Tooling
+ uses: taiki-e/install-action@v2
+ with:
+ tool: cargo-llvm-cov,cargo-deny,cargo-semver-checks,typos
- name: Setup Project
run: cargo xtask setup
- name: Run Tests & Coverage
run: cargo xtask ci
+ deny:
+ name: Audit Dependencies
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout Source Code
+ uses: actions/checkout@v7
+ - name: Install Rust Toolchain (stable)
+ uses: dtolnay/rust-toolchain@stable
+ - name: Install cargo-deny
+ uses: taiki-e/install-action@v2
+ with:
+ tool: cargo-deny
+ - name: Check Advisories, Licenses & Bans
+ run: cargo deny --all-features check
+
+ msrv:
+ name: Verify Minimum Supported Rust Version
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout Source Code
+ uses: actions/checkout@v7
+ - name: Read MSRV from Cargo.toml
+ id: msrv
+ run: |
+ version="$(grep -m1 '^rust-version' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
+ echo ":::: MSRV: ${version}"
+ echo "version=${version}" >> "$GITHUB_OUTPUT"
+ - name: Install Rust Toolchain (MSRV)
+ uses: dtolnay/rust-toolchain@master
+ with:
+ toolchain: ${{ steps.msrv.outputs.version }}
+ - name: Check Workspace Builds on MSRV
+ # NOTE: `rust-toolchain.toml` pins `stable`, which takes precedence over
+ # the toolchain installed above - `RUSTUP_TOOLCHAIN` overrides both, so
+ # without it this job would silently verify the wrong toolchain
+ env:
+ RUSTUP_TOOLCHAIN: ${{ steps.msrv.outputs.version }}
+ run: |
+ rustc --version
+ cargo check --workspace --all-features --all-targets
diff --git a/.gitignore b/.gitignore
index d788f26..b2d866b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,9 +3,9 @@
debug/
target/
-# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
-# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
-Cargo.lock
+# NOTE: `Cargo.lock` is intentionally committed. current guidance is to commit
+# it for *all* projects - libraries included - so CI builds are reproducible
+# see: https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
# These are backup files generated by rustfmt
**/*.rs.bk
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..4e5a7af
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,30 @@
+# AGENTS.md
+
+Instructions for AI coding agents working on this repository. **Read [README.md](README.md) first** — it covers setup, conventions, and the full command list, and takes precedence if this document conflicts with it.
+
+## Hard rules — require explicit approval
+
+- **Never** run `cargo add`. Ask; the user must say yes explicitly. Do not infer approval from a request to implement a feature that needs a dependency.
+- **Never** `git commit`. Make changes locally, then prompt the user to review and commit.
+- **Never** `git push`. Prompt the user, and explain why the push is needed.
+- **Never** publish a crate. See "[How to publish crates](README.md#develop-publish-crate)".
+
+## Commands
+
+All local dev commands are `xtask` scripts — run `cargo xtask help` for the full list.
+
+- `cargo xtask test` — fast inner loop while iterating.
+- `cargo xtask ci` — run before prompting the user to review. Chains `format --check`, `spellcheck`, `lint` (clippy), `doc --check` (doc tests + rustdoc warnings), and `coverage` (which runs the tests). Report results honestly.
+- `cargo xtask audit` — dependency audit (advisories, licenses, sources). Needs network access and runs as its own CI job, so it is **not** part of `cargo xtask ci`.
+- `cargo xtask semver` — checks the public API against the last published version. Versions are bumped at release time, not alongside the code, so breaking changes legitimately sit on `main` un-bumped — this must **not** gate CI. `crate:release` runs it automatically before prompting for the new version.
+
+Task naming follows one rule: `--check` is a non-mutating mode of a task that otherwise writes (`format`, `doc`); `name:sub` is a family of distinct operations on one noun (`crate:add`, `crate:list`, ...); everything else is a bare verb.
+- `cargo xtask crate:add` — the only supported way to add a crate. Never hand-create one under `crates/`.
+
+## Conventions
+
+- **Toolchain is pinned** to Rust 1.86 (`rust-toolchain.toml`, `Cargo.toml`). CI verifies this MSRV — don't use newer language or std features.
+- **Commit messages** are `[] ` (e.g. `[node-js-release-info] update docs`) and crate changes are staged separately from workspace changes. This drives automated changelogs. Workspace-level changes (including `xtask`) get no prefix.
+- **Don't hand-edit** the crate list in `README.md` — it's generated between the `` / `` markers.
+- **Document public interfaces** with inline rustdoc annotations.
+- **TODO comments** are formatted `// TODO (): `.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..6daf6cd
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,7 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+The instructions live in [AGENTS.md](AGENTS.md) — shared by every AI coding agent used here — and are imported below so they load automatically.
+
+@AGENTS.md
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..0e6d489
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,2320 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "assert-json-diff"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+ "pkg-config",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "cc"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "chacha20"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "colored"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossterm"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "derive_more",
+ "document-features",
+ "mio",
+ "parking_lot",
+ "rustix",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "detect-newline-style"
+version = "0.1.2"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "duct"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e66e9c0c03d094e1a0ba1be130b849034aa80c3a2ab8ee94316bc809f3fa684"
+dependencies = [
+ "libc",
+ "os_pipe",
+ "shared_child",
+ "shared_thread",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "fuzzy-matcher"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
+dependencies = [
+ "thread_local",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core 0.10.1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "inquire"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756"
+dependencies = [
+ "bitflags",
+ "crossterm",
+ "dyn-clone",
+ "fuzzy-matcher",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys",
+ "log",
+ "simd_cesu8",
+ "thiserror",
+ "walkdir",
+ "windows-link",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
+dependencies = [
+ "getrandom 0.4.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "litrs"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mio"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "mockito"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0"
+dependencies = [
+ "assert-json-diff",
+ "bytes",
+ "colored",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "log",
+ "pin-project-lite",
+ "rand 0.9.5",
+ "regex",
+ "serde_json",
+ "serde_urlencoded",
+ "similar",
+ "tokio",
+]
+
+[[package]]
+name = "node-js-release-info"
+version = "1.1.1"
+dependencies = [
+ "mockito",
+ "reqwest",
+ "semver",
+ "serde",
+ "serde_json",
+ "tokio",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "os_pipe"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
+dependencies = [
+ "aws-lc-rs",
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand 0.10.2",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
+dependencies = [
+ "rand_chacha",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "reqwest"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
+dependencies = [
+ "aws-lc-rs",
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+dependencies = [
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "jni",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[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 = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "shared_child"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
+dependencies = [
+ "libc",
+ "sigchld",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "shared_thread"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0"
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "sigchld"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
+dependencies = [
+ "libc",
+ "os_pipe",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd_cesu8"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "similar"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "libc",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.13+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[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 = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-registry"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+dependencies = [
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "xtask"
+version = "0.1.0"
+dependencies = [
+ "duct",
+ "inquire",
+ "regex",
+ "semver",
+ "toml_edit",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/Cargo.toml b/Cargo.toml
index bf0ffa8..dae0380 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,13 +1,42 @@
[workspace]
-resolver = "2"
+# resolver 3 is MSRV-aware - it will not select dependency versions that
+# require a newer `rustc` than `workspace.package.rust-version` below. note
+# this must be set explicitly: virtual manifests have no edition to infer from
+resolver = "3"
members = ["xtask", "crates/*"]
[workspace.package]
-edition = "2021"
+edition = "2024"
+rust-version = "1.86"
license = "MIT OR Apache-2.0"
authors = ["Busticated "]
repository = "https://github.com/busticated/rusty"
+# declare every dependency here, then reference it from a crate via
+# `.workspace = true` so versions cannot drift between crates
+[workspace.dependencies]
+duct = "1"
+inquire = "0.9"
+mockito = "1"
+regex = "1"
+reqwest = "0.13"
+semver = "1"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tokio = { version = "1", default-features = false }
+toml_edit = "0.25"
+
+# lints are configured once here, then inherited by each crate via
+# `[lints] workspace = true` - see: https://doc.rust-lang.org/cargo/reference/manifest.html#the-lints-section
+[workspace.lints.rust]
+unsafe_code = "forbid"
+missing_docs = "warn"
+missing_debug_implementations = "warn"
+unreachable_pub = "warn"
+
+[workspace.lints.clippy]
+uninlined_format_args = "warn"
+
[profile.dev]
# Faster builds, disable if you need
debug = 0
diff --git a/LICENSE-APACHE b/LICENSE-APACHE
new file mode 100644
index 0000000..8e78c1c
--- /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 2023 Rusty Contributors
+
+ 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 b/LICENSE-MIT
similarity index 100%
rename from LICENSE
rename to LICENSE-MIT
diff --git a/README.md b/README.md
index 301d583..e9e3854 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
# Rusty
-[](https://github.com/busticated/rusty/actions) [](https://releases.rs/) [](https://github.com/busticated/rusty/blob/master/LICENSE)
+[](https://github.com/busticated/rusty/actions) [](https://releases.rs/) [](https://github.com/busticated/rusty#license)
A `cargo` workspace ([docs](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html)) monorepo ([info](https://en.wikipedia.org/wiki/Monorepo)) hosting a collection of Rust utility crates.
-[Installation](#installation) | [Crates](#crates) | [Development](#development) | [Docs](#docs--resources)
+[Installation](#installation) | [Crates](#crates) | [Development](#development) | [Docs](#docs--resources) | [License](#license)
## Installation
@@ -107,11 +107,36 @@ To see code coverage stats for _all_ crates:
cargo xtask coverage --open
```
+Coverage is collected with [cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) and written to `tmp/coverage` as both an html report and `lcov.info`. The `xtask` crate and `tests/` directories are excluded. Note `cargo-llvm-cov` does _not_ run doc tests - those run via `cargo xtask doc --check`.
+
Run `cargo xtask help` to see any other coverage-related commands that are available.
+
How to create docs
@@ -223,3 +269,15 @@ Any `todo!()` macros in the source code will also be reported.
* [Reqwest](https://github.com/seanmonstar/reqwest)
* [Mockito](https://github.com/lipanski/mockito)
+
+## License
+
+Licensed under either of
+
+* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or [apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0))
+* MIT license ([LICENSE-MIT](LICENSE-MIT) or [opensource.org/licenses/MIT](https://opensource.org/licenses/MIT))
+
+at your option.
+
+Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this repository by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
+
diff --git a/crates/detect-newline-style/Cargo.toml b/crates/detect-newline-style/Cargo.toml
index 0a2ada5..2c20302 100644
--- a/crates/detect-newline-style/Cargo.toml
+++ b/crates/detect-newline-style/Cargo.toml
@@ -14,9 +14,12 @@ categories = [
"text-processing"
]
edition.workspace = true
+rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[dependencies]
-regex = "1.*"
+
+[lints]
+workspace = true
diff --git a/crates/detect-newline-style/LICENSE-APACHE b/crates/detect-newline-style/LICENSE-APACHE
new file mode 100644
index 0000000..8e78c1c
--- /dev/null
+++ b/crates/detect-newline-style/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 2023 Rusty Contributors
+
+ 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/crates/detect-newline-style/LICENSE-MIT b/crates/detect-newline-style/LICENSE-MIT
new file mode 100644
index 0000000..67113f0
--- /dev/null
+++ b/crates/detect-newline-style/LICENSE-MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Rusty Contributors
+
+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/crates/detect-newline-style/README.md b/crates/detect-newline-style/README.md
index dc0ccf1..fe10595 100644
--- a/crates/detect-newline-style/README.md
+++ b/crates/detect-newline-style/README.md
@@ -19,23 +19,63 @@ use detect_newline_style::LineEnding;
fn main() {
let text = "one\rtwo\r\nthree\nfour\n";
- let eol = LineEnding::find(text, LineEnding::LF);
+ let eol = LineEnding::find(text, LineEnding::Lf);
- assert_eq!(eol, LineEnding::LF);
+ assert_eq!(eol, LineEnding::Lf);
let text = "one\rtwo\r\nthree\n";
let eol = LineEnding::find_or_use_lf(text);
- assert_eq!(eol, LineEnding::LF);
+ assert_eq!(eol, LineEnding::Lf);
let text = "one\rtwo\r\nthree\n";
let eol = LineEnding::find_or_use_crlf(text);
- assert_eq!(eol, LineEnding::CRLF);
+ assert_eq!(eol, LineEnding::Crlf);
- assert_eq!(format!("{}", LineEnding::CR), "\r");
- assert_eq!(format!("{}", LineEnding::LF), "\n");
- assert_eq!(format!("{}", LineEnding::CRLF), "\r\n");
+ assert_eq!(format!("{}", LineEnding::Cr), "\r");
+ assert_eq!(format!("{}", LineEnding::Lf), "\n");
+ assert_eq!(format!("{}", LineEnding::Crlf), "\r\n");
}
```
+
+## Migrations
+
+
+0.x -> 1.x
+
+
+**Variant names now follow [RFC 430](https://rust-lang.github.io/rfcs/0430-finalizing-naming-conventions.html)**
+
+| before | after |
+| --- | --- |
+| `LineEnding::CR` | `LineEnding::Cr` |
+| `LineEnding::LF` | `LineEnding::Lf` |
+| `LineEnding::CRLF` | `LineEnding::Crlf` |
+
+**`FromStr` returns a concrete error type**
+
+`LineEnding::from_str` now fails with `ParseLineEndingError` instead of `Box`. The old type was neither `Send` nor `Sync`, so the error could not cross a thread boundary or compose with most application error types.
+
+Code that propagates with `?` into a function returning `Box` keeps working unchanged. Code that names the error type explicitly needs updating:
+
+```rust,ignore
+// before
+let eol: Result> = LineEnding::from_str("\n");
+// after
+let eol: Result = LineEnding::from_str("\n");
+```
+
+`from_str` also no longer lowercases its input. This has no observable effect, since the only valid inputs are `"\r"`, `"\n"` and `"\r\n"`.
+
+**`LineEnding` is now `Copy`**
+
+Existing code is unaffected - values that were previously moved are now copied.
+
+**The `regex` dependency is gone**
+
+This crate now has no dependencies at all. Behaviour is unchanged; see the tests covering `"\r\r\n"` and multi-byte input for the edge cases.
+
+
+
diff --git a/crates/detect-newline-style/src/lib.rs b/crates/detect-newline-style/src/lib.rs
index 874fd4b..b5b96a0 100644
--- a/crates/detect-newline-style/src/lib.rs
+++ b/crates/detect-newline-style/src/lib.rs
@@ -1,24 +1,50 @@
#![doc = include_str!("../README.md")]
-use regex::RegexBuilder;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
+/// Returned by [`LineEnding::from_str`] when the input is not one of `"\r"`,
+/// `"\n"` or `"\r\n"`
+///
+/// This is a concrete type rather than `Box` so it is `Send + Sync`
+/// and can cross thread boundaries
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
+pub struct ParseLineEndingError {
+ input: String,
+}
+
+impl ParseLineEndingError {
+ /// The unrecognized input that produced this error
+ pub fn input(&self) -> &str {
+ &self.input
+ }
+}
+
+impl Display for ParseLineEndingError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(f, "unrecognized line ending - received: '{}'", self.input)
+ }
+}
+
+impl Error for ParseLineEndingError {}
+
const CR: &str = "\r";
const LF: &str = "\n";
const CRLF: &str = "\r\n";
-#[derive(Clone, Debug, Default, PartialEq)]
+/// A newline style - see [`find`](LineEnding::find) to detect which style a
+/// given string prefers
+#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum LineEnding {
/// CR-style line ending (`"\r"`) rarely used, mostly on older systems
/// (e.g. classic MacOS - OS-X before 10.0)
- CR,
+ Cr,
/// LF-style line ending (`"\n"`) typically used on *nix and MacOS
#[default]
- LF,
+ Lf,
/// CRLF-style line ending (`"\r\n"`) typically used on Windows
- CRLF,
+ Crlf,
}
impl LineEnding {
@@ -34,16 +60,13 @@ impl LineEnding {
/// ```rust
/// use detect_newline_style::LineEnding;
/// let eol = LineEnding::new("\n");
- /// assert_eq!(eol, LineEnding::LF);
+ /// assert_eq!(eol, LineEnding::Lf);
/// ```
pub fn new>(kind: K) -> LineEnding {
- let kind = LineEnding::from_str(kind.as_ref());
-
- if kind.is_err() {
- return LineEnding::LF;
- }
-
- kind.unwrap()
+ // NOTE: unrecognized input falls back to the default (`Lf`) rather
+ // than failing - use [`from_str`](LineEnding::from_str) if you need
+ // to know the input was invalid
+ LineEnding::from_str(kind.as_ref()).unwrap_or_default()
}
/// Determines which newline style a given string uses (CR, LF, or CRLF)
@@ -57,41 +80,44 @@ impl LineEnding {
///
/// ```rust
/// use detect_newline_style::LineEnding;
- /// let eol = LineEnding::find("one\ntwo\r\nthree\n", LineEnding::CRLF);
- /// assert_eq!(eol, LineEnding::LF);
+ /// let eol = LineEnding::find("one\ntwo\r\nthree\n", LineEnding::Crlf);
+ /// assert_eq!(eol, LineEnding::Lf);
/// ```
pub fn find>(text: S, default: LineEnding) -> LineEnding {
- let text = text.as_ref();
- let ptn = r"(?:\r\n?|\n)";
- let re = RegexBuilder::new(ptn)
- .case_insensitive(true)
- .multi_line(true)
- .build()
- .unwrap();
-
- let matches = re.find_iter(text);
+ // NOTE: `\r` and `\n` are ASCII, so they can never appear inside a
+ // multi-byte UTF-8 sequence - scanning bytes is safe and avoids
+ // pulling in (and re-compiling, on every call) a regex
+ let bytes = text.as_ref().as_bytes();
let mut crlf_count = 0;
let mut cr_count = 0;
let mut lf_count = 0;
-
- for item in matches {
- let x = item.as_str();
-
- if x == CRLF {
- crlf_count += 1;
- } else if x == LF {
- lf_count += 1;
- } else if x == CR {
- cr_count += 1;
+ let mut i = 0;
+
+ while i < bytes.len() {
+ match bytes[i] {
+ b'\r' => {
+ if bytes.get(i + 1) == Some(&b'\n') {
+ crlf_count += 1;
+ i += 2;
+ } else {
+ cr_count += 1;
+ i += 1;
+ }
+ }
+ b'\n' => {
+ lf_count += 1;
+ i += 1;
+ }
+ _ => i += 1,
}
}
if crlf_count > lf_count && crlf_count > cr_count {
- return LineEnding::CRLF;
+ return LineEnding::Crlf;
} else if lf_count > crlf_count && lf_count > cr_count {
- return LineEnding::LF;
+ return LineEnding::Lf;
} else if cr_count > lf_count && cr_count > crlf_count {
- return LineEnding::CR;
+ return LineEnding::Cr;
}
default
@@ -109,12 +135,12 @@ impl LineEnding {
/// ```rust
/// use detect_newline_style::LineEnding;
/// let eol = LineEnding::find_or_use_crlf("one\ntwo\r\nthree\n");
- /// assert_eq!(eol, LineEnding::LF);
+ /// assert_eq!(eol, LineEnding::Lf);
/// let eol = LineEnding::find_or_use_crlf("one\ntwo\r\nthree\r");
- /// assert_eq!(eol, LineEnding::CRLF);
+ /// assert_eq!(eol, LineEnding::Crlf);
/// ```
pub fn find_or_use_crlf>(s: S) -> LineEnding {
- LineEnding::find(s, LineEnding::CRLF)
+ LineEnding::find(s, LineEnding::Crlf)
}
/// Determines which newline style a given string uses (CR, LF, or CRLF)
@@ -129,12 +155,12 @@ impl LineEnding {
/// ```rust
/// use detect_newline_style::LineEnding;
/// let eol = LineEnding::find_or_use_lf("one\r\ntwo\nthree\r\n");
- /// assert_eq!(eol, LineEnding::CRLF);
+ /// assert_eq!(eol, LineEnding::Crlf);
/// let eol = LineEnding::find_or_use_lf("one\ntwo\r\nthree\r");
- /// assert_eq!(eol, LineEnding::LF);
+ /// assert_eq!(eol, LineEnding::Lf);
/// ```
pub fn find_or_use_lf>(s: S) -> LineEnding {
- LineEnding::find(s, LineEnding::LF)
+ LineEnding::find(s, LineEnding::Lf)
}
/// Determines which newline style a given string uses (CR, LF, or CRLF)
@@ -149,36 +175,38 @@ impl LineEnding {
/// ```rust
/// use detect_newline_style::LineEnding;
/// let eol = LineEnding::find_or_use_cr("one\ntwo\r\nthree\n");
- /// assert_eq!(eol, LineEnding::LF);
+ /// assert_eq!(eol, LineEnding::Lf);
/// let eol = LineEnding::find_or_use_cr("one\ntwo\r\nthree\r");
- /// assert_eq!(eol, LineEnding::CR);
+ /// assert_eq!(eol, LineEnding::Cr);
/// ```
pub fn find_or_use_cr>(s: S) -> LineEnding {
- LineEnding::find(s, LineEnding::CR)
+ LineEnding::find(s, LineEnding::Cr)
}
}
impl Display for LineEnding {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let eol = match self {
- LineEnding::CR => CR,
- LineEnding::LF => LF,
- LineEnding::CRLF => CRLF,
+ LineEnding::Cr => CR,
+ LineEnding::Lf => LF,
+ LineEnding::Crlf => CRLF,
};
- write!(f, "{}", eol)
+ write!(f, "{eol}")
}
}
impl FromStr for LineEnding {
- type Err = Box;
-
- fn from_str(s: &str) -> Result> {
- match s.to_lowercase().as_str() {
- CR => Ok(LineEnding::CR),
- LF => Ok(LineEnding::LF),
- CRLF => Ok(LineEnding::CRLF),
- _ => Err(format!("Unrecognized input: {}", s).into()),
+ type Err = ParseLineEndingError;
+
+ fn from_str(s: &str) -> Result {
+ match s {
+ CR => Ok(LineEnding::Cr),
+ LF => Ok(LineEnding::Lf),
+ CRLF => Ok(LineEnding::Crlf),
+ _ => Err(ParseLineEndingError {
+ input: s.to_owned(),
+ }),
}
}
}
@@ -191,70 +219,89 @@ mod tests {
fn it_initializes_a_line_ending() {
let eol = LineEnding::new("\r");
- assert_eq!(eol, LineEnding::CR);
+ assert_eq!(eol, LineEnding::Cr);
let eol = LineEnding::new("\n");
- assert_eq!(eol, LineEnding::LF);
+ assert_eq!(eol, LineEnding::Lf);
let eol = LineEnding::new("\r\n");
- assert_eq!(eol, LineEnding::CRLF);
+ assert_eq!(eol, LineEnding::Crlf);
}
#[test]
fn it_uses_lf_line_ending_when_kind_is_unrecognized() {
let eol = LineEnding::new("NOPE!");
- assert_eq!(eol, LineEnding::LF);
+ assert_eq!(eol, LineEnding::Lf);
}
#[test]
fn it_serializes_a_line_ending() {
- assert_eq!("\r", format!("{}", LineEnding::CR));
- assert_eq!("\n", format!("{}", LineEnding::LF));
- assert_eq!("\r\n", format!("{}", LineEnding::CRLF));
+ assert_eq!("\r", format!("{}", LineEnding::Cr));
+ assert_eq!("\n", format!("{}", LineEnding::Lf));
+ assert_eq!("\r\n", format!("{}", LineEnding::Crlf));
}
#[test]
fn it_finds_preferred_line_ending_when_input_prefers_unix_style_endings() {
let input = "\nthis\nprefers\nunix-style endings\r\n";
- let eol = LineEnding::find(input, LineEnding::CRLF);
- assert_eq!(eol, LineEnding::LF);
+ let eol = LineEnding::find(input, LineEnding::Crlf);
+ assert_eq!(eol, LineEnding::Lf);
}
#[test]
fn it_finds_preferred_line_ending_when_input_prefers_windows_style_endings() {
let input = "\r\nthis\r\nprefers\r\nwindows-style endings\n";
- let eol = LineEnding::find(input, LineEnding::CRLF);
- assert_eq!(eol, LineEnding::CRLF);
+ let eol = LineEnding::find(input, LineEnding::Crlf);
+ assert_eq!(eol, LineEnding::Crlf);
}
#[test]
fn it_finds_preferred_line_ending_when_input_prefers_obsolete_style_endings() {
let input = "\rthis\rprefers\r\nobsolete endings\n";
- let eol = LineEnding::find(input, LineEnding::CRLF);
- assert_eq!(eol, LineEnding::CR);
+ let eol = LineEnding::find(input, LineEnding::Crlf);
+ assert_eq!(eol, LineEnding::Cr);
}
#[test]
fn it_uses_default_when_preference_cannot_be_determined() {
let input = "\r\nthis\r\nis\nambiguous\n?\r\r";
- let eol = LineEnding::find(input, LineEnding::LF);
- assert_eq!(eol, LineEnding::LF);
+ let eol = LineEnding::find(input, LineEnding::Lf);
+ assert_eq!(eol, LineEnding::Lf);
+ }
+
+ #[test]
+ fn it_counts_a_lone_cr_followed_by_crlf_separately() {
+ // "\r\r\n" is one CR then one CRLF - not two CRs, and not a CR plus
+ // an LF.
+ let eol = LineEnding::find("a\r\r\nb\r\nc\r\nd", LineEnding::Lf);
+ assert_eq!(eol, LineEnding::Crlf);
+
+ // ...and with the CRLFs removed, the lone CR wins
+ let eol = LineEnding::find("a\r\r\nb", LineEnding::Lf);
+ assert_eq!(eol, LineEnding::Lf);
+ }
+
+ #[test]
+ fn it_counts_line_breaks_around_multi_byte_characters() {
+ // `\r` / `\n` are ASCII and cannot appear inside a UTF-8 sequence
+ let eol = LineEnding::find("日本\r\n語\r\n🦀\n", LineEnding::Lf);
+ assert_eq!(eol, LineEnding::Crlf);
}
#[test]
fn it_uses_default_when_text_has_no_line_breaks() {
let input = "no line breaks";
- let eol = LineEnding::find(input, LineEnding::LF);
- assert_eq!(eol, LineEnding::LF);
+ let eol = LineEnding::find(input, LineEnding::Lf);
+ assert_eq!(eol, LineEnding::Lf);
}
#[test]
fn it_uses_default_when_text_is_empty() {
let input = "";
- let eol = LineEnding::find(input, LineEnding::LF);
- assert_eq!(eol, LineEnding::LF);
+ let eol = LineEnding::find(input, LineEnding::Lf);
+ assert_eq!(eol, LineEnding::Lf);
}
#[test]
@@ -262,12 +309,12 @@ mod tests {
let input = "\rthis\rprefers\r\nobsolete endings\n";
let eol = LineEnding::find_or_use_cr(input);
- assert_eq!(eol, LineEnding::CR);
+ assert_eq!(eol, LineEnding::Cr);
let input = "\r\nthis\r\nis\nambiguous\n?\r\r";
let eol = LineEnding::find_or_use_cr(input);
- assert_eq!(eol, LineEnding::CR);
+ assert_eq!(eol, LineEnding::Cr);
}
#[test]
@@ -275,12 +322,12 @@ mod tests {
let input = "\nthis\nprefers\nunix-style endings\r\n";
let eol = LineEnding::find_or_use_lf(input);
- assert_eq!(eol, LineEnding::LF);
+ assert_eq!(eol, LineEnding::Lf);
let input = "\r\nthis\r\nis\nambiguous\n?\r\r";
let eol = LineEnding::find_or_use_lf(input);
- assert_eq!(eol, LineEnding::LF);
+ assert_eq!(eol, LineEnding::Lf);
}
#[test]
@@ -288,11 +335,11 @@ mod tests {
let input = "\r\nthis\r\nprefers\r\nwindows-style endings\n";
let eol = LineEnding::find_or_use_crlf(input);
- assert_eq!(eol, LineEnding::CRLF);
+ assert_eq!(eol, LineEnding::Crlf);
let input = "\r\nthis\r\nis\nambiguous\n?\r\r";
let eol = LineEnding::find_or_use_crlf(input);
- assert_eq!(eol, LineEnding::CRLF);
+ assert_eq!(eol, LineEnding::Crlf);
}
}
diff --git a/crates/detect-newline-style/tests/integration.rs b/crates/detect-newline-style/tests/integration.rs
index d5b7ad4..b503108 100644
--- a/crates/detect-newline-style/tests/integration.rs
+++ b/crates/detect-newline-style/tests/integration.rs
@@ -1,62 +1,64 @@
+//! Integration tests exercising the crate's public API as a consumer would
+
use detect_newline_style::*;
#[test]
fn it_detects_cr_style_endings_while_defaulting_to_cr_endings() {
let input = "one\rtwo\r\nthree\rfour\n";
let eol = LineEnding::find_or_use_cr(input);
- assert_eq!(eol, LineEnding::CR);
+ assert_eq!(eol, LineEnding::Cr);
}
#[test]
fn it_detects_lf_style_endings_while_defaulting_to_cr_endings() {
let input = "one\rtwo\r\nthree\nfour\n";
let eol = LineEnding::find_or_use_cr(input);
- assert_eq!(eol, LineEnding::LF);
+ assert_eq!(eol, LineEnding::Lf);
}
#[test]
fn it_detects_crlf_style_endings_while_defaulting_to_cr_endings() {
let input = "one\rtwo\r\nthree\nfour\r\n";
let eol = LineEnding::find_or_use_cr(input);
- assert_eq!(eol, LineEnding::CRLF);
+ assert_eq!(eol, LineEnding::Crlf);
}
#[test]
fn it_detects_cr_style_endings_while_defaulting_to_lf_endings() {
let input = "one\rtwo\r\nthree\rfour\n";
let eol = LineEnding::find_or_use_lf(input);
- assert_eq!(eol, LineEnding::CR);
+ assert_eq!(eol, LineEnding::Cr);
}
#[test]
fn it_detects_lf_style_endings_while_defaulting_to_lf_endings() {
let input = "one\rtwo\r\nthree\nfour\n";
let eol = LineEnding::find_or_use_lf(input);
- assert_eq!(eol, LineEnding::LF);
+ assert_eq!(eol, LineEnding::Lf);
}
#[test]
fn it_detects_crlf_style_endings_while_defaulting_to_lf_endings() {
let input = "one\rtwo\r\nthree\nfour\r\n";
let eol = LineEnding::find_or_use_lf(input);
- assert_eq!(eol, LineEnding::CRLF);
+ assert_eq!(eol, LineEnding::Crlf);
}
#[test]
fn it_detects_cr_style_endings_while_defaulting_to_crlf_endings() {
let input = "one\rtwo\r\nthree\rfour\n";
let eol = LineEnding::find_or_use_crlf(input);
- assert_eq!(eol, LineEnding::CR);
+ assert_eq!(eol, LineEnding::Cr);
}
#[test]
fn it_detects_lf_style_endings_while_defaulting_to_crlf_endings() {
let input = "one\rtwo\r\nthree\nfour\n";
let eol = LineEnding::find_or_use_crlf(input);
- assert_eq!(eol, LineEnding::LF);
+ assert_eq!(eol, LineEnding::Lf);
}
#[test]
fn it_detects_crlf_style_endings_while_defaulting_to_crlf_endings() {
let input = "one\rtwo\r\nthree\nfour\r\n";
let eol = LineEnding::find_or_use_crlf(input);
- assert_eq!(eol, LineEnding::CRLF);
+ assert_eq!(eol, LineEnding::Crlf);
}
diff --git a/crates/node-js-release-info/Cargo.toml b/crates/node-js-release-info/Cargo.toml
index 346469c..22841b8 100644
--- a/crates/node-js-release-info/Cargo.toml
+++ b/crates/node-js-release-info/Cargo.toml
@@ -15,6 +15,7 @@ categories = [
"web-programming::http-client"
]
edition.workspace = true
+rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
@@ -22,14 +23,24 @@ repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-reqwest = { version = "0.11.*" }
-semver = "1.*"
-serde = { version = "1.*", features = ["derive"], optional = true }
-tokio = { version = "1.*", default-features = false, features = ["macros", "net", "time"] }
+reqwest.workspace = true
+semver.workspace = true
+serde = { workspace = true, optional = true }
+tokio = { workspace = true, features = ["macros", "net", "time"] }
[dev-dependencies]
-mockito = "1.*"
-serde_json = "1.*"
+mockito.workspace = true
+serde_json.workspace = true
+# doc examples use `#[tokio::main]`, which requires the multi-threaded runtime
+tokio = { workspace = true, features = ["rt-multi-thread"] }
[features]
json = ["dep:serde"]
+
+# NOTE: docs.rs builds with default features unless told otherwise, which
+# would hide everything the `json` feature adds
+[package.metadata.docs.rs]
+all-features = true
+
+[lints]
+workspace = true
diff --git a/crates/node-js-release-info/LICENSE-APACHE b/crates/node-js-release-info/LICENSE-APACHE
new file mode 100644
index 0000000..8e78c1c
--- /dev/null
+++ b/crates/node-js-release-info/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 2023 Rusty Contributors
+
+ 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/crates/node-js-release-info/LICENSE-MIT b/crates/node-js-release-info/LICENSE-MIT
new file mode 100644
index 0000000..67113f0
--- /dev/null
+++ b/crates/node-js-release-info/LICENSE-MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Rusty Contributors
+
+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/crates/node-js-release-info/README.md b/crates/node-js-release-info/README.md
index 2a65e43..ff3ffa2 100644
--- a/crates/node-js-release-info/README.md
+++ b/crates/node-js-release-info/README.md
@@ -21,20 +21,20 @@ cargo add tokio --features full
```
```rust
-use node_js_release_info::{NodeJSRelInfo, NodeJSRelInfoError};
+use node_js_release_info::{NodeJsRelInfo, NodeJsRelInfoError};
#[tokio::main]
-async fn main() -> Result<(), NodeJSRelInfoError> {
+async fn main() -> Result<(), NodeJsRelInfoError> {
// get a specific configuration
- let info = NodeJSRelInfo::new("20.6.1").macos().arm64().fetch().await?;
- assert_eq!(info.version, "20.6.1");
- assert_eq!(info.filename, "node-v20.6.1-darwin-arm64.tar.gz");
- assert_eq!(info.sha256, "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46");
- assert_eq!(info.url, "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz");
+ let info = NodeJsRelInfo::new("24.19.0").macos().arm64().fetch().await?;
+ assert_eq!(info.version, "24.19.0");
+ assert_eq!(info.filename, "node-v24.19.0-darwin-arm64.tar.gz");
+ assert_eq!(info.sha256, "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d");
+ assert_eq!(info.url, "https://nodejs.org/download/release/v24.19.0/node-v24.19.0-darwin-arm64.tar.gz");
// get all supported configurations
let all = info.fetch_all().await?;
- assert_eq!(all.len(), 24);
+ assert_eq!(all.len(), 19);
assert_eq!(all[2], info);
println!("{:?}", all);
Ok(())
@@ -49,15 +49,94 @@ Full `json` serialization + deserialization is available via the `json` feature.
cargo add node-js-release-info --features json
```
-```rust
-use node_js_release_info::{NodeJSRelInfo, NodeJSRelInfoError};
+```rust,ignore
+use node_js_release_info::NodeJsRelInfo;
#[tokio::main]
async fn main() {
- let info = NodeJSRelInfo::new("20.6.1").macos().arm64().to_owned();
+ let info = NodeJsRelInfo::new("24.19.0").macos().arm64();
let json = serde_json::to_string(&info).unwrap();
let info_deserialized = serde_json::from_str(&json).unwrap();
assert_eq!(info, info_deserialized);
}
```
+
+## Migrations
+
+
+1.x -> 2.x
+
+
+**Type and variant names now follow [RFC 430](https://rust-lang.github.io/rfcs/0430-finalizing-naming-conventions.html)**
+
+| before | after |
+| --- | --- |
+| `NodeJSRelInfo` | `NodeJsRelInfo` |
+| `NodeJSRelInfoError` | `NodeJsRelInfoError` |
+| `NodeJSOS` | `NodeJsOs` |
+| `NodeJSArch` | `NodeJsArch` |
+| `NodeJSPkgExt` | `NodeJsPkgExt` |
+| `NodeJSOS::AIX` | `NodeJsOs::Aix` |
+| `NodeJSArch::ARM64` | `NodeJsArch::Arm64` |
+| `NodeJSArch::ARMV7L` | `NodeJsArch::Armv7l` |
+| `NodeJSArch::PPC64` | `NodeJsArch::Ppc64` |
+| `NodeJSArch::PPC64LE` | `NodeJsArch::Ppc64le` |
+| `NodeJSArch::S390X` | `NodeJsArch::S390x` |
+
+**Builder methods consume `self`**
+
+They now return an owned value directly, so the trailing `to_owned()` is no longer needed - and `NodeJsRelInfo::to_owned()` has been removed. Use `.clone()` if you want a copy.
+
+```rust,ignore
+// before
+let info = NodeJSRelInfo::new("24.19.0").macos().arm64().to_owned();
+// after
+let info = NodeJsRelInfo::new("24.19.0").macos().arm64();
+```
+
+Calling a builder as a statement no longer works, since it takes `self`:
+
+```rust,ignore
+// before
+let mut info = NodeJSRelInfo::new("24.19.0");
+info.macos();
+// after
+let info = NodeJsRelInfo::new("24.19.0").macos();
+```
+
+**`fetch()` consumes `self`**
+
+It previously mutated in place *and* returned a clone. It now takes `self` and returns the populated value:
+
+```rust,ignore
+// before
+let mut info = NodeJSRelInfo::new("24.19.0");
+info.fetch().await?; // `info` updated in place
+// after
+let info = NodeJsRelInfo::new("24.19.0").fetch().await?;
+```
+
+**Enums are `#[non_exhaustive]`**
+
+`NodeJsOs`, `NodeJsArch`, `NodeJsPkgExt` and `NodeJsRelInfoError` may gain variants in a minor release, so a `match` over them needs a `_` arm. Node.js adds and removes target platforms over time, and this keeps that from being a breaking change.
+
+**Error messages changed**
+
+The `Error: ` prefix is gone and messages are lowercased, per [API guideline C-GOOD-ERR](https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved). Previously they composed into chains as `Error: Error: ...`.
+
+```text
+before: Error: Invalid Version! Received: 'x'
+after: invalid version - received: 'x'
+```
+
+`NodeJsRelInfoError` also implements `Error::source()` now, exposing the underlying `reqwest::Error` behind `HttpError`.
+
+**New variants**
+
+`NodeJsOs::SunOs` (`sunos`) and `NodeJsArch::Armv6l` (`armv6l`) were missing. Both appear in older Node.js releases, so fetching those versions previously failed with `UnrecognizedOs` / `UnrecognizedArch` on valid artifacts.
+
+Note that Node.js v24 *dropped* `linux-armv7l` and all 32-bit Windows builds, so `fetch_all` returns 19 configurations for v24 where v20 returned 24.
+
+
+
diff --git a/crates/node-js-release-info/src/arch.rs b/crates/node-js-release-info/src/arch.rs
index 0c80dc5..96ad36e 100644
--- a/crates/node-js-release-info/src/arch.rs
+++ b/crates/node-js-release-info/src/arch.rs
@@ -1,74 +1,103 @@
-use crate::error::NodeJSRelInfoError;
+use crate::error::NodeJsRelInfoError;
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
use std::env::consts::ARCH;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
-#[derive(Clone, Debug, PartialEq)]
+/// The CPU architecture a Node.js distributable targets
+///
+/// Non-exhaustive: Node.js has added and removed target architectures over
+/// time, so new variants may appear in a minor release
+#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "json", derive(Deserialize, Serialize))]
-pub enum NodeJSArch {
+#[non_exhaustive]
+pub enum NodeJsArch {
+ /// 64-bit x86 (`x64`)
+ #[default]
#[cfg_attr(feature = "json", serde(rename = "x64"))]
X64,
+ /// 32-bit x86 (`x86`)
#[cfg_attr(feature = "json", serde(rename = "x86"))]
X86,
+ /// 64-bit ARM (`arm64`)
#[cfg_attr(feature = "json", serde(rename = "arm64"))]
- ARM64,
+ Arm64,
+ /// 32-bit ARMv6 with hardware floating point (`armv6l`) - shipped up to
+ /// Node.js v11
+ #[cfg_attr(feature = "json", serde(rename = "armv6l"))]
+ Armv6l,
+ /// 32-bit ARMv7 with hardware floating point (`armv7l`) - shipped up to
+ /// Node.js v23
#[cfg_attr(feature = "json", serde(rename = "armv7l"))]
- ARMV7L,
+ Armv7l,
+ /// 64-bit PowerPC, big-endian (`ppc64`)
#[cfg_attr(feature = "json", serde(rename = "ppc64"))]
- PPC64,
+ Ppc64,
+ /// 64-bit PowerPC, little-endian (`ppc64le`)
#[cfg_attr(feature = "json", serde(rename = "ppc64le"))]
- PPC64LE,
+ Ppc64le,
+ /// 64-bit IBM Z (`s390x`)
#[cfg_attr(feature = "json", serde(rename = "s390x"))]
- S390X,
+ S390x,
}
-impl Default for NodeJSArch {
- fn default() -> Self {
- NodeJSArch::new()
- }
-}
-
-impl NodeJSArch {
- pub fn new() -> NodeJSArch {
- NodeJSArch::X64
+impl NodeJsArch {
+ /// Creates a new instance using the default architecture ([`X64`](NodeJsArch::X64))
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use node_js_release_info::NodeJsArch;
+ /// assert_eq!(NodeJsArch::new(), NodeJsArch::X64);
+ /// ```
+ pub fn new() -> NodeJsArch {
+ NodeJsArch::default()
}
- pub fn from_env() -> Result {
- NodeJSArch::from_str(ARCH)
+ /// Determines the architecture of the current environment via
+ /// [`std::env::consts::ARCH`]
+ ///
+ /// # Errors
+ ///
+ /// Returns [`NodeJsRelInfoError::UnrecognizedArch`] when the current
+ /// architecture has no corresponding Node.js distributable
+ pub fn from_env() -> Result {
+ NodeJsArch::from_str(ARCH)
}
}
-impl Display for NodeJSArch {
+impl Display for NodeJsArch {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let arch = match self {
- NodeJSArch::X64 => "x64",
- NodeJSArch::X86 => "x86",
- NodeJSArch::ARM64 => "arm64",
- NodeJSArch::ARMV7L => "armv7l",
- NodeJSArch::PPC64 => "ppc64",
- NodeJSArch::PPC64LE => "ppc64le",
- NodeJSArch::S390X => "s390x",
+ NodeJsArch::X64 => "x64",
+ NodeJsArch::X86 => "x86",
+ NodeJsArch::Arm64 => "arm64",
+ NodeJsArch::Armv6l => "armv6l",
+ NodeJsArch::Armv7l => "armv7l",
+ NodeJsArch::Ppc64 => "ppc64",
+ NodeJsArch::Ppc64le => "ppc64le",
+ NodeJsArch::S390x => "s390x",
};
- write!(f, "{}", arch)
+ write!(f, "{arch}")
}
}
-impl FromStr for NodeJSArch {
- type Err = NodeJSRelInfoError;
+impl FromStr for NodeJsArch {
+ type Err = NodeJsRelInfoError;
- fn from_str(s: &str) -> Result {
+ fn from_str(s: &str) -> Result {
match s {
- "x64" | "x86_64" => Ok(NodeJSArch::X64),
- "x86" => Ok(NodeJSArch::X86),
- "arm64" | "aarch64" => Ok(NodeJSArch::ARM64),
- "arm" | "armv7l" => Ok(NodeJSArch::ARMV7L),
- "ppc64" | "powerpc64" => Ok(NodeJSArch::PPC64),
- "ppc64le" => Ok(NodeJSArch::PPC64LE),
- "s390x" => Ok(NodeJSArch::S390X),
- _ => Err(NodeJSRelInfoError::UnrecognizedArch(s.to_string())),
+ "x64" | "x86_64" => Ok(NodeJsArch::X64),
+ "x86" => Ok(NodeJsArch::X86),
+ "arm64" | "aarch64" => Ok(NodeJsArch::Arm64),
+ "armv6l" => Ok(NodeJsArch::Armv6l),
+ "arm" | "armv7l" => Ok(NodeJsArch::Armv7l),
+ "ppc64" | "powerpc64" => Ok(NodeJsArch::Ppc64),
+ "ppc64le" => Ok(NodeJsArch::Ppc64le),
+ "s390x" => Ok(NodeJsArch::S390x),
+ _ => Err(NodeJsRelInfoError::UnrecognizedArch(s.to_string())),
}
}
}
@@ -79,107 +108,114 @@ mod tests {
#[test]
fn it_initializes() {
- let arch = NodeJSArch::new();
- assert_eq!(arch, NodeJSArch::X64);
+ let arch = NodeJsArch::new();
+ assert_eq!(arch, NodeJsArch::X64);
}
#[test]
fn it_initializes_with_defaults() {
- let arch = NodeJSArch::default();
- assert_eq!(arch, NodeJSArch::X64);
+ let arch = NodeJsArch::default();
+ assert_eq!(arch, NodeJsArch::X64);
}
#[test]
fn it_initializes_from_str() {
- let arch = NodeJSArch::from_str("x64").unwrap();
+ let arch = NodeJsArch::from_str("x64").unwrap();
+
+ assert_eq!(arch, NodeJsArch::X64);
- assert_eq!(arch, NodeJSArch::X64);
+ let arch = NodeJsArch::from_str("x86_64").unwrap();
- let arch = NodeJSArch::from_str("x86_64").unwrap();
+ assert_eq!(arch, NodeJsArch::X64);
- assert_eq!(arch, NodeJSArch::X64);
+ let arch = NodeJsArch::from_str("x86").unwrap();
- let arch = NodeJSArch::from_str("x86").unwrap();
+ assert_eq!(arch, NodeJsArch::X86);
- assert_eq!(arch, NodeJSArch::X86);
+ let arch = NodeJsArch::from_str("arm64").unwrap();
- let arch = NodeJSArch::from_str("arm64").unwrap();
+ assert_eq!(arch, NodeJsArch::Arm64);
- assert_eq!(arch, NodeJSArch::ARM64);
+ let arch = NodeJsArch::from_str("aarch64").unwrap();
- let arch = NodeJSArch::from_str("aarch64").unwrap();
+ assert_eq!(arch, NodeJsArch::Arm64);
- assert_eq!(arch, NodeJSArch::ARM64);
+ let arch = NodeJsArch::from_str("arm").unwrap();
- let arch = NodeJSArch::from_str("arm").unwrap();
+ assert_eq!(arch, NodeJsArch::Armv7l);
- assert_eq!(arch, NodeJSArch::ARMV7L);
+ let arch = NodeJsArch::from_str("armv6l").unwrap();
- let arch = NodeJSArch::from_str("ppc64").unwrap();
+ assert_eq!(arch, NodeJsArch::Armv6l);
- assert_eq!(arch, NodeJSArch::PPC64);
+ let arch = NodeJsArch::from_str("ppc64").unwrap();
- let arch = NodeJSArch::from_str("ppc64le").unwrap();
+ assert_eq!(arch, NodeJsArch::Ppc64);
- assert_eq!(arch, NodeJSArch::PPC64LE);
+ let arch = NodeJsArch::from_str("ppc64le").unwrap();
- let arch = NodeJSArch::from_str("powerpc64").unwrap();
+ assert_eq!(arch, NodeJsArch::Ppc64le);
- assert_eq!(arch, NodeJSArch::PPC64);
+ let arch = NodeJsArch::from_str("powerpc64").unwrap();
- let arch = NodeJSArch::from_str("s390x").unwrap();
+ assert_eq!(arch, NodeJsArch::Ppc64);
- assert_eq!(arch, NodeJSArch::S390X);
+ let arch = NodeJsArch::from_str("s390x").unwrap();
+
+ assert_eq!(arch, NodeJsArch::S390x);
}
#[test]
fn it_serializes_to_str() {
- let text = format!("{}", NodeJSArch::X64);
+ let text = format!("{}", NodeJsArch::X64);
assert_eq!(text, "x64");
- let text = format!("{}", NodeJSArch::X86);
+ let text = format!("{}", NodeJsArch::X86);
assert_eq!(text, "x86");
- let text = format!("{}", NodeJSArch::ARM64);
+ let text = format!("{}", NodeJsArch::Arm64);
assert_eq!(text, "arm64");
- let text = format!("{}", NodeJSArch::ARMV7L);
+ let text = format!("{}", NodeJsArch::Armv7l);
assert_eq!(text, "armv7l");
- let text = format!("{}", NodeJSArch::PPC64);
+ let text = format!("{}", NodeJsArch::Armv6l);
+
+ assert_eq!(text, "armv6l");
+
+ let text = format!("{}", NodeJsArch::Ppc64);
assert_eq!(text, "ppc64");
- let text = format!("{}", NodeJSArch::PPC64LE);
+ let text = format!("{}", NodeJsArch::Ppc64le);
assert_eq!(text, "ppc64le");
- let text = format!("{}", NodeJSArch::S390X);
+ let text = format!("{}", NodeJsArch::S390x);
assert_eq!(text, "s390x");
}
#[test]
fn it_initializes_using_current_environment() {
- NodeJSArch::from_env().unwrap();
+ NodeJsArch::from_env().unwrap();
}
#[test]
- #[should_panic(
- expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedArch(\"NOPE!\")"
- )]
fn it_fails_when_arch_is_unrecognized() {
- NodeJSArch::from_str("NOPE!").unwrap();
+ let err = NodeJsArch::from_str("NOPE!").unwrap_err();
+ assert!(matches!(err, NodeJsRelInfoError::UnrecognizedArch(x) if x == "NOPE!"));
}
#[test]
+ #[cfg(feature = "json")]
fn it_serializes_and_deserializes() {
- let arch_json = serde_json::to_string(&NodeJSArch::X64).unwrap();
- let arch: NodeJSArch = serde_json::from_str(&arch_json).unwrap();
- assert_eq!(arch, NodeJSArch::X64);
+ let arch_json = serde_json::to_string(&NodeJsArch::X64).unwrap();
+ let arch: NodeJsArch = serde_json::from_str(&arch_json).unwrap();
+ assert_eq!(arch, NodeJsArch::X64);
}
}
diff --git a/crates/node-js-release-info/src/error.rs b/crates/node-js-release-info/src/error.rs
index f77128a..e1e9993 100644
--- a/crates/node-js-release-info/src/error.rs
+++ b/crates/node-js-release-info/src/error.rs
@@ -1,16 +1,20 @@
use std::error::Error;
use std::fmt::{Display, Formatter, Result};
+/// The error type returned by all fallible operations in this crate
+///
+/// Non-exhaustive: new variants may appear in a minor release
#[derive(Debug)]
-pub enum NodeJSRelInfoError {
+#[non_exhaustive]
+pub enum NodeJsRelInfoError {
/// The operating system for the Node.js distributable you are targeting is
- /// unrecognized - see: [`NodeJSOS`](crate::NodeJSOS) for options
+ /// unrecognized - see: [`NodeJsOs`](crate::NodeJsOs) for options
UnrecognizedOs(String),
/// The CPU architecture for the Node.js distributable you are targeting is
- /// unrecognized - see: [`NodeJSArch`](crate::NodeJSArch) for options
+ /// unrecognized - see: [`NodeJsArch`](crate::NodeJsArch) for options
UnrecognizedArch(String),
/// The file extension of the Node.js distributable you are targeting is
- /// unrecognized - see: [`NodeJSPkgExt`](crate::NodeJSPkgExt) for options
+ /// unrecognized - see: [`NodeJsPkgExt`](crate::NodeJsPkgExt) for options
UnrecognizedExt(String),
/// The version string provided is invalid - see: [semver](https://semver.org)
InvalidVersion(String),
@@ -22,39 +26,49 @@ pub enum NodeJSRelInfoError {
HttpError(reqwest::Error),
}
-impl Error for NodeJSRelInfoError {}
+impl Error for NodeJsRelInfoError {
+ /// Exposes the underlying [`reqwest::Error`] behind
+ /// [`HttpError`](NodeJsRelInfoError::HttpError) so callers (and error
+ /// reporters like `anyhow`) can walk the full cause chain
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
+ match self {
+ NodeJsRelInfoError::HttpError(e) => Some(e),
+ _ => None,
+ }
+ }
+}
-impl Display for NodeJSRelInfoError {
+impl Display for NodeJsRelInfoError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let message = match self {
- NodeJSRelInfoError::UnrecognizedOs(input) => {
- format!("Unrecognized OS! Received: '{}'", input)
+ NodeJsRelInfoError::UnrecognizedOs(input) => {
+ format!("unrecognized os - received: '{input}'")
}
- NodeJSRelInfoError::UnrecognizedArch(input) => {
- format!("Unrecognized Arch! Received: '{}'", input)
+ NodeJsRelInfoError::UnrecognizedArch(input) => {
+ format!("unrecognized arch - received: '{input}'")
}
- NodeJSRelInfoError::UnrecognizedExt(input) => {
- format!("Unrecognized File Extension! Received: '{}'", input)
+ NodeJsRelInfoError::UnrecognizedExt(input) => {
+ format!("unrecognized file extension - received: '{input}'")
}
- NodeJSRelInfoError::InvalidVersion(input) => {
- format!("Invalid Version! Received: '{}'", input)
+ NodeJsRelInfoError::InvalidVersion(input) => {
+ format!("invalid version - received: '{input}'")
}
- NodeJSRelInfoError::UnrecognizedVersion(input) => {
- format!("Unrecognized Version! Received: '{}'", input)
+ NodeJsRelInfoError::UnrecognizedVersion(input) => {
+ format!("unrecognized version - received: '{input}'")
}
- NodeJSRelInfoError::UnrecognizedConfiguration(input) => {
- format!("Unrecognized Configuration! Received: '{}'", input)
+ NodeJsRelInfoError::UnrecognizedConfiguration(input) => {
+ format!("unrecognized configuration - received: '{input}'")
}
- NodeJSRelInfoError::HttpError(e) => return write!(f, "{}", e),
+ NodeJsRelInfoError::HttpError(e) => return write!(f, "{e}"),
};
- write!(f, "Error: {}", message)
+ write!(f, "{message}")
}
}
-impl From for NodeJSRelInfoError {
+impl From for NodeJsRelInfoError {
fn from(e: reqwest::Error) -> Self {
- NodeJSRelInfoError::HttpError(e)
+ NodeJsRelInfoError::HttpError(e)
}
}
@@ -64,69 +78,64 @@ mod tests {
#[test]
fn it_prints_expected_message_when_os_is_unrecognized() {
- let err = NodeJSRelInfoError::UnrecognizedOs("unknown-os".to_string());
- assert_eq!(
- format!("{err}"),
- "Error: Unrecognized OS! Received: 'unknown-os'"
- );
+ let err = NodeJsRelInfoError::UnrecognizedOs("unknown-os".to_string());
+ assert_eq!(format!("{err}"), "unrecognized os - received: 'unknown-os'");
}
#[test]
fn it_prints_expected_message_when_arch_is_unrecognized() {
- let err = NodeJSRelInfoError::UnrecognizedArch("unknown-arch".to_string());
+ let err = NodeJsRelInfoError::UnrecognizedArch("unknown-arch".to_string());
assert_eq!(
format!("{err}"),
- "Error: Unrecognized Arch! Received: 'unknown-arch'"
+ "unrecognized arch - received: 'unknown-arch'"
);
}
#[test]
fn it_prints_expected_message_when_extension_is_unrecognized() {
- let err = NodeJSRelInfoError::UnrecognizedExt("unknown-ext".to_string());
+ let err = NodeJsRelInfoError::UnrecognizedExt("unknown-ext".to_string());
assert_eq!(
format!("{err}"),
- "Error: Unrecognized File Extension! Received: 'unknown-ext'"
+ "unrecognized file extension - received: 'unknown-ext'"
);
}
#[test]
fn it_prints_expected_message_when_version_is_invalid() {
- let err = NodeJSRelInfoError::InvalidVersion("invalid-ver".to_string());
+ let err = NodeJsRelInfoError::InvalidVersion("invalid-ver".to_string());
assert_eq!(
format!("{err}"),
- "Error: Invalid Version! Received: 'invalid-ver'"
+ "invalid version - received: 'invalid-ver'"
);
}
#[test]
fn it_prints_expected_message_when_version_is_unrecognized() {
- let err = NodeJSRelInfoError::UnrecognizedVersion("unknown-ver".to_string());
+ let err = NodeJsRelInfoError::UnrecognizedVersion("unknown-ver".to_string());
assert_eq!(
format!("{err}"),
- "Error: Unrecognized Version! Received: 'unknown-ver'"
+ "unrecognized version - received: 'unknown-ver'"
);
}
#[test]
fn it_prints_expected_message_when_configuration_is_unrecognized() {
- let err = NodeJSRelInfoError::UnrecognizedConfiguration("unknown-cfg".to_string());
+ let err = NodeJsRelInfoError::UnrecognizedConfiguration("unknown-cfg".to_string());
assert_eq!(
format!("{err}"),
- "Error: Unrecognized Configuration! Received: 'unknown-cfg'"
+ "unrecognized configuration - received: 'unknown-cfg'"
);
}
#[tokio::test]
async fn it_prints_expected_message_upon_http_error() {
- let err = fake_http_error().await.unwrap_err();
- assert_eq!(
- format!("{err}"),
- "builder error: relative URL without a base"
- );
- }
+ let source = reqwest::get("not-a-url").await.unwrap_err();
+ // NOTE: `HttpError` delegates to the wrapped `reqwest::Error` verbatim
+ // so assert on that rather than on reqwest's exact wording, which
+ // changes between releases
+ let expected = source.to_string();
+ let err = NodeJsRelInfoError::from(source);
- async fn fake_http_error() -> std::result::Result<(), NodeJSRelInfoError> {
- let error = reqwest::get("not-a-url").await.unwrap_err();
- Err(NodeJSRelInfoError::from(error))
+ assert_eq!(format!("{err}"), expected);
}
}
diff --git a/crates/node-js-release-info/src/ext.rs b/crates/node-js-release-info/src/ext.rs
index c9979bf..bf94715 100644
--- a/crates/node-js-release-info/src/ext.rs
+++ b/crates/node-js-release-info/src/ext.rs
@@ -1,60 +1,73 @@
-use crate::error::NodeJSRelInfoError;
+use crate::error::NodeJsRelInfoError;
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::FromStr;
-#[derive(Clone, Debug, PartialEq)]
+/// The file extension of a Node.js distributable
+///
+/// Non-exhaustive: Node.js has added and removed package formats over time,
+/// so new variants may appear in a minor release
+#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "json", derive(Deserialize, Serialize))]
-pub enum NodeJSPkgExt {
+#[non_exhaustive]
+pub enum NodeJsPkgExt {
+ /// gzip-compressed tarball (`tar.gz`)
+ #[default]
#[cfg_attr(feature = "json", serde(rename = "tar.gz"))]
Targz,
+ /// xz-compressed tarball (`tar.xz`)
#[cfg_attr(feature = "json", serde(rename = "tar.xz"))]
Tarxz,
+ /// zip archive (`zip`) - Windows only
#[cfg_attr(feature = "json", serde(rename = "zip"))]
Zip,
+ /// Windows installer package (`msi`)
#[cfg_attr(feature = "json", serde(rename = "msi"))]
Msi,
+ /// 7-Zip archive (`7z`) - Windows only
#[cfg_attr(feature = "json", serde(rename = "7z"))]
S7z, // can't start w/ a number (X_x)
}
-impl Default for NodeJSPkgExt {
- fn default() -> Self {
- NodeJSPkgExt::new()
+impl NodeJsPkgExt {
+ /// Creates a new instance using the default extension ([`Targz`](NodeJsPkgExt::Targz))
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use node_js_release_info::NodeJsPkgExt;
+ /// assert_eq!(NodeJsPkgExt::new(), NodeJsPkgExt::Targz);
+ /// ```
+ pub fn new() -> NodeJsPkgExt {
+ NodeJsPkgExt::default()
}
}
-
-impl NodeJSPkgExt {
- pub fn new() -> NodeJSPkgExt {
- NodeJSPkgExt::Targz
- }
-}
-impl Display for NodeJSPkgExt {
+impl Display for NodeJsPkgExt {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let arch = match self {
- NodeJSPkgExt::Targz => "tar.gz",
- NodeJSPkgExt::Tarxz => "tar.xz",
- NodeJSPkgExt::Zip => "zip",
- NodeJSPkgExt::Msi => "msi",
- NodeJSPkgExt::S7z => "7z",
+ NodeJsPkgExt::Targz => "tar.gz",
+ NodeJsPkgExt::Tarxz => "tar.xz",
+ NodeJsPkgExt::Zip => "zip",
+ NodeJsPkgExt::Msi => "msi",
+ NodeJsPkgExt::S7z => "7z",
};
- write!(f, "{}", arch)
+ write!(f, "{arch}")
}
}
-impl FromStr for NodeJSPkgExt {
- type Err = NodeJSRelInfoError;
+impl FromStr for NodeJsPkgExt {
+ type Err = NodeJsRelInfoError;
- fn from_str(s: &str) -> Result {
+ fn from_str(s: &str) -> Result {
match s {
- "tar.gz" => Ok(NodeJSPkgExt::Targz),
- "tar.xz" => Ok(NodeJSPkgExt::Tarxz),
- "zip" => Ok(NodeJSPkgExt::Zip),
- "msi" => Ok(NodeJSPkgExt::Msi),
- "7z" => Ok(NodeJSPkgExt::S7z),
- _ => Err(NodeJSRelInfoError::UnrecognizedExt(s.to_string())),
+ "tar.gz" => Ok(NodeJsPkgExt::Targz),
+ "tar.xz" => Ok(NodeJsPkgExt::Tarxz),
+ "zip" => Ok(NodeJsPkgExt::Zip),
+ "msi" => Ok(NodeJsPkgExt::Msi),
+ "7z" => Ok(NodeJsPkgExt::S7z),
+ _ => Err(NodeJsRelInfoError::UnrecognizedExt(s.to_string())),
}
}
}
@@ -65,74 +78,73 @@ mod tests {
#[test]
fn it_initializes() {
- let ext = NodeJSPkgExt::new();
- assert_eq!(ext, NodeJSPkgExt::Targz);
+ let ext = NodeJsPkgExt::new();
+ assert_eq!(ext, NodeJsPkgExt::Targz);
}
#[test]
fn it_initializes_with_defaults() {
- let ext = NodeJSPkgExt::default();
- assert_eq!(ext, NodeJSPkgExt::Targz);
+ let ext = NodeJsPkgExt::default();
+ assert_eq!(ext, NodeJsPkgExt::Targz);
}
#[test]
fn it_initializes_from_str() {
- let ext = NodeJSPkgExt::from_str("tar.gz").unwrap();
+ let ext = NodeJsPkgExt::from_str("tar.gz").unwrap();
- assert_eq!(ext, NodeJSPkgExt::Targz);
+ assert_eq!(ext, NodeJsPkgExt::Targz);
- let ext = NodeJSPkgExt::from_str("tar.xz").unwrap();
+ let ext = NodeJsPkgExt::from_str("tar.xz").unwrap();
- assert_eq!(ext, NodeJSPkgExt::Tarxz);
+ assert_eq!(ext, NodeJsPkgExt::Tarxz);
- let ext = NodeJSPkgExt::from_str("zip").unwrap();
+ let ext = NodeJsPkgExt::from_str("zip").unwrap();
- assert_eq!(ext, NodeJSPkgExt::Zip);
+ assert_eq!(ext, NodeJsPkgExt::Zip);
- let ext = NodeJSPkgExt::from_str("msi").unwrap();
+ let ext = NodeJsPkgExt::from_str("msi").unwrap();
- assert_eq!(ext, NodeJSPkgExt::Msi);
+ assert_eq!(ext, NodeJsPkgExt::Msi);
- let ext = NodeJSPkgExt::from_str("7z").unwrap();
+ let ext = NodeJsPkgExt::from_str("7z").unwrap();
- assert_eq!(ext, NodeJSPkgExt::S7z);
+ assert_eq!(ext, NodeJsPkgExt::S7z);
}
#[test]
fn it_serializes_to_str() {
- let text = format!("{}", NodeJSPkgExt::Targz);
+ let text = format!("{}", NodeJsPkgExt::Targz);
assert_eq!(text, "tar.gz");
- let text = format!("{}", NodeJSPkgExt::Tarxz);
+ let text = format!("{}", NodeJsPkgExt::Tarxz);
assert_eq!(text, "tar.xz");
- let text = format!("{}", NodeJSPkgExt::Zip);
+ let text = format!("{}", NodeJsPkgExt::Zip);
assert_eq!(text, "zip");
- let text = format!("{}", NodeJSPkgExt::Msi);
+ let text = format!("{}", NodeJsPkgExt::Msi);
assert_eq!(text, "msi");
- let text = format!("{}", NodeJSPkgExt::S7z);
+ let text = format!("{}", NodeJsPkgExt::S7z);
assert_eq!(text, "7z");
}
#[test]
- #[should_panic(
- expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedExt(\"NOPE!\")"
- )]
- fn it_fails_when_arch_is_unrecognized() {
- NodeJSPkgExt::from_str("NOPE!").unwrap();
+ fn it_fails_when_ext_is_unrecognized() {
+ let err = NodeJsPkgExt::from_str("NOPE!").unwrap_err();
+ assert!(matches!(err, NodeJsRelInfoError::UnrecognizedExt(x) if x == "NOPE!"));
}
#[test]
+ #[cfg(feature = "json")]
fn it_serializes_and_deserializes() {
- let ext_json = serde_json::to_string(&NodeJSPkgExt::Tarxz).unwrap();
- let ext: NodeJSPkgExt = serde_json::from_str(&ext_json).unwrap();
- assert_eq!(ext, NodeJSPkgExt::Tarxz);
+ let ext_json = serde_json::to_string(&NodeJsPkgExt::Tarxz).unwrap();
+ let ext: NodeJsPkgExt = serde_json::from_str(&ext_json).unwrap();
+ assert_eq!(ext, NodeJsPkgExt::Tarxz);
}
}
diff --git a/crates/node-js-release-info/src/lib.rs b/crates/node-js-release-info/src/lib.rs
index 316244f..7da6362 100644
--- a/crates/node-js-release-info/src/lib.rs
+++ b/crates/node-js-release-info/src/lib.rs
@@ -7,24 +7,31 @@ mod os;
mod specs;
mod url;
-pub use crate::arch::NodeJSArch;
-pub use crate::error::NodeJSRelInfoError;
-pub use crate::ext::NodeJSPkgExt;
-pub use crate::os::NodeJSOS;
-use crate::url::NodeJSURLFormatter;
+pub use crate::arch::NodeJsArch;
+pub use crate::error::NodeJsRelInfoError;
+pub use crate::ext::NodeJsPkgExt;
+pub use crate::os::NodeJsOs;
+use crate::url::NodeJsUrlFormatter;
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
use std::string::ToString;
-#[derive(Clone, Debug, Default, PartialEq)]
+/// Metadata describing a single Node.js distributable
+///
+/// Build one with [`new`](NodeJsRelInfo::new) or
+/// [`from_env`](NodeJsRelInfo::from_env), narrow it with the builder methods
+/// (e.g. [`macos`](NodeJsRelInfo::macos), [`arm64`](NodeJsRelInfo::arm64)),
+/// then call [`fetch`](NodeJsRelInfo::fetch) to populate `filename`, `sha256`
+/// and `url` from the downloads server
+#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "json", derive(Deserialize, Serialize))]
-pub struct NodeJSRelInfo {
+pub struct NodeJsRelInfo {
/// The operating system for the Node.js distributable you are targeting
- pub os: NodeJSOS,
+ pub os: NodeJsOs,
/// The CPU architecture for the Node.js distributable you are targeting
- pub arch: NodeJSArch,
+ pub arch: NodeJsArch,
/// The file extension for the Node.js distributable you are targeting
- pub ext: NodeJSPkgExt,
+ pub ext: NodeJsPkgExt,
/// The version of Node.js you are targeting as a [semver](https://semver.org) string
pub version: String,
/// The filename of the Node.js distributable (populated after fetching)
@@ -34,10 +41,10 @@ pub struct NodeJSRelInfo {
/// The fully qualified url for the Node.js distributable (populated after fetching)
pub url: String,
#[cfg_attr(feature = "json", serde(skip))]
- url_fmt: NodeJSURLFormatter,
+ url_fmt: NodeJsUrlFormatter,
}
-impl NodeJSRelInfo {
+impl NodeJsRelInfo {
/// Creates a new instance using default settings
///
/// # Arguments
@@ -47,11 +54,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1");
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0");
/// ```
pub fn new>(semver: T) -> Self {
- NodeJSRelInfo {
+ NodeJsRelInfo {
version: semver.as_ref().to_owned(),
..Default::default()
}
@@ -66,16 +73,16 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::from_env("20.6.1");
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::from_env("24.19.0");
/// ```
- pub fn from_env>(semver: T) -> Result {
- let mut info = NodeJSRelInfo::new(semver);
- info.os = NodeJSOS::from_env()?;
- info.arch = NodeJSArch::from_env()?;
+ pub fn from_env>(semver: T) -> Result {
+ let mut info = NodeJsRelInfo::new(semver);
+ info.os = NodeJsOs::from_env()?;
+ info.arch = NodeJsArch::from_env()?;
info.ext = match info.os {
- NodeJSOS::Windows => NodeJSPkgExt::Zip,
- _ => NodeJSPkgExt::Targz,
+ NodeJsOs::Windows => NodeJsPkgExt::Zip,
+ _ => NodeJsPkgExt::Targz,
};
Ok(info)
}
@@ -85,11 +92,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").macos();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").macos();
/// ```
- pub fn macos(&mut self) -> &mut Self {
- self.os = NodeJSOS::Darwin;
+ pub fn macos(mut self) -> Self {
+ self.os = NodeJsOs::Darwin;
self
}
@@ -98,11 +105,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").linux();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").linux();
/// ```
- pub fn linux(&mut self) -> &mut Self {
- self.os = NodeJSOS::Linux;
+ pub fn linux(mut self) -> Self {
+ self.os = NodeJsOs::Linux;
self
}
@@ -111,11 +118,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").windows();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").windows();
/// ```
- pub fn windows(&mut self) -> &mut Self {
- self.os = NodeJSOS::Windows;
+ pub fn windows(mut self) -> Self {
+ self.os = NodeJsOs::Windows;
self
}
@@ -124,11 +131,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").aix();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").aix();
/// ```
- pub fn aix(&mut self) -> &mut Self {
- self.os = NodeJSOS::AIX;
+ pub fn aix(mut self) -> Self {
+ self.os = NodeJsOs::Aix;
self
}
@@ -137,11 +144,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").x64();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").x64();
/// ```
- pub fn x64(&mut self) -> &mut Self {
- self.arch = NodeJSArch::X64;
+ pub fn x64(mut self) -> Self {
+ self.arch = NodeJsArch::X64;
self
}
@@ -150,11 +157,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").x86();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").x86();
/// ```
- pub fn x86(&mut self) -> &mut Self {
- self.arch = NodeJSArch::X86;
+ pub fn x86(mut self) -> Self {
+ self.arch = NodeJsArch::X86;
self
}
@@ -163,11 +170,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").arm64();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").arm64();
/// ```
- pub fn arm64(&mut self) -> &mut Self {
- self.arch = NodeJSArch::ARM64;
+ pub fn arm64(mut self) -> Self {
+ self.arch = NodeJsArch::Arm64;
self
}
@@ -176,11 +183,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").armv7l();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").armv7l();
/// ```
- pub fn armv7l(&mut self) -> &mut Self {
- self.arch = NodeJSArch::ARMV7L;
+ pub fn armv7l(mut self) -> Self {
+ self.arch = NodeJsArch::Armv7l;
self
}
@@ -189,11 +196,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").ppc64();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").ppc64();
/// ```
- pub fn ppc64(&mut self) -> &mut Self {
- self.arch = NodeJSArch::PPC64;
+ pub fn ppc64(mut self) -> Self {
+ self.arch = NodeJsArch::Ppc64;
self
}
@@ -202,11 +209,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").ppc64le();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").ppc64le();
/// ```
- pub fn ppc64le(&mut self) -> &mut Self {
- self.arch = NodeJSArch::PPC64LE;
+ pub fn ppc64le(mut self) -> Self {
+ self.arch = NodeJsArch::Ppc64le;
self
}
@@ -215,11 +222,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").s390x();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").s390x();
/// ```
- pub fn s390x(&mut self) -> &mut Self {
- self.arch = NodeJSArch::S390X;
+ pub fn s390x(mut self) -> Self {
+ self.arch = NodeJsArch::S390x;
self
}
@@ -228,11 +235,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").tar_gz();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").tar_gz();
/// ```
- pub fn tar_gz(&mut self) -> &mut Self {
- self.ext = NodeJSPkgExt::Targz;
+ pub fn tar_gz(mut self) -> Self {
+ self.ext = NodeJsPkgExt::Targz;
self
}
@@ -241,11 +248,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").tar_xz();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").tar_xz();
/// ```
- pub fn tar_xz(&mut self) -> &mut Self {
- self.ext = NodeJSPkgExt::Tarxz;
+ pub fn tar_xz(mut self) -> Self {
+ self.ext = NodeJsPkgExt::Tarxz;
self
}
@@ -254,11 +261,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").zip();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").zip();
/// ```
- pub fn zip(&mut self) -> &mut Self {
- self.ext = NodeJSPkgExt::Zip;
+ pub fn zip(mut self) -> Self {
+ self.ext = NodeJsPkgExt::Zip;
self
}
@@ -267,11 +274,11 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").s7z();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").s7z();
/// ```
- pub fn s7z(&mut self) -> &mut Self {
- self.ext = NodeJSPkgExt::S7z;
+ pub fn s7z(mut self) -> Self {
+ self.ext = NodeJsPkgExt::S7z;
self
}
@@ -280,59 +287,62 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").msi();
+ /// use node_js_release_info::NodeJsRelInfo;
+ /// let info = NodeJsRelInfo::new("24.19.0").msi();
/// ```
- pub fn msi(&mut self) -> &mut Self {
- self.ext = NodeJSPkgExt::Msi;
+ pub fn msi(mut self) -> Self {
+ self.ext = NodeJsPkgExt::Msi;
self
}
/// Creates owned data from reference for convenience when chaining
///
- /// # Examples
- ///
- /// ```rust
- /// use node_js_release_info::NodeJSRelInfo;
- /// let info = NodeJSRelInfo::new("20.6.1").windows().x64().zip().to_owned();
- /// ```
- pub fn to_owned(&self) -> Self {
- self.clone()
- }
-
/// Fetches Node.js metadata for specified configuration from the
/// [releases download server](https://nodejs.org/download/release/)
///
+ /// # Errors
+ ///
+ /// Returns [`InvalidVersion`](NodeJsRelInfoError::InvalidVersion) when
+ /// `version` is not valid semver,
+ /// [`UnrecognizedVersion`](NodeJsRelInfoError::UnrecognizedVersion) when
+ /// the release does not exist,
+ /// [`UnrecognizedConfiguration`](NodeJsRelInfoError::UnrecognizedConfiguration)
+ /// when the release exists but ships no such os/arch/ext combination, and
+ /// [`HttpError`](NodeJsRelInfoError::HttpError) when the request fails
+ ///
/// # Examples
///
/// ```rust
- /// use node_js_release_info::{NodeJSRelInfo, NodeJSRelInfoError};
+ /// use node_js_release_info::{NodeJsRelInfo, NodeJsRelInfoError};
///
/// #[tokio::main]
- /// async fn main() -> Result<(), NodeJSRelInfoError> {
- /// let info = NodeJSRelInfo::new("20.6.1").macos().arm64().fetch().await?;
- /// assert_eq!(info.version, "20.6.1");
- /// assert_eq!(info.filename, "node-v20.6.1-darwin-arm64.tar.gz");
- /// assert_eq!(info.sha256, "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46");
- /// assert_eq!(info.url, "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz");
+ /// async fn main() -> Result<(), NodeJsRelInfoError> {
+ /// let info = NodeJsRelInfo::new("24.19.0").macos().arm64().fetch().await?;
+ /// assert_eq!(info.version, "24.19.0");
+ /// assert_eq!(info.filename, "node-v24.19.0-darwin-arm64.tar.gz");
+ /// assert_eq!(info.sha256, "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d");
+ /// assert_eq!(info.url, "https://nodejs.org/download/release/v24.19.0/node-v24.19.0-darwin-arm64.tar.gz");
/// Ok(())
/// }
/// ```
- pub async fn fetch(&mut self) -> Result {
+ pub async fn fetch(mut self) -> Result {
let version = specs::validate_version(self.version.as_str())?;
let specs = specs::fetch(&version, &self.url_fmt).await?;
let filename = self.filename();
let info = specs.lines().find(|&line| line.contains(filename.as_str()));
- let mut specs = match info {
- None => return Err(NodeJSRelInfoError::UnrecognizedConfiguration(filename))?,
- Some(s) => s.split_whitespace(),
+ let Some(line) = info else {
+ return Err(NodeJsRelInfoError::UnrecognizedConfiguration(filename));
+ };
+
+ let Some(sha256) = line.split_whitespace().next() else {
+ return Err(NodeJsRelInfoError::UnrecognizedConfiguration(filename));
};
self.filename = filename;
- self.sha256 = specs.nth(0).unwrap().to_string();
+ self.sha256 = sha256.to_string();
self.url = self.url_fmt.pkg(&self.version, &self.filename);
- Ok(self.to_owned())
+ Ok(self)
}
/// Fetches Node.js metadata for all supported configurations from the
@@ -341,34 +351,34 @@ impl NodeJSRelInfo {
/// # Examples
///
/// ```rust
- /// use node_js_release_info::{NodeJSRelInfo, NodeJSRelInfoError};
+ /// use node_js_release_info::{NodeJsRelInfo, NodeJsRelInfoError};
///
/// #[tokio::main]
- /// async fn main() -> Result<(), NodeJSRelInfoError> {
- /// let info = NodeJSRelInfo::new("20.6.1");
+ /// async fn main() -> Result<(), NodeJsRelInfoError> {
+ /// let info = NodeJsRelInfo::new("24.19.0");
/// let all = info.fetch_all().await?;
- /// assert_eq!(all.len(), 24);
- /// assert_eq!(all[2].version, "20.6.1");
- /// assert_eq!(all[2].filename, "node-v20.6.1-darwin-arm64.tar.gz");
- /// assert_eq!(all[2].sha256, "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46");
- /// assert_eq!(all[2].url, "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz");
+ /// assert_eq!(all.len(), 19);
+ /// assert_eq!(all[2].version, "24.19.0");
+ /// assert_eq!(all[2].filename, "node-v24.19.0-darwin-arm64.tar.gz");
+ /// assert_eq!(all[2].sha256, "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d");
+ /// assert_eq!(all[2].url, "https://nodejs.org/download/release/v24.19.0/node-v24.19.0-darwin-arm64.tar.gz");
/// Ok(())
/// }
/// ```
- pub async fn fetch_all(&self) -> Result, NodeJSRelInfoError> {
+ pub async fn fetch_all(&self) -> Result, NodeJsRelInfoError> {
let version = specs::validate_version(self.version.as_str())?;
let specs = specs::fetch(&version, &self.url_fmt).await?;
let specs = match specs::parse(&version, specs) {
Some(s) => s,
None => {
- return Err(NodeJSRelInfoError::UnrecognizedVersion(version.clone()));
+ return Err(NodeJsRelInfoError::UnrecognizedVersion(version.clone()));
}
};
- let mut all: Vec = vec![];
+ let mut all: Vec = vec![];
for (os, arch, ext, sha256, filename) in specs.into_iter() {
let version = version.clone();
- let mut info = NodeJSRelInfo {
+ let mut info = NodeJsRelInfo {
os,
arch,
version,
@@ -389,7 +399,7 @@ impl NodeJSRelInfo {
let arch = self.arch.to_string();
let ext = self.ext.to_string();
- if self.ext == NodeJSPkgExt::Msi {
+ if self.ext == NodeJsPkgExt::Msi {
return format!("node-v{}-{}.{}", self.version, arch, ext);
}
@@ -408,23 +418,23 @@ mod tests {
#[test]
fn it_initializes() {
- let info = NodeJSRelInfo::new("1.0.0");
- assert_eq!(info.os, NodeJSOS::Linux);
- assert_eq!(info.arch, NodeJSArch::X64);
- assert_eq!(info.ext, NodeJSPkgExt::Targz);
+ let info = NodeJsRelInfo::new("1.0.0");
+ assert_eq!(info.os, NodeJsOs::Linux);
+ assert_eq!(info.arch, NodeJsArch::X64);
+ assert_eq!(info.ext, NodeJsPkgExt::Targz);
assert_eq!(info.version, "1.0.0".to_string());
assert_eq!(info.filename, "".to_string());
assert_eq!(info.sha256, "".to_string());
assert_eq!(info.url, "".to_string());
- is_thread_safe::();
+ is_thread_safe::();
}
#[test]
fn it_initializes_with_defaults() {
- let info = NodeJSRelInfo::default();
- assert_eq!(info.os, NodeJSOS::Linux);
- assert_eq!(info.arch, NodeJSArch::X64);
- assert_eq!(info.ext, NodeJSPkgExt::Targz);
+ let info = NodeJsRelInfo::default();
+ assert_eq!(info.os, NodeJsOs::Linux);
+ assert_eq!(info.arch, NodeJsArch::X64);
+ assert_eq!(info.ext, NodeJsPkgExt::Targz);
assert_eq!(info.version, "".to_string());
assert_eq!(info.filename, "".to_string());
assert_eq!(info.sha256, "".to_string());
@@ -434,139 +444,92 @@ mod tests {
#[test]
#[cfg_attr(not(target_os = "macos"), ignore)]
fn it_initializes_using_current_environment_on_macos() {
- let info = NodeJSRelInfo::from_env("1.0.0").unwrap();
- assert_eq!(info.ext, NodeJSPkgExt::Targz);
+ let info = NodeJsRelInfo::from_env("1.0.0").unwrap();
+ assert_eq!(info.ext, NodeJsPkgExt::Targz);
}
#[test]
#[cfg_attr(not(target_os = "linux"), ignore)]
fn it_initializes_using_current_environment_on_linux() {
- let info = NodeJSRelInfo::from_env("1.0.0").unwrap();
- assert_eq!(info.ext, NodeJSPkgExt::Targz);
+ let info = NodeJsRelInfo::from_env("1.0.0").unwrap();
+ assert_eq!(info.ext, NodeJsPkgExt::Targz);
}
#[test]
#[cfg_attr(not(target_os = "windows"), ignore)]
fn it_initializes_using_current_environment_on_windows() {
- let info = NodeJSRelInfo::from_env("1.0.0").unwrap();
- assert_eq!(info.ext, NodeJSPkgExt::Zip);
+ let info = NodeJsRelInfo::from_env("1.0.0").unwrap();
+ assert_eq!(info.ext, NodeJsPkgExt::Zip);
}
#[test]
fn it_sets_os() {
- let mut info = NodeJSRelInfo::new("1.0.0");
-
- assert_eq!(info.os, NodeJSOS::Linux);
-
- info.windows();
+ let info = NodeJsRelInfo::new("1.0.0");
- assert_eq!(info.os, NodeJSOS::Windows);
-
- info.macos();
-
- assert_eq!(info.os, NodeJSOS::Darwin);
-
- info.linux();
-
- assert_eq!(info.os, NodeJSOS::Linux);
-
- info.aix();
-
- assert_eq!(info.os, NodeJSOS::AIX);
+ assert_eq!(info.os, NodeJsOs::Linux);
+ assert_eq!(info.clone().windows().os, NodeJsOs::Windows);
+ assert_eq!(info.clone().macos().os, NodeJsOs::Darwin);
+ assert_eq!(info.clone().linux().os, NodeJsOs::Linux);
+ assert_eq!(info.clone().aix().os, NodeJsOs::Aix);
}
#[test]
fn it_sets_arch() {
- let mut info = NodeJSRelInfo::new("1.0.0");
-
- info.x86();
-
- assert_eq!(info.arch, NodeJSArch::X86);
-
- info.x64();
-
- assert_eq!(info.arch, NodeJSArch::X64);
-
- info.arm64();
-
- assert_eq!(info.arch, NodeJSArch::ARM64);
+ let info = NodeJsRelInfo::new("1.0.0");
- info.armv7l();
-
- assert_eq!(info.arch, NodeJSArch::ARMV7L);
-
- info.ppc64();
-
- assert_eq!(info.arch, NodeJSArch::PPC64);
-
- info.ppc64le();
-
- assert_eq!(info.arch, NodeJSArch::PPC64LE);
-
- info.s390x();
-
- assert_eq!(info.arch, NodeJSArch::S390X);
+ assert_eq!(info.clone().x86().arch, NodeJsArch::X86);
+ assert_eq!(info.clone().x64().arch, NodeJsArch::X64);
+ assert_eq!(info.clone().arm64().arch, NodeJsArch::Arm64);
+ assert_eq!(info.clone().armv7l().arch, NodeJsArch::Armv7l);
+ assert_eq!(info.clone().ppc64().arch, NodeJsArch::Ppc64);
+ assert_eq!(info.clone().ppc64le().arch, NodeJsArch::Ppc64le);
+ assert_eq!(info.clone().s390x().arch, NodeJsArch::S390x);
}
#[test]
fn it_sets_ext() {
- let mut info = NodeJSRelInfo::new("1.0.0");
-
- info.zip();
-
- assert_eq!(info.ext, NodeJSPkgExt::Zip);
-
- info.tar_gz();
-
- assert_eq!(info.ext, NodeJSPkgExt::Targz);
-
- info.tar_xz();
+ let info = NodeJsRelInfo::new("1.0.0");
- assert_eq!(info.ext, NodeJSPkgExt::Tarxz);
-
- info.msi();
-
- assert_eq!(info.ext, NodeJSPkgExt::Msi);
-
- info.s7z();
-
- assert_eq!(info.ext, NodeJSPkgExt::S7z);
+ assert_eq!(info.clone().zip().ext, NodeJsPkgExt::Zip);
+ assert_eq!(info.clone().tar_gz().ext, NodeJsPkgExt::Targz);
+ assert_eq!(info.clone().tar_xz().ext, NodeJsPkgExt::Tarxz);
+ assert_eq!(info.clone().msi().ext, NodeJsPkgExt::Msi);
+ assert_eq!(info.clone().s7z().ext, NodeJsPkgExt::S7z);
}
#[test]
- fn it_gets_owned_copy() {
- let mut info1 = NodeJSRelInfo::new("1.0.0");
- let info2 = info1.to_owned();
+ fn it_clones() {
+ let info1 = NodeJsRelInfo::new("1.0.0");
+ let info2 = info1.clone();
assert_eq!(info1, info2);
-
- info1.windows();
-
- assert_ne!(info1, info2);
+ // builders consume, so the clone is unaffected by further chaining
+ assert_ne!(info1.windows(), info2);
}
#[test]
fn it_formats_filename() {
- let info = NodeJSRelInfo::new("1.0.0").macos().x64().zip().to_owned();
+ let info = NodeJsRelInfo::new("1.0.0").macos().x64().zip();
assert_eq!(info.filename(), "node-v1.0.0-darwin-x64.zip");
- let info = NodeJSRelInfo::new("1.0.0").windows().x64().msi().to_owned();
+ let info = NodeJsRelInfo::new("1.0.0").windows().x64().msi();
assert_eq!(info.filename(), "node-v1.0.0-x64.msi");
}
#[test]
+ #[cfg(feature = "json")]
fn it_serializes_and_deserializes() {
let version = "20.6.1".to_string();
let filename = "node-v20.6.1-darwin-arm64.tar.gz".to_string();
let sha256 = "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46".to_string();
let url = "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz"
.to_string();
- let info_orig = NodeJSRelInfo {
- os: NodeJSOS::Darwin,
- arch: NodeJSArch::ARM64,
- ext: NodeJSPkgExt::Targz,
+ let info_orig = NodeJsRelInfo {
+ os: NodeJsOs::Darwin,
+ arch: NodeJsArch::Arm64,
+ ext: NodeJsPkgExt::Targz,
version: version.clone(),
filename: filename.clone(),
sha256: sha256.clone(),
@@ -574,10 +537,10 @@ mod tests {
..Default::default()
};
let info_json = serde_json::to_string(&info_orig).unwrap();
- let info: NodeJSRelInfo = serde_json::from_str(&info_json).unwrap();
- assert_eq!(info.os, NodeJSOS::Darwin);
- assert_eq!(info.arch, NodeJSArch::ARM64);
- assert_eq!(info.ext, NodeJSPkgExt::Targz);
+ let info: NodeJsRelInfo = serde_json::from_str(&info_json).unwrap();
+ assert_eq!(info.os, NodeJsOs::Darwin);
+ assert_eq!(info.arch, NodeJsArch::Arm64);
+ assert_eq!(info.ext, NodeJsPkgExt::Targz);
assert_eq!(info.version, "20.6.1".to_string());
assert_eq!(
info.filename,
@@ -595,20 +558,16 @@ mod tests {
}
#[tokio::test]
- #[should_panic(
- expected = "called `Result::unwrap()` on an `Err` value: InvalidVersion(\"NOPE!\")"
- )]
async fn it_fails_to_fetch_info_when_version_is_invalid() {
- let mut info = NodeJSRelInfo::new("NOPE!");
- info.fetch().await.unwrap();
+ let info = NodeJsRelInfo::new("NOPE!");
+ let err = info.fetch().await.unwrap_err();
+
+ assert!(matches!(err, NodeJsRelInfoError::InvalidVersion(x) if x == "NOPE!"));
}
#[tokio::test]
- #[should_panic(
- expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedVersion(\"1.0.0\")"
- )]
async fn it_fails_to_fetch_info_when_version_is_unrecognized() {
- let mut info = NodeJSRelInfo::new("1.0.0");
+ let mut info = NodeJsRelInfo::new("1.0.0");
let mut server = Server::new_async().await;
let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
.with_body(specs::get_fake_specs())
@@ -616,36 +575,39 @@ mod tests {
.create_async()
.await;
- info.fetch().await.unwrap();
+ let err = info.fetch().await.unwrap_err();
mock.assert_async().await;
+
+ assert!(matches!(err, NodeJsRelInfoError::UnrecognizedVersion(x) if x == "1.0.0"));
}
#[tokio::test]
- #[should_panic(
- expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedConfiguration(\"node-v20.6.1-linux-x64.zip\")"
- )]
async fn it_fails_to_fetch_info_when_configuration_is_unrecognized() {
let mut server = Server::new_async().await;
- let mut info = NodeJSRelInfo::new("20.6.1").linux().zip().to_owned();
+ let mut info = NodeJsRelInfo::new("20.6.1").linux().zip();
let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
.with_body(specs::get_fake_specs())
.create_async()
.await;
- info.fetch().await.unwrap();
+ let err = info.fetch().await.unwrap_err();
mock.assert_async().await;
+
+ assert!(
+ matches!(err, NodeJsRelInfoError::UnrecognizedConfiguration(x) if x == "node-v20.6.1-linux-x64.zip")
+ );
}
#[tokio::test]
async fn it_fetches_node_js_release_info() {
- let mut info = NodeJSRelInfo::new("20.6.1");
+ let mut info = NodeJsRelInfo::new("20.6.1");
let mut server = Server::new_async().await;
let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
.with_body(specs::get_fake_specs())
.create_async()
.await;
- info.fetch().await.unwrap();
+ let info = info.fetch().await.unwrap();
mock.assert_async().await;
assert_eq!(info.filename, "node-v20.6.1-linux-x64.tar.gz");
@@ -665,14 +627,14 @@ mod tests {
#[tokio::test]
async fn it_fetches_node_js_release_info_when_ext_is_msi() {
- let mut info = NodeJSRelInfo::new("20.6.1").arm64().msi().to_owned();
+ let mut info = NodeJsRelInfo::new("20.6.1").arm64().msi();
let mut server = Server::new_async().await;
let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
.with_body(specs::get_fake_specs())
.create_async()
.await;
- info.fetch().await.unwrap();
+ let info = info.fetch().await.unwrap();
mock.assert_async().await;
assert_eq!(info.filename, "node-v20.6.1-arm64.msi");
@@ -692,7 +654,7 @@ mod tests {
#[tokio::test]
async fn it_fetches_all_supported_node_js_configurations() {
- let mut info = NodeJSRelInfo::new("20.6.1");
+ let mut info = NodeJsRelInfo::new("20.6.1");
let mut server = Server::new_async().await;
let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
.with_body(specs::get_fake_specs())
@@ -704,9 +666,9 @@ mod tests {
assert_eq!(all.len(), 24);
assert_eq!(all[2].version, "20.6.1");
- assert_eq!(all[2].os, NodeJSOS::Darwin);
- assert_eq!(all[2].arch, NodeJSArch::ARM64);
- assert_eq!(all[2].ext, NodeJSPkgExt::Targz);
+ assert_eq!(all[2].os, NodeJsOs::Darwin);
+ assert_eq!(all[2].arch, NodeJsArch::Arm64);
+ assert_eq!(all[2].ext, NodeJsPkgExt::Targz);
assert_eq!(all[2].filename, "node-v20.6.1-darwin-arm64.tar.gz");
assert_eq!(
all[2].sha256,
@@ -719,18 +681,17 @@ mod tests {
}
#[tokio::test]
- #[should_panic(
- expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedVersion(\"1.0.0\")"
- )]
async fn it_fails_to_fetch_all_supported_node_js_configurations_when_version_is_unrecognized() {
- let mut info = NodeJSRelInfo::new("1.0.0");
+ let mut info = NodeJsRelInfo::new("1.0.0");
let mut server = Server::new_async().await;
let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server)
.with_body(String::from(""))
.create_async()
.await;
- info.fetch_all().await.unwrap();
+ let err = info.fetch_all().await.unwrap_err();
mock.assert_async().await;
+
+ assert!(matches!(err, NodeJsRelInfoError::UnrecognizedVersion(x) if x == "1.0.0"));
}
}
diff --git a/crates/node-js-release-info/src/os.rs b/crates/node-js-release-info/src/os.rs
index 5d8b823..eff1447 100644
--- a/crates/node-js-release-info/src/os.rs
+++ b/crates/node-js-release-info/src/os.rs
@@ -1,62 +1,86 @@
-use crate::error::NodeJSRelInfoError;
+use crate::error::NodeJsRelInfoError;
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
use std::env::consts::OS;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
-#[derive(Clone, Debug, PartialEq)]
+/// The operating system a Node.js distributable targets
+///
+/// Non-exhaustive: Node.js has added and removed target platforms over time,
+/// so new variants may appear in a minor release
+#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "json", derive(Deserialize, Serialize))]
-pub enum NodeJSOS {
+#[non_exhaustive]
+pub enum NodeJsOs {
+ /// Linux (`linux`)
+ #[default]
#[cfg_attr(feature = "json", serde(rename = "linux"))]
Linux,
+ /// macOS (`darwin`)
#[cfg_attr(feature = "json", serde(rename = "darwin"))]
Darwin,
+ /// Windows (`win`)
#[cfg_attr(feature = "json", serde(rename = "win"))]
Windows,
+ /// IBM AIX (`aix`)
#[cfg_attr(feature = "json", serde(rename = "aix"))]
- AIX,
+ Aix,
+ /// illumos / Solaris (`sunos`) - shipped up to Node.js v14
+ #[cfg_attr(feature = "json", serde(rename = "sunos"))]
+ SunOs,
}
-impl Default for NodeJSOS {
- fn default() -> Self {
- NodeJSOS::new()
- }
-}
-
-impl NodeJSOS {
- pub fn new() -> NodeJSOS {
- NodeJSOS::Linux
+impl NodeJsOs {
+ /// Creates a new instance using the default OS ([`Linux`](NodeJsOs::Linux))
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use node_js_release_info::NodeJsOs;
+ /// assert_eq!(NodeJsOs::new(), NodeJsOs::Linux);
+ /// ```
+ pub fn new() -> NodeJsOs {
+ NodeJsOs::default()
}
- pub fn from_env() -> Result {
- NodeJSOS::from_str(OS)
+ /// Determines the OS of the current environment via
+ /// [`std::env::consts::OS`]
+ ///
+ /// # Errors
+ ///
+ /// Returns [`NodeJsRelInfoError::UnrecognizedOs`] when the current OS has
+ /// no corresponding Node.js distributable
+ pub fn from_env() -> Result {
+ NodeJsOs::from_str(OS)
}
}
-impl Display for NodeJSOS {
+impl Display for NodeJsOs {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let os = match self {
- NodeJSOS::Linux => "linux",
- NodeJSOS::Darwin => "darwin",
- NodeJSOS::Windows => "win",
- NodeJSOS::AIX => "aix",
+ NodeJsOs::Linux => "linux",
+ NodeJsOs::Darwin => "darwin",
+ NodeJsOs::Windows => "win",
+ NodeJsOs::Aix => "aix",
+ NodeJsOs::SunOs => "sunos",
};
- write!(f, "{}", os)
+ write!(f, "{os}")
}
}
-impl FromStr for NodeJSOS {
- type Err = NodeJSRelInfoError;
+impl FromStr for NodeJsOs {
+ type Err = NodeJsRelInfoError;
- fn from_str(s: &str) -> Result {
+ fn from_str(s: &str) -> Result {
match s {
- "linux" => Ok(NodeJSOS::Linux),
- "darwin" | "macos" => Ok(NodeJSOS::Darwin),
- "windows" | "win" => Ok(NodeJSOS::Windows),
- "aix" => Ok(NodeJSOS::AIX),
- _ => Err(NodeJSRelInfoError::UnrecognizedOs(s.to_string())),
+ "linux" => Ok(NodeJsOs::Linux),
+ "darwin" | "macos" => Ok(NodeJsOs::Darwin),
+ "windows" | "win" => Ok(NodeJsOs::Windows),
+ "sunos" | "solaris" | "illumos" => Ok(NodeJsOs::SunOs),
+ "aix" => Ok(NodeJsOs::Aix),
+ _ => Err(NodeJsRelInfoError::UnrecognizedOs(s.to_string())),
}
}
}
@@ -67,79 +91,90 @@ mod tests {
#[test]
fn it_initializes() {
- let os = NodeJSOS::new();
- assert_eq!(os, NodeJSOS::Linux);
+ let os = NodeJsOs::new();
+ assert_eq!(os, NodeJsOs::Linux);
}
#[test]
fn it_initializes_with_defaults() {
- let os = NodeJSOS::default();
- assert_eq!(os, NodeJSOS::Linux);
+ let os = NodeJsOs::default();
+ assert_eq!(os, NodeJsOs::Linux);
}
#[test]
fn it_initializes_from_str() {
- let os = NodeJSOS::from_str("linux").unwrap();
+ let os = NodeJsOs::from_str("linux").unwrap();
+
+ assert_eq!(os, NodeJsOs::Linux);
+
+ let os = NodeJsOs::from_str("darwin").unwrap();
- assert_eq!(os, NodeJSOS::Linux);
+ assert_eq!(os, NodeJsOs::Darwin);
- let os = NodeJSOS::from_str("darwin").unwrap();
+ let os = NodeJsOs::from_str("macos").unwrap();
- assert_eq!(os, NodeJSOS::Darwin);
+ assert_eq!(os, NodeJsOs::Darwin);
- let os = NodeJSOS::from_str("macos").unwrap();
+ let os = NodeJsOs::from_str("windows").unwrap();
- assert_eq!(os, NodeJSOS::Darwin);
+ assert_eq!(os, NodeJsOs::Windows);
- let os = NodeJSOS::from_str("windows").unwrap();
+ let os = NodeJsOs::from_str("win").unwrap();
- assert_eq!(os, NodeJSOS::Windows);
+ assert_eq!(os, NodeJsOs::Windows);
- let os = NodeJSOS::from_str("win").unwrap();
+ let os = NodeJsOs::from_str("aix").unwrap();
- assert_eq!(os, NodeJSOS::Windows);
+ assert_eq!(os, NodeJsOs::Aix);
- let os = NodeJSOS::from_str("aix").unwrap();
+ let os = NodeJsOs::from_str("sunos").unwrap();
- assert_eq!(os, NodeJSOS::AIX);
+ assert_eq!(os, NodeJsOs::SunOs);
+
+ let os = NodeJsOs::from_str("solaris").unwrap();
+
+ assert_eq!(os, NodeJsOs::SunOs);
}
#[test]
fn it_serializes_to_str() {
- let text = format!("{}", NodeJSOS::Linux);
+ let text = format!("{}", NodeJsOs::Linux);
assert_eq!(text, "linux");
- let text = format!("{}", NodeJSOS::Darwin);
+ let text = format!("{}", NodeJsOs::Darwin);
assert_eq!(text, "darwin");
- let text = format!("{}", NodeJSOS::Windows);
+ let text = format!("{}", NodeJsOs::Windows);
assert_eq!(text, "win");
- let text = format!("{}", NodeJSOS::AIX);
+ let text = format!("{}", NodeJsOs::Aix);
assert_eq!(text, "aix");
+
+ let text = format!("{}", NodeJsOs::SunOs);
+
+ assert_eq!(text, "sunos");
}
#[test]
fn it_initializes_using_current_environment() {
- NodeJSOS::from_env().unwrap();
+ NodeJsOs::from_env().unwrap();
}
#[test]
- #[should_panic(
- expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedOs(\"NOPE!\")"
- )]
fn it_fails_when_os_cannot_be_determined_from_str() {
- NodeJSOS::from_str("NOPE!").unwrap();
+ let err = NodeJsOs::from_str("NOPE!").unwrap_err();
+ assert!(matches!(err, NodeJsRelInfoError::UnrecognizedOs(x) if x == "NOPE!"));
}
#[test]
+ #[cfg(feature = "json")]
fn it_serializes_and_deserializes() {
- let os_json = serde_json::to_string(&NodeJSOS::Darwin).unwrap();
- let os: NodeJSOS = serde_json::from_str(&os_json).unwrap();
- assert_eq!(os, NodeJSOS::Darwin);
+ let os_json = serde_json::to_string(&NodeJsOs::Darwin).unwrap();
+ let os: NodeJsOs = serde_json::from_str(&os_json).unwrap();
+ assert_eq!(os, NodeJsOs::Darwin);
}
}
diff --git a/crates/node-js-release-info/src/specs.rs b/crates/node-js-release-info/src/specs.rs
index b807383..6218726 100644
--- a/crates/node-js-release-info/src/specs.rs
+++ b/crates/node-js-release-info/src/specs.rs
@@ -1,46 +1,44 @@
-use crate::arch::NodeJSArch;
-use crate::error::NodeJSRelInfoError;
-use crate::ext::NodeJSPkgExt;
-use crate::os::NodeJSOS;
-use crate::url::NodeJSURLFormatter;
+use crate::arch::NodeJsArch;
+use crate::error::NodeJsRelInfoError;
+use crate::ext::NodeJsPkgExt;
+use crate::os::NodeJsOs;
+use crate::url::NodeJsUrlFormatter;
use semver::Version;
use std::str::FromStr;
-pub fn validate_version>(semver: T) -> Result {
+pub(crate) fn validate_version>(semver: T) -> Result {
match Version::parse(semver.as_ref()) {
Ok(v) => Ok(v.to_string()),
- Err(_) => {
- return Err(NodeJSRelInfoError::InvalidVersion(
- semver.as_ref().to_owned(),
- ))
- }
+ Err(_) => Err(NodeJsRelInfoError::InvalidVersion(
+ semver.as_ref().to_owned(),
+ )),
}
}
-pub async fn fetch(
+pub(crate) async fn fetch(
version: &String,
- url_fmt: &NodeJSURLFormatter,
-) -> Result {
+ url_fmt: &NodeJsUrlFormatter,
+) -> Result {
let info_url = url_fmt.info(version);
let res = match reqwest::get(info_url.as_str()).await {
- Err(e) => return Err(NodeJSRelInfoError::HttpError(e)),
+ Err(e) => return Err(NodeJsRelInfoError::HttpError(e)),
Ok(r) => r,
};
// TODO (busticated): handle 5xx errors
if res.status().as_u16() >= 400 {
- return Err(NodeJSRelInfoError::UnrecognizedVersion(version.clone()));
+ return Err(NodeJsRelInfoError::UnrecognizedVersion(version.clone()));
}
match res.text().await {
- Err(e) => Err(NodeJSRelInfoError::HttpError(e)),
+ Err(e) => Err(NodeJsRelInfoError::HttpError(e)),
Ok(b) => Ok(b),
}
}
-pub type ParsedSpecs = Vec<(NodeJSOS, NodeJSArch, NodeJSPkgExt, String, String)>;
+pub(crate) type ParsedSpecs = Vec<(NodeJsOs, NodeJsArch, NodeJsPkgExt, String, String)>;
-pub fn parse(version: &String, specs: String) -> Option {
+pub(crate) fn parse(version: &String, specs: String) -> Option {
let mut all: ParsedSpecs = vec![];
for line in specs.lines() {
let (sha256, filename) = match line.trim().split_once(' ') {
@@ -52,7 +50,7 @@ pub fn parse(version: &String, specs: String) -> Option {
continue;
}
- if !filename.starts_with(format!("node-v{}", &version).as_str()) {
+ if !filename.starts_with(format!("node-v{version}").as_str()) {
continue;
}
@@ -65,7 +63,7 @@ pub fn parse(version: &String, specs: String) -> Option {
}
let os = if is_msi { "win" } else { parts[2] };
- let os = match NodeJSOS::from_str(os) {
+ let os = match NodeJsOs::from_str(os) {
Ok(os) => os,
Err(_) => {
continue;
@@ -79,14 +77,14 @@ pub fn parse(version: &String, specs: String) -> Option {
}
};
- let arch = match NodeJSArch::from_str(arch) {
+ let arch = match NodeJsArch::from_str(arch) {
Ok(a) => a,
Err(_) => {
continue;
}
};
- let ext = match NodeJSPkgExt::from_str(ext) {
+ let ext = match NodeJsPkgExt::from_str(ext) {
Ok(ext) => ext,
Err(_) => {
continue;
@@ -105,10 +103,79 @@ pub fn parse(version: &String, specs: String) -> Option {
Some(all)
}
+#[cfg(test)]
+use mockito::{Mock, Server};
+
+#[cfg(test)]
+pub(crate) fn setup_server_mock(
+ version: &str,
+ url_fmt: &mut NodeJsUrlFormatter,
+ server: &mut Server,
+) -> Mock {
+ url_fmt.host = server.host_with_port();
+ url_fmt.protocol = "http:".to_string();
+ server.mock("GET", url_fmt.info_pathname(version).as_str())
+}
+
+#[cfg(test)]
+pub(crate) fn get_fake_specs() -> &'static str {
+ "ea52b4feaf917e08cd2c729c1186585fcacef07c261a01310c91333b9e41d93c node-v20.6.1-aix-ppc64.tar.gz
+ 9471bd6dc491e09c31b0f831f5953284b8a6842ed4ccb98f5c62d13e6086c471 node-v20.6.1-arm64.msi
+ d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46 node-v20.6.1-darwin-arm64.tar.gz
+ 9c61b0d60fce962244d5e54549dc912e28b3c5f5e23149bfd15f66f8f7269129 node-v20.6.1-darwin-arm64.tar.xz
+ 365ec544c6596f194afff9a613554abfc68d4a2274181b7651386d9a11cf5862 node-v20.6.1-darwin-x64.tar.gz
+ 9b10c16670781e3a5af722656d28f264cdd8ebb3140f62692b33813100391349 node-v20.6.1-darwin-x64.tar.xz
+ d8271461ced2887f65af413949caee19db3e80d22bbefdaf01252ca998570052 node-v20.6.1-headers.tar.gz
+ 60963e3ee60b6739e97e0c7b8ffb25848a82649c0c277af728400c570fd9db6d node-v20.6.1-headers.tar.xz
+ d38fe2e41e3fe8ae81b517b4cf49521f500e181e54f4c3d05e2b2d691a57b2ca node-v20.6.1-linux-arm64.tar.gz
+ 6823720796b287465bb4aa8e7611143322ffd6cbdb9c6e3b149576f6d87953bf node-v20.6.1-linux-arm64.tar.xz
+ 459510281ea51cf5d89fc666e36fbba80793ae4b90c3a7f89dd6666c65c825b3 node-v20.6.1-linux-armv7l.tar.gz
+ 9dbd4fd7f804a28de91ffb8792df6e89bbb4f934fccd013624b3dabf8bf809ac node-v20.6.1-linux-armv7l.tar.xz
+ ca00f1aa8b2535fa167258cf5f2cfce4b79d83c442dd5e46f5e17d6a5749ec0f node-v20.6.1-linux-ppc64le.tar.gz
+ 27884935b025b6676e4b8737f334673ee825947d0baef61aa0326374597aeb05 node-v20.6.1-linux-ppc64le.tar.xz
+ 4a3f29cfc8a7ed1e9e44fcacb78e2fbaa3ce01be1efc4971a42710ad1e9e45d1 node-v20.6.1-linux-s390x.tar.gz
+ 3968d629989b6de16b8872b6d7ee6e6cdf1204def99c43412a6ee28203ed0022 node-v20.6.1-linux-s390x.tar.xz
+ 26dd13a6f7253f0ab9bcab561353985a297d927840771d905566735b792868da node-v20.6.1-linux-x64.tar.gz
+ 591f9f274104f266a8cf085d2c7d5d2848ba73b98ae323d501db2d4c4b7026e5 node-v20.6.1-linux-x64.tar.xz
+ d9acf82d9576dd0350c8e66b55f6fc2750fa9f4aa23d6453ffc58e32af995894 node-v20.6.1.pkg
+ 0053c09a01b1b355bca5af82927cae376124c13d74fa53567f08f4cfb085e6aa node-v20.6.1.tar.gz
+ 3aec5e728daa38800c343b129221d3488064a2529a39bb5467bc55be226c6a2b node-v20.6.1.tar.xz
+ 337549faf397deb0da3bccd4e27db45a619d89de4ea12830d16d9dfaded8e92c node-v20.6.1-win-arm64.7z
+ 0e62045bfc9d7c38360bd7da152c75ed82087242d5e4b401fa23a439588d36f6 node-v20.6.1-win-arm64.zip
+ c6cfe7824770a266a30bee8c33f485d0e89b94254c682250a239d83adfb7ce77 node-v20.6.1-win-x64.7z
+ 88371914f1f75d594bb367570e163cf5ecebeb514fd54cc765093819ebb0ed48 node-v20.6.1-win-x64.zip
+ 87d631b294a25386400d0f44d227330da62a1326e2a4fbb98bda3d7c431257f1 node-v20.6.1-win-x86.7z
+ 578cff623601aa8878a035f06edbf69190338ee3b345e7a096e804cb80c4ce24 node-v20.6.1-win-x86.zip
+ 5c2616da46728dd1326645c7db114e78ad87138a258c0724a035269258c23509 node-v20.6.1-x64.msi
+ cb83586af83182187e760b7e01aa7c7b2bacb521d60ceefed3ac6fc62c222449 node-v20.6.1-x86.msi
+ 7cc3240fd7ce7926eef1cbbad33b033f7c5d97b3f3e527d65ff1e2c3f7638a11 win-arm64/node.exe
+ deb027ded744371657811cfe52e774881ea928d36779924af84aa9a7a31104d2 win-arm64/node.lib
+ dcb6b4bc6f2a78bf0f759853b59e94ddbe9ad6b9f32d24fdcf590d74c6350bc2 win-arm64/node_pdb.7z
+ bdcd574e99646ec4a03bb13b3661c957f5a7ca837f5c33827075c4262d449689 win-arm64/node_pdb.zip
+ 5b824f3a375cca06dfd7dc70fa341a6ef8bb0b2e912358d8602a0c7ad273b9a4 win-x64/node.exe
+ d275cfc4d637d2feaf4c39e1a5f5cd84f5b474fa713c15013e940c329feed13b win-x64/node.lib
+ fea6c0fcff45739a6e5af9843ec45455c97ff8677167bd649fd48cbef59ca52d win-x64/node_pdb.7z
+ bc13f5e63c1510cd41f82dc20725f40bbfa378252e09a00a8531cddabbf1b106 win-x64/node_pdb.zip
+ 837db0d8fb7fa194ebe23cd34ac7bedc02d1132de67cf4f147d694574be5cc4e win-x86/node.exe
+ a0738dec64427ae73eeb1d036081652c1c0223a679a63e0459c2af667f284f58 win-x86/node.lib
+ 516ac820f05eb8478be541ac12386c3b5b5c07624f73934bcf0b11a3fcdb1c95 win-x86/node_pdb.7z
+ 9b68f3e1f1717a2f6a090e1679f8cc627566ed064c657c35eddd0dba9484e310 win-x86/node_pdb.zip"
+}
+
#[cfg(test)]
mod tests {
use super::*;
+ fn assert_is_darwin_arm64_targz_specs(specs: ParsedSpecs) {
+ assert_eq!(specs.len(), 1);
+ let (os, arch, ext, sha256, filename) = &specs[0];
+ assert_eq!(*os, NodeJsOs::Darwin);
+ assert_eq!(*arch, NodeJsArch::Arm64);
+ assert_eq!(*ext, NodeJsPkgExt::Targz);
+ assert_eq!(filename, "node-v20.6.1-darwin-arm64.tar.gz");
+ assert_eq!(sha256, "FAKESHA");
+ }
+
#[test]
fn it_validates_a_version_string() {
let version = validate_version("20.6.1").unwrap();
@@ -117,14 +184,11 @@ mod tests {
let error = validate_version("NOPE").unwrap_err();
- assert_eq!(
- format!("{error}"),
- "Error: Invalid Version! Received: 'NOPE'"
- );
+ assert_eq!(format!("{error}"), "invalid version - received: 'NOPE'");
let error = validate_version("").unwrap_err();
- assert_eq!(format!("{error}"), "Error: Invalid Version! Received: ''");
+ assert_eq!(format!("{error}"), "invalid version - received: ''");
}
#[test]
@@ -134,9 +198,9 @@ mod tests {
let specs = parse(&version, specs_raw).unwrap();
assert_eq!(specs.len(), 24);
let (os, arch, ext, sha256, filename) = &specs[2];
- assert_eq!(*os, NodeJSOS::Darwin);
- assert_eq!(*arch, NodeJSArch::ARM64);
- assert_eq!(*ext, NodeJSPkgExt::Targz);
+ assert_eq!(*os, NodeJsOs::Darwin);
+ assert_eq!(*arch, NodeJsArch::Arm64);
+ assert_eq!(*ext, NodeJsPkgExt::Targz);
assert_eq!(filename, "node-v20.6.1-darwin-arm64.tar.gz");
assert_eq!(
sha256,
@@ -239,7 +303,7 @@ mod tests {
#[tokio::test]
async fn it_fetches_node_js_specs() {
let version = String::from("20.6.1");
- let mut url_fmt = NodeJSURLFormatter::new();
+ let mut url_fmt = NodeJsUrlFormatter::new();
let mut server = Server::new_async().await;
let mock = setup_server_mock(&version, &mut url_fmt, &mut server)
.with_body(get_fake_specs())
@@ -252,12 +316,9 @@ mod tests {
}
#[tokio::test]
- #[should_panic(
- expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedVersion(\"1.0.0\")"
- )]
async fn it_fails_to_fetch_node_js_specs_when_version_is_unrecognized() {
let version = String::from("1.0.0");
- let mut url_fmt = NodeJSURLFormatter::new();
+ let mut url_fmt = NodeJsUrlFormatter::new();
let mut server = Server::new_async().await;
let mock = setup_server_mock(&version, &mut url_fmt, &mut server)
.with_body(get_fake_specs())
@@ -265,77 +326,9 @@ mod tests {
.create_async()
.await;
- fetch(&version, &url_fmt).await.unwrap();
+ let err = fetch(&version, &url_fmt).await.unwrap_err();
mock.assert_async().await;
- }
-}
-
-#[cfg(test)]
-use mockito::{Mock, Server};
-
-#[cfg(test)]
-fn assert_is_darwin_arm64_targz_specs(specs: ParsedSpecs) {
- assert_eq!(specs.len(), 1);
- let (os, arch, ext, sha256, filename) = &specs[0];
- assert_eq!(*os, NodeJSOS::Darwin);
- assert_eq!(*arch, NodeJSArch::ARM64);
- assert_eq!(*ext, NodeJSPkgExt::Targz);
- assert_eq!(filename, "node-v20.6.1-darwin-arm64.tar.gz");
- assert_eq!(sha256, "FAKESHA");
-}
-#[cfg(test)]
-pub fn setup_server_mock(
- version: &str,
- url_fmt: &mut NodeJSURLFormatter,
- server: &mut Server,
-) -> Mock {
- url_fmt.host = server.host_with_port();
- url_fmt.protocol = "http:".to_string();
- server.mock("GET", url_fmt.info_pathname(version).as_str())
-}
-
-#[cfg(test)]
-pub fn get_fake_specs() -> &'static str {
- "ea52b4feaf917e08cd2c729c1186585fcacef07c261a01310c91333b9e41d93c node-v20.6.1-aix-ppc64.tar.gz
- 9471bd6dc491e09c31b0f831f5953284b8a6842ed4ccb98f5c62d13e6086c471 node-v20.6.1-arm64.msi
- d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46 node-v20.6.1-darwin-arm64.tar.gz
- 9c61b0d60fce962244d5e54549dc912e28b3c5f5e23149bfd15f66f8f7269129 node-v20.6.1-darwin-arm64.tar.xz
- 365ec544c6596f194afff9a613554abfc68d4a2274181b7651386d9a11cf5862 node-v20.6.1-darwin-x64.tar.gz
- 9b10c16670781e3a5af722656d28f264cdd8ebb3140f62692b33813100391349 node-v20.6.1-darwin-x64.tar.xz
- d8271461ced2887f65af413949caee19db3e80d22bbefdaf01252ca998570052 node-v20.6.1-headers.tar.gz
- 60963e3ee60b6739e97e0c7b8ffb25848a82649c0c277af728400c570fd9db6d node-v20.6.1-headers.tar.xz
- d38fe2e41e3fe8ae81b517b4cf49521f500e181e54f4c3d05e2b2d691a57b2ca node-v20.6.1-linux-arm64.tar.gz
- 6823720796b287465bb4aa8e7611143322ffd6cbdb9c6e3b149576f6d87953bf node-v20.6.1-linux-arm64.tar.xz
- 459510281ea51cf5d89fc666e36fbba80793ae4b90c3a7f89dd6666c65c825b3 node-v20.6.1-linux-armv7l.tar.gz
- 9dbd4fd7f804a28de91ffb8792df6e89bbb4f934fccd013624b3dabf8bf809ac node-v20.6.1-linux-armv7l.tar.xz
- ca00f1aa8b2535fa167258cf5f2cfce4b79d83c442dd5e46f5e17d6a5749ec0f node-v20.6.1-linux-ppc64le.tar.gz
- 27884935b025b6676e4b8737f334673ee825947d0baef61aa0326374597aeb05 node-v20.6.1-linux-ppc64le.tar.xz
- 4a3f29cfc8a7ed1e9e44fcacb78e2fbaa3ce01be1efc4971a42710ad1e9e45d1 node-v20.6.1-linux-s390x.tar.gz
- 3968d629989b6de16b8872b6d7ee6e6cdf1204def99c43412a6ee28203ed0022 node-v20.6.1-linux-s390x.tar.xz
- 26dd13a6f7253f0ab9bcab561353985a297d927840771d905566735b792868da node-v20.6.1-linux-x64.tar.gz
- 591f9f274104f266a8cf085d2c7d5d2848ba73b98ae323d501db2d4c4b7026e5 node-v20.6.1-linux-x64.tar.xz
- d9acf82d9576dd0350c8e66b55f6fc2750fa9f4aa23d6453ffc58e32af995894 node-v20.6.1.pkg
- 0053c09a01b1b355bca5af82927cae376124c13d74fa53567f08f4cfb085e6aa node-v20.6.1.tar.gz
- 3aec5e728daa38800c343b129221d3488064a2529a39bb5467bc55be226c6a2b node-v20.6.1.tar.xz
- 337549faf397deb0da3bccd4e27db45a619d89de4ea12830d16d9dfaded8e92c node-v20.6.1-win-arm64.7z
- 0e62045bfc9d7c38360bd7da152c75ed82087242d5e4b401fa23a439588d36f6 node-v20.6.1-win-arm64.zip
- c6cfe7824770a266a30bee8c33f485d0e89b94254c682250a239d83adfb7ce77 node-v20.6.1-win-x64.7z
- 88371914f1f75d594bb367570e163cf5ecebeb514fd54cc765093819ebb0ed48 node-v20.6.1-win-x64.zip
- 87d631b294a25386400d0f44d227330da62a1326e2a4fbb98bda3d7c431257f1 node-v20.6.1-win-x86.7z
- 578cff623601aa8878a035f06edbf69190338ee3b345e7a096e804cb80c4ce24 node-v20.6.1-win-x86.zip
- 5c2616da46728dd1326645c7db114e78ad87138a258c0724a035269258c23509 node-v20.6.1-x64.msi
- cb83586af83182187e760b7e01aa7c7b2bacb521d60ceefed3ac6fc62c222449 node-v20.6.1-x86.msi
- 7cc3240fd7ce7926eef1cbbad33b033f7c5d97b3f3e527d65ff1e2c3f7638a11 win-arm64/node.exe
- deb027ded744371657811cfe52e774881ea928d36779924af84aa9a7a31104d2 win-arm64/node.lib
- dcb6b4bc6f2a78bf0f759853b59e94ddbe9ad6b9f32d24fdcf590d74c6350bc2 win-arm64/node_pdb.7z
- bdcd574e99646ec4a03bb13b3661c957f5a7ca837f5c33827075c4262d449689 win-arm64/node_pdb.zip
- 5b824f3a375cca06dfd7dc70fa341a6ef8bb0b2e912358d8602a0c7ad273b9a4 win-x64/node.exe
- d275cfc4d637d2feaf4c39e1a5f5cd84f5b474fa713c15013e940c329feed13b win-x64/node.lib
- fea6c0fcff45739a6e5af9843ec45455c97ff8677167bd649fd48cbef59ca52d win-x64/node_pdb.7z
- bc13f5e63c1510cd41f82dc20725f40bbfa378252e09a00a8531cddabbf1b106 win-x64/node_pdb.zip
- 837db0d8fb7fa194ebe23cd34ac7bedc02d1132de67cf4f147d694574be5cc4e win-x86/node.exe
- a0738dec64427ae73eeb1d036081652c1c0223a679a63e0459c2af667f284f58 win-x86/node.lib
- 516ac820f05eb8478be541ac12386c3b5b5c07624f73934bcf0b11a3fcdb1c95 win-x86/node_pdb.7z
- 9b68f3e1f1717a2f6a090e1679f8cc627566ed064c657c35eddd0dba9484e310 win-x86/node_pdb.zip"
+ assert!(matches!(err, NodeJsRelInfoError::UnrecognizedVersion(x) if x == "1.0.0"));
+ }
}
diff --git a/crates/node-js-release-info/src/url.rs b/crates/node-js-release-info/src/url.rs
index 7f5281d..f119e80 100644
--- a/crates/node-js-release-info/src/url.rs
+++ b/crates/node-js-release-info/src/url.rs
@@ -1,26 +1,26 @@
-#[derive(Clone, Debug, PartialEq)]
-pub struct NodeJSURLFormatter {
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
+pub(crate) struct NodeJsUrlFormatter {
pub protocol: String,
pub host: String,
pub pathname: String,
}
-impl Default for NodeJSURLFormatter {
+impl Default for NodeJsUrlFormatter {
fn default() -> Self {
- NodeJSURLFormatter::new()
+ NodeJsUrlFormatter::new()
}
}
-impl NodeJSURLFormatter {
- pub fn new() -> NodeJSURLFormatter {
- NodeJSURLFormatter {
+impl NodeJsUrlFormatter {
+ pub(crate) fn new() -> NodeJsUrlFormatter {
+ NodeJsUrlFormatter {
protocol: String::from("https:"),
host: String::from("nodejs.org"),
pathname: String::from("/download/release"),
}
}
- pub fn info>(&self, version: V) -> String {
+ pub(crate) fn info>(&self, version: V) -> String {
format!(
"{}//{}{}",
self.protocol,
@@ -29,7 +29,7 @@ impl NodeJSURLFormatter {
)
}
- pub fn info_pathname>(&self, version: V) -> String {
+ pub(crate) fn info_pathname>(&self, version: V) -> String {
format!(
"{}/v{}/SHASUMS256.txt",
self.pathname,
@@ -37,7 +37,7 @@ impl NodeJSURLFormatter {
)
}
- pub fn pkg, F: AsRef>(&self, version: V, filename: F) -> String {
+ pub(crate) fn pkg, F: AsRef>(&self, version: V, filename: F) -> String {
format!(
"{}//{}{}",
self.protocol,
@@ -46,7 +46,11 @@ impl NodeJSURLFormatter {
)
}
- pub fn pkg_pathname, F: AsRef>(&self, version: V, filename: F) -> String {
+ pub(crate) fn pkg_pathname, F: AsRef>(
+ &self,
+ version: V,
+ filename: F,
+ ) -> String {
format!(
"{}/v{}/{}",
self.pathname,
@@ -62,7 +66,7 @@ mod tests {
#[test]
fn it_initializes() {
- let url_fmt = NodeJSURLFormatter::new();
+ let url_fmt = NodeJsUrlFormatter::new();
assert_eq!(url_fmt.protocol, "https:");
assert_eq!(url_fmt.host, "nodejs.org");
assert_eq!(url_fmt.pathname, "/download/release");
@@ -70,13 +74,13 @@ mod tests {
#[test]
fn it_initializes_with_defaults() {
- let url_fmt = NodeJSURLFormatter::default();
- assert_eq!(url_fmt, NodeJSURLFormatter::new());
+ let url_fmt = NodeJsUrlFormatter::default();
+ assert_eq!(url_fmt, NodeJsUrlFormatter::new());
}
#[test]
fn it_formats_url_for_node_js_release_info() {
- let url_fmt = NodeJSURLFormatter::new();
+ let url_fmt = NodeJsUrlFormatter::new();
assert_eq!(
url_fmt.info("1.0.0"),
"https://nodejs.org/download/release/v1.0.0/SHASUMS256.txt"
@@ -85,7 +89,7 @@ mod tests {
#[test]
fn it_formats_url_for_node_js_package() {
- let url_fmt = NodeJSURLFormatter::new();
+ let url_fmt = NodeJsUrlFormatter::new();
assert_eq!(
url_fmt.pkg("1.0.0", "fake-filename"),
"https://nodejs.org/download/release/v1.0.0/fake-filename"
diff --git a/crates/node-js-release-info/tests/integration.rs b/crates/node-js-release-info/tests/integration.rs
index f99da0d..792ef4d 100644
--- a/crates/node-js-release-info/tests/integration.rs
+++ b/crates/node-js-release-info/tests/integration.rs
@@ -1,13 +1,20 @@
+//! Integration tests exercising the crate's public API as a consumer would
+//!
+//! NOTE: these hit the live Node.js downloads server
+
use node_js_release_info::*;
-const VERSION: &str = "20.7.0";
+const VERSION: &str = "24.19.0";
+const DARWIN_X64_URL: &str =
+ "https://nodejs.org/download/release/v24.19.0/node-v24.19.0-darwin-x64.tar.gz";
+const DARWIN_X64_SHA256: &str = "d1b5e999db158c62fe8f7267a4476b035d8bd93b1a605bac24a3f0dd166e3316";
#[test]
fn it_provides_expected_resources() {
- let info = NodeJSRelInfo::new(VERSION);
- let os = NodeJSOS::Linux;
- let arch = NodeJSArch::X64;
- let ext = NodeJSPkgExt::Targz;
+ let info = NodeJsRelInfo::new(VERSION);
+ let os = NodeJsOs::Linux;
+ let arch = NodeJsArch::X64;
+ let ext = NodeJsPkgExt::Targz;
assert_eq!(info.version, VERSION);
assert_eq!(info.os, os);
assert_eq!(info.arch, arch);
@@ -16,29 +23,26 @@ fn it_provides_expected_resources() {
#[tokio::test]
async fn it_fetches_node_js_release_info_for_a_given_configuration() {
- let mut info = NodeJSRelInfo::new(VERSION);
- let result = info.macos().x64().tar_gz().fetch().await.unwrap();
- assert_eq!(
- result.url,
- "https://nodejs.org/download/release/v20.7.0/node-v20.7.0-darwin-x64.tar.gz"
- );
- assert_eq!(
- result.sha256,
- "ceeba829f44e7573949f2ce2ad5def27f1d6daa55f2860bea82964851fae01bc"
- );
+ let result = NodeJsRelInfo::new(VERSION)
+ .macos()
+ .x64()
+ .tar_gz()
+ .fetch()
+ .await
+ .unwrap();
+
+ assert_eq!(result.url, DARWIN_X64_URL);
+ assert_eq!(result.sha256, DARWIN_X64_SHA256);
}
#[tokio::test]
async fn it_fetches_node_js_release_info_for_all_supported_configurations() {
- let info = NodeJSRelInfo::new(VERSION);
+ let info = NodeJsRelInfo::new(VERSION);
let result = info.fetch_all().await.unwrap();
- assert_eq!(result.len(), 24);
- assert_eq!(
- result[4].url,
- "https://nodejs.org/download/release/v20.7.0/node-v20.7.0-darwin-x64.tar.gz"
- );
- assert_eq!(
- result[4].sha256,
- "ceeba829f44e7573949f2ce2ad5def27f1d6daa55f2860bea82964851fae01bc"
- );
+
+ // NOTE: v24 dropped `linux-armv7l` and all three 32-bit Windows builds,
+ // taking the recognized configuration count from 24 (as of v20) down to 19
+ assert_eq!(result.len(), 19);
+ assert_eq!(result[4].url, DARWIN_X64_URL);
+ assert_eq!(result[4].sha256, DARWIN_X64_SHA256);
}
diff --git a/deny.toml b/deny.toml
new file mode 100644
index 0000000..3de9222
--- /dev/null
+++ b/deny.toml
@@ -0,0 +1,41 @@
+# see: https://embarkstudios.github.io/cargo-deny/
+#
+# run via `cargo xtask deny`
+
+[graph]
+all-features = true
+
+[advisories]
+# fail on any crate with a known security advisory or that is unmaintained
+version = 2
+yanked = "deny"
+
+[licenses]
+version = 2
+# this workspace publishes under `MIT OR Apache-2.0` - only accept dependency
+# licenses that are compatible with distributing under those terms
+allow = [
+ "Apache-2.0",
+ "Apache-2.0 WITH LLVM-exception",
+ "BSD-3-Clause",
+ # covers the Mozilla CA root certificate *data* bundled by
+ # `webpki-root-certs` (via `reqwest` -> `rustls`), not code. permissive,
+ # no copyleft - see: https://cdla.dev/permissive-2-0/
+ "CDLA-Permissive-2.0",
+ "ISC",
+ "MIT",
+ "MIT-0",
+ "Unicode-3.0",
+]
+confidence-threshold = 0.93
+
+[bans]
+# duplicate versions of the same crate bloat build times - warn so they stay
+# visible without blocking CI on a transitive dependency we do not control
+multiple-versions = "warn"
+wildcards = "deny"
+
+[sources]
+unknown-registry = "deny"
+unknown-git = "deny"
+allow-registry = ["https://github.com/rust-lang/crates.io-index"]
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000..61563f9
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,8 @@
+# see: https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file
+#
+# pins the toolchain used for local development and CI so everyone gets the
+# same `rustc` + tooling. note this is *not* the project's MSRV - that is
+# declared via `rust-version` in `Cargo.toml`
+[toolchain]
+channel = "stable"
+components = ["clippy", "llvm-tools-preview", "rustfmt"]
diff --git a/rustfmt.toml b/rustfmt.toml
index eda3988..d4e8a88 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,4 +1,8 @@
# see: https://rust-lang.github.io/rustfmt/
#
+# pin formatting rules to the 2024 style edition so output does not shift when
+# the toolchain updates. this is independent of the crates' `edition`
+style_edition = "2024"
+
# reorder_modules = false
# reorder_imports = false
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index fcaf193..16da364 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -2,13 +2,21 @@
name = "xtask"
description = "internal-only crate used to orchestrate repo tasks"
version = "0.1.0"
-edition = "2021"
+publish = false
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+authors.workspace = true
+repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-duct = "0.13.*"
-inquire = "0.6.*"
-regex = "1.*"
-semver = "1.*"
-toml_edit = "0.20.*"
+duct.workspace = true
+inquire.workspace = true
+regex.workspace = true
+semver.workspace = true
+toml_edit.workspace = true
+
+[lints]
+workspace = true
diff --git a/xtask/src/cargo.rs b/xtask/src/cargo.rs
index e5cd171..074d686 100644
--- a/xtask/src/cargo.rs
+++ b/xtask/src/cargo.rs
@@ -10,7 +10,7 @@ use std::path::PathBuf;
type DynError = Box;
#[derive(Clone, Debug, PartialEq)]
-pub struct Cargo<'a> {
+pub(crate) struct Cargo<'a> {
pub bin: String,
opts: &'a Options,
}
@@ -26,12 +26,12 @@ impl<'a> Execute for Cargo<'a> {
}
impl<'a> Cargo<'a> {
- pub fn new(opts: &'a Options) -> Cargo<'a> {
+ pub(crate) fn new(opts: &'a Options) -> Cargo<'a> {
let bin = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
Cargo { bin, opts }
}
- pub fn workspace_path(&self) -> Result {
+ pub(crate) fn workspace_path(&self) -> Result {
let (args, envs) = self.workspace_path_params();
let stdout = self.exec_safe(args, envs).read()?;
Ok(PathBuf::from(stdout.replace("Cargo.toml", "").trim()))
@@ -45,7 +45,7 @@ impl<'a> Cargo<'a> {
(args, None)
}
- pub fn create(&self, path: P, arguments: U) -> Expression
+ pub(crate) fn create
(&self, path: P, arguments: U) -> Expression
where
P: Into,
U: IntoIterator,
@@ -65,7 +65,7 @@ impl<'a> Cargo<'a> {
(args, None)
}
- pub fn install(&self, arguments: U) -> Expression
+ pub(crate) fn install(&self, arguments: U) -> Expression
where
U: IntoIterator,
U::Item: Into,
@@ -79,11 +79,17 @@ impl<'a> Cargo<'a> {
U: IntoIterator,
U::Item: Into,
{
- let args = self.build_args([OsString::from("install")], arguments);
+ // NOTE: `--locked` makes the tool build from its own committed
+ // `Cargo.lock` - without it, an unrelated upstream release can break
+ // `setup` on a repo where nothing changed
+ let args = self.build_args(
+ [OsString::from("install"), OsString::from("--locked")],
+ arguments,
+ );
(args, None)
}
- pub fn build(&self, arguments: U) -> Expression
+ pub(crate) fn build(&self, arguments: U) -> Expression
where
U: IntoIterator,
U::Item: Into,
@@ -101,7 +107,7 @@ impl<'a> Cargo<'a> {
(args, None)
}
- pub fn clean(&self, arguments: U) -> Expression
+ pub(crate) fn clean(&self, arguments: U) -> Expression
where
U: IntoIterator,
U::Item: Into,
@@ -119,7 +125,7 @@ impl<'a> Cargo<'a> {
(args, None)
}
- pub fn test(&self, arguments: U) -> Expression
+ pub(crate) fn test(&self, arguments: U) -> Expression
where
U: IntoIterator,
U::Item: Into,
@@ -137,31 +143,72 @@ impl<'a> Cargo<'a> {
(args, None)
}
- pub fn coverage(&self, path: P) -> Expression
+ pub(crate) fn coverage_clean(&self) -> Expression {
+ let (args, envs) = self.coverage_clean_params();
+ self.exec_unsafe(args, envs)
+ }
+
+ fn coverage_clean_params(&self) -> (Vec, EnvVars) {
+ // NOTE: profiling data accumulates across runs - without this, stale
+ // data from a previous run is folded into the report
+ let args = self.build_args([OsString::from("llvm-cov")], ["clean", "--workspace"]);
+ (args, None)
+ }
+
+ pub(crate) fn coverage(&self) -> Expression {
+ let (args, envs) = self.coverage_params();
+ self.exec_unsafe(args, envs)
+ }
+
+ fn coverage_params(&self) -> (Vec, EnvVars) {
+ // NOTE: `--no-report` runs the tests and collects profiling data but
+ // renders nothing - `coverage_report()` below turns that single
+ // collection run into both html and lcov output
+ let args = self.build_args(
+ [OsString::from("llvm-cov")],
+ ["--workspace", "--all-features", "--no-report"],
+ );
+ (args, None)
+ }
+
+ pub(crate) fn coverage_report(&self, arguments: U) -> Expression
where
- P: Into,
+ U: IntoIterator,
+ U::Item: Into,
{
- let (args, envs) = self.coverage_params(path);
+ let (args, envs) = self.coverage_report_params(arguments);
self.exec_unsafe(args, envs)
}
- fn coverage_params(&self, path: P) -> (Vec, EnvVars)
+ fn coverage_report_params(&self, arguments: U) -> (Vec, EnvVars)
where
- P: Into,
+ U: IntoIterator,
+ U::Item: Into,
{
- let mut profile_ptn: OsString = path.into();
- profile_ptn.push("/cargo-test-%p-%m.profraw");
- let args = self.build_args([OsString::from("test")], ["--all-features"]);
- let envs = HashMap::from([
- ("CARGO_INCREMENTAL".into(), "0".into()),
- ("RUSTFLAGS".into(), "-Cinstrument-coverage".into()),
- ("LLVM_PROFILE_FILE".into(), profile_ptn),
- ]);
+ let args = self.build_args(
+ [OsString::from("llvm-cov"), OsString::from("report")],
+ arguments,
+ );
+ (args, None)
+ }
- (args, Some(envs))
+ pub(crate) fn format(&self, check: bool) -> Expression {
+ let (args, envs) = self.format_params(check);
+ self.exec_safe(args, envs)
+ }
+
+ fn format_params(&self, check: bool) -> (Vec, EnvVars) {
+ let mut arguments = vec![OsString::from("--all")];
+
+ if check {
+ arguments.push("--check".into());
+ }
+
+ let args = self.build_args([OsString::from("fmt")], arguments);
+ (args, None)
}
- pub fn lint(&self) -> Expression {
+ pub(crate) fn lint(&self) -> Expression {
let (args, envs) = self.lint_params();
self.exec_safe(args, envs)
}
@@ -172,11 +219,10 @@ impl<'a> Cargo<'a> {
["--all-targets", "--all-features", "--no-deps"],
);
let envs = HashMap::from([("RUSTFLAGS".into(), "-Dwarnings".into())]);
-
(args, Some(envs))
}
- pub fn doc(&self, arguments: U) -> Expression
+ pub(crate) fn doc(&self, arguments: U) -> Expression
where
U: IntoIterator,
U::Item: Into,
@@ -191,10 +237,13 @@ impl<'a> Cargo<'a> {
U::Item: Into,
{
let args = self.build_args([OsString::from("doc")], arguments);
- (args, None)
+ // NOTE: rustdoc only *warns* on problems like broken intra-doc links,
+ // so they slip by unnoticed - this promotes them to build failures
+ let envs = HashMap::from([("RUSTDOCFLAGS".into(), "-Dwarnings".into())]);
+ (args, Some(envs))
}
- pub fn publish_package>(&self, name: N) -> Expression {
+ pub(crate) fn publish_package>(&self, name: N) -> Expression {
let (args, envs) = self.publish_package_params(name);
self.exec_unsafe(args, envs)
}
@@ -240,7 +289,7 @@ mod tests {
let opts = Options::new(vec![], task_flags! {}).unwrap();
let cargo = Cargo::new(&opts);
let (args, envs) = cargo.install_params(["grcov"]);
- assert_eq!(args, ["install", "grcov"]);
+ assert_eq!(args, ["install", "--locked", "grcov"]);
assert_eq!(envs, None);
}
@@ -275,19 +324,56 @@ mod tests {
fn it_builds_args_for_the_coverage_subcommand() {
let opts = Options::new(vec![], task_flags! {}).unwrap();
let cargo = Cargo::new(&opts);
- let path = PathBuf::from("fake-coverage-path");
- let (args, envs) = cargo.coverage_params(path);
- let expected_envs = HashMap::from([
- ("CARGO_INCREMENTAL".into(), "0".into()),
- ("RUSTFLAGS".into(), "-Cinstrument-coverage".into()),
- (
- "LLVM_PROFILE_FILE".into(),
- "fake-coverage-path/cargo-test-%p-%m.profraw".into(),
- ),
- ]);
-
- assert_eq!(args, ["test", "--all-features"]);
- assert_eq!(envs, Some(expected_envs));
+ let (args, envs) = cargo.coverage_params();
+
+ assert_eq!(
+ args,
+ ["llvm-cov", "--workspace", "--all-features", "--no-report"]
+ );
+ assert_eq!(envs, None);
+ }
+
+ #[test]
+ fn it_builds_args_for_the_coverage_clean_subcommand() {
+ let opts = Options::new(vec![], task_flags! {}).unwrap();
+ let cargo = Cargo::new(&opts);
+ let (args, envs) = cargo.coverage_clean_params();
+
+ assert_eq!(args, ["llvm-cov", "clean", "--workspace"]);
+ assert_eq!(envs, None);
+ }
+
+ #[test]
+ fn it_builds_args_for_the_coverage_report_subcommand() {
+ let opts = Options::new(vec![], task_flags! {}).unwrap();
+ let cargo = Cargo::new(&opts);
+ let (args, envs) =
+ cargo.coverage_report_params(["--html", "--output-dir", "fake-coverage-path"]);
+
+ assert_eq!(
+ args,
+ [
+ "llvm-cov",
+ "report",
+ "--html",
+ "--output-dir",
+ "fake-coverage-path"
+ ]
+ );
+ assert_eq!(envs, None);
+ }
+
+ #[test]
+ fn it_builds_args_for_the_format_subcommand() {
+ let opts = Options::new(vec![], task_flags! {}).unwrap();
+ let cargo = Cargo::new(&opts);
+ let (args, envs) = cargo.format_params(false);
+ assert_eq!(args, ["fmt", "--all"]);
+ assert_eq!(envs, None);
+
+ let (args, envs) = cargo.format_params(true);
+ assert_eq!(args, ["fmt", "--all", "--check"]);
+ assert_eq!(envs, None);
}
#[test]
@@ -308,8 +394,10 @@ mod tests {
let opts = Options::new(vec![], task_flags! {}).unwrap();
let cargo = Cargo::new(&opts);
let (args, envs) = cargo.doc_params(["--workspace", "--no-deps"]);
+ let expected_envs = HashMap::from([("RUSTDOCFLAGS".into(), "-Dwarnings".into())]);
+
assert_eq!(args, ["doc", "--workspace", "--no-deps"]);
- assert_eq!(envs, None);
+ assert_eq!(envs, Some(expected_envs));
}
#[test]
diff --git a/xtask/src/changelog.rs b/xtask/src/changelog.rs
index 6ea80f3..98e8323 100644
--- a/xtask/src/changelog.rs
+++ b/xtask/src/changelog.rs
@@ -13,50 +13,50 @@ const MARKER_START: &str = "";
const MARKER_END: &str = "";
#[derive(Clone, Debug, Default, PartialEq)]
-pub struct Changelog {
+pub(crate) struct Changelog {
pub path: PathBuf,
text: String,
}
impl Changelog {
- pub fn new(crate_root: PathBuf) -> Self {
+ pub(crate) fn new(crate_root: PathBuf) -> Self {
Changelog {
text: String::new(),
path: crate_root.join(CHANGELOG_MD),
}
}
- pub fn from_path(crate_root: PathBuf) -> Result {
+ pub(crate) fn from_path(crate_root: PathBuf) -> Result {
let mut changelog = Changelog::new(crate_root);
changelog.load()
}
- pub fn read(&self) -> Result {
+ pub(crate) fn read(&self) -> Result {
// TODO (busticated): pull into FS wrapper?
Ok(fs::read_to_string(&self.path)?)
}
- pub fn load(&mut self) -> Result {
+ pub(crate) fn load(&mut self) -> Result {
self.text = self.read()?;
Ok(self.clone())
}
- pub fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> {
+ pub(crate) fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> {
self.text = self.render(&krate.name, &krate.version);
self.save(fs)
}
- pub fn save(&self, fs: &FS) -> Result<(), DynError> {
+ pub(crate) fn save(&self, fs: &FS) -> Result<(), DynError> {
Ok(fs.write(&self.path, &self.text)?)
}
- pub fn render>(&self, name: N, version: &Version) -> String {
+ pub(crate) fn render>(&self, name: N, version: &Version) -> String {
let name = name.as_ref();
- let lines = vec![
- format!("# `{}` Changelog", name),
+ let lines = [
+ format!("# `{name}` Changelog"),
MARKER_START.to_string(),
MARKER_END.to_string(),
- format!("## v{}", version),
+ format!("## v{version}"),
"".to_string(),
"* Initial release 🎊🎉".to_string(),
"".to_string(),
@@ -64,20 +64,25 @@ impl Changelog {
lines.join("\n")
}
- pub fn update(&mut self, fs: &FS, krate: &Krate, log: Vec) -> Result<(), DynError> {
+ pub(crate) fn update(
+ &mut self,
+ fs: &FS,
+ krate: &Krate,
+ log: Vec,
+ ) -> Result<(), DynError> {
if log.is_empty() {
return Ok(());
}
self.load()?;
- let mut changes = format!("{}\n{}\n", MARKER_START, MARKER_END);
- changes.push_str(format!("## v{}\n\n", &krate.version).as_str());
+ let mut changes = format!("{MARKER_START}\n{MARKER_END}\n");
+ changes.push_str(format!("## v{}\n\n", krate.version).as_str());
for msg in log.iter() {
if !msg.is_empty() {
- changes.push_str(format!("* {}\n", &msg).as_str());
+ changes.push_str(format!("* {msg}\n").as_str());
}
}
changes.push('\n');
- let ptn = format!(r"{}[\s\S]*?{}", MARKER_START, MARKER_END);
+ let ptn = format!(r"{MARKER_START}[\s\S]*?{MARKER_END}");
let re = RegexBuilder::new(ptn.as_str())
.case_insensitive(true)
.multi_line(true)
diff --git a/xtask/src/exec.rs b/xtask/src/exec.rs
index a2d571e..75b3442 100644
--- a/xtask/src/exec.rs
+++ b/xtask/src/exec.rs
@@ -1,11 +1,11 @@
use crate::options::Options;
-use duct::{cmd, Expression};
+use duct::{Expression, cmd};
use std::collections::HashMap;
use std::ffi::OsString;
-pub type EnvVars = Option>;
+pub(crate) type EnvVars = Option>;
-pub trait Execute {
+pub(crate) trait Execute {
fn bin(&self) -> String;
fn opts(&self) -> &Options;
diff --git a/xtask/src/fs.rs b/xtask/src/fs.rs
index 28f83b5..01aedf5 100644
--- a/xtask/src/fs.rs
+++ b/xtask/src/fs.rs
@@ -5,46 +5,46 @@ use std::path::Path;
type IOResult = std::io::Result<()>;
#[derive(Clone, Debug, PartialEq)]
-pub struct FS<'a> {
+pub(crate) struct FS<'a> {
opts: &'a Options,
}
impl<'a> FS<'a> {
- pub fn new(opts: &'a Options) -> FS<'a> {
+ pub(crate) fn new(opts: &'a Options) -> FS<'a> {
FS { opts }
}
- pub fn write, D: AsRef<[u8]>>(&self, path: P, data: D) -> IOResult {
+ pub(crate) fn write, D: AsRef<[u8]>>(&self, path: P, data: D) -> IOResult {
if self.opts.has("dry-run") {
let path = path.as_ref().to_string_lossy();
- println!("Skipping: write {}", path);
+ println!("Skipping: write {path}");
return Ok(());
}
fs::write(path, data)
}
- pub fn remove_dir_all>(&self, path: P) -> IOResult {
+ pub(crate) fn remove_dir_all>(&self, path: P) -> IOResult {
if self.opts.has("dry-run") {
let path = path.as_ref().to_string_lossy();
- println!("Skipping: remove_dir_all {}", path);
+ println!("Skipping: remove_dir_all {path}");
return Ok(());
}
fs::remove_dir_all(path)
}
- pub fn create_dir_all>(&self, path: P) -> IOResult {
+ pub(crate) fn create_dir_all>(&self, path: P) -> IOResult {
if self.opts.has("dry-run") {
let path = path.as_ref().to_string_lossy();
- println!("Skipping: create_dir_all {}", path);
+ println!("Skipping: create_dir_all {path}");
return Ok(());
}
fs::create_dir_all(path)
}
- pub fn read_dir>(&self, path: P) -> std::io::Result {
+ pub(crate) fn read_dir>(&self, path: P) -> std::io::Result {
fs::read_dir(path)
}
}
diff --git a/xtask/src/git.rs b/xtask/src/git.rs
index b026c9a..8f5c5ec 100644
--- a/xtask/src/git.rs
+++ b/xtask/src/git.rs
@@ -1,6 +1,6 @@
+use crate::Krate;
use crate::exec::Execute;
use crate::options::Options;
-use crate::Krate;
use duct::Expression;
use std::error::Error;
use std::ffi::OsString;
@@ -9,7 +9,7 @@ use std::path::Path;
type DynError = Box;
#[derive(Clone, Debug, PartialEq)]
-pub struct Git<'a> {
+pub(crate) struct Git<'a> {
pub bin: String,
opts: &'a Options,
}
@@ -25,12 +25,12 @@ impl<'a> Execute for Git<'a> {
}
impl<'a> Git<'a> {
- pub fn new(opts: &'a Options) -> Git<'a> {
+ pub(crate) fn new(opts: &'a Options) -> Git<'a> {
let bin = "git".to_string();
Git { bin, opts }
}
- pub fn add(&self, path: P, arguments: U) -> Expression
+ pub(crate) fn add
(&self, path: P, arguments: U) -> Expression
where
P: AsRef,
U: IntoIterator,
@@ -52,7 +52,7 @@ impl<'a> Git<'a> {
)
}
- pub fn commit(&self, message: M, arguments: U) -> Expression
+ pub(crate) fn commit(&self, message: M, arguments: U) -> Expression
where
M: AsRef,
U: IntoIterator,
@@ -71,7 +71,7 @@ impl<'a> Git<'a> {
self.build_args(["commit", "--message", message.as_ref()], arguments)
}
- pub fn tag(&self, arguments: U) -> Expression
+ pub(crate) fn tag(&self, arguments: U) -> Expression
where
U: IntoIterator,
U::Item: Into,
@@ -88,7 +88,7 @@ impl<'a> Git<'a> {
self.build_args(["tag"], arguments)
}
- pub fn create_tag(&self, tag: T) -> Expression
+ pub(crate) fn create_tag(&self, tag: T) -> Expression
where
T: AsRef,
{
@@ -103,7 +103,7 @@ impl<'a> Git<'a> {
self.tag_params([tag.as_ref(), "--message", tag.as_ref()])
}
- pub fn todos(&self) -> Expression {
+ pub(crate) fn todos(&self) -> Expression {
let args = self.todos_params();
self.exec_safe(args, None)
}
@@ -127,22 +127,23 @@ impl<'a> Git<'a> {
"--",
":!./target/*",
":!./tmp/*",
+ ":!./README.md",
],
[""],
)
}
- pub fn get_changelog(&self, krate: &Krate) -> Result, DynError> {
+ pub(crate) fn get_changelog(&self, krate: &Krate) -> Result, DynError> {
let (prefix, args) = self.get_changelog_params(krate);
let history = self.exec_safe(args, None).read()?;
Ok(self.fmt_changelog(prefix, history))
}
fn get_changelog_params(&self, krate: &Krate) -> (String, Vec) {
- let range = format!("{}@{}..HEAD", &krate.name, &krate.version);
- let query = format!(r"--grep=\[{}\]", &krate.name);
+ let range = format!("{}@{}..HEAD", krate.name, krate.version);
+ let query = format!(r"--grep=\[{}\]", krate.name);
let fmt = String::from("--pretty=format:%B");
- let prefix = format!("[{}]", &krate.name);
+ let prefix = format!("[{}]", krate.name);
let args = self.build_args(["log"], [range, query, fmt]);
(prefix, args)
}
@@ -218,7 +219,8 @@ mod tests {
"--line-number",
"--",
":!./target/*",
- ":!./tmp/*"
+ ":!./tmp/*",
+ ":!./README.md",
]
);
}
diff --git a/xtask/src/krate.rs b/xtask/src/krate.rs
index 1165c53..6b2b2f0 100644
--- a/xtask/src/krate.rs
+++ b/xtask/src/krate.rs
@@ -16,7 +16,7 @@ const SRC_DIRNAME: &str = "src";
const LIB_FILENAME: &str = "lib.rs";
#[derive(Clone, Debug)]
-pub struct Krate {
+pub(crate) struct Krate {
pub kind: KrateKind,
pub version: Version,
pub name: String,
@@ -57,7 +57,7 @@ impl Default for Krate {
}
impl Krate {
- pub fn new, V: AsRef, N: AsRef, D: AsRef>(
+ pub(crate) fn new, V: AsRef, N: AsRef, D: AsRef>(
kind: K,
version: V,
name: N,
@@ -83,7 +83,7 @@ impl Krate {
}
}
- pub fn from_path(path: PathBuf) -> Result {
+ pub(crate) fn from_path(path: PathBuf) -> Result {
let toml = Toml::from_path(path.clone())?;
let readme = Readme::from_path(path.clone())?;
let changelog = Changelog::from_path(path.clone())?;
@@ -105,17 +105,17 @@ impl Krate {
Ok(krate)
}
- pub fn id(&self) -> String {
- format!("{}@{}", &self.name, self.version)
+ pub(crate) fn id(&self) -> String {
+ format!("{}@{}", self.name, self.version)
}
- pub fn set_version(&mut self, version: Version) -> Result<(), DynError> {
+ pub(crate) fn set_version(&mut self, version: Version) -> Result<(), DynError> {
self.version = version;
self.toml.set_version(&self.version)?;
Ok(())
}
- pub fn clean(&self, fs: &FS) -> Result<(), DynError> {
+ pub(crate) fn clean(&self, fs: &FS) -> Result<(), DynError> {
use std::io::ErrorKind;
match fs.remove_dir_all(self.tmp_path()) {
@@ -125,12 +125,12 @@ impl Krate {
}
}
- pub fn create_dirs(&self, fs: &FS) -> Result<(), DynError> {
+ pub(crate) fn create_dirs(&self, fs: &FS) -> Result<(), DynError> {
Ok(fs.create_dir_all(self.coverage_path())?)
}
}
-pub trait KratePaths {
+pub(crate) trait KratePaths {
fn path(&self) -> PathBuf;
fn tmp_path(&self) -> PathBuf {
@@ -143,14 +143,14 @@ pub trait KratePaths {
}
#[derive(Clone, Debug, Default, PartialEq)]
-pub enum KrateKind {
+pub(crate) enum KrateKind {
#[default]
Library,
Binary,
}
impl KrateKind {
- pub fn new>(kind: K) -> KrateKind {
+ pub(crate) fn new>(kind: K) -> KrateKind {
let kind = KrateKind::from_str(kind.as_ref());
if kind.is_err() {
@@ -160,7 +160,7 @@ impl KrateKind {
kind.unwrap()
}
- pub fn from_path(path: PathBuf) -> Result {
+ pub(crate) fn from_path(path: PathBuf) -> Result {
let path = path.join(SRC_DIRNAME).join(LIB_FILENAME);
if !path.is_file() {
@@ -178,7 +178,7 @@ impl Display for KrateKind {
KrateKind::Library => "--lib",
};
- write!(f, "{}", arch)
+ write!(f, "{arch}")
}
}
@@ -189,7 +189,7 @@ impl FromStr for KrateKind {
match s.to_lowercase().trim() {
"binary" | "bin" | "--bin" => Ok(KrateKind::Binary),
"library" | "lib" | "--lib" => Ok(KrateKind::Library),
- _ => Err(format!("Unrecognized input: {}", s).into()),
+ _ => Err(format!("Unrecognized input: {s}").into()),
}
}
}
@@ -252,11 +252,9 @@ mod tests {
}
#[test]
- #[should_panic(
- expected = "called `Result::unwrap()` on an `Err` value: \"Unrecognized input: NOPE!\""
- )]
fn it_fails_to_initialize_when_krate_kind_cannot_be_determined_from_str() {
- KrateKind::from_str("NOPE!").unwrap();
+ let err = KrateKind::from_str("NOPE!").unwrap_err();
+ assert_eq!(err.to_string(), "Unrecognized input: NOPE!");
}
#[test]
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index e05ceb1..005e127 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -1,3 +1,9 @@
+//! Internal-only task runner for this workspace - see `cargo xtask help`
+//!
+//! This crate is not published. Each repo task (test, lint, coverage,
+//! changelog, release, ...) is declared as a [`Task`] in
+//! [`init_tasks`] and dispatched by name from the command line.
+
mod cargo;
mod changelog;
mod exec;
@@ -28,7 +34,7 @@ type DynError = Box;
fn main() {
if let Err(e) = try_main() {
- eprintln!("{:?}", e);
+ eprintln!("{e:?}");
std::process::exit(-1);
}
}
@@ -38,7 +44,7 @@ fn try_main() -> Result<(), DynError> {
args.remove(0); // drop executable path
- let cmd = match args.get(0) {
+ let cmd = match args.first() {
Some(x) => x.clone(),
None => "".to_string(),
};
@@ -50,8 +56,8 @@ fn try_main() -> Result<(), DynError> {
println!("::::::::::::::::::::::");
println!(":::: Running Task ::::");
println!("::::::::::::::::::::::");
- println!("Name: {}", cmd);
- println!("Args: {:?}", args);
+ println!("Name: {cmd}");
+ println!("Args: {args:?}");
println!();
let tasks = init_tasks();
@@ -61,6 +67,19 @@ fn try_main() -> Result<(), DynError> {
}
}
+/// Reports whether a tool is already usable by running a probe command (e.g.
+/// `cargo llvm-cov --version`). Returns `false` when the binary is missing or
+/// the probe exits non-zero - never errors, so a failed probe just means
+/// "install it".
+fn is_tool_available(bin: &str, args: &[&str]) -> bool {
+ cmd(bin, args)
+ .stdout_null()
+ .stderr_null()
+ .unchecked()
+ .run()
+ .is_ok_and(|out| out.status.success())
+}
+
fn print_help(cmd: String, _args: Vec, tasks: Tasks) -> Result<(), DynError> {
println!(":::::::::::::::::::::::::");
println!(":::: Tasks & Options ::::");
@@ -70,7 +89,7 @@ fn print_help(cmd: String, _args: Vec, tasks: Tasks) -> Result<(), DynEr
println!();
if !(cmd.is_empty() || cmd == "help" || cmd == "--help") {
- let msg = format!("Unrecognized Command! Received: '{}'", cmd);
+ let msg = format!("Unrecognized Command! Received: '{cmd}'");
return Err(msg.into());
}
@@ -97,18 +116,18 @@ fn init_tasks() -> Tasks {
for tag in tags_text.lines() {
let (name, version) = match tag.split_once('@') {
- None => return Err(format!("Invalid tag: {}", tag).into()),
+ None => return Err(format!("Invalid tag: {tag}").into()),
Some((n, v)) => (n.trim().to_string(), v.trim().to_string()),
};
tags.insert(name, version);
}
- for (name, _version) in tags.iter() {
- let krate = krates.get(name).unwrap_or_else(|| panic!("Could Not Find Crate: `{}`!", name));
+ for name in tags.keys() {
+ let krate = krates.get(name).unwrap_or_else(|| panic!("Could Not Find Crate: `{name}`!"));
let log = git.get_changelog(krate)?;
- println!(":::: {} [changes: {}]", &krate.name, log.len());
+ println!(":::: {} [changes: {}]", krate.name, log.len());
if log.is_empty() {
println!("\t--- n/a ---");
@@ -118,7 +137,7 @@ fn init_tasks() -> Tasks {
for l in log.iter() {
- println!("* {}", l);
+ println!("* {l}");
}
println!();
@@ -139,6 +158,10 @@ fn init_tasks() -> Tasks {
println!(":::::::::::::::::::::::::::::::::");
println!();
+ tasks
+ .get("format")
+ .unwrap()
+ .exec(vec!["--check".into()], tasks)?;
tasks
.get("spellcheck")
.unwrap()
@@ -147,6 +170,10 @@ fn init_tasks() -> Tasks {
.get("lint")
.unwrap()
.exec(vec![], tasks)?;
+ tasks
+ .get("doc")
+ .unwrap()
+ .exec(vec!["--check".into()], tasks)?;
tasks
.get("coverage")
.unwrap()
@@ -176,11 +203,6 @@ fn init_tasks() -> Tasks {
},
},
Task {
- // TODO (busticated): oof. coverage is a bit h0rked atm - see:
- // https://github.com/mozilla/grcov/issues/1103
- // https://github.com/mozilla/grcov/issues/556
- // https://github.com/mozilla/grcov/issues/802
- // https://github.com/mozilla/grcov/issues/1042
name: "coverage".into(),
description: "run tests and generate html code coverage report".into(),
flags: task_flags! {
@@ -193,10 +215,16 @@ fn init_tasks() -> Tasks {
println!();
let coverage_root = String::from("tmp/coverage");
- let report = format!("{}/html/index.html", &coverage_root);
-
+ let report = format!("{coverage_root}/html/index.html");
+ let lcov = format!("{coverage_root}/lcov.info");
+ // NOTE: `xtask` is tooling and `tests/` holds the tests
+ // themselves - neither belongs in the crates' coverage numbers
+ let ignore = r"(^|/)(xtask|tests)/";
+
+ // NOTE: `clean` wipes stale profiling data, which would
+ // otherwise be folded into this run's report
tasks.get("clean").unwrap().exec(vec![], tasks)?;
- cargo.coverage(&coverage_root).run()?;
+ cargo.coverage().run()?;
println!(":::: Done!");
println!();
@@ -205,35 +233,29 @@ fn init_tasks() -> Tasks {
println!(":::::::::::::::::::::::::::");
println!();
- cmd!(
- "grcov",
- ".",
- "--binary-path",
- "./target/debug/deps",
- "--source-dir",
- ".",
- "--output-types",
- "html,lcov",
- "--branch",
- "--ignore-not-existing",
- "--ignore",
- "../*",
- "--ignore",
- "/*",
- "--ignore",
- "xtask/*",
- "--ignore",
- "*/tests/*",
- "--output-path",
+ cargo.coverage_report([
+ "--html",
+ "--output-dir",
&coverage_root,
- )
+ "--ignore-filename-regex",
+ ignore,
+ ])
+ .run()?;
+
+ cargo.coverage_report([
+ "--lcov",
+ "--output-path",
+ &lcov,
+ "--ignore-filename-regex",
+ ignore,
+ ])
.run()?;
if opts.has("open"){
cmd!("open", &report).run()?;
}
- println!(":::: Report: {}", report);
+ println!(":::: Report: {report}");
println!(":::: Done!");
println!();
Ok(())
@@ -343,10 +365,10 @@ fn init_tasks() -> Tasks {
}
for tag in tags {
- let (name, _ver) = tag.split_once('@').unwrap_or_else(|| panic!("Invalid Tag: `{}`!", tag));
- let krate = krates.get(name).unwrap_or_else(|| panic!("Could Not Find Crate: `{}`!", name));
- let message = format!("Publishing: {} at v{}", &krate.name, &krate.version);
- println!("{}", &message);
+ let (name, _ver) = tag.split_once('@').unwrap_or_else(|| panic!("Invalid Tag: `{tag}`!"));
+ let krate = krates.get(name).unwrap_or_else(|| panic!("Could Not Find Crate: `{name}`!"));
+ let message = format!("Publishing: {} at v{}", krate.name, krate.version);
+ println!("{message}");
cargo.publish_package(&krate.name).run()?;
}
@@ -385,6 +407,21 @@ fn init_tasks() -> Tasks {
for mut krate in krates.values().cloned() {
let log = git.get_changelog(&krate)?;
let version = krate.toml.get_version()?;
+
+ println!();
+ println!(":::: Checking `{}` for breaking API changes...", krate.name);
+ println!();
+
+ // NOTE: advisory only - "requires new major version" is the
+ // *expected* result here, since versions are bumped below
+ // rather than alongside the code. `.unchecked()` keeps that
+ // non-zero exit from aborting the release
+ cmd!("cargo", "semver-checks", "--package", &krate.name)
+ .unchecked()
+ .run()?;
+
+ println!();
+
let options = VersionChoice::options(&version);
let message = format!("Version for `{}` [current: {}]", krate.name, version);
let question = InquireSelect::new(&message, options);
@@ -432,17 +469,26 @@ fn init_tasks() -> Tasks {
name: "doc".into(),
description: "build project documentation".into(),
flags: task_flags! {
+ "check" => "test examples and render docs without updating the README",
"dry-run" => "run thru steps but do not generate docs",
"open" => "open rendered docs for viewing"
},
run: |opts, fs, _git, cargo, mut workspace, _tasks| {
+ let check = opts.has("check");
+
println!(":::::::::::::::::::::::::::");
- println!(":::: Building All Docs ::::");
+ if check {
+ println!(":::: Checking All Docs ::::");
+ } else {
+ println!(":::: Building All Docs ::::");
+ }
println!(":::::::::::::::::::::::::::");
println!();
println!(":::: Testing Examples...");
println!();
+ // NOTE: `cargo llvm-cov` does not run doc tests, so the
+ // `coverage` task does not cover them - they run here
cargo.test(["--doc", "--all-features"]).run()?;
println!(":::: Rendering Docs...");
@@ -456,6 +502,15 @@ fn init_tasks() -> Tasks {
cargo.doc(args).run()?;
+ // NOTE: `--check` verifies only - rewriting the README here
+ // would mutate the tree mid-CI
+ if check {
+ println!();
+ println!(":::: Done!");
+ println!();
+ return Ok(());
+ }
+
println!();
println!(":::: Updating Workspace README...");
@@ -464,7 +519,7 @@ fn init_tasks() -> Tasks {
workspace.readme.update_crates_list(&fs, krates)?;
- println!(":::: Updated: {:?}", readme_path);
+ println!(":::: Updated: {readme_path:?}");
if opts.has("open") {
cmd!("open", readme_path.to_str().unwrap()).run()?;
@@ -475,6 +530,68 @@ fn init_tasks() -> Tasks {
Ok(())
},
},
+ Task {
+ name: "audit".into(),
+ description: "audit dependencies for advisories, licenses & bans".into(),
+ flags: task_flags! {},
+ run: |_opts, _fs, _git, _cargo, _workspace, _tasks| {
+ println!("::::::::::::::::::::::::::::::");
+ println!(":::: Auditing Dependencies ::::");
+ println!("::::::::::::::::::::::::::::::");
+ println!();
+
+ cmd!("cargo", "deny", "--all-features", "check").run()?;
+
+ println!(":::: Done!");
+ println!();
+ Ok(())
+ },
+ },
+ Task {
+ name: "semver".into(),
+ description: "check public APIs for accidental breaking changes".into(),
+ flags: task_flags! {},
+ run: |_opts, _fs, _git, _cargo, _workspace, _tasks| {
+ println!(":::::::::::::::::::::::::::");
+ println!(":::: Checking Semver ::::");
+ println!(":::::::::::::::::::::::::::");
+ println!();
+
+ // NOTE: baselines are the last published version of each
+ // crate, so this needs network access. crates marked
+ // `publish = false` (e.g. `xtask`) are skipped automatically
+ cmd!("cargo", "semver-checks", "--workspace").run()?;
+
+ println!(":::: Done!");
+ println!();
+ Ok(())
+ },
+ },
+ Task {
+ name: "format".into(),
+ description: "format source code (rustfmt)".into(),
+ flags: task_flags! {
+ "check" => "check formatting without making changes"
+ },
+ run: |opts, _fs, _git, cargo, _workspace, _tasks| {
+ let check = opts.has("check");
+
+ println!("::::::::::::::::::::::::::::");
+ if check {
+ println!(":::: Checking Formatting ::::");
+ } else {
+ println!(":::: Formatting Project ::::");
+ }
+ println!("::::::::::::::::::::::::::::");
+ println!();
+
+ cargo.format(check).run()?;
+
+ println!(":::: Done!");
+ println!();
+ Ok(())
+ },
+ },
Task {
name: "lint".into(),
description: "run the linter (clippy)".into(),
@@ -507,12 +624,34 @@ fn init_tasks() -> Tasks {
// to 'C:\Users\runneradmin\.cargo\bin\cargo.exe'"
// see: https://github.com/rust-lang/rustup/issues/1367
//cmd!("rustup", "update").run()?;
+ // NOTE: the toolchain and its components (clippy, rustfmt,
+ // llvm-tools-preview) are installed by `rustup` automatically
+ // per `rust-toolchain.toml`
cmd!("rustup", "toolchain", "list", "--verbose").run()?;
- // TODO (busticated): is there a way to includes these in Cargo.toml or similar?
- cmd!("rustup", "component", "add", "clippy").run()?;
- cmd!("rustup", "component", "add", "llvm-tools-preview").run()?;
- cargo.install(["grcov"]).run()?;
- cargo.install(["typos-cli"]).run()?;
+
+ // NOTE: each entry is (crate to install, command that proves
+ // it is usable). `cargo install` decides whether to skip by
+ // consulting its own ledger, so it rebuilds from source for a
+ // binary put in place by anything else - probing the tool
+ // itself makes this a no-op wherever it is already available
+ // (e.g. CI, where prebuilt binaries are fetched beforehand)
+ let bin = cargo.bin.as_str();
+ let tools = [
+ ("cargo-llvm-cov", bin, vec!["llvm-cov", "--version"]),
+ ("cargo-deny", bin, vec!["deny", "--version"]),
+ ("cargo-semver-checks", bin, vec!["semver-checks", "--version"]),
+ ("typos-cli", "typos", vec!["--version"]),
+ ];
+
+ for (krate, probe_bin, probe_args) in tools {
+ if is_tool_available(probe_bin, &probe_args) {
+ println!(":::: Found: {krate} - skipping");
+ continue;
+ }
+
+ println!(":::: Installing: {krate}...");
+ cargo.install([krate]).run()?;
+ }
println!(":::: Done!");
println!();
@@ -574,3 +713,29 @@ fn init_tasks() -> Tasks {
tasks
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn it_detects_an_available_tool() {
+ // `cargo` is always present - we are running under it
+ assert!(is_tool_available("cargo", &["--version"]));
+ }
+
+ #[test]
+ fn it_detects_a_missing_tool() {
+ assert!(!is_tool_available(
+ "xtask-definitely-not-a-real-binary",
+ &["--version"]
+ ));
+ }
+
+ #[test]
+ fn it_detects_a_tool_whose_probe_fails() {
+ // binary exists but the subcommand does not, so the probe exits
+ // non-zero - treated as "not available" rather than erroring
+ assert!(!is_tool_available("cargo", &["xtask-not-a-subcommand"]));
+ }
+}
diff --git a/xtask/src/options.rs b/xtask/src/options.rs
index 33f51bb..7729861 100644
--- a/xtask/src/options.rs
+++ b/xtask/src/options.rs
@@ -6,13 +6,13 @@ type DynError = Box;
type TaskFlags = BTreeMap;
#[derive(Clone, Debug, Default, PartialEq)]
-pub struct Options {
+pub(crate) struct Options {
pub args: Vec,
pub flags: TaskFlags,
}
impl Options {
- pub fn new(args: Vec, flags: TaskFlags) -> Result {
+ pub(crate) fn new(args: Vec, flags: TaskFlags) -> Result {
let re = Regex::new(r"^-*")?;
let args = args
.iter()
@@ -21,14 +21,14 @@ impl Options {
for arg in &args {
if !flags.contains_key(arg) {
- return Err(format!("Unrecognized argument! {}", arg).into());
+ return Err(format!("Unrecognized argument! {arg}").into());
}
}
Ok(Options { args, flags })
}
- pub fn has>(&self, flag: F) -> bool {
+ pub(crate) fn has>(&self, flag: F) -> bool {
let flag = flag.as_ref().trim().to_lowercase();
for arg in &self.args {
if arg == &flag {
@@ -40,6 +40,13 @@ impl Options {
}
}
+/// Builds the flag name / description map used when declaring a [`Task`](crate::tasks::Task)
+///
+/// ```ignore
+/// task_flags! {
+/// "dry-run" => "run thru steps but do not publish",
+/// }
+/// ```
#[macro_export]
macro_rules! task_flags {
($($k:expr => $v:expr),* $(,)?) => {{
@@ -61,13 +68,11 @@ mod tests {
}
#[test]
- #[should_panic(
- expected = "called `Result::unwrap()` on an `Err` value: \"Unrecognized argument! nope\""
- )]
fn it_fails_to_initialize_when_args_has_unrecognized_items() {
let flags = task_flags! {};
let args = vec!["nope".into()];
- Options::new(args, flags).unwrap();
+ let err = Options::new(args, flags).unwrap_err();
+ assert_eq!(err.to_string(), "Unrecognized argument! nope");
}
#[test]
diff --git a/xtask/src/readme.rs b/xtask/src/readme.rs
index 8e695e3..4634988 100644
--- a/xtask/src/readme.rs
+++ b/xtask/src/readme.rs
@@ -11,44 +11,44 @@ type DynError = Box;
const README_MD: &str = "README.md";
#[derive(Clone, Debug, Default, PartialEq)]
-pub struct Readme {
+pub(crate) struct Readme {
pub path: PathBuf,
text: String,
}
impl Readme {
- pub fn new(crate_root: PathBuf) -> Self {
+ pub(crate) fn new(crate_root: PathBuf) -> Self {
Readme {
text: String::new(),
path: crate_root.join(README_MD),
}
}
- pub fn from_path(crate_root: PathBuf) -> Result {
+ pub(crate) fn from_path(crate_root: PathBuf) -> Result {
let mut readme = Readme::new(crate_root);
readme.load()
}
- pub fn read(&self) -> Result {
+ pub(crate) fn read(&self) -> Result {
// TODO (busticated): pull into FS wrapper?
Ok(fs::read_to_string(&self.path)?)
}
- pub fn load(&mut self) -> Result {
+ pub(crate) fn load(&mut self) -> Result {
self.text = self.read()?;
Ok(self.clone())
}
- pub fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> {
+ pub(crate) fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> {
self.text = self.render(&krate.name, &krate.description);
self.save(fs)
}
- pub fn save(&self, fs: &FS) -> Result<(), DynError> {
+ pub(crate) fn save(&self, fs: &FS) -> Result<(), DynError> {
Ok(fs.write(&self.path, &self.text)?)
}
- pub fn render, D: AsRef>(&self, name: N, description: D) -> String {
+ pub(crate) fn render, D: AsRef>(&self, name: N, description: D) -> String {
let name = name.as_ref();
let description = description.as_ref();
let lines = vec![
@@ -69,7 +69,7 @@ impl Readme {
lines.join("\n")
}
- pub fn update_crates_list(
+ pub(crate) fn update_crates_list(
&mut self,
fs: &FS,
mut krates: BTreeMap,
@@ -78,7 +78,7 @@ impl Readme {
let marker_start = "";
let marker_end = "";
let mut entries = String::from(marker_start);
- let ptn = format!(r"{}[\s\S]*?{}", marker_start, marker_end);
+ let ptn = format!(r"{marker_start}[\s\S]*?{marker_end}");
let re = RegexBuilder::new(ptn.as_str())
.case_insensitive(true)
.multi_line(true)
@@ -88,7 +88,7 @@ impl Readme {
krate.toml.load()?;
let name = krate.toml.get_name()?;
let description = krate.toml.get_description()?;
- let entry = format!("\n* [{}](crates/{})\n\t* {}", name, name, description);
+ let entry = format!("\n* [{name}](crates/{name})\n\t* {description}");
entries.push_str(&entry);
}
diff --git a/xtask/src/semver.rs b/xtask/src/semver.rs
index 96fea8a..8f69dbe 100644
--- a/xtask/src/semver.rs
+++ b/xtask/src/semver.rs
@@ -2,7 +2,7 @@ use semver::{BuildMetadata, Prerelease, Version};
use std::fmt::{Display, Formatter};
#[derive(Clone, Debug, PartialEq)]
-pub enum VersionChoice {
+pub(crate) enum VersionChoice {
Major(Version),
Minor(Version),
Patch(Version),
@@ -11,17 +11,17 @@ pub enum VersionChoice {
impl Display for VersionChoice {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
let msg = match self {
- VersionChoice::Major(v) => format!("Major: {}", v),
- VersionChoice::Minor(v) => format!("Minor: {}", v),
- VersionChoice::Patch(v) => format!("Patch: {}", v),
+ VersionChoice::Major(v) => format!("Major: {v}"),
+ VersionChoice::Minor(v) => format!("Minor: {v}"),
+ VersionChoice::Patch(v) => format!("Patch: {v}"),
};
- write!(f, "{}", msg)
+ write!(f, "{msg}")
}
}
impl VersionChoice {
- pub fn options(version: &Version) -> Vec {
+ pub(crate) fn options(version: &Version) -> Vec {
vec![
VersionChoice::Major(increment_major(version)),
VersionChoice::Minor(increment_minor(version)),
@@ -29,7 +29,7 @@ impl VersionChoice {
]
}
- pub fn get_version(&self) -> Version {
+ pub(crate) fn get_version(&self) -> Version {
match self {
VersionChoice::Major(v) => v.clone(),
VersionChoice::Minor(v) => v.clone(),
@@ -38,7 +38,7 @@ impl VersionChoice {
}
}
-pub fn increment_major(version: &Version) -> Version {
+pub(crate) fn increment_major(version: &Version) -> Version {
let mut v = version.clone();
v.major += 1;
v.minor = 0;
@@ -48,7 +48,7 @@ pub fn increment_major(version: &Version) -> Version {
v
}
-pub fn increment_minor(version: &Version) -> Version {
+pub(crate) fn increment_minor(version: &Version) -> Version {
let mut v = version.clone();
v.minor += 1;
v.patch = 0;
@@ -57,7 +57,7 @@ pub fn increment_minor(version: &Version) -> Version {
v
}
-pub fn increment_patch(version: &Version) -> Version {
+pub(crate) fn increment_patch(version: &Version) -> Version {
let mut v = version.clone();
v.patch += 1;
v.pre = Prerelease::EMPTY;
@@ -88,7 +88,7 @@ mod tests {
#[test]
fn it_displays_version_choice_text() {
let choice = VersionChoice::Major(Version::new(1, 0, 0));
- assert_eq!(format!("{}", choice), "Major: 1.0.0");
+ assert_eq!(format!("{choice}"), "Major: 1.0.0");
}
#[test]
diff --git a/xtask/src/tasks.rs b/xtask/src/tasks.rs
index a12b885..16cabd4 100644
--- a/xtask/src/tasks.rs
+++ b/xtask/src/tasks.rs
@@ -16,8 +16,8 @@ type TaskRunner = fn(
tasks: &Tasks,
) -> Result<(), DynError>;
-#[derive(Clone, Debug, PartialEq)]
-pub struct Task {
+#[derive(Clone, Debug)]
+pub(crate) struct Task {
pub name: String,
pub description: String,
pub flags: BTreeMap,
@@ -26,7 +26,7 @@ pub struct Task {
impl Task {
#[allow(dead_code)]
- pub fn new, D: AsRef>(
+ pub(crate) fn new, D: AsRef>(
name: N,
description: D,
flags: BTreeMap,
@@ -40,7 +40,7 @@ impl Task {
}
}
- pub fn exec(&self, args: Vec, tasks: &Tasks) -> Result<(), DynError> {
+ pub(crate) fn exec(&self, args: Vec, tasks: &Tasks) -> Result<(), DynError> {
let opts = Options::new(args, self.flags.clone())?;
let cargo = Cargo::new(&opts);
let git = Git::new(&opts);
@@ -51,29 +51,29 @@ impl Task {
}
}
-#[derive(Clone, Debug, PartialEq)]
-pub struct Tasks {
+#[derive(Clone, Debug)]
+pub(crate) struct Tasks {
map: BTreeMap,
}
impl Tasks {
- pub fn new() -> Self {
+ pub(crate) fn new() -> Self {
Tasks {
map: BTreeMap::new(),
}
}
- pub fn add(&mut self, tasks: Vec) {
+ pub(crate) fn add(&mut self, tasks: Vec) {
for task in tasks.iter() {
self.map.insert(task.name.clone(), task.clone());
}
}
- pub fn get>(&self, name: T) -> Option<&Task> {
+ pub(crate) fn get>(&self, name: T) -> Option<&Task> {
self.map.get(name.as_ref())
}
- pub fn help(&self) -> Result {
+ pub(crate) fn help(&self) -> Result {
let separator = ".".to_string();
let mut lines = String::new();
let mut max_col_width = 0;
@@ -90,18 +90,16 @@ impl Tasks {
for task in self.map.values() {
let char_count = task.name.char_indices().count();
let spaces = separator.repeat(max_col_width - char_count + padding);
- let line = format!(">> {}{}{}\n", task.name, spaces, task.description);
+ let line = format!(" {}{}{}\n", task.name, spaces, task.description);
lines.push_str(&line);
for (name, description) in task.flags.iter() {
let separator = " ".to_string();
let spaces = separator.repeat(max_col_width + padding);
- let line = format!("\n{} >> --{} | {}\n", spaces, name, description);
+ let line = format!("{spaces} ⮑ --{name} | {description}\n");
lines.push_str(&line);
}
-
- lines.push('\n');
}
Ok(lines)
@@ -192,16 +190,11 @@ mod tests {
assert_eq!(
tasks.help().unwrap(),
[
- ">> one....task 01",
- "",
- " >> --bar | enables bar",
- "",
- " >> --foo | does the foo",
- "",
- ">> two....task 02",
- "",
- " >> --baz | invokes a baz",
- "",
+ " one....task 01",
+ " ⮑ --bar | enables bar",
+ " ⮑ --foo | does the foo",
+ " two....task 02",
+ " ⮑ --baz | invokes a baz",
"",
]
.join("\n")
diff --git a/xtask/src/toml.rs b/xtask/src/toml.rs
index 4d10e59..991453b 100644
--- a/xtask/src/toml.rs
+++ b/xtask/src/toml.rs
@@ -4,53 +4,53 @@ use semver::Version;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
-use toml_edit::{value as toml_value, Document};
+use toml_edit::{DocumentMut, value as toml_value};
type DynError = Box;
const CARGO_TOML: &str = "Cargo.toml";
#[derive(Clone, Debug, Default)]
-pub struct Toml {
+pub(crate) struct Toml {
pub path: PathBuf,
- data: Document,
+ data: DocumentMut,
}
impl Toml {
- pub fn new(crate_root: PathBuf) -> Self {
+ pub(crate) fn new(crate_root: PathBuf) -> Self {
Toml {
path: crate_root.join(CARGO_TOML),
..Default::default()
}
}
- pub fn from_path(crate_root: PathBuf) -> Result {
+ pub(crate) fn from_path(crate_root: PathBuf) -> Result {
let mut toml = Toml::new(crate_root);
toml.load()
}
- pub fn read(&self) -> Result {
+ pub(crate) fn read(&self) -> Result {
// TODO (busticated): pull into FS wrapper?
let text = fs::read_to_string(&self.path)?;
- Ok(text.parse::()?)
+ Ok(text.parse::()?)
}
- pub fn load(&mut self) -> Result {
+ pub(crate) fn load(&mut self) -> Result {
self.data = self.read()?;
Ok(self.clone())
}
- pub fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> {
+ pub(crate) fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> {
let text = self.render(&krate.name, &krate.description);
- self.data = text.parse::()?;
+ self.data = text.parse::()?;
self.save(fs)
}
- pub fn save(&self, fs: &FS) -> Result<(), DynError> {
+ pub(crate) fn save(&self, fs: &FS) -> Result<(), DynError> {
Ok(fs.write(&self.path, self.data.to_string())?)
}
- pub fn render, D: AsRef>(&self, name: N, description: D) -> String {
+ pub(crate) fn render, D: AsRef>(&self, name: N, description: D) -> String {
let name = name.as_ref();
let description = description.as_ref();
let lines = vec![
@@ -59,16 +59,20 @@ impl Toml {
format!("description = \"{}\"", description),
"version = \"0.1.0\"".to_string(),
"edition.workspace = true".to_string(),
+ "rust-version.workspace = true".to_string(),
"license.workspace = true".to_string(),
"authors.workspace = true".to_string(),
"repository.workspace = true".to_string(),
"".to_string(),
"[dependencies]".to_string(),
+ "".to_string(),
+ "[lints]".to_string(),
+ "workspace = true".to_string(),
];
lines.join("\n")
}
- pub fn get_version(&self) -> Result {
+ pub(crate) fn get_version(&self) -> Result {
let pkg = self
.data
.get("package")
@@ -82,12 +86,12 @@ impl Toml {
Ok(Version::parse(version)?)
}
- pub fn set_version(&mut self, version: &Version) -> Result<(), DynError> {
+ pub(crate) fn set_version(&mut self, version: &Version) -> Result<(), DynError> {
self.data["package"]["version"] = toml_value(version.to_string());
Ok(())
}
- pub fn get_name(&self) -> Result {
+ pub(crate) fn get_name(&self) -> Result {
let pkg = self
.data
.get("package")
@@ -101,7 +105,7 @@ impl Toml {
Ok(name.to_string())
}
- pub fn get_description(&self) -> Result {
+ pub(crate) fn get_description(&self) -> Result {
let pkg = self
.data
.get("package")
@@ -165,11 +169,15 @@ mod tests {
"description = \"my-crate description\"",
"version = \"0.1.0\"",
"edition.workspace = true",
+ "rust-version.workspace = true",
"license.workspace = true",
"authors.workspace = true",
"repository.workspace = true",
"",
"[dependencies]",
+ "",
+ "[lints]",
+ "workspace = true",
]
.join("\n")
);
diff --git a/xtask/src/workspace.rs b/xtask/src/workspace.rs
index 81d7b45..b02ed16 100644
--- a/xtask/src/workspace.rs
+++ b/xtask/src/workspace.rs
@@ -12,9 +12,12 @@ type DynError = Box;
const CRATES_DIRNAME: &str = "crates";
#[derive(Clone, Debug, Default)]
-pub struct Workspace {
+pub(crate) struct Workspace {
pub path: PathBuf,
pub readme: Readme,
+ // NOTE: unread for now, but loading it validates the workspace manifest
+ // parses on every task run - and mirrors `Krate`'s shape
+ #[allow(dead_code)]
pub toml: Toml,
}
@@ -26,25 +29,25 @@ impl KratePaths for Workspace {
impl Workspace {
#[allow(dead_code)]
- pub fn new>(path: P) -> Self {
+ pub(crate) fn new>(path: P) -> Self {
let path = path.as_ref().to_owned();
let readme = Readme::new(path.clone());
let toml = Toml::new(path.clone());
Workspace { path, readme, toml }
}
- pub fn from_path>(path: P) -> Result {
+ pub(crate) fn from_path>(path: P) -> Result {
let path = path.as_ref().to_owned();
let readme = Readme::from_path(path.clone())?;
let toml = Toml::from_path(path.clone())?;
Ok(Workspace { path, readme, toml })
}
- pub fn krates_path(&self) -> PathBuf {
+ pub(crate) fn krates_path(&self) -> PathBuf {
self.path().join(CRATES_DIRNAME)
}
- pub fn krates(&self, fs: &FS) -> Result, DynError> {
+ pub(crate) fn krates(&self, fs: &FS) -> Result, DynError> {
let mut krates = BTreeMap::new();
for entry in fs.read_dir(self.krates_path())? {
@@ -59,7 +62,12 @@ impl Workspace {
Ok(krates)
}
- pub fn add_krate(&self, fs: &FS, cargo: &Cargo, mut krate: Krate) -> Result {
+ pub(crate) fn add_krate(
+ &self,
+ fs: &FS,
+ cargo: &Cargo,
+ mut krate: Krate,
+ ) -> Result {
let kind = krate.kind.to_string();
let args = ["--name", &krate.name, &kind];
let krate_copy = krate.clone(); // TODO (mirande): deal w/ "cannot borrow as mutable because it is also borrowed as immutable" errors
@@ -70,7 +78,7 @@ impl Workspace {
Ok(krate)
}
- pub fn clean(&self, fs: &FS, cargo: &Cargo) -> Result<(), DynError> {
+ pub(crate) fn clean(&self, fs: &FS, cargo: &Cargo) -> Result<(), DynError> {
use std::io::ErrorKind;
match fs.remove_dir_all(self.tmp_path()) {
@@ -86,10 +94,14 @@ impl Workspace {
}
cargo.clean(["--release"]).run()?;
+ // NOTE: `cargo-llvm-cov` keeps its instrumented build and `.profraw`
+ // data under `target/llvm-cov-target`, which neither the `tmp/` wipe
+ // above nor `cargo clean --release` touches - it is hundreds of MB
+ cargo.coverage_clean().run()?;
Ok(())
}
- pub fn create_dirs(&self, fs: &FS) -> Result<(), DynError> {
+ pub(crate) fn create_dirs(&self, fs: &FS) -> Result<(), DynError> {
fs.create_dir_all(self.coverage_path())?;
let krates = self.krates(fs)?;