diff --git a/.commitlintrc.json b/.commitlintrc.json index c30e5a9..f4fbb7d 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -1,3 +1,3 @@ { - "extends": ["@commitlint/config-conventional"] + "extends": ["@commitlint/config-conventional"] } diff --git a/.devcontainer.json b/.devcontainer.json index ab836c5..d9d70ca 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,22 +1,16 @@ { - "name": "axum_typed_multipart", - "image": "mcr.microsoft.com/devcontainers/rust:1", - "features": { - "ghcr.io/devcontainers-contrib/features/pre-commit:2": {}, - "ghcr.io/devcontainers-contrib/features/prettier:1": {} - }, - "customizations": { - "vscode": { - "extensions": [ - "editorconfig.editorconfig", - "esbenp.prettier-vscode", - "github.vscode-github-actions" - ], - "settings": { - "[json,jsonc,markdown,yaml]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "name": "axum_typed_multipart", + "image": "mcr.microsoft.com/devcontainers/rust:1", + "features": { + "ghcr.io/devcontainers-contrib/features/pre-commit:2": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "github.vscode-github-actions" + ] } - } } - } } diff --git a/.editorconfig b/.editorconfig index 4f6fc82..9d8c8a5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,12 +5,10 @@ root = true [*] indent_style = space -indent_size = 2 +indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true max_line_length = 100 -[*.rs] -indent_size = 4 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 83150d3..2777d0b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,22 +1,22 @@ version: 2 updates: - - package-ecosystem: github-actions - directory: / - schedule: - interval: daily - ignore: - - dependency-name: "*" - update-types: - - version-update:semver-minor - - version-update:semver-patch + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + ignore: + - dependency-name: "*" + update-types: + - version-update:semver-minor + - version-update:semver-patch - - package-ecosystem: cargo - directory: / - schedule: - interval: daily - ignore: - - dependency-name: "*" - update-types: - - version-update:semver-minor - - version-update:semver-patch + - package-ecosystem: cargo + directory: / + schedule: + interval: daily + ignore: + - dependency-name: "*" + update-types: + - version-update:semver-minor + - version-update:semver-patch diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 803c3bf..4bf5719 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -1,21 +1,29 @@ +name: audit + on: - workflow_dispatch: - schedule: - - cron: "0 0 * * MON" # Every Monday at 00:00 UTC - push: - branches: - - main - paths: - - "**/Cargo.toml" - - "**/Cargo.lock" + workflow_dispatch: + schedule: + - cron: "0 0 * * MON" # Every Monday at 00:00 UTC + pull_request: + branches: + - main + paths: + - "**/Cargo.toml" + - "**/Cargo.lock" + push: + branches: + - main + tags: + - "[0-9]+.[0-9]+.[0-9]+" + paths: + - "**/Cargo.toml" + - "**/Cargo.lock" jobs: - audit: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: actions-rs/audit-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/audit@v1 + with: + denyWarnings: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2c29e9a..c8852dc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,17 +1,19 @@ +name: lint + on: - workflow_call: + pull_request: + branches: + - main + push: + branches: + - main + tags: + - "[0-9]+.[0-9]+.[0-9]+" jobs: - lint: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - - uses: Swatinem/rust-cache@v2 - - - uses: pre-commit/action@v3.0.0 + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml deleted file mode 100644 index a629a93..0000000 --- a/.github/workflows/pr.yml +++ /dev/null @@ -1,11 +0,0 @@ -on: - pull_request: - branches: - - main - -jobs: - lint: - uses: ./.github/workflows/lint.yml - - test: - uses: ./.github/workflows/test.yml diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml deleted file mode 100644 index 65a352c..0000000 --- a/.github/workflows/push.yml +++ /dev/null @@ -1,55 +0,0 @@ -on: - push: - branches: - - main - tags: - - "[0-9]+.[0-9]+.[0-9]+" - -jobs: - lint: - uses: ./.github/workflows/lint.yml - - test: - uses: ./.github/workflows/test.yml - - publish: - if: startsWith(github.ref, 'refs/tags/') - - needs: - - lint - - test - - env: - CARGO_REGISTRY_TOKEN: ${{secrets.CARGO_REGISTRY_TOKEN}} - - permissions: - contents: write - packages: write - pull-requests: read - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - - uses: Swatinem/rust-cache@v2 - - - uses: actions-rs/cargo@v1 - with: - command: publish - args: --locked --package axum_typed_multipart_macros - - - uses: actions-rs/cargo@v1 - with: - command: publish - args: --locked - - - uses: ncipollo/release-action@v1 - with: - generateReleaseNotes: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d0577d3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: release + +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + +jobs: + publish: + runs-on: ubuntu-latest + + permissions: + contents: write + packages: write + pull-requests: read + + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: taiki-e/install-action@v2 + with: + tool: cargo-edit + - run: cargo set-version --workspace ${GITHUB_REF#refs/tags/} + - run: | + # See https://github.com/orgs/community/discussions/40405#discussioncomment-8361451 + git config user.name "${{ github.actor }}" + git config user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com" + git add -A + git commit -m "chore(${{ github.workflow }}): bump version to ${GITHUB_REF#refs/tags/}" + - uses: lewagon/wait-on-check-action@v1.3.4 + with: + ref: ${{ github.ref }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + running-workflow-name: ${{ github.job }} + - run: cargo publish --locked --package axum_typed_multipart_macros + - run: cargo publish --locked + - uses: ncipollo/release-action@v1 + with: + generateReleaseNotes: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e41ad9c..66f20b7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,133 +1,85 @@ +name: test + on: - workflow_call: + pull_request: + branches: + - main + push: + branches: + - main + tags: + - "[0-9]+.[0-9]+.[0-9]+" jobs: - test: - strategy: - matrix: - include: - - toolchain: beta - os: ubuntu-latest - - toolchain: nightly - os: ubuntu-latest - - toolchain: stable - os: macos-latest - - toolchain: stable - os: windows-latest - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.toolchain }} - profile: minimal - - - uses: Swatinem/rust-cache@v2 - - - uses: actions-rs/cargo@v1 - with: - command: test - args: --workspace --all-features --all-targets - - # See https://users.rust-lang.org/t/psa-please-specify-precise-dependency-versions-in-cargo-toml/71277 - minimal-version-test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - profile: minimal - - - uses: Swatinem/rust-cache@v2 - - - uses: taiki-e/install-action@cargo-hack - - uses: taiki-e/install-action@cargo-minimal-versions - - - uses: actions-rs/cargo@v1 - with: - command: minimal-versions - args: test --workspace --all-features --all-targets - - doctest: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - - - uses: Swatinem/rust-cache@v2 - - - uses: actions-rs/cargo@v1 - with: - command: test - args: --workspace --all-features --doc - - coverage: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - components: llvm-tools-preview - - - uses: actions-rs/cargo@v1 - with: - command: test - args: --workspace --all-features --all-targets - env: - CARGO_INCREMENTAL: "0" - RUSTFLAGS: "-Cinstrument-coverage -Cdebug-assertions=no" - LLVM_PROFILE_FILE: "target/coverage/coverage-%p-%m.profraw" - - - run: wget -qO- $GRCOV_URL | tar xvj -C "/usr/local/bin" - env: - GRCOV_URL: https://github.com/mozilla/grcov/releases/download/v0.8.18/grcov-x86_64-unknown-linux-gnu.tar.bz2 - - # grcov is still a bit rough around the edges, so we need to explicitly - # exclude some of the code from the coverage report. - # See https://github.com/mozilla/grcov/issues/476 - - run: > - grcov target/coverage macros/target/coverage - --source-dir . - --binary-path target/debug - --output-type lcov - --output-path target/coverage/coverage.lcov - --branch - --llvm - --ignore-not-existing - --ignore '**/tests/**' - --ignore '**/examples/**' - --ignore '/*' - --excl-line '(//(/|!)?|mod [^\s]+;|\#\[derive\()' - --excl-br-line '(//(/|!)?|mod [^\s]+;|\#\[derive\()' - --excl-start '^mod tests \{$' - --excl-br-start '^mod tests \{$' - --excl-stop '^\}$' - --excl-br-stop '^\}$' - - - uses: codecov/codecov-action@v3 - with: - files: target/coverage/coverage.lcov - fail_ci_if_error: true + test: + strategy: + matrix: + include: + - toolchain: beta + os: ubuntu-latest + - toolchain: nightly + os: ubuntu-latest + - toolchain: stable + os: macos-latest + - toolchain: stable + os: windows-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + - run: cargo test --workspace --all-features --all-targets + + # See https://users.rust-lang.org/t/psa-please-specify-precise-dependency-versions-in-cargo-toml/71277 + minimal-version-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + - uses: taiki-e/install-action@cargo-hack + - uses: taiki-e/install-action@cargo-minimal-versions + - run: cargo minimal-versions test --direct --workspace --all-features --all-targets + + no-default-features-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - run: cargo test --workspace --all-targets --no-default-features + + doctest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - run: cargo test --workspace --all-features --doc + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + components: llvm-tools-preview # needed for llvm-cov + - uses: taiki-e/install-action@cargo-llvm-cov + - run: cargo llvm-cov --workspace --all-features --all-targets --lcov --output-path target/coverage.lcov + - uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + files: target/coverage.lcov + fail_ci_if_error: true diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..3f20ba9 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,3 @@ +{ + "extends": "markdownlint/style/prettier" +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c396bca..b3d3665 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,79 +4,80 @@ fail_fast: true default_install_hook_types: - - pre-commit - - commit-msg + - pre-commit + - commit-msg repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 - hooks: - - id: check-case-conflict # checks for files that would conflict in case-insensitive filesystems. - - id: check-executables-have-shebangs # ensures that (non-binary) executables have a shebang. - - id: check-json # checks json files for parseable syntax. - - id: check-merge-conflict # checks for files that contain merge conflict strings. - - id: check-symlinks # checks for symlinks which do not point to anything. - - id: check-toml # checks toml files for parseable syntax. - - id: check-vcs-permalinks # ensures that links to vcs websites are permalinks. - - id: check-xml # checks xml files for parseable syntax. - - id: check-yaml # checks yaml files for parseable syntax. - - id: fix-byte-order-marker # removes utf-8 byte order marker. + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-added-large-files # Prevents giant files from being committed. + - id: check-case-conflict # Checks for files that would conflict in case-insensitive filesystems. + - id: check-merge-conflict # Checks for files that contain merge conflict strings. + - id: check-symlinks # Checks for symlinks which do not point to anything. + - id: check-vcs-permalinks # Ensures that links to vcs websites are permalinks. + - id: destroyed-symlinks # Detects symlinks which are changed to regular files with a content of a path which that symlink was pointing to. - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 - hooks: - - id: prettier - stages: [commit] + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v9.18.0 + hooks: + - id: commitlint + stages: [commit-msg] + additional_dependencies: ["@commitlint/config-conventional"] - - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook - rev: v9.9.0 - hooks: - - id: commitlint - stages: [commit-msg] - additional_dependencies: ["@commitlint/config-conventional"] + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: "v0.42.0" + hooks: + - id: markdownlint - - repo: https://github.com/editorconfig-checker/editorconfig-checker.python - rev: 2.7.3 - hooks: - - id: editorconfig-checker - args: [--disable-indent-size, --disable-max-line-length] + - repo: https://github.com/ComPWA/taplo-pre-commit + rev: v0.9.3 + hooks: + - id: taplo-format + - id: taplo-lint - - repo: local - hooks: - - id: cargo-fmt - name: cargo-fmt - language: system - types: [rust] - entry: cargo fmt - args: - - --all - - --check - - -- + - repo: local + hooks: + - id: prettier + name: prettier + entry: prettier --write --ignore-unknown --cache --cache-location=target/.prettier-cache + language: node + types: [text] + stages: [commit] + additional_dependencies: ["prettier@3"] - - id: cargo-clippy - name: cargo-clippy - language: system - types: [rust] - pass_filenames: false - entry: cargo clippy - args: - - --workspace - - --all-features - - --all-targets - - --no-deps - - -- - - -Dwarnings - - -Dclippy::all + - id: cargo-fmt + name: cargo-fmt + language: system + types: [rust] + entry: cargo fmt + args: + - --all + - -- - - id: cargo-doc - name: cargo-doc - language: system - types: [rust] - pass_filenames: false - entry: cargo doc - args: - - --config=build.rustdocflags = ["-Dwarnings", "-Drustdoc::all"] - - --workspace - - --all-features - - --no-deps - - -- + - id: cargo-clippy + name: cargo-clippy + language: system + types: [rust] + pass_filenames: false + entry: cargo clippy + args: + - --workspace + - --all-features + - --all-targets + - -- + - -Dwarnings + - -Dclippy::all + + - id: cargo-doc + name: cargo-doc + language: system + types: [rust] + pass_filenames: false + entry: cargo doc + args: + - --config=build.rustdocflags = ["-Dwarnings", "-Drustdoc::all"] + - --workspace + - --all-features + - --no-deps + - -- diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b84882c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[toml]": { "editor.defaultFormatter": "tamasfe.even-better-toml" } +} diff --git a/Cargo.toml b/Cargo.toml index 757afc9..3850b72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,40 +1,60 @@ -[package] -name = "axum_typed_multipart" +[workspace] +members = ["macros"] + +[workspace.package] +authors = ["Lorenzo Murarotto "] +categories = ["web-programming"] description = "Type safe multipart/form-data handling for axum." +edition = "2021" +keywords = ["axum", "form", "multipart"] license = "MIT" repository = "https://github.com/murar8/axum_typed_multipart" -authors = ["Lorenzo Murarotto "] -edition = "2021" -version = "0.10.2" -categories = ["web-programming"] -keywords = ["axum", "multipart", "form"] +version = "0.0.0" +[workspace.dependencies] +axum = "0.7.0" +axum-test-helper = "0.4.0" +reqwest = "0.11.23" +tokio = "1.25.0" -[workspace] -members = ["macros"] +[package] +name = "axum_typed_multipart" + +authors.workspace = true +categories.workspace = true +description.workspace = true +edition.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true +[features] +default = ["chrono_0_4", "tempfile_3", "uuid_1"] + +chrono_0_4 = ["dep:chrono_0_4"] +tempfile_3 = ["dep:tempfile_3", "dep:tokio"] +uuid_1 = ["dep:uuid_1"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] } [dependencies] anyhow = "1.0.72" -axum = { version = "0.6.0", features = ["multipart"] } -axum_typed_multipart_macros = { version = "0.10.2", path = "macros" } +axum = { workspace = true, features = ["multipart"] } +axum_typed_multipart_macros = { path = "macros", version = "0.0.0" } bytes = "1.4.0" futures-core = "0.3.28" futures-util = "0.3.28" -tempfile = "3.0.2" -thiserror = "1.0.44" -uuid = "1.0.0" - -[dependencies.tokio] -version = "1.30.0" -features = ["macros", "rt-multi-thread", "fs", "io-util"] +thiserror = "1.0.7" +chrono_0_4 = { package = "chrono", version = "0.4.0", optional = true } +tempfile_3 = { package = "tempfile", version = "3.1.0", optional = true } +tokio = { workspace = true, features = ["fs", "io-util"], optional = true } +uuid_1 = { package = "uuid", version = "1.0.0", optional = true } [dev-dependencies] -axum-test-helper = "0.3.0" -reqwest = "0.11.18" -serde = { version = "1.0.183", features = ["derive"] } - - -[build-dependencies] -openssl = "0.10.35" +axum-test-helper = { workspace = true } +reqwest = { workspace = true } +serde = { version = "1.0.193", features = ["derive"] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/README.md b/README.md index 3523b25..f932f4d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # axum_typed_multipart -[![](https://img.shields.io/crates/v/axum_typed_multipart.svg)](https://crates.io/crates/axum_typed_multipart) -[![](https://docs.rs/axum_typed_multipart/badge.svg)](https://docs.rs/axum_typed_multipart) -[![.github/workflows/push.yml](https://github.com/murar8/axum_typed_multipart/actions/workflows/push.yml/badge.svg)](https://github.com/murar8/axum_typed_multipart/actions/workflows/push.yml) +[![crates.io](https://img.shields.io/crates/v/axum_typed_multipart.svg)](https://crates.io/crates/axum_typed_multipart) +![Crates.io Size](https://img.shields.io/crates/size/axum_typed_multipart) +![Crates.io Downloads (recent)](https://img.shields.io/crates/dr/axum_typed_multipart) +[![docs.rs](https://docs.rs/axum_typed_multipart/badge.svg)](https://docs.rs/axum_typed_multipart) [![.github/workflows/audit.yml](https://github.com/murar8/axum_typed_multipart/actions/workflows/audit.yml/badge.svg)](https://github.com/murar8/axum_typed_multipart/actions/workflows/audit.yml) [![codecov](https://codecov.io/gh/murar8/axum_typed_multipart/branch/main/graph/badge.svg?token=AUQ4P8EFVK)](https://codecov.io/gh/murar8/axum_typed_multipart) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) @@ -23,7 +24,7 @@ Direct push to the `main` branch is not allowed, any updates require a pull requ Commit messages should follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) specification. -The project comes with an optional pre-configured development container with all the required tools. For more information on how to use it please refer to https://containers.dev +The project comes with an optional pre-configured development container with all the required tools. For more information on how to use it please refer to To make sure your changes match the project style you can install the pre-commit hooks with `pre-commit install`. This requires [pre-commit](https://pre-commit.com/) to be installed on your system. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..e1e9787 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,5 @@ +coverage: + status: + project: + default: + threshold: 1% diff --git a/examples/basic.rs b/examples/basic.rs index fb73766..e3dd37f 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -2,7 +2,6 @@ use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; -use std::net::SocketAddr; #[derive(TryFromMultipart)] struct CreateUserRequest { @@ -17,8 +16,7 @@ async fn create_user(data: TypedMultipart) -> StatusCode { #[tokio::main] async fn main() { - axum::Server::bind(&SocketAddr::from(([127, 0, 0, 1], 3000))) - .serve(Router::new().route("/users/create", post(create_user)).into_make_service()) - .await - .unwrap(); + let app = Router::new().route("/users/create", post(create_user)).into_make_service(); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + axum::serve(listener, app).await.unwrap(); } diff --git a/examples/custom_error.rs b/examples/custom_error.rs index b67ce7d..1cc9600 100644 --- a/examples/custom_error.rs +++ b/examples/custom_error.rs @@ -4,7 +4,6 @@ use axum::routing::post; use axum::{Json, Router}; use axum_typed_multipart::{BaseMultipart, TryFromMultipart, TypedMultipartError}; use serde::Serialize; -use std::net::SocketAddr; // Step 1: Define a custom error type. #[derive(Serialize)] @@ -46,8 +45,7 @@ async fn update_position(data: CustomMultipart) -> Status #[tokio::main] async fn main() { - axum::Server::bind(&SocketAddr::from(([127, 0, 0, 1], 3000))) - .serve(Router::new().route("/position/update", post(update_position)).into_make_service()) - .await - .unwrap(); + let app = Router::new().route("/position/update", post(update_position)).into_make_service(); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + axum::serve(listener, app).await.unwrap(); } diff --git a/examples/type_safe_enums.rs b/examples/type_safe_enums.rs index a0ac5ab..6b5e3c5 100644 --- a/examples/type_safe_enums.rs +++ b/examples/type_safe_enums.rs @@ -1,8 +1,7 @@ +use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_typed_multipart::{TryFromField, TryFromMultipart, TypedMultipart}; -use reqwest::StatusCode; -use std::net::SocketAddr; #[derive(Debug, TryFromField)] pub enum Sex { @@ -23,8 +22,7 @@ async fn test_multipart(multipart: TypedMultipart) -> StatusCode #[tokio::main] async fn main() { - axum::Server::bind(&SocketAddr::from(([127, 0, 0, 1], 3000))) - .serve(Router::new().route("/", post(test_multipart)).into_make_service()) - .await - .unwrap(); + let app = Router::new().route("/", post(test_multipart)).into_make_service(); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + axum::serve(listener, app).await.unwrap(); } diff --git a/examples/upload.rs b/examples/upload.rs index a582a8e..98e2c23 100644 --- a/examples/upload.rs +++ b/examples/upload.rs @@ -3,9 +3,8 @@ use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart}; -use std::net::SocketAddr; use std::path::Path; -use tempfile::NamedTempFile; +use tempfile_3::NamedTempFile; #[derive(TryFromMultipart)] struct UploadAssetRequest { @@ -34,13 +33,12 @@ async fn upload_asset( #[tokio::main] async fn main() { - let router = Router::new() + let app = Router::new() .route("/", post(upload_asset)) // The default axum body size limit is 2MiB, so we increase it to 1GiB. - .layer(DefaultBodyLimit::max(1024 * 1024 * 1024)); + .layer(DefaultBodyLimit::max(1024 * 1024 * 1024)) + .into_make_service(); - axum::Server::bind(&SocketAddr::from(([127, 0, 0, 1], 3000))) - .serve(router.into_make_service()) - .await - .unwrap(); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + axum::serve(listener, app).await.unwrap(); } diff --git a/macros/Cargo.toml b/macros/Cargo.toml index d0b0c0f..57d0a45 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -1,29 +1,32 @@ [package] name = "axum_typed_multipart_macros" -description = "Macros for axum_typed_multipart." -license = "MIT" -repository = "https://github.com/murar8/axum_typed_multipart" -authors = ["Lorenzo Murarotto "] -edition = "2021" -version = "0.10.2" +authors.workspace = true +categories.workspace = true +description.workspace = true +edition.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true [lib] -proc_macro = true +proc-macro = true +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] } [dependencies] darling = "0.20.3" -proc-macro-error = "1.0.4" -quote = "1.0.32" -syn = "2.0.28" -ubyte = "0.10.3" -heck = "0.4.1" - +heck = "0.5.0" +proc-macro-error2 = "2.0.1" +quote = "1.0.33" +syn = "2.0.39" +ubyte = "0.10.4" [dev-dependencies] -axum = "0.6.12" +axum = { workspace = true } +axum-test-helper = { workspace = true } axum_typed_multipart = { path = ".." } -axum-test-helper = "0.3.0" -reqwest = "0.11.18" -tokio = { version = "1.30.0" } +reqwest = { workspace = true } +tokio = { workspace = true, features = ["macros"] } diff --git a/macros/src/case_conversion.rs b/macros/src/case_conversion.rs index e4da46a..3082815 100644 --- a/macros/src/case_conversion.rs +++ b/macros/src/case_conversion.rs @@ -1,4 +1,4 @@ -use proc_macro_error::abort; +use proc_macro_error2::abort; use std::error::Error; use std::fmt::{self, Display, Formatter}; @@ -63,6 +63,7 @@ impl<'a> TryFrom<&'a str> for RenameCase { } #[cfg(test)] +#[cfg_attr(all(coverage_nightly, test), coverage(off))] mod tests { use super::*; diff --git a/macros/src/impls/mod.rs b/macros/src/impls/mod.rs index 5da2dc4..07a8566 100644 --- a/macros/src/impls/mod.rs +++ b/macros/src/impls/mod.rs @@ -1,2 +1,5 @@ +// See: https://github.com/cloudflare/foundations/issues/50 +#![allow(clippy::manual_unwrap_or_default)] + pub mod try_from_field; pub mod try_from_multipart; diff --git a/macros/src/impls/try_from_field.rs b/macros/src/impls/try_from_field.rs index eb9e187..0779dfa 100644 --- a/macros/src/impls/try_from_field.rs +++ b/macros/src/impls/try_from_field.rs @@ -2,7 +2,7 @@ use crate::case_conversion::RenameCase; use crate::util::strip_leading_rawlit; use darling::{FromDeriveInput, FromVariant}; use proc_macro::TokenStream; -use proc_macro_error::abort; +use proc_macro_error2::abort; use quote::quote; use syn::{Lit, LitStr}; diff --git a/macros/src/impls/try_from_multipart.rs b/macros/src/impls/try_from_multipart.rs index cdaf601..869d4e3 100644 --- a/macros/src/impls/try_from_multipart.rs +++ b/macros/src/impls/try_from_multipart.rs @@ -2,7 +2,7 @@ use crate::case_conversion::RenameCase; use crate::util::{matches_option_signature, matches_vec_signature, strip_leading_rawlit}; use darling::{FromDeriveInput, FromField}; use proc_macro::TokenStream; -use proc_macro_error::abort; +use proc_macro_error2::abort; use quote::quote; use ubyte::ByteUnit; diff --git a/macros/src/lib.rs b/macros/src/lib.rs index d63a4c3..83ba8d2 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,11 +1,13 @@ //! Macros for axum-typed-multipart. +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + mod case_conversion; mod impls; mod util; use proc_macro::TokenStream; -use proc_macro_error::proc_macro_error; +use proc_macro_error2::proc_macro_error; #[proc_macro_error] #[proc_macro_derive(TryFromMultipart, attributes(try_from_multipart, form_data))] diff --git a/macros/tests/test_defaults.rs b/macros/tests/test_defaults.rs index d18e177..6b7b8f0 100644 --- a/macros/tests/test_defaults.rs +++ b/macros/tests/test_defaults.rs @@ -1,9 +1,11 @@ +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + +use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_test_helper::TestClient; use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; use reqwest::multipart::Form; -use reqwest::StatusCode; #[derive(TryFromMultipart)] struct Data { @@ -26,6 +28,7 @@ async fn test_defaults() { }; let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(Form::new().text("data", "bar")) .send() diff --git a/macros/tests/test_enum.rs b/macros/tests/test_enum.rs index 612140e..e1c2bf8 100644 --- a/macros/tests/test_enum.rs +++ b/macros/tests/test_enum.rs @@ -1,9 +1,11 @@ +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + +use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_test_helper::TestClient; use axum_typed_multipart::{TryFromField, TryFromMultipart, TypedMultipart}; use reqwest::multipart::Form; -use reqwest::StatusCode; #[derive(Debug, PartialEq, TryFromField)] #[try_from_field(rename_all = "UPPERCASE")] @@ -36,6 +38,7 @@ async fn test_enum() { .text("interests", "PROGRAMMING"); let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(form) .send() diff --git a/macros/tests/test_identifiers.rs b/macros/tests/test_identifiers.rs index 6cd07f1..5a6da85 100644 --- a/macros/tests/test_identifiers.rs +++ b/macros/tests/test_identifiers.rs @@ -1,9 +1,11 @@ +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + +use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_test_helper::TestClient; use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; use reqwest::multipart::Form; -use reqwest::StatusCode; #[derive(TryFromMultipart)] struct Data { @@ -29,6 +31,7 @@ async fn test_identifiers() { .text("source_field", "baz"); let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(form) .send() diff --git a/macros/tests/test_lax.rs b/macros/tests/test_lax.rs index 3d10e9f..f022038 100644 --- a/macros/tests/test_lax.rs +++ b/macros/tests/test_lax.rs @@ -1,9 +1,11 @@ +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + +use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_test_helper::TestClient; use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; use reqwest::multipart::Form; -use reqwest::StatusCode; #[derive(TryFromMultipart)] struct Data { @@ -28,6 +30,7 @@ async fn test_field_order() { .text("", "data"); // should be ignored let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(form) .send() @@ -38,9 +41,12 @@ async fn test_field_order() { #[tokio::test] async fn test_missing_field() { - let handler = |_: TypedMultipart| async { panic!("should not be called") }; + async fn handler(_: TypedMultipart) { + panic!("should not be called"); + } let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(Form::new().text("unknown_field", "data")) .send() diff --git a/macros/tests/test_limit.rs b/macros/tests/test_limit.rs index df64432..9b79eac 100644 --- a/macros/tests/test_limit.rs +++ b/macros/tests/test_limit.rs @@ -1,10 +1,12 @@ +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + use axum::body::Bytes; +use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_test_helper::TestClient; use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; use reqwest::multipart::Form; -use reqwest::StatusCode; #[allow(dead_code)] #[derive(TryFromMultipart)] @@ -63,6 +65,7 @@ async fn test_limit() { for Test { field, size, status, error } in tests.into_iter() { let res = TestClient::new(Router::new().route("/", post(|_: TypedMultipart| async {}))) + .await .post("/") .multipart(Form::new().text(field, "x".repeat(size))) .send() diff --git a/macros/tests/test_list.rs b/macros/tests/test_list.rs index 3ff7c47..f374d1c 100644 --- a/macros/tests/test_list.rs +++ b/macros/tests/test_list.rs @@ -1,9 +1,11 @@ +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + +use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_test_helper::TestClient; use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; use reqwest::multipart::Form; -use reqwest::StatusCode; /// The fields are declared this way to make sure the derive macro supports all /// [Vec] signatures. @@ -21,6 +23,7 @@ async fn test_list() { }; let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(Form::new().text("vec_field", "Apple").text("vec_field", "Orange")) .send() diff --git a/macros/tests/test_option.rs b/macros/tests/test_option.rs index c877de3..61a082e 100644 --- a/macros/tests/test_option.rs +++ b/macros/tests/test_option.rs @@ -1,9 +1,11 @@ +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + +use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_test_helper::TestClient; use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; use reqwest::multipart::Form; -use reqwest::StatusCode; /// The fields are declared this way to make sure the derive macro supports all /// [Option] signatures. @@ -23,6 +25,7 @@ async fn test_option() { }; let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(Form::new().text("option_field", "John")) .send() diff --git a/macros/tests/test_strict.rs b/macros/tests/test_strict.rs index 0d05975..1217ad6 100644 --- a/macros/tests/test_strict.rs +++ b/macros/tests/test_strict.rs @@ -1,9 +1,11 @@ +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] + +use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_test_helper::TestClient; use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; use reqwest::multipart::Form; -use reqwest::StatusCode; #[derive(TryFromMultipart)] #[try_from_multipart(strict)] @@ -21,6 +23,7 @@ async fn test_strict() { let form = Form::new().text("name", "data").text("items", "bread").text("items", "cheese"); let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(form) .send() @@ -31,9 +34,12 @@ async fn test_strict() { #[tokio::test] async fn test_strict_unknown_field() { - let handler = |_: TypedMultipart| async move { panic!("should not be called") }; + async fn handler(_: TypedMultipart) { + panic!("should not be called"); + } let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(Form::new().text("unknown_field", "data")) .send() @@ -45,9 +51,12 @@ async fn test_strict_unknown_field() { #[tokio::test] async fn test_strict_deplicate_field() { - let handler = |_: TypedMultipart| async move { panic!("should not be called") }; + async fn handler(_: TypedMultipart) { + panic!("should not be called"); + } let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(Form::new().text("name", "data").text("name", "bar")) .send() @@ -59,7 +68,9 @@ async fn test_strict_deplicate_field() { #[tokio::test] async fn test_strict_missing_field_name() { - let handler = |_: TypedMultipart| async move { panic!("should not be called") }; + async fn handler(_: TypedMultipart) { + panic!("should not be called"); + } // TODO: The multipart/form-data spec allows for having fields without a // name, but reqwest does not support adding them to the form. Currently we @@ -68,6 +79,7 @@ async fn test_strict_missing_field_name() { let form = Form::new().text("", "data"); let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(form) .send() diff --git a/rustfmt.toml b/rustfmt.toml index 11b0c56..931c3d0 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,11 +1,3 @@ +match_arm_leading_pipes = "Preserve" use_field_init_shorthand = true use_small_heuristics = "Max" -match_arm_leading_pipes = "Preserve" - -# UNSTABLE FEATURES - -# format_code_in_doc_comments = true -# group_imports = "One" -# imports_granularity = "Module" -# overflow_delimited_expr = true -# reorder_impl_items = true diff --git a/src/base_multipart.rs b/src/base_multipart.rs index b4fab95..96afe60 100644 --- a/src/base_multipart.rs +++ b/src/base_multipart.rs @@ -1,9 +1,7 @@ use crate::{TryFromMultipart, TypedMultipartError}; -use axum::body::{Bytes, HttpBody}; -use axum::extract::{FromRequest, Multipart}; -use axum::http::Request; +use axum::async_trait; +use axum::extract::{FromRequest, Multipart, Request}; use axum::response::IntoResponse; -use axum::{async_trait, BoxError}; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; @@ -55,18 +53,15 @@ impl DerefMut for BaseMultipart { } #[async_trait] -impl FromRequest for BaseMultipart +impl FromRequest for BaseMultipart where S: Send + Sync, - B: HttpBody + Send + 'static, - B::Data: Into, - B::Error: Into, T: TryFromMultipart, R: IntoResponse + From, { type Rejection = R; - async fn from_request(req: Request, state: &S) -> Result { + async fn from_request(req: Request, state: &S) -> Result { let multipart = &mut Multipart::from_request(req, state).await.map_err(Into::into)?; let data = T::try_from_multipart(multipart).await?; Ok(Self { data, rejection: PhantomData }) @@ -74,6 +69,7 @@ where } #[cfg(test)] +#[cfg_attr(all(coverage_nightly, test), coverage(off))] mod tests { use super::*; use axum::extract::Multipart; @@ -98,6 +94,7 @@ mod tests { } TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(Form::new()) .send() diff --git a/src/field_data.rs b/src/field_data.rs index 00e0957..49411d2 100644 --- a/src/field_data.rs +++ b/src/field_data.rs @@ -89,15 +89,16 @@ impl TryFromField for FieldData { } #[cfg(test)] +#[cfg_attr(all(coverage_nightly, test), coverage(off))] mod tests { use super::*; use crate::TryFromField; use axum::extract::Multipart; + use axum::http::StatusCode; use axum::routing::post; use axum::Router; use axum_test_helper::TestClient; use reqwest::multipart::{Form, Part}; - use reqwest::StatusCode; #[tokio::test] async fn test_field_data() { @@ -114,6 +115,7 @@ mod tests { let part = Part::text("test").file_name("test.txt").mime_str("text/plain").unwrap(); let res = TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(Form::new().part("input_file", part)) .send() diff --git a/src/lib.rs b/src/lib.rs index 43c0329..782019e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ -//! Designed to seamlessly integrate with -//! [Axum](https://github.com/tokio-rs/axum), this crate simplifies the process -//! of handling `multipart/form-data` requests in your web application by +//! Designed to seamlessly integrate with [Axum](https://github.com/tokio-rs/axum), this crate +//! simplifies the process of handling `multipart/form-data` requests in your web application by //! allowing you to parse the request body into a type-safe struct. //! //! ## Installation @@ -9,21 +8,27 @@ //! cargo add axum_typed_multipart //! ``` //! +//! ### Features +//! +//! All features are enabled by default. +//! +//! - `chrono_0_4`: Enables support for [chrono::DateTime](chrono_0_4::DateTime) (v0.4) +//! - `tempfile_3`: Enables support for [tempfile::NamedTempFile](tempfile_3::NamedTempFile) (v3) +//! - `uuid_1`: Enables support for [uuid::Uuid](uuid_1::Uuid) (v1) +//! //! ## Usage //! //! ### Getting started //! -//! To get started you will need to define a struct with the desired fields and -//! implement the [TryFromMultipart](crate::TryFromMultipart) trait. In the vast -//! majority of cases you will want to use the derive macro to generate the -//! implementation automatically. +//! To get started you will need to define a struct with the desired fields and implement the +//! [TryFromMultipart](crate::TryFromMultipart) trait. In the vast majority of cases you will want +//! to use the derive macro to generate the implementation automatically. //! -//! To be able to derive the [TryFromMultipart](crate::TryFromMultipart) trait -//! every field in the struct must implement the -//! [TryFromField](crate::TryFromField) trait. +//! To be able to derive the [TryFromMultipart](crate::TryFromMultipart) trait every field in the +//! struct must implement the [TryFromField](crate::TryFromField) trait. //! -//! The [TryFromField](crate::TryFromField) trait is implemented by default for -//! the following types: +//! The [TryFromField](crate::TryFromField) trait is implemented by default for the following +//! types: //! - [i8], [i16], [i32], [i64], [i128], [isize] //! - [u8], [u16], [u32], [u64], [u128], [usize] //! - [f32], [f64] @@ -31,14 +36,14 @@ //! - [char] //! - [String] //! - [axum::body::Bytes] -//! - [tempfile::NamedTempFile] (v3) -//! - [uuid::Uuid] (v1) +//! - [chrono::DateTime](chrono_0_4::DateTime) (feature: `chrono_0_4`) +//! - [tempfile::NamedTempFile](tempfile_3::NamedTempFile) (feature: `tempfile_3`) +//! - [uuid::Uuid](uuid_1::Uuid) (feature: `uuid_1`) //! //! If the request body is malformed the request will be aborted with an error. //! -//! An error will be returned if at least one field is missing, with the -//! exception of [Option] and [Vec] types, which will be set respectively as -//! [Option::None] and `[]`. +//! An error will be returned if at least one field is missing, with the exception of [Option] and +//! [Vec] types, which will be set respectively as [Option::None] and `[]`. //! //! ```rust,no_run #![doc = include_str!("../examples/basic.rs")] @@ -46,8 +51,8 @@ //! //! ### Optional fields //! -//! If a field is declared as an [Option] the value will default to [None] when -//! the field is missing from the request body. +//! If a field is declared as an [Option] the value will default to [None] when the field is +//! missing from the request body. //! ```rust //! use axum_typed_multipart::TryFromMultipart; //! @@ -59,8 +64,8 @@ //! //! ### Renaming fields //! -//! If you would like to assign a custom name for the source field you can use -//! the `field_name` parameter of the `form_data` attribute. +//! If you would like to assign a custom name for the source field you can use the `field_name` +//! parameter of the `form_data` attribute. //! ```rust //! use axum_typed_multipart::TryFromMultipart; //! @@ -71,9 +76,9 @@ //! } //! ``` //! -//! The `rename_all` parameter from the `try_from_multipart` attribute can be -//! used to automatically rename each field of your struct to a specific case. -//! It works the same way as `#[serde(rename_all = "...")]`. +//! The `rename_all` parameter from the `try_from_multipart` attribute can be used to automatically +//! rename each field of your struct to a specific case. It works the same way as +//! `#[serde(rename_all = "...")]`. //! //! Supported cases: //! - `snake_case` @@ -93,14 +98,13 @@ //! } //! ``` //! -//! NOTE: If the `#[form_data(field_name = "...")]` attribute is specified, the -//! `rename_all` rule will not be applied. +//! NOTE: If the `#[form_data(field_name = "...")]` attribute is specified, the `rename_all` rule +//! will not be applied. //! //! ### Default values //! -//! If the `default` parameter in the `form_data` attribute is present the value -//! will be populated using the type's [Default] implementation when the field -//! is not supplied in the request. +//! If the `default` parameter in the `form_data` attribute is present the value will be populated +//! using the type's [Default] implementation when the field is not supplied in the request. //! ```rust //! use axum_typed_multipart::TryFromMultipart; //! @@ -113,9 +117,8 @@ //! //! ### Field metadata //! -//! If you need access to the field metadata (e.g. the field headers like file -//! name or content type) you can use the [FieldData](crate::FieldData) struct -//! to wrap your field. +//! If you need access to the field metadata (e.g. the field headers like file name or content +//! type) you can use the [FieldData](crate::FieldData) struct to wrap your field. //! ```rust //! use axum::body::Bytes; //! use axum_typed_multipart::{FieldData, TryFromMultipart}; @@ -128,37 +131,34 @@ //! //! ### Large uploads //! -//! For large uploads you can save the contents of the field to the file system -//! using [tempfile::NamedTempFile]. This will efficiently stream the field data -//! directly to the file system, without needing to fit all the data in memory. -//! Once the upload is complete, you can then save the contents to a location of -//! your choice. For more information check out the -//! [NamedTempFile](tempfile::NamedTempFile) documentation. +//! For large uploads you can save the contents of the field to the file system using +//! [tempfile::NamedTempFile](tempfile_3::NamedTempFile). This will efficiently stream the field +//! data directly to the file system, without needing to fit all the data in memory. Once the +//! upload is complete, you can then save the contents to a location of your choice. For more +//! information check out the [NamedTempFile](tempfile_3::NamedTempFile) documentation. //! //! #### **Warning** -//! Field size limits for [Vec] fields are applied to **each** occurrence of the -//! field. This means that if you have a 1GiB field limit and the field contains +//! Field size limits for [Vec] fields are applied to **each** occurrence of the field. This means +//! that if you have a 1GiB field limit and the field contains //! 5 entries, the total size of the request body will be 5GiB. //! //! #### **Note** -//! When handling large uploads you will need to increase both the request body -//! size limit and the field size limit. The request body size limit can be -//! increased using the [DefaultBodyLimit](axum::extract::DefaultBodyLimit) -//! middleware, while the field size limit can be increased using the `limit` -//! parameter of the `form_data` attribute. +//! When handling large uploads you will need to increase both the request body size limit and the +//! field size limit. The request body size limit can be increased using the +//! [DefaultBodyLimit](axum::extract::DefaultBodyLimit) middleware, while the field size limit can +//! be increased using the `limit` parameter of the `form_data` attribute. //! ```rust,no_run #![doc = include_str!("../examples/upload.rs")] //! ``` //! //! ### Lists //! -//! If the incoming request will include multiple fields that share the same -//! name (AKA lists) the field can be declared as a [Vec], allowing for all -//! occurrences of the field to be stored. +//! If the incoming request will include multiple fields that share the same name (AKA lists) the +//! field can be declared as a [Vec], allowing for all occurrences of the field to be stored. //! //! #### **Warning** -//! Field size limits for [Vec] fields are applied to **each** occurrence of the -//! field. This means that if you have a 1GiB field limit and the field contains +//! Field size limits for [Vec] fields are applied to **each** occurrence of the field. This means +//! that if you have a 1GiB field limit and the field contains //! 5 entries, the total size of the request body will be 5GiB. //! ```rust //! use axum::http::StatusCode; @@ -172,11 +172,10 @@ //! //! ### Strict mode //! -//! By default the derive macro will store the last occurrence of a field and it -//! will ignore unknown fields. This behavior can be changed by using the -//! `strict` parameter in the derive macro. This will make the macro throw an -//! error if the request contains multiple fields with the same name or if it -//! contains unknown fields. In addition when using strict mode sending fields +//! By default the derive macro will store the last occurrence of a field and it will ignore +//! unknown fields. This behavior can be changed by using the `strict` parameter in the derive +//! macro. This will make the macro throw an error if the request contains multiple fields with the +//! same name or if it contains unknown fields. In addition when using strict mode sending fields //! with a missing or empty name will result in an error. //! ```rust //! use axum_typed_multipart::TryFromMultipart; @@ -228,33 +227,38 @@ //! ### Custom types //! //! If you would like to use a custom type for a field you need to implement the -//! [TryFromField](crate::TryFromField) trait for your type. This will allow the -//! derive macro to generate the [TryFromMultipart](crate::TryFromMultipart) -//! implementation automatically. Instead of implementing the trait directly, it -//! is recommended to implement the [TryFromChunks](crate::TryFromChunks) trait -//! and the [TryFromField](crate::TryFromField) trait will be implemented -//! automatically. This is recommended since you won't need to manually +//! [TryFromField](crate::TryFromField) trait for your type. This will allow the derive macro to +//! generate the [TryFromMultipart](crate::TryFromMultipart) implementation automatically. Instead +//! of implementing the trait directly, it is recommended to implement the +//! [TryFromChunks](crate::TryFromChunks) trait and the [TryFromField](crate::TryFromField) trait +//! will be implemented automatically. This is recommended since you won't need to manually //! implement the size limit logic. //! -//! To implement the [TryFromChunks](crate::TryFromChunks) trait for external -//! types you will need to create a newtype wrapper and implement the trait for -//! the wrapper. +//! To implement the [TryFromChunks](crate::TryFromChunks) trait for external types you will need +//! to create a newtype wrapper and implement the trait for the wrapper. //! //! ### Custom error format //! -//! When using [TypedMultipart](crate::TypedMultipart) as an argument for your -//! handlers, when the request is malformed, the error will be serialized as a -//! string. If you would like to customize the error format you can use the -//! [BaseMultipart](crate::BaseMultipart) struct instead. This struct is used -//! internally by [TypedMultipart](crate::TypedMultipart) and it can be used to +//! When using [TypedMultipart](crate::TypedMultipart) as an argument for your handlers, when the +//! request is malformed, the error will be serialized as a string. If you would like to customize +//! the error format you can use the [BaseMultipart](crate::BaseMultipart) struct instead. This +//! struct is used internally by [TypedMultipart](crate::TypedMultipart) and it can be used to //! customize the error type. //! -//! To customize the error you will need to define a custom error type and -//! implement [IntoResponse](axum::response::IntoResponse) and -//! `From`. +//! To customize the error you will need to define a custom error type and implement +//! [IntoResponse](axum::response::IntoResponse) and `From`. //! ```rust,no_run #![doc = include_str!("../examples/custom_error.rs")] //! ``` +//! +//! ### Validation +//! +//! In order to perform validation on the various attributes of a field, I would recommend using +//! the [validator](https://crates.io/crates/validator) crate together with the +//! [axum-valid](https://crates.io/crates/axum-valid) crate. A nice example can be found at +//! [docs.rs](https://docs.rs/axum-valid/0.19.0/axum_valid/#-validatede-modifiede-validifiede-and-validifiedbyrefe). + +#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))] pub use axum_typed_multipart_macros::{TryFromField, TryFromMultipart}; diff --git a/src/try_from_chunks.rs b/src/try_from_chunks.rs index f06ab28..35387b0 100644 --- a/src/try_from_chunks.rs +++ b/src/try_from_chunks.rs @@ -5,10 +5,7 @@ use bytes::BytesMut; use futures_core::stream::Stream; use futures_util::stream::StreamExt; use std::any::type_name; -use tempfile::NamedTempFile; -use tokio::fs::File as AsyncFile; -use tokio::io::AsyncWriteExt; -use uuid::Uuid; +use std::str::FromStr; /// Types that can be created from a [Stream] of [Bytes]. /// @@ -122,15 +119,17 @@ gen_try_from_chunks_impl!(f64); gen_try_from_chunks_impl!(bool); // TODO?: Consider accepting any thruthy value. gen_try_from_chunks_impl!(char); +#[cfg(feature = "tempfile_3")] #[async_trait] -impl TryFromChunks for NamedTempFile { +impl TryFromChunks for tempfile_3::NamedTempFile { async fn try_from_chunks( mut chunks: impl Stream> + Send + Sync + Unpin, _: FieldMetadata, ) -> Result { - let temp_file = NamedTempFile::new().map_err(anyhow::Error::new)?; + use tokio::io::AsyncWriteExt as _; + let temp_file = tempfile_3::NamedTempFile::new().map_err(anyhow::Error::new)?; let std_file = temp_file.reopen().map_err(anyhow::Error::new)?; - let mut async_file = AsyncFile::from_std(std_file); + let mut async_file = tokio::fs::File::from_std(std_file); while let Some(chunk) = chunks.next().await { let chunk = chunk?; @@ -143,22 +142,53 @@ impl TryFromChunks for NamedTempFile { } } +#[cfg(feature = "uuid_1")] #[async_trait] -impl TryFromChunks for Uuid { +impl TryFromChunks for uuid_1::Uuid { async fn try_from_chunks( chunks: impl Stream> + Send + Sync + Unpin, metadata: FieldMetadata, ) -> Result { let field_name = get_field_name(&metadata.name); let bytes = Bytes::try_from_chunks(chunks, metadata).await?; - Uuid::try_parse_ascii(&bytes).map_err(|err| TypedMultipartError::WrongFieldType { + uuid_1::Uuid::try_parse_ascii(&bytes).map_err(|err| TypedMultipartError::WrongFieldType { field_name, - wanted_type: type_name::().to_string(), + wanted_type: type_name::().to_string(), source: err.into(), }) } } +#[cfg(feature = "chrono_0_4")] +#[async_trait] +impl TryFromChunks for chrono_0_4::DateTime +where + Err: Into, + Tz: chrono_0_4::TimeZone, + chrono_0_4::DateTime: FromStr, +{ + async fn try_from_chunks( + chunks: impl Stream> + Send + Sync + Unpin, + metadata: FieldMetadata, + ) -> Result { + let field_name = get_field_name(&metadata.name); + let bytes = Bytes::try_from_chunks(chunks, metadata).await?; + let body_str = + std::str::from_utf8(&bytes).map_err(|err| TypedMultipartError::WrongFieldType { + field_name: field_name.clone(), + wanted_type: type_name::>().to_string(), + source: err.into(), + })?; + chrono_0_4::DateTime::::from_str(body_str).map_err(|err| { + TypedMultipartError::WrongFieldType { + field_name, + wanted_type: type_name::>().to_string(), + source: err.into(), + } + }) + } +} + fn get_field_name(name: &Option) -> String { // Theoretically, the name should always be present, but it's better to be // safe than sorry. @@ -166,6 +196,7 @@ fn get_field_name(name: &Option) -> String { } #[cfg(test)] +#[cfg_attr(all(coverage_nightly, test), coverage(off))] mod tests { use super::*; use bytes::Bytes; @@ -316,16 +347,34 @@ mod tests { #[tokio::test] async fn test_try_from_chunks_uuid() { let valid_input = "550e8400-e29b-41d4-a716-446655440000"; - let valid_output = Uuid::parse_str(valid_input).unwrap(); - test_try_from_chunks_valid::(valid_input, valid_output).await; - test_try_from_chunks_invalid::("invalid").await; + let valid_output = uuid_1::Uuid::parse_str(valid_input).unwrap(); + test_try_from_chunks_valid::(valid_input, valid_output).await; + test_try_from_chunks_invalid::("invalid").await; + } + + #[tokio::test] + async fn test_try_from_chunks_chrono_datetime_fixed() { + type DateTime = chrono_0_4::DateTime; + let valid_input = "2024-01-01T04:20:00Z"; + let valid_output = DateTime::parse_from_rfc3339(valid_input).unwrap(); + test_try_from_chunks_valid::(valid_input, valid_output).await; + test_try_from_chunks_invalid::("invalid").await; + } + + #[tokio::test] + async fn test_try_from_chunks_chrono_datetime_utc() { + type DateTime = chrono_0_4::DateTime; + let valid_input = "2024-01-01T04:20:00Z"; + let valid_output = DateTime::from_str(valid_input).unwrap(); + test_try_from_chunks_valid::(valid_input, valid_output).await; + test_try_from_chunks_invalid::(Bytes::from_static(&[0, 159, 146, 150])).await; } #[tokio::test] async fn test_try_from_chunks_named_temp_file() { let chunks = create_chunks("Hello, dear world!"); let metadata = FieldMetadata { name: Some("test".into()), ..Default::default() }; - let file = NamedTempFile::try_from_chunks(chunks, metadata).await.unwrap(); + let file = tempfile_3::NamedTempFile::try_from_chunks(chunks, metadata).await.unwrap(); let mut buffer = String::new(); file.reopen().unwrap().read_to_string(&mut buffer).unwrap(); diff --git a/src/try_from_field.rs b/src/try_from_field.rs index 31339d7..be86eff 100644 --- a/src/try_from_field.rs +++ b/src/try_from_field.rs @@ -61,6 +61,7 @@ where } #[cfg(test)] +#[cfg_attr(all(coverage_nightly, test), coverage(off))] mod tests { use super::*; use axum::extract::Multipart; @@ -97,6 +98,7 @@ mod tests { }; TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(Form::new().text("data", input)) .send() diff --git a/src/typed_multipart.rs b/src/typed_multipart.rs index 7f0f111..6de54da 100644 --- a/src/typed_multipart.rs +++ b/src/typed_multipart.rs @@ -1,8 +1,6 @@ use crate::{BaseMultipart, TryFromMultipart, TypedMultipartError}; -use axum::body::{Bytes, HttpBody}; -use axum::extract::FromRequest; -use axum::http::Request; -use axum::{async_trait, BoxError}; +use axum::async_trait; +use axum::extract::{FromRequest, Request}; use std::ops::{Deref, DerefMut}; /// Used as as an argument for axum [Handlers](axum::handler::Handler). @@ -48,23 +46,21 @@ impl DerefMut for TypedMultipart { } #[async_trait] -impl FromRequest for TypedMultipart +impl FromRequest for TypedMultipart where T: TryFromMultipart, - B: HttpBody + Send + 'static, - B::Data: Into, - B::Error: Into, S: Send + Sync, { type Rejection = TypedMultipartError; - async fn from_request(req: Request, state: &S) -> Result { + async fn from_request(req: Request, state: &S) -> Result { let base = BaseMultipart::::from_request(req, state).await?; Ok(Self(base.data)) } } #[cfg(test)] +#[cfg_attr(all(coverage_nightly, test), coverage(off))] mod tests { use super::*; use axum::extract::Multipart; @@ -89,6 +85,7 @@ mod tests { } TestClient::new(Router::new().route("/", post(handler))) + .await .post("/") .multipart(Form::new()) .send() diff --git a/src/typed_multipart_error.rs b/src/typed_multipart_error.rs index 7898efa..fdf88a3 100644 --- a/src/typed_multipart_error.rs +++ b/src/typed_multipart_error.rs @@ -66,44 +66,41 @@ impl IntoResponse for TypedMultipartError { } #[cfg(test)] +#[cfg_attr(all(coverage_nightly, test), coverage(off))] mod tests { use super::*; - use axum::body::HttpBody; - use axum::extract::{FromRequest, Multipart}; - use axum::http::{Request, StatusCode}; + use axum::extract::{FromRequest, Multipart, Request}; + use axum::http::{header, StatusCode}; use axum::routing::post; - use axum::{async_trait, BoxError, Router}; + use axum::{async_trait, Router}; use axum_test_helper::TestClient; - use bytes::Bytes; - use reqwest::header; struct Data(); #[async_trait] - impl FromRequest for Data + impl FromRequest for Data where - B: HttpBody + Send + 'static, - B::Data: Into, - B::Error: Into, S: Send + Sync, { type Rejection = TypedMultipartError; - async fn from_request(req: Request, state: &S) -> Result { + async fn from_request(req: Request, state: &S) -> Result { let multipart = &mut Multipart::from_request(req, state).await?; while multipart.next_field().await?.is_some() {} unreachable!() } } - fn create_client() -> TestClient { - let handler = |_: Data| async { panic!("should never be called") }; - TestClient::new(Router::new().route("/", post(handler))) + async fn create_client() -> TestClient { + async fn handler(_: Data) { + panic!("should never be called") + } + TestClient::new(Router::new().route("/", post(handler))).await } #[tokio::test] async fn test_invalid_request() { - let res = create_client().post("/").json(&"{}").send().await; + let res = create_client().await.post("/").json(&"{}").send().await; assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert!(res.text().await.contains("request is malformed")); } @@ -111,8 +108,9 @@ mod tests { #[tokio::test] async fn test_invalid_request_body() { let res = create_client() + .await .post("/") - .header(header::CONTENT_TYPE, "multipart/form-data; boundary=BOUNDARY") + .header(header::CONTENT_TYPE.as_str(), "multipart/form-data; boundary=BOUNDARY") .body("BOUNDARY\r\n\r\nBOUNDARY--\r\n") .send() .await; diff --git a/taplo.toml b/taplo.toml new file mode 100644 index 0000000..a67c9a3 --- /dev/null +++ b/taplo.toml @@ -0,0 +1,6 @@ +[formatting] +allowed_blank_lines = 1 +column_width = 100 +indent_string = " " +reorder_arrays = true +reorder_keys = true