From 55445b0dab47f8ae1d3a8d24a43a93ffdb5f2674 Mon Sep 17 00:00:00 2001 From: Kaspar Lyngsie Date: Mon, 3 Aug 2026 09:26:14 +0200 Subject: [PATCH 1/2] fix: adding guarded minimum rust version checks --- .github/workflows/ci.yml | 35 +- Cargo.toml | 9 + Makefile | 31 +- README.md | 2 + crates/oapi-codegen/Cargo.toml | 23 + crates/oapi-codegen/src/console.rs | 8 + crates/oapi-codegen/src/deps.rs | 75 + ...rated_code_table_without_rust_version.toml | 13 + .../fixtures/manifests/two_rust_versions.toml | 13 + crates/oapi-codegen/tests/generated.rs | 18 +- .../oapi-codegen/tests/msrv-check/Cargo.lock | 1310 +++++++++++++++++ .../oapi-codegen/tests/msrv-check/Cargo.toml | 44 + crates/oapi-codegen/tests/msrv-check/build.rs | 56 + .../oapi-codegen/tests/msrv-check/src/lib.rs | 29 + crates/oapi-codegen/tests/msrv_manifest.rs | 227 +++ crates/oapi-codegen/tests/support/apimodel.rs | 26 + docs/configuration.md | 4 + docs/msrv.md | 123 ++ examples/bookstore/README.md | 4 + 19 files changed, 2032 insertions(+), 18 deletions(-) create mode 100644 crates/oapi-codegen/tests/fixtures/manifests/generated_code_table_without_rust_version.toml create mode 100644 crates/oapi-codegen/tests/fixtures/manifests/two_rust_versions.toml create mode 100644 crates/oapi-codegen/tests/msrv-check/Cargo.lock create mode 100644 crates/oapi-codegen/tests/msrv-check/Cargo.toml create mode 100644 crates/oapi-codegen/tests/msrv-check/build.rs create mode 100644 crates/oapi-codegen/tests/msrv-check/src/lib.rs create mode 100644 crates/oapi-codegen/tests/msrv_manifest.rs create mode 100644 crates/oapi-codegen/tests/support/apimodel.rs create mode 100644 docs/msrv.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7b822d..f2ee3ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -183,6 +183,38 @@ jobs: - name: Check generated code is up to date run: make verify-generated + # The generator reports a Rust version to every consumer after a write, as the + # version needed to compile the emitted code. This job compiles every committed + # generated file on exactly that toolchain, so the reported number is measured + # rather than claimed. + # + # The floor is set by the crates the output depends on, and not by the syntax the + # emitters produce, so it moves on a dependency bump and not on a generator + # change. That makes it exactly the kind of claim that rots without a check. See + # docs/msrv.md. + # + # This job does not use ./.github/actions/setup-rust: that action installs the + # pinned build toolchain, which is much newer than this floor and would defeat + # the measurement. `make verify-msrv` installs the right one from the manifest. + generated-code-msrv: + name: generated-code-msrv + runs-on: ubuntu-latest + steps: + - *checkout-code + + # Keyed separately from rust-checks because this job builds a different crate + # on a different toolchain. Sharing the key would thrash both caches. + - name: Cache Cargo dependencies + uses: Swatinem/rust-cache@v2 + with: + shared-key: generated-code-msrv + cache-targets: "true" + cache-all-crates: "true" + workspaces: crates/oapi-codegen/tests/msrv-check + + - name: Compile generated code on the consumer Rust floor + run: make verify-msrv + lint-prettier: runs-on: ubuntu-latest needs: install-prettier @@ -233,12 +265,13 @@ jobs: name: CI Passed runs-on: ubuntu-latest if: always() - needs: [rust-checks, lint-prettier, cargo-deny] + needs: [rust-checks, generated-code-msrv, lint-prettier, cargo-deny] steps: - name: Verify all jobs passed or were skipped run: | results=( "${{ needs.rust-checks.result }}" + "${{ needs.generated-code-msrv.result }}" "${{ needs.lint-prettier.result }}" "${{ needs.cargo-deny.result }}" ) diff --git a/Cargo.toml b/Cargo.toml index a7feaac..118adbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,17 @@ members = ["crates/*", "examples/*"] version = "0.1.0" edition = "2024" repository = "https://github.com/alchemaxinc/oapi-codegen-rust" +# The project has no site of its own, so this is the repository. Cargo shows the +# two as separate links on a crates.io page, and an absent `homepage` reads as +# missing rather than as "the same as the repository". +homepage = "https://github.com/alchemaxinc/oapi-codegen-rust" license = "MIT" +# The Rust version needed to build the tools in this workspace. It is not the +# version needed to compile the code the generator emits, which lands in a +# consumer's crate and carries its own floor. See `docs/msrv.md`. +rust-version = "1.97" + # Trap silent integer wraparound on spec-derived arithmetic in release builds. [profile.release] overflow-checks = true diff --git a/Makefile b/Makefile index cd14f32..817d288 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,10 @@ update-generated: ## Refresh generated files from the coverage fixtures update-docs: ## Refresh docs/cli.md from the clap CLI definition UPDATE_DOCS=1 cargo test -p oapi-codegen --test cli_docs +.PHONY: update-msrv-manifest +update-msrv-manifest: ## Refresh the msrv-check manifest from the dependency report + UPDATE_MSRV_MANIFEST=1 cargo test -p oapi-codegen --test msrv_manifest + .PHONY: generate-example generate-example: ## Regenerate the bookstore example from its OpenAPI specification cd examples/bookstore && \ @@ -82,15 +86,34 @@ verify-generated: ## Regenerate all generated files and fail when they differ fr $(MAKE) verify-example $(MAKE) update-generated $(MAKE) update-docs - @if [ -n "$$(git status --porcelain -- examples/bookstore/generated crates/oapi-codegen/tests/generated docs/cli.md)" ]; then \ + $(MAKE) update-msrv-manifest + @if [ -n "$$(git status --porcelain -- examples/bookstore/generated crates/oapi-codegen/tests/generated docs/cli.md crates/oapi-codegen/tests/msrv-check/Cargo.toml)" ]; then \ echo "ERROR: generated files are out of date."; \ - echo "Run 'make generate-example', 'make update-generated' and 'make update-docs'. Commit the result."; \ - git status --porcelain -- examples/bookstore/generated crates/oapi-codegen/tests/generated docs/cli.md; \ - git --no-pager diff -- examples/bookstore/generated crates/oapi-codegen/tests/generated docs/cli.md; \ + echo "Run 'make generate-example', 'make update-generated', 'make update-docs' and 'make update-msrv-manifest'. Commit the result."; \ + git status --porcelain -- examples/bookstore/generated crates/oapi-codegen/tests/generated docs/cli.md crates/oapi-codegen/tests/msrv-check/Cargo.toml; \ + git --no-pager diff -- examples/bookstore/generated crates/oapi-codegen/tests/generated docs/cli.md crates/oapi-codegen/tests/msrv-check/Cargo.toml; \ exit 1; \ fi @echo "Generated files are up to date." +# The Rust version a consumer needs to compile the emitted code. It is declared +# once, in the crate manifest, and read here rather than repeated. The generator +# reports the same number to every consumer after a write, so it has to be true. +GENERATED_MSRV := $(shell sed -nE '/^\[package\.metadata\.generated-code\]/,/^\[/{s/^[[:space:]]*rust-version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p;}' crates/oapi-codegen/Cargo.toml) + +.PHONY: verify-msrv +verify-msrv: ## Compile every generated file on the Rust version a consumer needs + @test -n "$(GENERATED_MSRV)" || { \ + echo "ERROR: no rust-version under [package.metadata.generated-code] in crates/oapi-codegen/Cargo.toml."; \ + exit 1; \ + } + rustup toolchain install $(GENERATED_MSRV) --profile minimal + @echo "Compiling every generated file on Rust $(GENERATED_MSRV)." + # `msrv-check` is not a workspace member, so it needs its own manifest path. + # Compiling is the whole assertion: the crate holds no test of its own. + cargo +$(GENERATED_MSRV) build --manifest-path crates/oapi-codegen/tests/msrv-check/Cargo.toml + @echo "Generated code compiles on Rust $(GENERATED_MSRV)." + .PHONY: docs docs: ## Generate and open Rust documentation cargo doc --no-deps --open diff --git a/README.md b/README.md index 8f2a5f4..004b99a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ $ oapi-codegen --config-file examples/bookstore/oapi-codegen-server.yaml example cargo add http@1.4.2 cargo add axum@0.8.9 --features multipart cargo add axum-extra@0.12.6 --features query + note: the generated code needs Rust 1.88 or newer. ``` @@ -86,6 +87,7 @@ against [`.github/openapi-versions.json`](.github/openapi-versions.json). > They have been checked and verified by humans, and can be read if interested. - [Installation](docs/installation.md) +- [Rust versions](docs/msrv.md) - [Build workflow](docs/workflow.md) - [Configuration](docs/configuration.md) - [OpenAPI extensions](docs/extensions.md) diff --git a/crates/oapi-codegen/Cargo.toml b/crates/oapi-codegen/Cargo.toml index bef4e23..04c4c21 100644 --- a/crates/oapi-codegen/Cargo.toml +++ b/crates/oapi-codegen/Cargo.toml @@ -4,12 +4,35 @@ description = "Generate client and server boilerplate from OpenAPI 3 specificati version.workspace = true edition.workspace = true repository.workspace = true +homepage.workspace = true license.workspace = true +rust-version.workspace = true +readme = "../../README.md" +# `keywords` is capped at five entries and `categories` at five, and both must +# match the slugs crates.io knows. These are the terms a reader searches for the +# tool by, and not the terms the tool uses inside itself. +keywords = ["openapi", "codegen", "axum", "swagger", "rest"] +categories = ["development-tools", "web-programming", "command-line-utilities"] [[bin]] name = "oapi-codegen" path = "src/main.rs" +# The Rust version a consumer needs to compile the code this generator *emits*. +# It is not `rust-version` above, which is the floor for building the generator +# itself. The two move independently, and a consumer only ever needs this one. +# +# The value lives here, and not as a constant in the source, because three places +# read it: `deps.rs` embeds this manifest already and parses it out, the CI job +# installs this toolchain and compiles the golden files with it, and `docs/msrv.md` +# quotes it. A number with three homes goes stale in two of them. +# +# The floor comes from the dependencies the emitted code needs, and not from the +# syntax it emits. The newest construct the emitters produce is `-> impl Future` +# in a trait, which is Rust 1.75. See `docs/msrv.md` for the measurement. +[package.metadata.generated-code] +rust-version = "1.88" + [lints] workspace = true diff --git a/crates/oapi-codegen/src/console.rs b/crates/oapi-codegen/src/console.rs index 1a07dc8..cb337b4 100644 --- a/crates/oapi-codegen/src/console.rs +++ b/crates/oapi-codegen/src/console.rs @@ -176,6 +176,14 @@ pub fn report_dependencies(deps: &[Dependency]) { for dep in deps { eprintln!(" {}", dep.cargo_add().dimmed()); } + // The Rust floor belongs with the dependency list, because the dependencies + // are what set it. A consumer who reads one and not the other adds the crates + // and then meets a `rustc` error from inside a crate they did not name. + eprintln!( + " {} the generated code needs Rust {} or newer.", + "note:".cyan().bold(), + oapi_codegen::deps::generated_code_rust_version() + ); } /// Ask whether to run the `cargo add` commands now. Returns `false` on EOF or a diff --git a/crates/oapi-codegen/src/deps.rs b/crates/oapi-codegen/src/deps.rs index c33a672..55b99b2 100644 --- a/crates/oapi-codegen/src/deps.rs +++ b/crates/oapi-codegen/src/deps.rs @@ -79,6 +79,56 @@ fn parse_manifest_version<'a>(manifest: &'a str, crate_name: &str) -> Option<&'a return None; } +/// The `Cargo.toml` table that records the Rust version a consumer needs to +/// compile the emitted code. +const GENERATED_CODE_TABLE: &str = "[package.metadata.generated-code]"; + +/// The Rust version a consumer needs to compile the code this generator emits. +/// +/// This is not the version needed to build the generator, which is the +/// `rust-version` of the `[package]` table. A consumer runs a released binary and +/// needs no Rust to do it, so the generator's own floor never reaches them. The +/// floor that does reach them is this one, and it applies to the crate the +/// generated file lands in. +/// +/// The value is read from the manifest rather than written here, so the CI job +/// that compiles the golden files on this toolchain and the documentation that +/// quotes it both read one number. See `docs/msrv.md`. +pub fn generated_code_rust_version() -> &'static str { + match parse_generated_code_rust_version(MANIFEST) { + Some(version) => return version, + None => panic!( + "`{GENERATED_CODE_TABLE}` declares no `rust-version` in oapi-codegen's manifest; the generated-code Rust floor is unknown" + ), + } +} + +/// Read `rust-version` from the [`GENERATED_CODE_TABLE`] table of `manifest`. +/// +/// The scan starts at that table header, so it cannot pick up the `[package]` +/// table's own `rust-version`. Those two are different numbers, and returning the +/// wrong one would report a floor that no measurement backs. +fn parse_generated_code_rust_version(manifest: &str) -> Option<&str> { + let table = manifest.find(GENERATED_CODE_TABLE)?; + let rest = manifest.get(table + GENERATED_CODE_TABLE.len()..)?; + for line in rest.lines() { + let line = line.trim(); + // A later table header ends this one. Stopping here keeps the scan from + // reading a key that belongs to a different table. + if line.starts_with('[') { + return None; + } + let Some(value) = line.strip_prefix("rust-version") else { + continue; + }; + let value = value.trim_start().strip_prefix('=')?.trim_start(); + let after = value.strip_prefix('"')?; + let close = after.find('"')?; + return after.get(..close); + } + return None; +} + /// A crate the generated code references, with the version requirement and Cargo /// features a consumer must declare in `Cargo.toml`. #[derive(Debug, Clone, PartialEq, Eq)] @@ -420,6 +470,31 @@ mod tests { assert_eq!(extra.features, vec!["cookie"]); } + #[test] + fn generated_code_rust_version_is_read_from_the_manifest() { + let version = generated_code_rust_version(); + assert!( + version.split('.').count() >= 2 && version.split('.').all(|part| return part.parse::().is_ok()), + "the generated-code Rust version must be a dotted number, and reads as `{version}`", + ); + } + + #[test] + fn generated_code_rust_version_is_not_the_package_rust_version() { + // The fixture declares both floors. The parse must return the + // generated-code one and not fall back to the `[package]` one. + let manifest = include_str!("../tests/fixtures/manifests/two_rust_versions.toml"); + assert_eq!(parse_generated_code_rust_version(manifest), Some("1.88")); + } + + #[test] + fn generated_code_rust_version_stops_at_the_next_table() { + // The fixture's generated-code table declares no `rust-version`, and the + // table after it does. Reading across the header reports that value. + let manifest = include_str!("../tests/fixtures/manifests/generated_code_table_without_rust_version.toml"); + assert_eq!(parse_generated_code_rust_version(manifest), None); + } + #[test] fn no_dependencies_for_dependency_free_output() { assert!(required_dependencies("pub const SERVER_URL: &str = \"https://x\";").is_empty()); diff --git a/crates/oapi-codegen/tests/fixtures/manifests/generated_code_table_without_rust_version.toml b/crates/oapi-codegen/tests/fixtures/manifests/generated_code_table_without_rust_version.toml new file mode 100644 index 0000000..2ca25da --- /dev/null +++ b/crates/oapi-codegen/tests/fixtures/manifests/generated_code_table_without_rust_version.toml @@ -0,0 +1,13 @@ +[package] +name = "consumer" +version = "0.1.0" +edition = "2024" + +# The table exists and declares no `rust-version`. The next table declares one, so +# a scan that reads past the header reports that neighbor's value as the +# generated-code floor. +[package.metadata.generated-code] +note = "nothing here" + +[dependencies] +rust-version = "1.60" diff --git a/crates/oapi-codegen/tests/fixtures/manifests/two_rust_versions.toml b/crates/oapi-codegen/tests/fixtures/manifests/two_rust_versions.toml new file mode 100644 index 0000000..c2e60b5 --- /dev/null +++ b/crates/oapi-codegen/tests/fixtures/manifests/two_rust_versions.toml @@ -0,0 +1,13 @@ +[package] +name = "consumer" +version = "0.1.0" +edition = "2024" +# The floor for building the generator itself. The generated-code parse must not +# return this one: the two answer different questions, and this value is the +# larger of the two, so a fallback to it would report a floor no measurement +# backs. +rust-version = "1.97" + +[package.metadata.generated-code] +# The floor for compiling the emitted code in a consumer's crate. +rust-version = "1.88" diff --git a/crates/oapi-codegen/tests/generated.rs b/crates/oapi-codegen/tests/generated.rs index d41f247..283fc1e 100644 --- a/crates/oapi-codegen/tests/generated.rs +++ b/crates/oapi-codegen/tests/generated.rs @@ -173,24 +173,16 @@ mod generated { } /// Stand-in for the models crate the `server_refs` fixture's `import-mapping` -/// points its cross-file `$ref` bodies at (`crate::apimodel`). Real projects -/// generate this module from the referenced schema file; here a minimal struct -/// proves the emitted `crate::apimodel::CreateWidget` path resolves and that the -/// generated handler can decode it as a JSON body. +/// points its cross-file `$ref` bodies at (`crate::apimodel`). +/// +/// The body lives in `tests/support/apimodel.rs`, because the `msrv-check` crate +/// needs the same module at its own crate root. See that file for the rest. #[allow( dead_code, reason = "test-only stand-in for the models crate the server_refs fixture's import-mapping targets; only CreateWidget is constructed here" )] mod apimodel { - #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] - pub struct CreateWidget { - pub name: String, - } - - #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] - pub struct NewThing { - pub name: String, - } + include!("support/apimodel.rs"); } #[test] diff --git a/crates/oapi-codegen/tests/msrv-check/Cargo.lock b/crates/oapi-codegen/tests/msrv-check/Cargo.lock new file mode 100644 index 0000000..2ebad1f --- /dev/null +++ b/crates/oapi-codegen/tests/msrv-check/Cargo.lock @@ -0,0 +1,1310 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "form_urlencoded", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde_core", + "serde_html_form", + "serde_path_to_error", + "tower-layer", + "tower-service", + "tracing", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[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 = "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 = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[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 = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[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", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[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", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[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 = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +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 = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[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 = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "msrv-check" +version = "0.0.0" +dependencies = [ + "axum", + "axum-extra", + "chrono", + "http", + "percent-encoding", + "reqwest", + "serde", + "serde_json", + "serde_urlencoded", + "uuid", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[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 = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[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 = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[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 = "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_html_form" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde_core", +] + +[[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_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[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 = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[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", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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 = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[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 = "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", + "tracing", +] + +[[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 = [ + "log", + "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 = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[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 = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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 = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[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 = "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 = "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/crates/oapi-codegen/tests/msrv-check/Cargo.toml b/crates/oapi-codegen/tests/msrv-check/Cargo.toml new file mode 100644 index 0000000..558a8cf --- /dev/null +++ b/crates/oapi-codegen/tests/msrv-check/Cargo.toml @@ -0,0 +1,44 @@ +# Generated by `make update-msrv-manifest`. DO NOT EDIT. +# +# A crate that compiles every committed golden file on the Rust version a +# consumer needs, and does nothing else. +# +# It is not a workspace member, and it must not become one. The workspace builds +# on the pinned toolchain in `rust-toolchain.toml`, which is far newer than the +# floor this crate measures. A member would be compiled by every `cargo` command +# in the repository on that newer toolchain, which is the opposite of the point. +# The `[workspace]` table below detaches it, and the detachment works at this +# depth as well as at the repository root. See `docs/msrv.md`. +[package] +name = "msrv-check" +version = "0.0.0" +publish = false + +# Edition 2021 and not 2024, because edition 2024 needs Rust 1.85. The declared +# floor is above that today, so this is headroom and not a current requirement. +# The emitted code compiles under both editions, so the choice does not change +# what is measured. +edition = "2021" + +[workspace] + +[lib] +path = "src/lib.rs" + +# The union of every crate the golden files reference, taken from +# `required_dependencies` in `crates/oapi-codegen/src/deps.rs`. That function also +# produces the report a consumer follows, so this crate resolves the graph a +# consumer resolves. +# +# Regenerate with `make update-msrv-manifest`. +[dependencies] +axum = { version = "0.8.9", features = ["multipart"] } +axum-extra = { version = "0.12.6", features = ["cookie", "query"] } +chrono = { version = "0.4.45", features = ["serde"] } +http = "1.4.2" +percent-encoding = "2.3.2" +reqwest = { version = "0.13.4", default-features = false, features = ["blocking", "form", "json", "multipart", "query"] } +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" +serde_urlencoded = "0.7.1" +uuid = { version = "1.24.0", features = ["serde"] } diff --git a/crates/oapi-codegen/tests/msrv-check/build.rs b/crates/oapi-codegen/tests/msrv-check/build.rs new file mode 100644 index 0000000..2040054 --- /dev/null +++ b/crates/oapi-codegen/tests/msrv-check/build.rs @@ -0,0 +1,56 @@ +//! Write a module list covering every committed golden file. +//! +//! The list is derived from the directory rather than written by hand, because a +//! hand-written list is a second place to add a golden file, and a file left out +//! of it is not compiled and reports nothing. `tests/generated.rs` in the +//! generator crate keeps such a list, and a coverage test guards it. There is no +//! equivalent guard here, so this crate reads the directory instead. + +use std::path::Path; + +fn main() { + // This crate sits at `crates/oapi-codegen/tests/msrv-check`, so the golden + // directory is a sibling one level up. + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("the manifest directory has a parent") + .join("generated"); + + // Re-run when a golden file is added or removed. Without this the module list + // is cached from the first build and a new file is never compiled. + println!("cargo:rerun-if-changed={}", root.display()); + + let mut stems: Vec = std::fs::read_dir(&root) + .unwrap_or_else(|error| panic!("cannot read `{}`: {error}", root.display())) + .map(|entry| { + return entry.expect("a readable directory entry").path(); + }) + .filter(|path| { + return path.extension().is_some_and(|extension| return extension == "rs"); + }) + .map(|path| { + return path + .file_stem() + .expect("a `.rs` file has a stem") + .to_string_lossy() + .into_owned(); + }) + .collect(); + // Sorted so the generated file is byte-identical across platforms, which keeps + // the build cache valid between a local run and a CI run. + stems.sort(); + + assert!(!stems.is_empty(), "no golden files found in `{}`", root.display()); + + let mut source = String::new(); + for stem in &stems { + let path = root.join(format!("{stem}.rs")); + source.push_str(&format!( + "pub mod {stem} {{\n include!(r\"{}\");\n}}\n", + path.display() + )); + } + + let out = Path::new(&std::env::var("OUT_DIR").expect("OUT_DIR is set by cargo")).join("goldens.rs"); + std::fs::write(&out, source).unwrap_or_else(|error| panic!("cannot write `{}`: {error}", out.display())); +} diff --git a/crates/oapi-codegen/tests/msrv-check/src/lib.rs b/crates/oapi-codegen/tests/msrv-check/src/lib.rs new file mode 100644 index 0000000..e2b1779 --- /dev/null +++ b/crates/oapi-codegen/tests/msrv-check/src/lib.rs @@ -0,0 +1,29 @@ +//! Compile every committed golden file on the Rust version a consumer needs. +//! +//! The generated-code floor is declared once, under +//! `[package.metadata.generated-code]` in `crates/oapi-codegen/Cargo.toml`. That +//! number is reported to every consumer after a write, so it must be true rather +//! than aspirational. A dependency that raises its own floor raises this one +//! without touching a line of the generator, so the claim needs a check that runs +//! on the toolchain it names. +//! +//! There are no assertions here. Compiling is the assertion: if a golden file +//! needs a newer `rustc` than the declared floor, this crate does not build. +//! +//! Run it with `make verify-msrv`. See `docs/msrv.md`. + +// Generated code is ordinary idiomatic Rust, and the workspace lint set targets +// first-party source. This crate holds nothing but generated code, so the +// exceptions cover the whole of it. +#![allow(dead_code, reason = "a golden file declares types this crate never constructs")] +#![allow(clippy::all, reason = "generated code is not linted against the workspace rules")] + +/// Stand-in for the models crate two fixtures point their cross-file `$ref` +/// bodies at. The emitted path is `crate::apimodel`, so it must sit at this +/// crate's root. The body is shared with `tests/generated.rs`, which is now a +/// sibling two levels up. +mod apimodel { + include!("../../support/apimodel.rs"); +} + +include!(concat!(env!("OUT_DIR"), "/goldens.rs")); diff --git a/crates/oapi-codegen/tests/msrv_manifest.rs b/crates/oapi-codegen/tests/msrv_manifest.rs new file mode 100644 index 0000000..9fb17e0 --- /dev/null +++ b/crates/oapi-codegen/tests/msrv_manifest.rs @@ -0,0 +1,227 @@ +//! Keeps `tests/msrv-check/Cargo.toml` in lockstep with the dependency report. +//! +//! The `msrv-check` crate compiles every golden file on the Rust version a +//! consumer needs. To measure a floor that applies to a consumer, it must resolve +//! the same dependency graph a consumer resolves. A consumer builds that graph +//! from the report [`oapi_codegen::deps::required_dependencies`] produces, so that +//! function is the source of truth for this manifest as well. +//! +//! The manifest was written by hand before this test existed. A hand-written copy +//! holds three kinds of drift, and only one of them is loud: +//! +//! 1. A version that differs. Silent: the graph still resolves, and the measured +//! floor belongs to a dependency set no test exercises. +//! 2. A feature that is absent. Usually loud, because a golden file that uses the +//! feature fails to compile. Silent when the report recommends a feature that +//! no golden file exercises. +//! 3. A crate that is absent. Loud, for the same reason. +//! +//! Generating the file closes all three at once. The committed file stays readable +//! and reviewable, and this test fails when it goes stale, which is the pattern +//! `cli_docs.rs` uses for `docs/cli.md`. +//! +//! Set `UPDATE_MSRV_MANIFEST=1` (via `make update-msrv-manifest`) to write the +//! file instead of comparing. See `docs/msrv.md`. + +use std::collections::BTreeMap; +use std::path::Path; +use std::path::PathBuf; + +use oapi_codegen::deps::Dependency; +use oapi_codegen::deps::required_dependencies; + +/// Message at the top of the generated manifest so a reader does not edit it. +const GENERATED_NOTICE: &str = "Generated by `make update-msrv-manifest`. DO NOT EDIT."; + +/// The directory holding the committed golden files. +fn generated_dir() -> PathBuf { + return PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/generated"); +} + +/// Absolute path to the generated manifest. +fn manifest_path() -> PathBuf { + return PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/msrv-check/Cargo.toml"); +} + +/// The union of the crates every golden file references. +/// +/// `msrv-check` compiles every golden file as one crate, so it needs one +/// dependency set covering all of them. Two rules make the union safe to compile: +/// a feature any file needs is enabled, and default features stay on unless every +/// file that names the crate can do without them. Both rules err towards a larger +/// graph, because a graph that is too small fails to compile and a graph that is +/// too large only measures a floor that is too high. Neither error is silent. +fn union_of_golden_dependencies() -> Vec { + let dir = generated_dir(); + let mut entries: Vec = std::fs::read_dir(&dir) + .unwrap_or_else(|error| panic!("cannot read `{}`: {error}", dir.display())) + .map(|entry| { + return entry + .unwrap_or_else(|error| panic!("cannot read an entry of `{}`: {error}", dir.display())) + .path(); + }) + .filter(|path| { + return path.extension().is_some_and(|extension| return extension == "rs"); + }) + .collect(); + // Sorted so the rendered manifest is byte-identical between a local run and a + // CI run, which is what lets this test compare rather than merely parse. + entries.sort(); + + assert!( + !entries.is_empty(), + "no golden files found in `{}`; the union would be empty and the measured floor meaningless", + dir.display() + ); + + // Keyed by crate name and ordered by it, so the rendered file is stable. + let mut union: BTreeMap<&'static str, Dependency> = BTreeMap::new(); + for path in &entries { + let code = + std::fs::read_to_string(path).unwrap_or_else(|error| panic!("cannot read `{}`: {error}", path.display())); + for dep in required_dependencies(&code) { + let merged = union.entry(dep.name).or_insert_with(|| { + return Dependency { + name: dep.name, + version: dep.version, + // Start from "not needed" so the fold below can only turn this + // on. Starting from `true` would keep default features for a + // crate no file needs them for, which enlarges the graph for + // every consumer of the measurement. + default_features: false, + features: Vec::new(), + }; + }); + assert_eq!( + merged.version, dep.version, + "two golden files recommend different versions of `{}`; the report reads one manifest, so this cannot happen", + dep.name + ); + merged.default_features = merged.default_features || dep.default_features; + for feature in dep.features { + if !merged.features.contains(&feature) { + merged.features.push(feature); + } + } + } + } + + return union + .into_values() + .map(|mut dep| { + // Sorted so a change in golden-file read order cannot rewrite the file. + dep.features.sort_unstable(); + return dep; + }) + .collect(); +} + +/// Render the whole `msrv-check` manifest. +fn render_manifest(deps: &[Dependency]) -> String { + let mut out = String::new(); + out.push_str(&format!("# {GENERATED_NOTICE}\n")); + out.push_str( + "# +# A crate that compiles every committed golden file on the Rust version a +# consumer needs, and does nothing else. +# +# It is not a workspace member, and it must not become one. The workspace builds +# on the pinned toolchain in `rust-toolchain.toml`, which is far newer than the +# floor this crate measures. A member would be compiled by every `cargo` command +# in the repository on that newer toolchain, which is the opposite of the point. +# The `[workspace]` table below detaches it, and the detachment works at this +# depth as well as at the repository root. See `docs/msrv.md`. +[package] +name = \"msrv-check\" +version = \"0.0.0\" +publish = false + +# Edition 2021 and not 2024, because edition 2024 needs Rust 1.85. The declared +# floor is above that today, so this is headroom and not a current requirement. +# The emitted code compiles under both editions, so the choice does not change +# what is measured. +edition = \"2021\" + +[workspace] + +[lib] +path = \"src/lib.rs\" + +# The union of every crate the golden files reference, taken from +# `required_dependencies` in `crates/oapi-codegen/src/deps.rs`. That function also +# produces the report a consumer follows, so this crate resolves the graph a +# consumer resolves. +# +# Regenerate with `make update-msrv-manifest`. +[dependencies] +", + ); + for dep in deps { + out.push_str(&dep.toml()); + out.push('\n'); + } + return out; +} + +#[test] +fn msrv_check_manifest_is_up_to_date() { + let generated = render_manifest(&union_of_golden_dependencies()); + let path = manifest_path(); + + if std::env::var_os("UPDATE_MSRV_MANIFEST").is_some() { + std::fs::write(&path, &generated) + .unwrap_or_else(|error| panic!("writing `{}` failed: {error}", path.display())); + return; + } + + let expected = std::fs::read_to_string(&path).unwrap_or_else(|error| { + panic!( + "reading `{}` failed (run `make update-msrv-manifest`): {error}", + path.display() + ) + }); + assert_eq!( + generated, + expected.replace("\r\n", "\n"), + "`tests/msrv-check/Cargo.toml` drifted from the dependency report; run `make update-msrv-manifest`", + ); +} + +/// Every crate the union names must be a dependency of this crate. +/// +/// The versions come from this crate's own manifest, so a crate absent from it +/// cannot be given a version at all. `required_dependencies` panics in that case. +/// This test states the requirement directly, so the reason is visible here and +/// not only in a panic message from a generated file. +#[test] +fn every_union_crate_has_a_version() { + for dep in union_of_golden_dependencies() { + assert!( + !dep.version.is_empty(), + "`{}` has no version requirement in oapi-codegen's manifest", + dep.name + ); + } +} + +/// The generated manifest must sit next to the crate it configures. +/// +/// A path typo would write a manifest nothing reads, and the check above would +/// then compare a file against itself forever. +#[test] +fn the_generated_manifest_configures_the_msrv_check_crate() { + let root = manifest_path(); + let dir = root + .parent() + .unwrap_or_else(|| panic!("the manifest path has a parent directory")); + assert!( + Path::new(&dir.join("src/lib.rs")).is_file(), + "no `src/lib.rs` next to `{}`; the generated manifest configures nothing", + root.display() + ); + assert!( + Path::new(&dir.join("build.rs")).is_file(), + "no `build.rs` next to `{}`; the golden module list is what that script writes", + root.display() + ); +} diff --git a/crates/oapi-codegen/tests/support/apimodel.rs b/crates/oapi-codegen/tests/support/apimodel.rs new file mode 100644 index 0000000..8f36e31 --- /dev/null +++ b/crates/oapi-codegen/tests/support/apimodel.rs @@ -0,0 +1,26 @@ +// Stand-in for the models crate the `server_refs` and `server_xfile_refs` +// fixtures point their cross-file `$ref` bodies at through `import-mapping`. +// +// A real project generates this module from the referenced schema file. Here a +// minimal struct per referenced schema proves the emitted +// `crate::apimodel::CreateWidget` path resolves, and that the generated handler +// can decode it as a JSON body. +// +// This file is `include!`d rather than declared, because two crates need it at +// their own crate root: `tests/generated.rs` and the `msrv-check` crate. The +// emitted path is absolute (`crate::apimodel`), so each crate must hold its own +// copy of the module, and a copy written by hand in each place drifts as soon as +// a fixture references a new schema. +// +// The comments here are `//` and not `//!`. An `include!` expansion sits after the +// start of the module body, and an inner doc comment is only valid at that start. + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] +pub struct CreateWidget { + pub name: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] +pub struct NewThing { + pub name: String, +} diff --git a/docs/configuration.md b/docs/configuration.md index 6581352..c74314e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -70,6 +70,10 @@ parameters) `axum-extra`; a `reqwest` client needs `reqwest`, `percent-encoding` and, for form responses, `serde_urlencoded`. `chrono`, `uuid`, and `serde_json` are added when the schemas use dates, UUIDs, or free-form values. +The report ends with the Rust version needed to compile the generated code. Those +crates are what set that version, so the two are reported together. See +[Rust versions](msrv.md). + ### `output-options` | Key | Purpose | diff --git a/docs/msrv.md b/docs/msrv.md new file mode 100644 index 0000000..d5ad950 --- /dev/null +++ b/docs/msrv.md @@ -0,0 +1,123 @@ +# Rust versions + +Three different Rust versions apply to this project. Each one answers a different +question. Only one of them reaches a consumer of the generated code. + +| Question | Version | Where the project declares it | +| ------------------------------------------- | -------- | --------------------------------------------------------- | +| What builds the generator from source? | **1.97** | `rust-version` in the workspace `Cargo.toml` | +| What runs the released generator binary? | **none** | A binary needs no toolchain | +| What compiles the code the generator emits? | **1.88** | `[package.metadata.generated-code]` in the crate manifest | + +A consumer needs the third row. The other two rows belong to a contributor and to +CI. + +The first row has a second number next to it. `rust-toolchain.toml` pins the exact +channel that a contributor gets, and that channel is `1.97.1`. The `rust-version` +key states the floor. The pin states the one version that CI uses. A +`cargo install` from source reads the floor, so both numbers matter. + +## The generated-code floor + +The generator emits Rust source into your crate. Your toolchain compiles that +source, so your toolchain must be new enough for it. The floor is **Rust 1.88**. + +The generator reports this number after each write, next to the list of crates that +the output references. Both facts come from the same place, because the crates set +the floor. + +### The dependencies set the floor, and the syntax does not + +The newest language construct that the emitters produce is `-> impl Future` in a +trait method. Rust made that construct stable in **1.75**. The emitted trait uses +this form and not `async fn` in a trait. This keeps the floor low, and it prevents +a `Send` bound that a caller cannot state. + +Each other construct in the output is older than that one. The output has no +`let`-else, no `let` chain, no `gen` block, and no C string literal. + +The syntax floor is therefore 1.75, and the true floor is 1.88. The crates that the +output depends on cause the difference. An axum server pulls `axum-extra`, which +pulls `cookie`, which pulls `time`. Version 0.3.54 of `time` declares 1.88. That one +transitive crate sets the number for the whole project. + +The floor therefore moves when a dependency raises its own floor. It does not move +when the emitters change. Watch a dependency bump. + +### The floor for each output kind + +A models-only file needs a lower floor than a server. Each measured floor below is +the highest `rust-version` that any crate in the resolved graph declares. + +| Output | Crates that set it | Floor | +| ---------------- | ------------------------------------------ | -------- | +| Models only | `serde`, `serde_json` | **1.71** | +| `reqwest` client | `reqwest` and its TLS and ICU dependencies | **1.86** | +| `axum` server | `axum-extra` → `cookie` → `time` | **1.88** | + +The project declares one number, and that number is the highest of the three. One +floor is more simple to state and to test than a table that a consumer must first +match against their own configuration. A consumer who generates models only can use +an older toolchain than the declared floor, and nothing prevents this. + +### How the project measured the number + +The dependency manifests alone do not give the floor. The project compiled each +output kind in a scratch crate on the candidate toolchain. Such a compile shows two +kinds of error that a manifest hides. A crate can declare a floor that it does not +need. A crate can also need a floor that it does not declare. + +CI does the same compile for each pull request. Read the `generated-code-msrv` job +in `.github/workflows/ci.yml`. The job installs the declared toolchain and compiles +each golden file with it. A dependency bump that raises the true floor therefore +fails the build, and it does not reach a consumer. + +### The crate that does the compile + +The crate is `crates/oapi-codegen/tests/msrv-check`. It holds no test of its own, +because compiling is the assertion. + +Two properties of it are deliberate. It is not a workspace member, and the +`[workspace]` table in its manifest is what detaches it. A member gets built by +every `cargo` command in the repository, on the pinned toolchain, which is the +opposite of the measurement. It also sits under `tests/`, next to the golden files +it compiles. Cargo skips a detached crate at any depth, does not treat the +directory as a test target, and leaves it out of `cargo package`. + +Its manifest is generated, and the top line of the file says so. The dependency +list comes from `required_dependencies` in `crates/oapi-codegen/src/deps.rs`, which +is the same function that tells a consumer which crates to add. The crate therefore +resolves the graph a consumer resolves. Regenerate the manifest with +`make update-msrv-manifest`. The `msrv_manifest` test fails when the committed file +is stale, in the way that the `cli_docs` test guards `docs/cli.md`. + +A hand-written list held three kinds of drift. A wrong version still compiles, so +it measures a floor for a dependency set that no test exercises. An absent crate or +feature usually stops the compile, but not when the report recommends a feature +that no golden file uses. Generation closes all three. + +### The dependency-update bot + +`update-deps-cargo.yml` bumps dependencies each week and merges when the tests +pass. It finds each `Cargo.toml` in the repository, so it reaches the `msrv-check` +manifest as well. + +The bot only rewrites a version for a crate that the file already names, and it +never touches a feature. It therefore cannot keep a hand-written copy correct. The +generated manifest does not depend on the bot for this: the `msrv_manifest` test +compares the file against the report on each run. + +When a dependency raises the true floor, the `generated-code-msrv` job fails and +blocks that merge. This is the correct result. A person must then decide to raise +the declared floor, because a higher floor is a breaking change for a consumer. No +bot makes that decision. + +## How to raise the floor + +A higher generated-code floor is a breaking change for a consumer, because this +project does not control a consumer's toolchain. + +To raise the floor, change `rust-version` under +`[package.metadata.generated-code]` in `crates/oapi-codegen/Cargo.toml`. That one +edit updates the CI job, the terminal report, and this document, because all three +read that key. diff --git a/examples/bookstore/README.md b/examples/bookstore/README.md index b77b703..5dcdd5c 100644 --- a/examples/bookstore/README.md +++ b/examples/bookstore/README.md @@ -10,6 +10,7 @@ $ oapi-codegen --config-file oapi-codegen-common.yaml schemas/common.yaml serde = { version = "1.0.229", features = ["derive"] } # or: cargo add serde@1.0.229 --features derive + note: the generated code needs Rust 1.88 or newer. $ oapi-codegen --config-file oapi-codegen-catalog.yaml schemas/catalog.yaml ✓ wrote generated/apimodel/catalog.rs @@ -17,6 +18,7 @@ $ oapi-codegen --config-file oapi-codegen-catalog.yaml schemas/catalog.yaml serde = { version = "1.0.229", features = ["derive"] } # or: cargo add serde@1.0.229 --features derive + note: the generated code needs Rust 1.88 or newer. $ oapi-codegen --config-file oapi-codegen-server.yaml openapi.yaml ✓ wrote generated/restapi.rs @@ -32,6 +34,7 @@ $ oapi-codegen --config-file oapi-codegen-server.yaml openapi.yaml cargo add http@1.4.2 cargo add axum@0.8.9 --features multipart cargo add axum-extra@0.12.6 --features query + note: the generated code needs Rust 1.88 or newer. $ oapi-codegen --config-file oapi-codegen-client.yaml openapi.yaml ✓ wrote generated/restclient.rs @@ -45,6 +48,7 @@ $ oapi-codegen --config-file oapi-codegen-client.yaml openapi.yaml cargo add http@1.4.2 cargo add reqwest@0.13.4 --no-default-features --features blocking,json,form,query,multipart cargo add percent-encoding@2.3.2 + note: the generated code needs Rust 1.88 or newer. ``` From 2980d136d6634411a0d7056ac03c7160d284e8f5 Mon Sep 17 00:00:00 2001 From: Kaspar Lyngsie Date: Mon, 3 Aug 2026 10:25:47 +0200 Subject: [PATCH 2/2] fix: pr comments / rewording --- Makefile | 10 ++++++- crates/oapi-codegen/Cargo.toml | 2 +- crates/oapi-codegen/src/deps.rs | 4 +-- crates/oapi-codegen/tests/coverage.rs | 4 +-- .../oapi-codegen/tests/msrv-check/Cargo.toml | 4 +-- crates/oapi-codegen/tests/msrv-check/build.rs | 21 ++++++++----- .../oapi-codegen/tests/msrv-check/src/lib.rs | 8 ++--- crates/oapi-codegen/tests/msrv_manifest.rs | 30 +++++++++---------- docs/msrv.md | 20 +++++++++++-- 9 files changed, 65 insertions(+), 38 deletions(-) diff --git a/Makefile b/Makefile index 817d288..b51479f 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,15 @@ verify-msrv: ## Compile every generated file on the Rust version a consumer need @echo "Compiling every generated file on Rust $(GENERATED_MSRV)." # `msrv-check` is not a workspace member, so it needs its own manifest path. # Compiling is the whole assertion: the crate holds no test of its own. - cargo +$(GENERATED_MSRV) build --manifest-path crates/oapi-codegen/tests/msrv-check/Cargo.toml + # + # `--locked` because the measured floor is a property of one dependency graph, + # and the committed lockfile is what names that graph. Without the flag, a + # manifest that gained a crate resolves it and rewrites the lockfile in place, + # so the run reports a floor for a graph nobody reviewed. The manifest is + # generated, so gaining a crate needs no edit here and is the expected case. + # Run `make update-msrv-manifest` and then `cargo update` for that manifest to + # refresh both together. + cargo +$(GENERATED_MSRV) build --locked --manifest-path crates/oapi-codegen/tests/msrv-check/Cargo.toml @echo "Generated code compiles on Rust $(GENERATED_MSRV)." .PHONY: docs diff --git a/crates/oapi-codegen/Cargo.toml b/crates/oapi-codegen/Cargo.toml index 04c4c21..11c3998 100644 --- a/crates/oapi-codegen/Cargo.toml +++ b/crates/oapi-codegen/Cargo.toml @@ -24,7 +24,7 @@ path = "src/main.rs" # # The value lives here, and not as a constant in the source, because three places # read it: `deps.rs` embeds this manifest already and parses it out, the CI job -# installs this toolchain and compiles the golden files with it, and `docs/msrv.md` +# installs this toolchain and compiles the generated fixtures with it, and `docs/msrv.md` # quotes it. A number with three homes goes stale in two of them. # # The floor comes from the dependencies the emitted code needs, and not from the diff --git a/crates/oapi-codegen/src/deps.rs b/crates/oapi-codegen/src/deps.rs index 55b99b2..5548b69 100644 --- a/crates/oapi-codegen/src/deps.rs +++ b/crates/oapi-codegen/src/deps.rs @@ -29,7 +29,7 @@ const MANIFEST: &str = include_str!("../Cargo.toml"); /// /// Deriving the recommended version from our manifest (rather than a hardcoded /// constant) keeps the report in lockstep with the versions the generated -/// goldens actually compile against, so a dependency bump here updates the +/// generated fixtures actually compile against, so a dependency bump here updates the /// report automatically. Every crate the report can name is a (dev-)dependency /// of this crate — enforced by `reported_crates_have_manifest_versions` — so an /// absent entry is a programming error, not runtime input. @@ -92,7 +92,7 @@ const GENERATED_CODE_TABLE: &str = "[package.metadata.generated-code]"; /// generated file lands in. /// /// The value is read from the manifest rather than written here, so the CI job -/// that compiles the golden files on this toolchain and the documentation that +/// that compiles the generated fixtures on this toolchain and the documentation that /// quotes it both read one number. See `docs/msrv.md`. pub fn generated_code_rust_version() -> &'static str { match parse_generated_code_rust_version(MANIFEST) { diff --git a/crates/oapi-codegen/tests/coverage.rs b/crates/oapi-codegen/tests/coverage.rs index a9512f5..131351e 100644 --- a/crates/oapi-codegen/tests/coverage.rs +++ b/crates/oapi-codegen/tests/coverage.rs @@ -486,7 +486,7 @@ const COMBINED_UNSUPPORTED_FIXTURES: &[&str] = &[ "combined_reserved_name_client_error", ]; -/// Fixtures for name collisions. The golden-file tests do not cover these, +/// Fixtures for name collisions. The generated-fixture tests do not cover these, /// because some must fail and others need a config option or an extension. /// /// `type_name_collision_error` must fail. Two schema names collapse onto one Rust @@ -1816,7 +1816,7 @@ fn empty_response_suffix_falls_back_to_default() { /// names is one the generator can actually emit. This ties `required_dependencies` /// to emitted code (not just synthetic strings), so a new crate the emitters /// start referencing — which will also force a new dev-dependency to compile the -/// goldens — is a prompt to extend the report. +/// generated fixtures — is a prompt to extend the report. #[test] fn dependency_report_reflects_generated_output() { let fixture = tests_dir().join("fixtures").join("combined_server_client.yaml"); diff --git a/crates/oapi-codegen/tests/msrv-check/Cargo.toml b/crates/oapi-codegen/tests/msrv-check/Cargo.toml index 558a8cf..3629b3b 100644 --- a/crates/oapi-codegen/tests/msrv-check/Cargo.toml +++ b/crates/oapi-codegen/tests/msrv-check/Cargo.toml @@ -1,6 +1,6 @@ # Generated by `make update-msrv-manifest`. DO NOT EDIT. # -# A crate that compiles every committed golden file on the Rust version a +# A crate that compiles every committed fixture on the Rust version a # consumer needs, and does nothing else. # # It is not a workspace member, and it must not become one. The workspace builds @@ -25,7 +25,7 @@ edition = "2021" [lib] path = "src/lib.rs" -# The union of every crate the golden files reference, taken from +# The union of every crate the fixtures reference, taken from # `required_dependencies` in `crates/oapi-codegen/src/deps.rs`. That function also # produces the report a consumer follows, so this crate resolves the graph a # consumer resolves. diff --git a/crates/oapi-codegen/tests/msrv-check/build.rs b/crates/oapi-codegen/tests/msrv-check/build.rs index 2040054..04a5b4d 100644 --- a/crates/oapi-codegen/tests/msrv-check/build.rs +++ b/crates/oapi-codegen/tests/msrv-check/build.rs @@ -1,7 +1,7 @@ -//! Write a module list covering every committed golden file. +//! Write a module list covering every committed fixture. //! //! The list is derived from the directory rather than written by hand, because a -//! hand-written list is a second place to add a golden file, and a file left out +//! hand-written list is a second place to add a fixture, and a file left out //! of it is not compiled and reports nothing. `tests/generated.rs` in the //! generator crate keeps such a list, and a coverage test guards it. There is no //! equivalent guard here, so this crate reads the directory instead. @@ -9,14 +9,14 @@ use std::path::Path; fn main() { - // This crate sits at `crates/oapi-codegen/tests/msrv-check`, so the golden + // This crate sits at `crates/oapi-codegen/tests/msrv-check`, so the fixture // directory is a sibling one level up. let root = Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .expect("the manifest directory has a parent") .join("generated"); - // Re-run when a golden file is added or removed. Without this the module list + // Re-run when a fixture is added or removed. Without this the module list // is cached from the first build and a new file is never compiled. println!("cargo:rerun-if-changed={}", root.display()); @@ -36,11 +36,16 @@ fn main() { .into_owned(); }) .collect(); - // Sorted so the generated file is byte-identical across platforms, which keeps - // the build cache valid between a local run and a CI run. + // Sorted so the module order does not follow directory read order, which no + // filesystem promises to keep stable. Two runs on one machine then write the + // same file, and the build cache stays valid. + // + // The file is not identical between machines, because each `include!` below + // holds an absolute path. Nothing needs it to be: the file lives in `OUT_DIR` + // and is rewritten by this script on each machine. stems.sort(); - assert!(!stems.is_empty(), "no golden files found in `{}`", root.display()); + assert!(!stems.is_empty(), "no fixtures found in `{}`", root.display()); let mut source = String::new(); for stem in &stems { @@ -51,6 +56,6 @@ fn main() { )); } - let out = Path::new(&std::env::var("OUT_DIR").expect("OUT_DIR is set by cargo")).join("goldens.rs"); + let out = Path::new(&std::env::var("OUT_DIR").expect("OUT_DIR is set by cargo")).join("fixtures.rs"); std::fs::write(&out, source).unwrap_or_else(|error| panic!("cannot write `{}`: {error}", out.display())); } diff --git a/crates/oapi-codegen/tests/msrv-check/src/lib.rs b/crates/oapi-codegen/tests/msrv-check/src/lib.rs index e2b1779..d53a75d 100644 --- a/crates/oapi-codegen/tests/msrv-check/src/lib.rs +++ b/crates/oapi-codegen/tests/msrv-check/src/lib.rs @@ -1,4 +1,4 @@ -//! Compile every committed golden file on the Rust version a consumer needs. +//! Compile every committed fixture on the Rust version a consumer needs. //! //! The generated-code floor is declared once, under //! `[package.metadata.generated-code]` in `crates/oapi-codegen/Cargo.toml`. That @@ -7,7 +7,7 @@ //! without touching a line of the generator, so the claim needs a check that runs //! on the toolchain it names. //! -//! There are no assertions here. Compiling is the assertion: if a golden file +//! There are no assertions here. Compiling is the assertion: if a fixture //! needs a newer `rustc` than the declared floor, this crate does not build. //! //! Run it with `make verify-msrv`. See `docs/msrv.md`. @@ -15,7 +15,7 @@ // Generated code is ordinary idiomatic Rust, and the workspace lint set targets // first-party source. This crate holds nothing but generated code, so the // exceptions cover the whole of it. -#![allow(dead_code, reason = "a golden file declares types this crate never constructs")] +#![allow(dead_code, reason = "a fixture declares types this crate never constructs")] #![allow(clippy::all, reason = "generated code is not linted against the workspace rules")] /// Stand-in for the models crate two fixtures point their cross-file `$ref` @@ -26,4 +26,4 @@ mod apimodel { include!("../../support/apimodel.rs"); } -include!(concat!(env!("OUT_DIR"), "/goldens.rs")); +include!(concat!(env!("OUT_DIR"), "/fixtures.rs")); diff --git a/crates/oapi-codegen/tests/msrv_manifest.rs b/crates/oapi-codegen/tests/msrv_manifest.rs index 9fb17e0..96fd6f8 100644 --- a/crates/oapi-codegen/tests/msrv_manifest.rs +++ b/crates/oapi-codegen/tests/msrv_manifest.rs @@ -1,6 +1,6 @@ //! Keeps `tests/msrv-check/Cargo.toml` in lockstep with the dependency report. //! -//! The `msrv-check` crate compiles every golden file on the Rust version a +//! The `msrv-check` crate compiles every fixture on the Rust version a //! consumer needs. To measure a floor that applies to a consumer, it must resolve //! the same dependency graph a consumer resolves. A consumer builds that graph //! from the report [`oapi_codegen::deps::required_dependencies`] produces, so that @@ -11,9 +11,9 @@ //! //! 1. A version that differs. Silent: the graph still resolves, and the measured //! floor belongs to a dependency set no test exercises. -//! 2. A feature that is absent. Usually loud, because a golden file that uses the +//! 2. A feature that is absent. Usually loud, because a fixture that uses the //! feature fails to compile. Silent when the report recommends a feature that -//! no golden file exercises. +//! no fixture exercises. //! 3. A crate that is absent. Loud, for the same reason. //! //! Generating the file closes all three at once. The committed file stays readable @@ -33,7 +33,7 @@ use oapi_codegen::deps::required_dependencies; /// Message at the top of the generated manifest so a reader does not edit it. const GENERATED_NOTICE: &str = "Generated by `make update-msrv-manifest`. DO NOT EDIT."; -/// The directory holding the committed golden files. +/// The directory holding the committed fixtures. fn generated_dir() -> PathBuf { return PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/generated"); } @@ -43,15 +43,15 @@ fn manifest_path() -> PathBuf { return PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/msrv-check/Cargo.toml"); } -/// The union of the crates every golden file references. +/// The union of the crates every fixture references. /// -/// `msrv-check` compiles every golden file as one crate, so it needs one +/// `msrv-check` compiles every fixture as one crate, so it needs one /// dependency set covering all of them. Two rules make the union safe to compile: /// a feature any file needs is enabled, and default features stay on unless every /// file that names the crate can do without them. Both rules err towards a larger /// graph, because a graph that is too small fails to compile and a graph that is /// too large only measures a floor that is too high. Neither error is silent. -fn union_of_golden_dependencies() -> Vec { +fn union_of_fixture_dependencies() -> Vec { let dir = generated_dir(); let mut entries: Vec = std::fs::read_dir(&dir) .unwrap_or_else(|error| panic!("cannot read `{}`: {error}", dir.display())) @@ -70,7 +70,7 @@ fn union_of_golden_dependencies() -> Vec { assert!( !entries.is_empty(), - "no golden files found in `{}`; the union would be empty and the measured floor meaningless", + "no fixtures found in `{}`; the union would be empty and the measured floor meaningless", dir.display() ); @@ -94,7 +94,7 @@ fn union_of_golden_dependencies() -> Vec { }); assert_eq!( merged.version, dep.version, - "two golden files recommend different versions of `{}`; the report reads one manifest, so this cannot happen", + "two fixtures recommend different versions of `{}`; the report reads one manifest, so this cannot happen", dep.name ); merged.default_features = merged.default_features || dep.default_features; @@ -109,7 +109,7 @@ fn union_of_golden_dependencies() -> Vec { return union .into_values() .map(|mut dep| { - // Sorted so a change in golden-file read order cannot rewrite the file. + // Sorted so a change in fixture read order cannot rewrite the file. dep.features.sort_unstable(); return dep; }) @@ -122,7 +122,7 @@ fn render_manifest(deps: &[Dependency]) -> String { out.push_str(&format!("# {GENERATED_NOTICE}\n")); out.push_str( "# -# A crate that compiles every committed golden file on the Rust version a +# A crate that compiles every committed fixture on the Rust version a # consumer needs, and does nothing else. # # It is not a workspace member, and it must not become one. The workspace builds @@ -147,7 +147,7 @@ edition = \"2021\" [lib] path = \"src/lib.rs\" -# The union of every crate the golden files reference, taken from +# The union of every crate the fixtures reference, taken from # `required_dependencies` in `crates/oapi-codegen/src/deps.rs`. That function also # produces the report a consumer follows, so this crate resolves the graph a # consumer resolves. @@ -165,7 +165,7 @@ path = \"src/lib.rs\" #[test] fn msrv_check_manifest_is_up_to_date() { - let generated = render_manifest(&union_of_golden_dependencies()); + let generated = render_manifest(&union_of_fixture_dependencies()); let path = manifest_path(); if std::env::var_os("UPDATE_MSRV_MANIFEST").is_some() { @@ -195,7 +195,7 @@ fn msrv_check_manifest_is_up_to_date() { /// not only in a panic message from a generated file. #[test] fn every_union_crate_has_a_version() { - for dep in union_of_golden_dependencies() { + for dep in union_of_fixture_dependencies() { assert!( !dep.version.is_empty(), "`{}` has no version requirement in oapi-codegen's manifest", @@ -221,7 +221,7 @@ fn the_generated_manifest_configures_the_msrv_check_crate() { ); assert!( Path::new(&dir.join("build.rs")).is_file(), - "no `build.rs` next to `{}`; the golden module list is what that script writes", + "no `build.rs` next to `{}`; the fixture module list is what that script writes", root.display() ); } diff --git a/docs/msrv.md b/docs/msrv.md index d5ad950..54a6bd2 100644 --- a/docs/msrv.md +++ b/docs/msrv.md @@ -69,7 +69,7 @@ need. A crate can also need a floor that it does not declare. CI does the same compile for each pull request. Read the `generated-code-msrv` job in `.github/workflows/ci.yml`. The job installs the declared toolchain and compiles -each golden file with it. A dependency bump that raises the true floor therefore +each fixture with it. A dependency bump that raises the true floor therefore fails the build, and it does not reach a consumer. ### The crate that does the compile @@ -80,7 +80,7 @@ because compiling is the assertion. Two properties of it are deliberate. It is not a workspace member, and the `[workspace]` table in its manifest is what detaches it. A member gets built by every `cargo` command in the repository, on the pinned toolchain, which is the -opposite of the measurement. It also sits under `tests/`, next to the golden files +opposite of the measurement. It also sits under `tests/`, next to the fixtures it compiles. Cargo skips a detached crate at any depth, does not treat the directory as a test target, and leaves it out of `cargo package`. @@ -94,7 +94,21 @@ is stale, in the way that the `cli_docs` test guards `docs/cli.md`. A hand-written list held three kinds of drift. A wrong version still compiles, so it measures a floor for a dependency set that no test exercises. An absent crate or feature usually stops the compile, but not when the report recommends a feature -that no golden file uses. Generation closes all three. +that no fixture uses. Generation closes all three. + +The crate holds a committed `Cargo.lock`, and the build passes `--locked`. A +measured floor belongs to one dependency graph, and the lockfile is what names that +graph. Without the flag, a manifest that gained a crate resolves that crate and +rewrites the lockfile in place. The run then reports a floor for a graph that nobody +reviewed. + +A new crate in the fixtures therefore needs the two steps below. Until both are +done, the build stops with `the lock file ... needs to be updated`. + +```sh +make update-msrv-manifest +cargo update --manifest-path crates/oapi-codegen/tests/msrv-check/Cargo.toml +``` ### The dependency-update bot