├── .cargo └── config.toml ├── .github ├── dependabot.yaml └── workflows │ ├── not_dependabot.yaml │ ├── publish.yaml │ ├── validate.yaml │ ├── validate_history.yaml │ └── validate_pr_commits.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.j2 ├── README.md ├── lazy_errors ├── Cargo.toml └── src │ ├── err.rs │ ├── error.rs │ ├── into_eyre.rs │ ├── lib.rs │ ├── or_create_stash.rs │ ├── or_stash.rs │ ├── or_wrap.rs │ ├── or_wrap_with.rs │ ├── prelude.rs │ ├── stash.rs │ ├── stash_err.rs │ ├── surrogate_error_trait │ ├── mod.rs │ └── prelude.rs │ ├── try2.rs │ ├── try_collect_or_stash.rs │ └── try_map_or_stash.rs ├── rustfmt.toml ├── tarpaulin.toml └── xtask ├── Cargo.toml └── src ├── ci.rs ├── main.rs └── version.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --" 3 | 4 | [env] 5 | MIRIFLAGS = "-Zmiri-disable-isolation" 6 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | versioning-strategy: lockfile-only 12 | # Dependabot can only handle direct dependencies in `Cargo.toml`, 13 | # but not transitive ones that are only part of `Cargo.lock`. 14 | # When a direct dependency needs an update, Dependabot will create a PR 15 | # that does _not_ update transitive dependencies. Since the PR 16 | # may have an outdated `Cargo.lock`, the build will fail. 17 | # Thus, we disable Dependabot for Cargo here until Dependabot is fixed. 18 | open-pull-requests-limit: 0 19 | 20 | - package-ecosystem: "github-actions" 21 | directory: "/" 22 | schedule: 23 | interval: "daily" 24 | -------------------------------------------------------------------------------- /.github/workflows/not_dependabot.yaml: -------------------------------------------------------------------------------- 1 | # This workflow is triggered on schedule and will check the primary branch(es) 2 | # of this repository for whether dependencies are secure, up-to-date, and 3 | # whether the build succeeds both with the checked-in `Cargo.lock`, 4 | # was well as after running `cargo update`. 5 | # 6 | # Using the checked-in `Cargo.lock` ensures that 7 | # local checkouts of this repository still build. 8 | # Using an updated `Cargo.lock` ensures that this crate can still be built 9 | # when it is used as a dependency: in that case, Cargo will resolve 10 | # dependencies of this crate in a semver-compatible way, based on 11 | # the entire set of dependencies of the project depending on this crate. 12 | # 13 | # The pipeline may fail for several reasons, such as: 14 | # 15 | # - Security vulnerabilities getting reported for dependencies 16 | # - New versions (major or minor) of dependencies getting released 17 | # - New clippy lints 18 | # - Different rustfmt behavior 19 | # - ... 20 | # 21 | # This pipeline will update `Cargo.lock` locally if necessary. 22 | # The updated `Cargo.lock` file will be discarded after the run. 23 | # Note that other pipelines in this repo _require_ `Cargo.lock` be up-to-date. 24 | # If it isn't, you'll have to run `cargo update`, commit `Cargo.lock`, and 25 | # rebase your branch to have that commit be the first one in your pull request. 26 | 27 | name: Not Dependabot 28 | 29 | on: 30 | 31 | schedule: 32 | # ┌───────────── minute (0 - 59) 33 | # │ ┌───────────── hour (0 - 23) 34 | # │ │ ┌───────────── day of the month (1 - 31) 35 | # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) 36 | # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) 37 | # │ │ │ │ │ 38 | # │ │ │ │ │ 39 | # │ │ │ │ │ 40 | - cron: '0 0 * * *' 41 | 42 | workflow_dispatch: 43 | 44 | jobs: 45 | 46 | # Determine the branch names on which to run the actual jobs below. 47 | init: 48 | timeout-minutes: 1 49 | runs-on: ubuntu-latest 50 | 51 | outputs: 52 | branches: ${{ steps.set_branches.outputs.branches }} 53 | 54 | steps: 55 | - id: set_branches 56 | name: "Determine branches to check" 57 | run: | 58 | case "${{ github.event_name }}" in 59 | "schedule") branches="[ \"main\" ]" ;; 60 | "workflow_dispatch") branches="[ \"${{ github.ref }}\" ]" ;; 61 | *) 62 | echo "Unknown event: ${{ github.event_name }}" >&2 63 | exit 1 64 | esac 65 | echo "branches=$branches" >> $GITHUB_OUTPUT 66 | 67 | deps: 68 | needs: init 69 | timeout-minutes: 15 70 | 71 | # Results should be the same on all systems. 72 | runs-on: ubuntu-latest 73 | 74 | strategy: 75 | matrix: 76 | branch: ${{ fromJson(needs.init.outputs.branches) }} 77 | 78 | steps: 79 | 80 | - name: Install stable Rust toolchain 81 | uses: dtolnay/rust-toolchain@stable 82 | 83 | - name: Install cargo-audit & cargo-upgrades 84 | uses: taiki-e/install-action@v2 85 | with: 86 | tool: cargo-audit, cargo-upgrades 87 | 88 | - uses: actions/checkout@v4 89 | with: 90 | ref: ${{ matrix.branch }} 91 | 92 | # `cargo audit` checks `Cargo.lock`, so run it before/without `cargo update` 93 | - name: Check dependencies for security vulnerabilities 94 | run: cargo --locked audit --deny warnings 95 | 96 | - name: Check dependencies for new major/minor versions 97 | run: cargo --locked upgrades 98 | 99 | build: 100 | needs: init 101 | timeout-minutes: 60 102 | runs-on: ${{ matrix.system }} 103 | 104 | strategy: 105 | matrix: 106 | system: [ ubuntu-latest, windows-latest, macos-latest ] 107 | branch: ${{ fromJson(needs.init.outputs.branches) }} 108 | cargo_update: [ false, true ] 109 | 110 | steps: 111 | 112 | - name: Install nightly Rust toolchain with rustfmt & miri 113 | uses: dtolnay/rust-toolchain@nightly 114 | with: 115 | components: rustfmt, miri 116 | 117 | - name: Install stable Rust toolchain with clippy 118 | uses: dtolnay/rust-toolchain@stable 119 | with: 120 | components: clippy 121 | 122 | - name: Install cargo-hack & cargo-tarpaulin 123 | uses: taiki-e/install-action@v2 124 | with: 125 | tool: cargo-hack, cargo-tarpaulin 126 | 127 | # Avoid linter errors due to changed line endings 128 | - run: git config --global core.autocrlf false 129 | 130 | - uses: actions/checkout@v4 131 | with: 132 | ref: ${{ matrix.branch }} 133 | 134 | - name: Update dependencies in Cargo.lock 135 | run: cargo update 136 | if: ${{ matrix.cargo_update }} 137 | 138 | # FIXME: `xtask ci miri` runs `cargo clean`, deleting its own binary. 139 | # FIXME: This is not possible on Windows, so install it before running it. 140 | - name: Install xtask 141 | run: cargo install --path xtask 142 | 143 | # Run the actual tests (note that we already checked dependencies above). 144 | - name: Run CI checks 145 | run: xtask ci all --skip-dependency-checks 146 | 147 | msrv: 148 | needs: init 149 | timeout-minutes: 60 150 | runs-on: ${{ matrix.system }} 151 | 152 | strategy: 153 | matrix: 154 | rust-version: [ "1.81", "1.77", "1.69", "1.66", "1.64", "1.61" ] 155 | system: [ ubuntu-latest, windows-latest, macos-latest ] 156 | branch: ${{ fromJson(needs.init.outputs.branches) }} 157 | profile: [ dev, release ] 158 | 159 | steps: 160 | 161 | # `xtask` needs a recent Rust version. 162 | - name: Install stable Rust toolchain 163 | uses: dtolnay/rust-toolchain@stable 164 | 165 | - name: Install cargo-hack 166 | uses: taiki-e/install-action@v2 167 | with: 168 | tool: cargo-hack 169 | 170 | - uses: actions/checkout@v4 171 | with: 172 | ref: ${{ matrix.branch }} 173 | 174 | # Install `xtask` using a recent Rust version to avoid build failures. 175 | # FIXME: `xtask ci build` tries to overwrite its own binary. 176 | # FIXME: This is not possible on Windows, so install it before running it. 177 | - name: Install xtask 178 | run: cargo install --path xtask 179 | 180 | - name: Install Rust ${{ matrix.rust-version }} toolchain with clippy 181 | uses: dtolnay/rust-toolchain@master 182 | with: 183 | toolchain: ${{ matrix.rust-version }} 184 | components: clippy 185 | 186 | # TODO: Find a way to downgrade the file format version while still being 187 | # able to run tests using the checked-in versions of dependencies. 188 | - name: Delete Cargo.lock to use up-to-date dependencies and correct file format version 189 | run: rm Cargo.lock 190 | 191 | - name: Run clippy 192 | run: > 193 | xtask ci clippy 194 | --rust-version ${{ matrix.rust-version }} 195 | --exclude-xtask 196 | --profile ${{ matrix.profile }} 197 | 198 | - name: Run tests 199 | run: > 200 | xtask ci test 201 | --rust-version ${{ matrix.rust-version }} 202 | --exclude-xtask 203 | --profile ${{ matrix.profile }} 204 | 205 | - name: Build artifacts 206 | run: > 207 | xtask ci build 208 | --rust-version ${{ matrix.rust-version }} 209 | --exclude-xtask 210 | --profile ${{ matrix.profile }} 211 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # When a tag matching a certain pattern is pushed, 2 | # do a quick validation of the software at that point in the git history, 3 | # import the version number from the tag name and write it to Cargo.toml, 4 | # publish the crate to crates.io, and create a GitHub release. 5 | # 6 | # We're only doing a quick validation in this pipeline. 7 | # In the past we've called the `validate.yaml` from this pipeline. 8 | # However, this introduces a “race condition” with the rest of the 9 | # Rust ecosystem: `validate.yaml` checks several “moving targets”, 10 | # such as whether `Cargo.lock` is up-to-date. Thus, any commit 11 | # that had already passed the `validate.yaml` check may fail it 12 | # if it is checked again at some later point in time, for example 13 | # when a new version of any (transitive) dependency gets released. 14 | # Since we've validated each commit on the main branch, 15 | # all commits on the main branch are good to release per se. 16 | # If we rejected such a release just because we're a few minutes 17 | # “too late” (whatever that means), we would have to publish 18 | # that version manually or we would have to delete the git tag. 19 | # Even if we noticed before creating the tag, this would require 20 | # us to create a “hotfix” branch for completely artificial reasons. 21 | 22 | name: Publish 23 | 24 | on: 25 | push: 26 | tags: 27 | - 'v*.*.*' 28 | 29 | jobs: 30 | 31 | validate: 32 | timeout-minutes: 60 33 | runs-on: ${{ matrix.system }} 34 | 35 | strategy: 36 | matrix: 37 | system: [ ubuntu-latest, windows-latest, macos-latest ] 38 | 39 | steps: 40 | 41 | # On some agents there are linter errors due to changed line endings. 42 | - name: Configure Git 43 | run: git config --global core.autocrlf false 44 | 45 | - uses: actions/checkout@v4 46 | 47 | - name: Install stable Rust toolchain 48 | uses: dtolnay/rust-toolchain@stable 49 | 50 | - name: Install cargo-hack 51 | uses: taiki-e/install-action@v2 52 | with: 53 | tool: cargo-hack 54 | 55 | # FIXME: `xtask ci` may run `cargo build`, overwriting its own binary. 56 | # FIXME: This is not possible on Windows, so install it before running it. 57 | - name: Run `xtask ci all --skip-moving-targets` 58 | run: | 59 | cargo install --path xtask 60 | xtask ci all --skip-moving-targets 61 | 62 | publish: 63 | needs: validate 64 | permissions: 65 | contents: write 66 | timeout-minutes: 15 67 | runs-on: ubuntu-latest 68 | 69 | steps: 70 | 71 | # Fetch all tags since the earliest release that we (still) want to 72 | # include in the changelog generation process. 73 | # Fetch the history of the current HEAD since that date as well, 74 | # so `git tag --merged` shows which tags are actually part of HEAD. 75 | # TODO: Use `--shallow-since $DATE` when it's not buggy anymore. 76 | - uses: actions/checkout@v4 77 | with: 78 | fetch-depth: 0 79 | 80 | # All tags are here but the one that triggered this workflow 81 | # will not be found by `git describe` yet. See: 82 | # https://github.com/actions/checkout/issues/290 83 | - name: Fetch missing tags 84 | run: git fetch --force --tags 85 | 86 | - name: Install stable Rust toolchain 87 | uses: dtolnay/rust-toolchain@stable 88 | 89 | - name: Install cargo-edit 90 | uses: taiki-e/install-action@v2 91 | with: 92 | tool: cargo-edit 93 | 94 | # Import the version number from git and write it to Cargo.{lock,toml}. 95 | # 96 | # FYI: https://github.com/rust-lang/cargo/issues/6583 97 | # TODO: Maybe move version setting from xtask into build.rs. 98 | - name: Set version number 99 | run: > 100 | cargo xtask version 101 | import git-describe --accept=major-minor-patch 102 | 103 | # Create a temporary commit to allow running `cargo publish` 104 | # without having to use `--allow-dirty` (may be too permissive). 105 | - name: Whitelist changed files 106 | run: | 107 | git config --global user.email "runner@github.com" 108 | git config --global user.name "GitHub, Runner" 109 | git commit -m 'Set version number' Cargo.lock '*/Cargo.toml' 110 | 111 | - name: Publish crate 112 | run: > 113 | cargo publish 114 | --package lazy_errors 115 | env: 116 | CARGO_REGISTRY_TOKEN: "${{ secrets.CRATES_IO_API_TOKEN }}" 117 | -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | # Runs the CI quality gate: compilation, linting, testing, dependency checking, 2 | # and so on. Parallel implementation of the steps defined in the `xtask` crate. 3 | # 4 | # This pipeline is triggered on push and on pull request events. 5 | # If triggered by a push, this pipeline will check the commit that was pushed 6 | # (i.e. the Git HEAD of the branch that was pushed to). 7 | # If triggered by a PR event, this pipeline will validate 8 | # what would be the result of merging that PR. 9 | # If you're pushing to a branch that has an open PR, both will be checked. 10 | # 11 | # Note that this pipeline requires `Cargo.lock` be up-to-date. 12 | # If it isn't, you'll have to run `cargo update`, commit `Cargo.lock`, and 13 | # rebase your branch to have that commit be the first one in your pull request. 14 | # 15 | # Please note the following refs: 16 | # 17 | # - github.ref → refs/heads/$branch_name, resp. refs/pull/$pr_id/merge 18 | # - github.ref_name → $branch_name, resp. $pr_id/merge 19 | # - github.head_ref → $source_branch_name, e.g. "feature/foobar" 20 | # - github.base_ref → $destination_branch, e.g. "main" 21 | 22 | name: Validate 23 | 24 | on: 25 | 26 | push: 27 | branches: 28 | - '**' 29 | 30 | pull_request: 31 | types: 32 | - opened 33 | - synchronize 34 | - reopened 35 | 36 | workflow_call: 37 | 38 | jobs: 39 | 40 | # Determine the branch names on which to run the actual jobs below. 41 | init: 42 | timeout-minutes: 1 43 | runs-on: ubuntu-latest 44 | 45 | outputs: 46 | branches: ${{ steps.set_branches.outputs.branches }} 47 | 48 | steps: 49 | - id: set_branches 50 | name: "Determine branches to check" 51 | run: | 52 | case "${{ github.event_name }}" in 53 | "push") branches="[ \"${{ github.ref_name }}\" ]" ;; 54 | "pull_request") branches="[ \"${{ github.ref }}\" ]" ;; 55 | *) 56 | echo "Unknown event: ${{ github.event_name }}" >&2 57 | exit 1 58 | esac 59 | echo "branches=$branches" >> $GITHUB_OUTPUT 60 | 61 | 62 | rustfmt: 63 | needs: init 64 | timeout-minutes: 5 65 | 66 | # Results should be the same on all systems. 67 | runs-on: ubuntu-latest 68 | 69 | strategy: 70 | matrix: 71 | branch: ${{ fromJson(needs.init.outputs.branches) }} 72 | 73 | steps: 74 | 75 | # Avoid linter errors due to changed line endings 76 | - run: git config --global core.autocrlf false 77 | 78 | - uses: actions/checkout@v4 79 | with: 80 | ref: ${{ matrix.branch }} 81 | 82 | - name: Install nightly Rust toolchain with rustfmt 83 | uses: dtolnay/rust-toolchain@nightly 84 | with: 85 | components: rustfmt 86 | 87 | - name: Run rustfmt 88 | run: cargo xtask ci rustfmt 89 | 90 | 91 | build: 92 | name: build & test 93 | needs: init 94 | timeout-minutes: 15 95 | runs-on: ${{ matrix.system }} 96 | 97 | strategy: 98 | matrix: 99 | system: [ ubuntu-latest, windows-latest, macos-latest ] 100 | branch: ${{ fromJson(needs.init.outputs.branches) }} 101 | profile: [ dev, release ] 102 | 103 | steps: 104 | 105 | - name: Install stable Rust toolchain with clippy 106 | uses: dtolnay/rust-toolchain@stable 107 | with: 108 | components: clippy 109 | 110 | - name: Install cargo-hack 111 | uses: taiki-e/install-action@v2 112 | with: 113 | tool: cargo-hack 114 | 115 | - uses: actions/checkout@v4 116 | with: 117 | ref: ${{ matrix.branch }} 118 | 119 | # FIXME: `xtask ci build` tries to overwrite its own binary. 120 | # FIXME: This is not possible on Windows, so install it before running it. 121 | - name: Install xtask 122 | run: cargo install --path xtask 123 | 124 | - name: Run clippy 125 | run: xtask ci clippy --profile ${{ matrix.profile }} 126 | 127 | - name: Run tests 128 | run: xtask ci test --profile ${{ matrix.profile }} 129 | 130 | - name: Build artifacts 131 | run: xtask ci build --profile ${{ matrix.profile }} 132 | 133 | 134 | coverage: 135 | needs: init 136 | timeout-minutes: 15 137 | runs-on: ${{ matrix.system }} 138 | 139 | strategy: 140 | matrix: 141 | system: [ ubuntu-latest ] 142 | branch: ${{ fromJson(needs.init.outputs.branches) }} 143 | profile: [ dev, release ] 144 | 145 | steps: 146 | 147 | - uses: actions/checkout@v4 148 | with: 149 | ref: ${{ matrix.branch }} 150 | 151 | # FIXME: Remove nightly once tarpaulin can run doctests on stable again 152 | - name: Install nightly Rust toolchain 153 | uses: dtolnay/rust-toolchain@nightly 154 | 155 | - name: Install stable Rust toolchain 156 | uses: dtolnay/rust-toolchain@stable 157 | 158 | - name: Install cargo-tarpaulin 159 | uses: taiki-e/install-action@v2 160 | with: 161 | tool: cargo-tarpaulin 162 | 163 | - name: Check coverage 164 | run: cargo xtask ci tarpaulin --profile ${{ matrix.profile }} 165 | 166 | 167 | miri: 168 | needs: init 169 | timeout-minutes: 15 170 | runs-on: ${{ matrix.system }} 171 | 172 | strategy: 173 | matrix: 174 | system: [ ubuntu-latest, windows-latest, macos-latest ] 175 | branch: ${{ fromJson(needs.init.outputs.branches) }} 176 | 177 | steps: 178 | 179 | - uses: actions/checkout@v4 180 | with: 181 | ref: ${{ matrix.branch }} 182 | 183 | - name: Install nightly Rust toolchain with miri 184 | uses: dtolnay/rust-toolchain@nightly 185 | with: 186 | components: miri 187 | 188 | - name: Install cargo-hack 189 | uses: taiki-e/install-action@v2 190 | with: 191 | tool: cargo-hack 192 | 193 | # FIXME: `xtask ci miri` runs `cargo clean`, deleting its own binary. 194 | # FIXME: This is not possible on Windows, so install it before running it. 195 | - name: Install xtask 196 | run: cargo install --path xtask 197 | 198 | - name: Run MIRI tests 199 | run: xtask ci miri 200 | 201 | 202 | docs: 203 | needs: init 204 | timeout-minutes: 5 205 | runs-on: ${{ matrix.system }} 206 | 207 | strategy: 208 | matrix: 209 | system: [ ubuntu-latest, windows-latest, macos-latest ] 210 | branch: ${{ fromJson(needs.init.outputs.branches) }} 211 | profile: [ dev, release ] 212 | 213 | steps: 214 | 215 | - uses: actions/checkout@v4 216 | with: 217 | ref: ${{ matrix.branch }} 218 | 219 | - name: Install stable Rust toolchain 220 | uses: dtolnay/rust-toolchain@stable 221 | 222 | - name: Install cargo-hack 223 | uses: taiki-e/install-action@v2 224 | with: 225 | tool: cargo-hack 226 | 227 | - name: Build documentation 228 | run: cargo xtask ci docs --profile ${{ matrix.profile }} 229 | 230 | 231 | deps: 232 | needs: init 233 | timeout-minutes: 5 234 | 235 | # Results should be the same on all systems. 236 | runs-on: ubuntu-latest 237 | 238 | strategy: 239 | matrix: 240 | branch: ${{ fromJson(needs.init.outputs.branches) }} 241 | 242 | steps: 243 | 244 | - uses: actions/checkout@v4 245 | with: 246 | ref: ${{ matrix.branch }} 247 | 248 | - name: Install stable Rust toolchain 249 | uses: dtolnay/rust-toolchain@stable 250 | 251 | - name: Install cargo-audit & cargo-upgrades 252 | uses: taiki-e/install-action@v2 253 | with: 254 | tool: cargo-audit, cargo-upgrades 255 | 256 | - name: Check dependencies 257 | run: cargo xtask ci deps 258 | 259 | 260 | msrv: 261 | needs: init 262 | timeout-minutes: 15 263 | runs-on: ${{ matrix.system }} 264 | 265 | strategy: 266 | matrix: 267 | rust-version: [ "1.81", "1.61" ] 268 | system: [ ubuntu-latest, windows-latest, macos-latest ] 269 | branch: ${{ fromJson(needs.init.outputs.branches) }} 270 | profile: [ dev, release ] 271 | 272 | steps: 273 | 274 | # `xtask` needs a recent Rust version. 275 | - name: Install stable Rust toolchain 276 | uses: dtolnay/rust-toolchain@stable 277 | 278 | - name: Install cargo-hack 279 | uses: taiki-e/install-action@v2 280 | with: 281 | tool: cargo-hack 282 | 283 | - uses: actions/checkout@v4 284 | with: 285 | ref: ${{ matrix.branch }} 286 | 287 | # Install `xtask` using a recent Rust version to avoid build failures. 288 | # FIXME: `xtask ci build` tries to overwrite its own binary. 289 | # FIXME: This is not possible on Windows, so install it before running it. 290 | - name: Install xtask 291 | run: cargo install --path xtask 292 | 293 | - name: Install Rust ${{ matrix.rust-version }} toolchain with clippy 294 | uses: dtolnay/rust-toolchain@master 295 | with: 296 | toolchain: ${{ matrix.rust-version }} 297 | components: clippy 298 | 299 | # Downgrade `Cargo.lock` file format version if necessary. Note that we 300 | # nevertheless have to make sure that the file is up-to-date per se. 301 | # That is done by the `deps` job above. 302 | - name: Delete Cargo.lock to use up-to-date dependencies and correct file format version 303 | run: rm Cargo.lock 304 | 305 | - name: Run clippy 306 | run: > 307 | xtask ci clippy 308 | --rust-version ${{ matrix.rust-version }} 309 | --exclude-xtask 310 | --profile ${{ matrix.profile }} 311 | 312 | - name: Run tests 313 | run: > 314 | xtask ci test 315 | --rust-version ${{ matrix.rust-version }} 316 | --exclude-xtask 317 | --profile ${{ matrix.profile }} 318 | 319 | - name: Build artifacts 320 | run: > 321 | xtask ci build 322 | --rust-version ${{ matrix.rust-version }} 323 | --exclude-xtask 324 | --profile ${{ matrix.profile }} 325 | -------------------------------------------------------------------------------- /.github/workflows/validate_history.yaml: -------------------------------------------------------------------------------- 1 | # Checks if all past commits pass the CI quality gate `cargo xtask ci`, 2 | # excluding moving targets (such as clippy or `cargo audit`). 3 | # This pipeline tries to indicate whether developers may use `git bisect`. 4 | 5 | name: Validate entire commit history 6 | 7 | on: 8 | schedule: 9 | # ┌───────────── minute (0 - 59) 10 | # │ ┌───────────── hour (0 - 23) 11 | # │ │ ┌───────────── day of the month (1 - 31) 12 | # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) 13 | # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) 14 | # │ │ │ │ │ 15 | # │ │ │ │ │ 16 | # │ │ │ │ │ 17 | - cron: '0 0 * * MON' 18 | 19 | workflow_dispatch: 20 | 21 | jobs: 22 | check_each_commit: 23 | name: check each commit 24 | timeout-minutes: 240 25 | runs-on: ${{ matrix.system }} 26 | 27 | strategy: 28 | matrix: 29 | system: [ ubuntu-latest, windows-latest, macos-latest ] 30 | branch: [ main ] 31 | 32 | steps: 33 | 34 | # For whatever reason, Git needs user name and email for the rebase 35 | # on some agents but not on others. 36 | # Also, sometimes there are linter errors due to changed line endings. 37 | - name: Configure Git 38 | run: | 39 | git config --global user.email "runner@github.com" 40 | git config --global user.name "GitHub, Runner" 41 | git config --global core.autocrlf false 42 | 43 | - uses: actions/checkout@v4 44 | with: 45 | ref: ${{ matrix.branch }} 46 | fetch-depth: 0 47 | 48 | # Rust 1.80 removed support of the `tarpaulin_include` attribute, 49 | # so commit `57cc064` and before only work with Rust 1.79. 50 | - name: Install 1.79 Rust toolchain 51 | uses: dtolnay/rust-toolchain@1.79 52 | 53 | - name: Install cargo-hack 54 | uses: taiki-e/install-action@v2 55 | with: 56 | tool: cargo-hack 57 | 58 | # Oops, I changed the API of the xtask package after commit `3705cd0`... 59 | # 60 | # FIXME: `xtask ci` may run `cargo build`, overwriting its own binary. 61 | # FIXME: This is not possible on Windows, so install it before running it. 62 | - name: Check old commits on ${{ matrix.branch }} (Rust 1.79) 63 | run: > 64 | git checkout 3705cd01e514bb3252eba6f4f69ec61168e141a3 65 | 66 | git rebase 67 | --root 68 | --rebase-merges 69 | --exec 'cargo install --path xtask' 70 | --exec 'xtask ci --skip-moving-targets' 71 | 72 | git checkout 57cc064646a878328324122dc7c00fd44fd2cc35 73 | 74 | git rebase 75 | 3705cd01e514bb3252eba6f4f69ec61168e141a3 76 | --rebase-merges 77 | --exec 'cargo install --path xtask' 78 | --exec 'xtask ci all --skip-moving-targets' 79 | 80 | - name: Install stable Rust toolchain 81 | uses: dtolnay/rust-toolchain@stable 82 | 83 | - name: Check remaining commits on ${{ matrix.branch }} (stable Rust) 84 | run: > 85 | git checkout ${{ github.sha }} 86 | 87 | git rebase 88 | 57cc064646a878328324122dc7c00fd44fd2cc35 89 | --rebase-merges 90 | --exec 'cargo install --path xtask' 91 | --exec 'xtask ci all --skip-moving-targets' 92 | -------------------------------------------------------------------------------- /.github/workflows/validate_pr_commits.yaml: -------------------------------------------------------------------------------- 1 | # Runs the CI quality gate: compilation, linting, testing, dependency checking, 2 | # and so on, on every single commit that would be added by the pull request, 3 | # each time the pull request receives a new push or is (re-)opened. 4 | # 5 | # Does NOT validate the result of the merge; see `validate.yaml` for that. 6 | # This pipeline is a "rebase --exec" variant of `validate.yaml`. 7 | # 8 | # Note that this pipeline requires `Cargo.lock` be up-to-date. 9 | # If it isn't, you'll have to run `cargo update`, commit `Cargo.lock`, and 10 | # rebase your branch to have that commit be the first one in your pull request. 11 | # 12 | # Please note the following refs: 13 | # 14 | # - github.ref → refs/heads/$branch_name, resp. refs/pull/$pr_id/merge 15 | # - github.ref_name → $branch_name, resp. $pr_id/merge 16 | # - github.head_ref → $source_branch_name, e.g. "feature/foobar" 17 | # - github.base_ref → $destination_branch, e.g. "main" 18 | 19 | name: Validate each commit 20 | 21 | on: 22 | 23 | pull_request: 24 | types: 25 | - opened 26 | - synchronize 27 | - reopened 28 | 29 | jobs: 30 | 31 | rustfmt: 32 | timeout-minutes: 5 33 | 34 | # Results should be the same on all systems. 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | 39 | # For whatever reason, Git needs user name and email for the rebase 40 | # on some agents but not on others. 41 | # Also, sometimes there are linter errors due to changed line endings. 42 | - name: Configure Git 43 | run: | 44 | git config --global user.email "runner@github.com" 45 | git config --global user.name "GitHub, Runner" 46 | git config --global core.autocrlf false 47 | 48 | - uses: actions/checkout@v4 49 | with: 50 | ref: ${{ github.head_ref }} 51 | fetch-depth: 0 52 | 53 | - name: Install nightly Rust toolchain with rustfmt 54 | uses: dtolnay/rust-toolchain@nightly 55 | with: 56 | components: rustfmt 57 | 58 | - name: Run rustfmt 59 | run: > 60 | git rebase 61 | --fork-point 'origin/${{ github.base_ref }}' 62 | --exec 'cargo xtask ci rustfmt' 63 | 64 | 65 | build: 66 | name: build & test 67 | timeout-minutes: 30 68 | runs-on: ${{ matrix.system }} 69 | 70 | strategy: 71 | matrix: 72 | system: [ ubuntu-latest, windows-latest, macos-latest ] 73 | profile: [ dev, release ] 74 | 75 | steps: 76 | 77 | - name: Install stable Rust toolchain with clippy 78 | uses: dtolnay/rust-toolchain@stable 79 | with: 80 | components: clippy 81 | 82 | - name: Install cargo-hack 83 | uses: taiki-e/install-action@v2 84 | with: 85 | tool: cargo-hack 86 | 87 | # For whatever reason, Git needs user name and email for the rebase 88 | # on some agents but not on others. 89 | - name: Configure Git 90 | run: | 91 | git config --global user.email "runner@github.com" 92 | git config --global user.name "GitHub, Runner" 93 | 94 | - uses: actions/checkout@v4 95 | with: 96 | ref: ${{ github.head_ref }} 97 | fetch-depth: 0 98 | 99 | # FIXME: `xtask ci build` tries to overwrite its own binary. 100 | # FIXME: This is not possible on Windows, so install it before running it. 101 | - name: Build and test 102 | run: > 103 | git rebase 104 | --fork-point 'origin/${{ github.base_ref }}' 105 | --exec 'cargo install --path xtask' 106 | --exec 'xtask ci clippy --profile ${{ matrix.profile }}' 107 | --exec 'xtask ci test --profile ${{ matrix.profile }}' 108 | --exec 'xtask ci build --profile ${{ matrix.profile }}' 109 | 110 | 111 | coverage: 112 | timeout-minutes: 30 113 | runs-on: ${{ matrix.system }} 114 | 115 | strategy: 116 | matrix: 117 | system: [ ubuntu-latest ] 118 | profile: [ dev, release ] 119 | 120 | steps: 121 | 122 | # For whatever reason, Git needs user name and email for the rebase 123 | # on some agents but not on others. 124 | - name: Configure Git 125 | run: | 126 | git config --global user.email "runner@github.com" 127 | git config --global user.name "GitHub, Runner" 128 | 129 | - uses: actions/checkout@v4 130 | with: 131 | ref: ${{ github.head_ref }} 132 | fetch-depth: 0 133 | 134 | # FIXME: Remove nightly once tarpaulin can run doctests on stable again 135 | - name: Install nightly Rust toolchain 136 | uses: dtolnay/rust-toolchain@nightly 137 | 138 | - name: Install stable Rust toolchain 139 | uses: dtolnay/rust-toolchain@stable 140 | 141 | - name: Install cargo-tarpaulin 142 | uses: taiki-e/install-action@v2 143 | with: 144 | tool: cargo-tarpaulin 145 | 146 | - name: Check coverage 147 | run: > 148 | git rebase 149 | --fork-point 'origin/${{ github.base_ref }}' 150 | --exec 'cargo xtask ci tarpaulin --profile ${{ matrix.profile }}' 151 | 152 | 153 | miri: 154 | timeout-minutes: 60 155 | runs-on: ${{ matrix.system }} 156 | 157 | strategy: 158 | matrix: 159 | system: [ ubuntu-latest, windows-latest, macos-latest ] 160 | 161 | steps: 162 | 163 | # For whatever reason, Git needs user name and email for the rebase 164 | # on some agents but not on others. 165 | - name: Configure Git 166 | run: | 167 | git config --global user.email "runner@github.com" 168 | git config --global user.name "GitHub, Runner" 169 | 170 | - uses: actions/checkout@v4 171 | with: 172 | ref: ${{ github.head_ref }} 173 | fetch-depth: 0 174 | 175 | - name: Install nightly Rust toolchain with miri 176 | uses: dtolnay/rust-toolchain@nightly 177 | with: 178 | components: miri 179 | 180 | - name: Install cargo-hack 181 | uses: taiki-e/install-action@v2 182 | with: 183 | tool: cargo-hack 184 | 185 | # FIXME: `xtask ci miri` runs `cargo clean`, deleting its own binary. 186 | # FIXME: This is not possible on Windows, so install it before running it. 187 | - name: Run MIRI tests 188 | run: > 189 | git rebase 190 | --fork-point 'origin/${{ github.base_ref }}' 191 | --exec 'cargo install --path xtask' 192 | --exec 'xtask ci miri' 193 | 194 | 195 | docs: 196 | timeout-minutes: 15 197 | runs-on: ${{ matrix.system }} 198 | 199 | strategy: 200 | matrix: 201 | system: [ ubuntu-latest, windows-latest, macos-latest ] 202 | profile: [ dev, release ] 203 | 204 | steps: 205 | 206 | # For whatever reason, Git needs user name and email for the rebase 207 | # on some agents but not on others. 208 | - name: Configure Git 209 | run: | 210 | git config --global user.email "runner@github.com" 211 | git config --global user.name "GitHub, Runner" 212 | 213 | - uses: actions/checkout@v4 214 | with: 215 | ref: ${{ github.head_ref }} 216 | fetch-depth: 0 217 | 218 | - name: Install stable Rust toolchain 219 | uses: dtolnay/rust-toolchain@stable 220 | 221 | - name: Install cargo-hack 222 | uses: taiki-e/install-action@v2 223 | with: 224 | tool: cargo-hack 225 | 226 | - name: Build documentation 227 | run: > 228 | git rebase 229 | --fork-point 'origin/${{ github.base_ref }}' 230 | --exec 'cargo xtask ci docs --profile ${{ matrix.profile }}' 231 | 232 | 233 | deps: 234 | timeout-minutes: 5 235 | 236 | # Results should be the same on all systems. 237 | runs-on: ubuntu-latest 238 | 239 | steps: 240 | 241 | # For whatever reason, Git needs user name and email for the rebase 242 | # on some agents but not on others. 243 | - name: Configure Git 244 | run: | 245 | git config --global user.email "runner@github.com" 246 | git config --global user.name "GitHub, Runner" 247 | 248 | - uses: actions/checkout@v4 249 | with: 250 | ref: ${{ github.head_ref }} 251 | fetch-depth: 0 252 | 253 | - name: Install stable Rust toolchain 254 | uses: dtolnay/rust-toolchain@stable 255 | 256 | - name: Install cargo-audit & cargo-upgrades 257 | uses: taiki-e/install-action@v2 258 | with: 259 | tool: cargo-audit, cargo-upgrades 260 | 261 | - name: Check dependencies 262 | run: > 263 | git rebase 264 | --fork-point 'origin/${{ github.base_ref }}' 265 | --exec 'cargo xtask ci deps' 266 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /target 3 | /tarpaulin-report-dev/tarpaulin-report.html 4 | /tarpaulin-report-release/tarpaulin-report.html 5 | /tarpaulin-report.html 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file documents all changes affecting the [semver] version of this project. 4 | 5 | ## New in this release 6 | 7 | ## [`v0.10.1`] (2025-02-14) 8 | 9 | ### Fixed 10 | 11 | - Updated URLs in README to point to current release on docs.rs 12 | 13 | ## [`v0.10.0`] (2025-02-14) 14 | 15 | ### Added 16 | 17 | - Added `ErrorStash::push_and_convert` which adds an error to the stash 18 | and returns the inner `StashWithErrors` by value, which is useful 19 | if you want to bail from a function after pushing a final error. 20 | 21 | ## [`v0.9.0`] (2024-10-11) 22 | 23 | ### Breaking Changes 24 | 25 | - The value wrapped by `Error` (`Box`) is now private; 26 | we already had `Deref`, `AsRef`, and `From` for `ErrorData` in place 27 | - `push` now returns a value instead of none (i.e. `()`) 28 | - `StashWithErrors::push` returns a `&mut StashWithErrors` to `self` 29 | - `ErrorStash::push` returns a `&mut StashWithErrors` to 30 | the wrapped inner `StashWithErrors` value 31 | - Usually, this update does not require changes to your code, 32 | except in some cases where you need to drop the return value explicitly, 33 | for example in `match` statements 34 | - `StashedResult`, when imported from any of the two preludes, 35 | now has its generic inner error type parameter hardcoded 36 | as the respective `Stashable` type from that prelude 37 | 38 | ### Added 39 | 40 | - Added `try_map_or_stash` on arrays of type `[T; _]` or `[Result>; _]` 41 | which is similar to `try_map` from the Rust standard library, 42 | except that it fails lazily (i.e. it does _not_ short-circuit) 43 | and moves all `Err` elements/results into an error stash 44 | - Added `try_collect_or_stash` on `Iterator>`, 45 | which is similar to `try_collect` from the Rust standard library, 46 | except that it fails lazily (i.e. it does _not_ short-circuit) 47 | and moves all `Err` items into an error stash 48 | - Added `stash_err` on `Iterator>`, 49 | which turns an `Iterator>` into an `Iterator`, 50 | moving any `E` item into an error stash as soon as it is encountered 51 | - `StashedResult` now implements `Debug` if its type parameters do so too 52 | 53 | ## [`v0.8.0`] (2024-09-20) 54 | 55 | ### Breaking Changes 56 | 57 | - Uses `core::error::Error` by default now (instead of `std::error::Error`) 58 | - Adds the `rust-v1.81` feature (enabled by default) 59 | because `core::error::Error` is stable only since Rust v1.81 60 | - Disables the `std` feature by default 61 | because it is not needed anymore since Rust v1.81 62 | - This is NOT a breaking change if you're using Rust v1.81 or later 63 | - You also DON'T need to change your code if you've been using `no_std` 64 | (i.e. types exported via the `surrogate_error_trait` module), 65 | regardless of the Rust toolchain version you're using 66 | - If you're using a Rust toolchain older than v1.81, please disable 67 | the `rust-v1.81` feature and either enable the `std` feature or use 68 | types from the `surrogate_error_trait` module 69 | 70 | ## [`v0.7.0`] (2024-07-09) 71 | 72 | ### Breaking Changes 73 | 74 | - Replaces the optional `color_eyre` dependency with `eyre`. 75 | This fixes the build on some older Rust toolchain versions, 76 | which broke due to a new version of a transitive dependency. 77 | 78 | ## [`v0.6.0`] (2024-06-25) 79 | 80 | This release comes with a few breaking changes. 81 | By introducing these breaking changes, several unexpected compilation failures 82 | that might have happened in certain edge cases should now be fixed in advance. 83 | For example, you can now compile `lazy_errors` on any Rust version since 1.61 84 | (depending on the set of enabled features). 85 | Additionally, you and/or your dependencies can now use both the 86 | `std` and the `no_std` feature set of `lazy_errors` simultaneously. 87 | Not only have conflicts been resolved, but the different data types 88 | are now compatible as well. 89 | 90 | While these breaking changes may require some additional effort now, 91 | they prepare you for either seamlessly switching to `core::error::Error`-based 92 | errors when that Rust version reaches stable, or benefiting from 93 | `lazy_errors` now being backwards compatible in that regard. 94 | 95 | Here's the details: 96 | 97 | ### Breaking Changes 98 | 99 | - Enable the `std` feature by default 100 | - If you have already been using the `std` feature, please _remove_ 101 | the `std` feature declaration from your `Cargo.toml` file 102 | to make it future-proof 103 | - `std::error::Error` was moved to `core::error` on nightly. 104 | When `core::error::Error` is stable, the `std` feature will be removed 105 | from `lazy_errors` because we won't need to depend on `std` anymore at all. 106 | - The `error_in_core` feature will probably be part of Rust 1.81 107 | - Require explicit opt-in for `#![no_std]` support 108 | - `lazy_errors::Reportable` (the surrogate `std::error::Error` trait) and 109 | associated type aliases are _not_ part of the _regular_ prelude anymore. 110 | If you need `#![no_std]` support, 111 | - simply import `lazy_errors::surrogate_error_trait::prelude::*` 112 | instead of the regular prelude, and 113 | - disable the `std` feature (`--no-default-features`). 114 | - This change avoids conflicts between `std` and `no_std` dependents: 115 | The old implementation of the `std` feature in `lazy_errors` 116 | violated cargo's “features should be additive” rule. 117 | When one of your dependencies depended on `lazy_errors` with `std` support, 118 | and another one depended on `lazy_errors` having `std` disabled, 119 | the dependencies may have failed to compile in some cases. 120 | - When `error_in_core` is part of stable Rust, you will be able to 121 | continue using the surrogate error trait (to support old Rust versions), 122 | and/or you will be able to set the feature flag that will enable 123 | `error_in_core` in `lazy_errors`. 124 | - Require inner errors be `Sync` in `no_std` mode as well 125 | - Previously, these errors types did not need to implement `Sync` 126 | - Now, `std` and `no_std` mode have identical auto-trait bounds 127 | - Using the aliased types from the two preludes, 128 | this will allow you to put `no_std` errors into `std` stashes, 129 | and vice-versa 130 | - You can always specify your own aliases if your error types aren't `Sync` 131 | 132 | ### Added 133 | 134 | - Add `ErrorData::children` and mark `ErrorData::childs` as deprecated 135 | - Add the `try2!` macro to the prelude 136 | - Hide “new” error types from `core` and `alloc` behind `rust-v*` feature flags 137 | (enabled by default) 138 | - Support all Rust versions since Rust 1.77 (all features & combinations) 139 | - Support all Rust versions since Rust 1.61 (by disabling some features) 140 | - Document feature flags in top-level docs 141 | - Clarify MSRV in top-level docs 142 | 143 | ### Fixed 144 | 145 | - Fix and clarify several parts of the documentation 146 | 147 | ## [`v0.5.0`] (2024-06-11) 148 | 149 | ### Breaking Changes 150 | 151 | - Hide the doctest line number helper better 152 | 153 | ### Added 154 | 155 | - Add `ErrorStash::ok` 156 | - Add Apache 2.0 license (project is now dual-licensed: MIT or Apache 2.0) 157 | - Add contribution info to `README.md` 158 | - Add rustdoc links and badges to `README.md` 159 | - Add better rustdoc examples 160 | - Add new generated README for v0.5.0 with new examples and links 161 | 162 | ## [`v0.4.0`] (2024-06-08) 163 | 164 | ### Breaking Changes 165 | 166 | - Hardcode `StashWithErrors` as `E` in `StashedResult` 167 | 168 | ### Added 169 | 170 | - Add the `try2!` macro (`?` operator on `StashedResult`) 171 | 172 | ## [`v0.3.0`] (2024-06-07) 173 | 174 | ### Added 175 | 176 | - Add crate `keywords` and `categories` 177 | - Add StashedResult::ok() 178 | 179 | ## [`v0.2.0`] (2024-06-07) 180 | 181 | ### Added 182 | 183 | - Add `ErrorStash::into_result()` 184 | 185 | ## [`v0.1.1`] (2024-05-24) 186 | 187 | ### Fixed 188 | 189 | - Remove `cargo readme` artifacts from `README.md` 190 | - Add truncated parts to `README.md` 191 | 192 | ## [`v0.1.0`] (2024-05-23) 193 | 194 | ### Added 195 | 196 | - Initial release 197 | 198 | [`v0.10.1`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.10.1 199 | [`v0.10.0`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.10.0 200 | [`v0.9.0`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.9.0 201 | [`v0.8.0`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.8.0 202 | [`v0.7.0`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.7.0 203 | [`v0.6.0`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.6.0 204 | [`v0.5.0`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.5.0 205 | [`v0.4.0`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.4.0 206 | [`v0.3.0`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.3.0 207 | [`v0.2.0`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.2.0 208 | [`v0.1.1`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.1.1 209 | [`v0.1.0`]: https://github.com/Lintermute/lazy_errors/releases/tag/v0.1.0 210 | 211 | [semver]: https://semver.org/spec/v2.0.0.html 212 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 49 | dependencies = [ 50 | "anstyle", 51 | "once_cell", 52 | "windows-sys", 53 | ] 54 | 55 | [[package]] 56 | name = "cfg-if" 57 | version = "1.0.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 60 | 61 | [[package]] 62 | name = "clap" 63 | version = "4.5.32" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" 66 | dependencies = [ 67 | "clap_builder", 68 | "clap_derive", 69 | ] 70 | 71 | [[package]] 72 | name = "clap_builder" 73 | version = "4.5.32" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" 76 | dependencies = [ 77 | "anstream", 78 | "anstyle", 79 | "clap_lex", 80 | "strsim", 81 | ] 82 | 83 | [[package]] 84 | name = "clap_derive" 85 | version = "4.5.32" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 88 | dependencies = [ 89 | "heck", 90 | "proc-macro2", 91 | "quote", 92 | "syn", 93 | ] 94 | 95 | [[package]] 96 | name = "clap_lex" 97 | version = "0.7.4" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 100 | 101 | [[package]] 102 | name = "colorchoice" 103 | version = "1.0.3" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 106 | 107 | [[package]] 108 | name = "eyre" 109 | version = "0.6.12" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 112 | dependencies = [ 113 | "indenter", 114 | "once_cell", 115 | ] 116 | 117 | [[package]] 118 | name = "heck" 119 | version = "0.5.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 122 | 123 | [[package]] 124 | name = "indenter" 125 | version = "0.3.3" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 128 | 129 | [[package]] 130 | name = "indoc" 131 | version = "2.0.6" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 134 | 135 | [[package]] 136 | name = "is_terminal_polyfill" 137 | version = "1.70.1" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 140 | 141 | [[package]] 142 | name = "lazy_errors" 143 | version = "0.0.0" 144 | dependencies = [ 145 | "eyre", 146 | "indoc", 147 | "thiserror", 148 | ] 149 | 150 | [[package]] 151 | name = "once_cell" 152 | version = "1.21.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" 155 | 156 | [[package]] 157 | name = "proc-macro2" 158 | version = "1.0.94" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 161 | dependencies = [ 162 | "unicode-ident", 163 | ] 164 | 165 | [[package]] 166 | name = "quote" 167 | version = "1.0.39" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" 170 | dependencies = [ 171 | "proc-macro2", 172 | ] 173 | 174 | [[package]] 175 | name = "strsim" 176 | version = "0.11.1" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 179 | 180 | [[package]] 181 | name = "syn" 182 | version = "2.0.100" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 185 | dependencies = [ 186 | "proc-macro2", 187 | "quote", 188 | "unicode-ident", 189 | ] 190 | 191 | [[package]] 192 | name = "test-case" 193 | version = "3.3.1" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" 196 | dependencies = [ 197 | "test-case-macros", 198 | ] 199 | 200 | [[package]] 201 | name = "test-case-core" 202 | version = "3.3.1" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" 205 | dependencies = [ 206 | "cfg-if", 207 | "proc-macro2", 208 | "quote", 209 | "syn", 210 | ] 211 | 212 | [[package]] 213 | name = "test-case-macros" 214 | version = "3.3.1" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" 217 | dependencies = [ 218 | "proc-macro2", 219 | "quote", 220 | "syn", 221 | "test-case-core", 222 | ] 223 | 224 | [[package]] 225 | name = "thiserror" 226 | version = "2.0.12" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 229 | dependencies = [ 230 | "thiserror-impl", 231 | ] 232 | 233 | [[package]] 234 | name = "thiserror-impl" 235 | version = "2.0.12" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 238 | dependencies = [ 239 | "proc-macro2", 240 | "quote", 241 | "syn", 242 | ] 243 | 244 | [[package]] 245 | name = "unicode-ident" 246 | version = "1.0.18" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 249 | 250 | [[package]] 251 | name = "utf8parse" 252 | version = "0.2.2" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 255 | 256 | [[package]] 257 | name = "windows-sys" 258 | version = "0.59.0" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 261 | dependencies = [ 262 | "windows-targets", 263 | ] 264 | 265 | [[package]] 266 | name = "windows-targets" 267 | version = "0.52.6" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 270 | dependencies = [ 271 | "windows_aarch64_gnullvm", 272 | "windows_aarch64_msvc", 273 | "windows_i686_gnu", 274 | "windows_i686_gnullvm", 275 | "windows_i686_msvc", 276 | "windows_x86_64_gnu", 277 | "windows_x86_64_gnullvm", 278 | "windows_x86_64_msvc", 279 | ] 280 | 281 | [[package]] 282 | name = "windows_aarch64_gnullvm" 283 | version = "0.52.6" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 286 | 287 | [[package]] 288 | name = "windows_aarch64_msvc" 289 | version = "0.52.6" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 292 | 293 | [[package]] 294 | name = "windows_i686_gnu" 295 | version = "0.52.6" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 298 | 299 | [[package]] 300 | name = "windows_i686_gnullvm" 301 | version = "0.52.6" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 304 | 305 | [[package]] 306 | name = "windows_i686_msvc" 307 | version = "0.52.6" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 310 | 311 | [[package]] 312 | name = "windows_x86_64_gnu" 313 | version = "0.52.6" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 316 | 317 | [[package]] 318 | name = "windows_x86_64_gnullvm" 319 | version = "0.52.6" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 322 | 323 | [[package]] 324 | name = "windows_x86_64_msvc" 325 | version = "0.52.6" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 328 | 329 | [[package]] 330 | name = "xtask" 331 | version = "0.0.0" 332 | dependencies = [ 333 | "clap", 334 | "lazy_errors", 335 | "test-case", 336 | ] 337 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ "lazy_errors", "xtask" ] 3 | default-members = [ "lazy_errors" ] 4 | resolver = "2" 5 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Andreas Waidler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.j2: -------------------------------------------------------------------------------- 1 | {# this is the default template from cargo-doc2readme + minor extensions #} 2 | 3 | {%- if crate -%} 4 | # {{ crate }} 5 | {%- if license %} ![License: {{ license }}](https://img.shields.io/badge/license-{{ license | replace("-", "--") | urlencode }}-blue) 6 | {%- else %} ![License](https://img.shields.io/crates/l/{{ crate | urlencode }}) 7 | {%- endif %} 8 | {%- if crate %} [![{{ crate }} on crates.io](https://img.shields.io/crates/v/{{ crate | urlencode }})](https://crates.io/crates/{{ crate | urlencode }}) 9 | {%- if target == "lib" %} [![{{ crate }} on docs.rs](https://docs.rs/{{ crate | urlencode }}/badge.svg)](https://docs.rs/{{ crate | urlencode }}) 10 | {%- endif %} 11 | {%- endif %} 12 | {%- if repository %} 13 | {%- if repository_host == "github.com" %} [![Source Code Repository](https://img.shields.io/badge/Code-On%20GitHub-blue?logo=GitHub)]({{ repository }}) 14 | {%- elif repository_host == "gitlab.com" %} [![Source Code Repository](https://img.shields.io/badge/Code-On%20GitLab-blue?logo=GitLab)]({{ repository }}) 15 | {%- elif repository_host == "codeberg.org" %} [![Source Code Repository](https://img.shields.io/badge/Code-On%20Codeberg-blue?logo=Codeberg)]({{ repository }}) 16 | {%- elif repository_host %} [![Source Code Repository](https://img.shields.io/badge/Code-On%20{{ repository_host | replace("-", "--") | urlencode }}-blue)]({{ repository }}) 17 | {%- endif %} 18 | {%- endif %} 19 | {%- if rust_version %} ![Rust Version: {{rust_version}}](https://img.shields.io/badge/rustc-{{ rust_version | urlencode }}-orange.svg) 20 | {%- endif %} 21 | {%- endif %} 22 | 23 | {{ readme }} 24 | 25 | ## License 26 | 27 | Licensed under either of 28 | 29 | * Apache License, Version 2.0 30 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 31 | * MIT license 32 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 33 | 34 | at your option. 35 | 36 | ## Contribution 37 | 38 | Unless you explicitly state otherwise, any contribution intentionally submitted 39 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 40 | dual licensed as above, without any additional terms or conditions. 41 | 42 | {%- if links != "" %} 43 | 44 | {{ links }} 45 | {%- endif -%} 46 | -------------------------------------------------------------------------------- /lazy_errors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lazy_errors" 3 | description = "Effortlessly create, group, and nest arbitrary errors, and defer error handling ergonomically." 4 | keywords = ["error-handling", "multiple", "errors", "tree", "no-std"] 5 | categories = ["rust-patterns", "no-std"] 6 | edition = "2021" 7 | readme = "../README.md" 8 | authors = ["Andreas Waidler "] 9 | repository = "https://github.com/Lintermute/lazy_errors" 10 | license = "MIT OR Apache-2.0" 11 | version = "0.0.0" 12 | 13 | [features] 14 | default = [ 15 | "rust-v1.81", 16 | "rust-v1.77", 17 | "rust-v1.69", 18 | "rust-v1.66", 19 | "rust-v1.64", 20 | ] 21 | eyre = ["std", "dep:eyre"] 22 | std = [] 23 | "rust-v1.81" = [] 24 | "rust-v1.77" = [] 25 | "rust-v1.69" = [] 26 | "rust-v1.66" = [] 27 | "rust-v1.64" = [] 28 | 29 | [package.metadata."docs.rs"] 30 | all-features = true 31 | 32 | [dependencies] 33 | eyre = { version = "0.6.2", optional = true } 34 | 35 | [dev-dependencies] 36 | indoc = "2.0.5" 37 | thiserror = "2.0.0" 38 | -------------------------------------------------------------------------------- /lazy_errors/src/err.rs: -------------------------------------------------------------------------------- 1 | /// Creates an ad-hoc [`Error`](crate::Error) 2 | /// from some message or format string. 3 | /// 4 | /// Use this macro if you want to bail early from a function 5 | /// that otherwise may return multiple errors 6 | /// or when your codebase is generally using the 7 | /// return type 8 | #[cfg_attr( 9 | any(feature = "rust-v1.81", feature = "std"), 10 | doc = r##" 11 | [`lazy_errors::Result`](crate::Result) 12 | or 13 | [`lazy_errors::surrogate_error_trait::Result`]. 14 | 15 | "## 16 | )] 17 | #[cfg_attr( 18 | not(any(feature = "rust-v1.81", feature = "std")), 19 | doc = r##" 20 | [`lazy_errors::surrogate_error_trait::Result`] 21 | (or `lazy_errors::Result` if any of 22 | the `rust-v1.81` or 23 | the `std` features is enabled). 24 | 25 | "## 26 | )] 27 | /// [`lazy_errors::surrogate_error_trait::Result`]: 28 | /// crate::surrogate_error_trait::Result 29 | /// 30 | /// A guard clause is a typical use case for this macro: 31 | /// 32 | /// ``` 33 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 34 | /// use lazy_errors::{err, Result}; 35 | /// 36 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 37 | /// use lazy_errors::{err, surrogate_error_trait::Result}; 38 | /// 39 | /// fn handle_ascii(text: &str) -> Result<()> { 40 | /// if !text.is_ascii() { 41 | /// return Err(err!("Not ASCII: '{text}'")); 42 | /// } 43 | /// 44 | /// // ... code that does the actual handling ... 45 | /// todo!() 46 | /// } 47 | /// 48 | /// assert!(handle_ascii("🦀").is_err()); 49 | /// ``` 50 | #[macro_export] 51 | macro_rules! err { 52 | ($($arg:tt)*) => {{ 53 | extern crate alloc; 54 | $crate::Error::from_message(alloc::format!($($arg)*)) 55 | }}; 56 | } 57 | -------------------------------------------------------------------------------- /lazy_errors/src/error.rs: -------------------------------------------------------------------------------- 1 | use core::{ 2 | fmt::{self, Debug, Display}, 3 | ops::Deref, 4 | }; 5 | 6 | use alloc::{boxed::Box, format, string::ToString}; 7 | 8 | pub type Location = &'static core::panic::Location<'static>; 9 | 10 | /// The primary error type to use when using this crate. 11 | /// 12 | /// [`Error`] wraps all kinds of errors 13 | /// that you may want to return from functions. 14 | /// The error variants are represented by the [`ErrorData`] enum. 15 | /// You can access the [`ErrorData`] variants 16 | /// from [`Error`] via [`Deref`], [`AsRef`], or [`From`]: 17 | /// 18 | /// ``` 19 | /// # use core::str::FromStr; 20 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 21 | /// use lazy_errors::prelude::*; 22 | /// 23 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 24 | /// use lazy_errors::surrogate_error_trait::prelude::*; 25 | /// 26 | /// let err: Error = u32::from_str("").or_wrap().unwrap_err(); 27 | /// 28 | /// let deref: &ErrorData = &*err; 29 | /// let asref: &ErrorData = err.as_ref(); 30 | /// let converted: ErrorData = err.into(); 31 | /// 32 | /// let err: WrappedError = match converted { 33 | /// ErrorData::Wrapped(err) => err, 34 | /// _ => unreachable!(), 35 | /// }; 36 | /// ``` 37 | /// 38 | /// ### Inner Error Type `I` 39 | /// 40 | /// The generic type parameter `I` is the _inner error type_. 41 | /// It enables us to support a wide range of use cases. 42 | /// In almost all trait implementations and method signatures 43 | /// in this crate, errors will have the generic type parameter 44 | /// `E: Into`. This trait bound allows us to work with both 45 | /// boxed errors as well as custom error types. 46 | /// 47 | /// #### `Stashable`: Boxed Errors 48 | /// 49 | /// Most of the time you will be using the _aliased_ re-export of [`Error`] 50 | /// and other containers from the [`prelude`]. 51 | /// In that case, `I` will be [`prelude::Stashable`] which is an alias for 52 | /// `Box`. 53 | /// This trait bound was chosen because 54 | /// `Into>` 55 | /// is implemented for many types via blanket implementations 56 | /// in `core`, `std`, and crates such as `anyhow` or `eyre`. 57 | /// 58 | /// Some examples of types that satisfy this constraint are: 59 | /// 60 | /// - `&str` 61 | /// - `String` 62 | /// - `anyhow::Error` 63 | /// - `eyre::Report` 64 | /// - `core::error::Error` 65 | /// - All error types from this crate 66 | /// 67 | /// Additionally, this trait bound makes `Error` satisfy 68 | /// `core::error::Error + Send + Sync + 'static` as well, 69 | /// so it can be consumed by other crates 70 | /// that support errors in general. 71 | /// 72 | /// In Rust versions before v1.81, `core::error::Error` is not stable. 73 | /// If you don't enable the `std` feature in that case, `lazy_errors` 74 | /// can access neither `core::error::Error` nor `std::error::Error`. 75 | /// For these situations, `lazy_errors` comes with a surrogate error trait: 76 | /// [`Reportable`]. `lazy_errors` implements [`Reportable`] 77 | /// for error types in `core` and `alloc` as well as for `&str` and `String`. 78 | /// `lazy_errors` also defines 79 | /// [`surrogate_error_trait::prelude::Stashable`] as an alias for 80 | /// `Box` 81 | /// and exports additional type aliases in [`surrogate_error_trait::prelude`] 82 | /// that allow you to use `lazy_errors` in the same way, 83 | /// regardless of which prelude you've imported. 84 | /// 85 | /// FYI, the [`Send`] trait bound 86 | /// [makes errors usable with `thread::spawn` and `task::spawn`][1]. 87 | /// 88 | /// #### Using Custom Error Types 89 | /// 90 | /// Usually, the inner error type `I` is [`prelude::Stashable`]. 91 | /// Nevertheless, you can choose the specific type to use for `I` arbitrarily: 92 | /// 93 | /// ``` 94 | /// use lazy_errors::Error; 95 | /// 96 | /// struct CustomError; 97 | /// let error: Error = Error::wrap(CustomError); 98 | /// ``` 99 | /// 100 | /// Note that you won't be able to print or debug-print errors 101 | /// if the inner error type does not implement [`Display`]/[`Debug`]. 102 | /// On the other hand, such error types are completely unsupported by `eyre`: 103 | /// 104 | /// ```compile_fail 105 | /// use eyre::Error; 106 | /// 107 | /// struct CustomError; 108 | /// let error: Error = Error::new(CustomError); 109 | /// ``` 110 | /// 111 | /// `lazy_errors` should support any custom inner error type 112 | /// as long as it is [`Sized`]. 113 | /// You can even choose a custom type to use for `I` 114 | /// that doesn't implement [`Send`] or [`Sync`]: 115 | /// 116 | /// ``` 117 | /// # extern crate alloc; 118 | /// use alloc::rc::Rc; 119 | /// use lazy_errors::Error; 120 | /// 121 | /// struct NeitherSendNorSync(Rc); 122 | /// let inner = NeitherSendNorSync(Rc::new(42)); 123 | /// let error: Error = Error::wrap(inner); 124 | /// ``` 125 | /// 126 | /// Even if you implemented `Debug`, `Display`, and `core::error::Error`, 127 | /// `eyre` still won't support your custom error type if it isn't 128 | /// `Send + Sync + 'static`: 129 | /// 130 | /// ```compile_fail 131 | /// # extern crate alloc; 132 | /// use alloc::rc::Rc; 133 | /// use eyre::eyre; 134 | /// 135 | /// #[derive(Debug)] 136 | /// struct NeitherSendNorSync(Rc); 137 | /// 138 | /// impl core::fmt::Display for NeitherSendNorSync 139 | /// { 140 | /// fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result 141 | /// { 142 | /// let deref = &self.0; 143 | /// write!(f, "{deref}") 144 | /// } 145 | /// } 146 | /// 147 | /// impl core::error::Error for NeitherSendNorSync {} 148 | /// 149 | /// let inner = NeitherSendNorSync(Rc::new(42)); 150 | /// let report = eyre!(inner); 151 | /// ``` 152 | /// 153 | /// [1]: https://github.com/dtolnay/anyhow/issues/81 154 | /// [`Reportable`]: crate::Reportable 155 | /// [`surrogate_error_trait::prelude`]: crate::surrogate_error_trait::prelude 156 | /// [`surrogate_error_trait::prelude::Stashable`]: 157 | /// crate::surrogate_error_trait::prelude::Stashable 158 | #[cfg_attr( 159 | any(feature = "rust-v1.81", feature = "std"), 160 | doc = r##" 161 | [`prelude`]: crate::prelude 162 | [`prelude::Stashable`]: crate::prelude::Stashable 163 | "## 164 | )] 165 | #[cfg_attr( 166 | not(any(feature = "rust-v1.81", feature = "std")), 167 | doc = r##" 168 | [`prelude`]: crate::surrogate_error_trait::prelude 169 | [`prelude::Stashable`]: crate::surrogate_error_trait::prelude::Stashable 170 | "## 171 | )] 172 | // Box to avoid introducing overhead on the 173 | // happy paths of functions returning `Result<_, Error>`. 174 | #[derive(Debug)] 175 | pub struct Error(Box>); 176 | 177 | /// Enum of all kinds of errors that you may want to return 178 | /// from functions when using this crate. 179 | /// 180 | /// The main reason to use this crate is to return an arbitrary number 181 | /// of errors from functions, i.e. `Result<_, StashedErrors>`, 182 | /// where [`StashedErrors`] is basically a `Vec`. At run-time, 183 | /// however, you may want to return some other error, 184 | /// for example when a guard clause evaluates to `false` 185 | /// or when a preliminary check evaluated to `Err`. 186 | /// In those cases, you can return an ad-hoc error 187 | /// or wrap that other error. This enum distinguishes these three cases. 188 | /// Since [`Error`] transparently wraps [`ErrorData`], 189 | /// you can thus return `Result<_, Error>` in all of these cases. 190 | #[derive(Debug)] 191 | pub enum ErrorData { 192 | Stashed(StashedErrors), 193 | Wrapped(WrappedError), 194 | AdHoc(AdHocError), 195 | } 196 | 197 | /// A list of one or more errors along with a message 198 | /// that summarizes all errors in the list. 199 | /// 200 | /// Most of the time this type is used only internally. 201 | /// 202 | /// Values of this type get created (internally) 203 | /// by converting a (non-empty) [`ErrorStash`] into `Result`, or 204 | /// by converting a [`StashWithErrors`] into [`Error`]. 205 | /// The error summary message will be set according to the parameter passed to 206 | /// [`ErrorStash::new`] (or [`or_create_stash`], respectively). 207 | /// 208 | /// [`ErrorStash`]: crate::ErrorStash 209 | /// [`ErrorStash::new`]: crate::ErrorStash::new 210 | /// [`StashWithErrors`]: crate::StashWithErrors 211 | /// [`or_create_stash`]: crate::OrCreateStash::or_create_stash 212 | #[derive(Debug)] 213 | pub struct StashedErrors { 214 | /// Summarizes all errors in the list. 215 | summary: Box, 216 | 217 | /// Guaranteed to contain at least one element. 218 | errors: Box<[I]>, 219 | 220 | /// Guaranteed to contain one element dedicated to each `errors` entry. 221 | locations: Box<[Location]>, 222 | } 223 | 224 | /// Wraps exactly one (custom or third-party) error, along with 225 | /// an optional message that informs users or developers about 226 | /// the context of the error. 227 | /// 228 | /// Most of the time this type is used only internally. 229 | /// 230 | /// Values of this type get created internally 231 | /// by [`or_wrap`] and [`or_wrap_with`]. 232 | /// `WrappedError` can wrap any value that can be converted into the 233 | /// [_inner error type_ `I`](Error#inner-error-type-i). 234 | /// 235 | /// You can print and pretty-print the error, 236 | /// regardless of whether the optional context message is present or not: 237 | /// 238 | /// ``` 239 | /// # use lazy_errors::doctest_line_num_helper as replace_line_numbers; 240 | /// # use core::str::FromStr; 241 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 242 | /// use lazy_errors::prelude::*; 243 | /// 244 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 245 | /// use lazy_errors::surrogate_error_trait::prelude::*; 246 | /// 247 | /// let err1: Error = u32::from_str("❌") 248 | /// .or_wrap() 249 | /// .unwrap_err(); 250 | /// 251 | /// let err2: Error = u32::from_str("❌") 252 | /// .or_wrap_with(|| "Not an u32") 253 | /// .unwrap_err(); 254 | /// 255 | /// let printed1 = format!("{err1}"); 256 | /// let printed2 = format!("{err2}"); 257 | /// assert_eq!(printed1, "invalid digit found in string"); 258 | /// assert_eq!(printed2, "Not an u32: invalid digit found in string"); 259 | /// 260 | /// let printed1 = format!("{err1:#}"); 261 | /// let printed1 = replace_line_numbers(&printed1); 262 | /// assert_eq!(printed1, indoc::indoc! {" 263 | /// invalid digit found in string 264 | /// at src/error.rs:1234:56"}); 265 | /// 266 | /// let printed2 = format!("{err2:#}"); 267 | /// let printed2 = replace_line_numbers(&printed2); 268 | /// assert_eq!(printed2, indoc::indoc! {" 269 | /// Not an u32: invalid digit found in string 270 | /// at src/error.rs:1234:56"}); 271 | /// ``` 272 | /// 273 | /// [`or_wrap`]: crate::OrWrap::or_wrap 274 | /// [`or_wrap_with`]: crate::OrWrapWith::or_wrap_with 275 | #[derive(Debug)] 276 | pub struct WrappedError { 277 | context: Option>, 278 | inner: I, 279 | location: Location, 280 | } 281 | 282 | /// A single, “one of a kind” [`Error`], created from an ad-hoc error message, 283 | /// with source location information that gets added implicitly 284 | /// when a value of this type is constructed. 285 | /// 286 | /// Most of the time this type is used only internally. 287 | /// 288 | /// Values of this type get created internally 289 | /// when the [`err!`](crate::err!) macro or 290 | /// when [`Error::from_message`] are called. 291 | /// 292 | /// `AdHocError` can be printed and supports “pretty-printing” as well: 293 | /// 294 | /// ``` 295 | /// # use lazy_errors::doctest_line_num_helper as replace_line_numbers; 296 | /// use lazy_errors::AdHocError; 297 | /// 298 | /// let err = AdHocError::from_message("Something went wrong"); 299 | /// 300 | /// let printed = format!("{err}"); 301 | /// assert_eq!(printed, "Something went wrong"); 302 | /// 303 | /// let printed = format!("{err:#}"); 304 | /// let printed = replace_line_numbers(&printed); 305 | /// assert_eq!(printed, indoc::indoc! {" 306 | /// Something went wrong 307 | /// at src/error.rs:1234:56"}); 308 | /// ``` 309 | #[derive(Debug)] 310 | pub struct AdHocError { 311 | message: Box, 312 | location: Location, 313 | } 314 | 315 | impl From> for Error { 316 | fn from(value: ErrorData) -> Self { 317 | Self(Box::new(value)) 318 | } 319 | } 320 | 321 | impl Deref for Error { 322 | type Target = ErrorData; 323 | 324 | fn deref(&self) -> &Self::Target { 325 | &self.0 326 | } 327 | } 328 | 329 | impl AsRef> for Error { 330 | fn as_ref(&self) -> &ErrorData { 331 | self.deref() 332 | } 333 | } 334 | 335 | impl From> for ErrorData { 336 | fn from(value: Error) -> Self { 337 | *value.0 338 | } 339 | } 340 | 341 | #[cfg(feature = "rust-v1.81")] 342 | impl core::error::Error for Error {} 343 | 344 | #[cfg(feature = "rust-v1.81")] 345 | impl core::error::Error for ErrorData {} 346 | 347 | #[cfg(feature = "rust-v1.81")] 348 | impl core::error::Error for StashedErrors {} 349 | 350 | #[cfg(feature = "rust-v1.81")] 351 | impl core::error::Error for WrappedError {} 352 | 353 | #[cfg(feature = "rust-v1.81")] 354 | impl core::error::Error for AdHocError {} 355 | 356 | #[cfg(all(not(feature = "rust-v1.81"), feature = "std"))] 357 | impl std::error::Error for Error {} 358 | 359 | #[cfg(all(not(feature = "rust-v1.81"), feature = "std"))] 360 | impl std::error::Error for ErrorData {} 361 | 362 | #[cfg(all(not(feature = "rust-v1.81"), feature = "std"))] 363 | impl std::error::Error for StashedErrors {} 364 | 365 | #[cfg(all(not(feature = "rust-v1.81"), feature = "std"))] 366 | impl std::error::Error for WrappedError {} 367 | 368 | #[cfg(all(not(feature = "rust-v1.81"), feature = "std"))] 369 | impl std::error::Error for AdHocError {} 370 | 371 | impl Display for Error { 372 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 373 | Display::fmt(&self.0, f) 374 | } 375 | } 376 | 377 | impl Display for ErrorData { 378 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 379 | let i: &dyn Display = match self { 380 | Self::AdHoc(err) => err, 381 | Self::Stashed(errs) => errs, 382 | Self::Wrapped(inner) => inner, 383 | }; 384 | 385 | if !f.alternate() { 386 | // `#` in format string 387 | write!(f, "{i}") 388 | } else { 389 | write!(f, "{i:#}") 390 | } 391 | } 392 | } 393 | 394 | impl Display for StashedErrors { 395 | /// Without additional flags, the output will be kept to a single line. 396 | /// To print the output as a list, pass the `#` “pretty-printing” sign. 397 | /// Doing so will also add source location information: 398 | /// 399 | /// ``` 400 | /// # use lazy_errors::doctest_line_num_helper as replace_line_numbers; 401 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 402 | /// use lazy_errors::prelude::*; 403 | /// 404 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 405 | /// use lazy_errors::surrogate_error_trait::prelude::*; 406 | /// 407 | /// let mut errs = ErrorStash::new(|| "Summary"); 408 | /// errs.push("Foo"); 409 | /// errs.push("Bar"); 410 | /// 411 | /// let res: Result<(), Error> = errs.into(); 412 | /// let err = res.unwrap_err(); 413 | /// 414 | /// let printed = format!("{err}"); 415 | /// assert_eq!(&printed, "Summary (2 errors)"); 416 | /// 417 | /// let printed = format!("{err:#}"); 418 | /// let printed = replace_line_numbers(&printed); 419 | /// assert_eq!(printed, indoc::indoc! {" 420 | /// Summary 421 | /// - Foo 422 | /// at src/error.rs:1234:56 423 | /// - Bar 424 | /// at src/error.rs:1234:56"}); 425 | /// ``` 426 | /// 427 | /// When there is only a single error in a group, that error's output 428 | /// will be printed in the same line along with the “group” summary 429 | /// when printing the “short” form (without the “pretty-print” flag). 430 | /// 431 | /// ``` 432 | /// # use lazy_errors::doctest_line_num_helper as replace_line_numbers; 433 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 434 | /// use lazy_errors::{prelude::*, Result}; 435 | /// 436 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 437 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 438 | /// 439 | /// fn run() -> Result<()> { 440 | /// let mut stash = ErrorStash::new(|| "Parent failed"); 441 | /// stash.push(parent().unwrap_err()); 442 | /// stash.into() 443 | /// } 444 | /// 445 | /// fn parent() -> Result<()> { 446 | /// let mut stash = ErrorStash::new(|| "Child failed"); 447 | /// stash.push(child().unwrap_err()); 448 | /// stash.into() 449 | /// } 450 | /// 451 | /// fn child() -> Result<(), &'static str> { 452 | /// Err("Root cause") 453 | /// } 454 | /// 455 | /// let err = run().unwrap_err(); 456 | /// 457 | /// let printed = format!("{err}"); 458 | /// assert_eq!(printed, "Parent failed: Child failed: Root cause"); 459 | /// 460 | /// let printed = format!("{err:#}"); 461 | /// let printed = replace_line_numbers(&printed); 462 | /// assert_eq!(printed, indoc::indoc! {" 463 | /// Parent failed 464 | /// - Child failed 465 | /// - Root cause 466 | /// at src/error.rs:1234:56 467 | /// at src/error.rs:1234:56"}); 468 | /// ``` 469 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 470 | // TODO: Limit recursion depth for multiple sequences of 471 | // “groups” that each only consist of a single element. 472 | // Downcasting requires `'a: 'static`. Find an alternative. 473 | // `request_ref` from feature `error_generic_member_access`? 474 | // Maybe use the `f.precision()` format flag? 475 | 476 | // TODO: Print the source location in the same line as the error 477 | // when pretty-printing the list: 478 | // `format!("{indent}- {error} ({location})")` 479 | // This requires us to check whether `error` is of a type 480 | // defined in this crate and then handle it accordingly. 481 | // This will only work with casting; see comment above. 482 | 483 | let errors = self.errors.as_ref(); 484 | let locations = self.locations.as_ref(); 485 | let summary = &self.summary; 486 | let is_pretty = f.alternate(); // `#` in format string 487 | 488 | match (errors, locations, is_pretty) { 489 | ([], ..) => write!(f, "{summary}: 0 errors"), 490 | (_, [], ..) => write!(f, "{summary}: 0 source locations"), 491 | ([e], _, false) => write!(f, "{summary}: {e}"), 492 | (errs, _, false) => { 493 | write!(f, "{summary} ({} errors)", errs.len()) 494 | } 495 | (errs, locs, true) => { 496 | write!(f, "{summary}")?; 497 | display_list_of_children(f, errs, locs) 498 | } 499 | } 500 | } 501 | } 502 | 503 | impl Display for WrappedError { 504 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 505 | let err = &self.inner; 506 | let loc = self.location; 507 | let is_pretty = f.alternate(); // `#` in format string 508 | 509 | match (&self.context, is_pretty) { 510 | (None, false) => write!(f, "{err}"), 511 | (None, true) => { 512 | write!(f, "{err:#}")?; 513 | 514 | // Note that the error may have printed its location already 515 | // in case it's an error type from our crate. In that case 516 | // we'd end up with duplicate locations. This is fine 517 | // as long as we're printing one location per line. 518 | display_location(f, "", loc) 519 | } 520 | (Some(context), false) => { 521 | // Refer to the note about recursion depth in `StashedErrors`. 522 | write!(f, "{context}: {err}") 523 | } 524 | (Some(context), true) => { 525 | // Refer to the note about recursion depth in `StashedErrors`. 526 | write!(f, "{context}: {err:#}")?; 527 | display_location(f, "", loc) 528 | } 529 | } 530 | } 531 | } 532 | 533 | impl Display for AdHocError { 534 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 535 | let is_pretty = f.alternate(); // `#` in format string 536 | if !is_pretty { 537 | write!(f, "{}", self.message) 538 | } else { 539 | writeln!(f, "{}", self.message)?; 540 | write!(f, "at {}", self.location) 541 | } 542 | } 543 | } 544 | 545 | impl Error { 546 | /// Creates an [`AdHocError`] variant of [`Error`] from a message. 547 | #[track_caller] 548 | pub fn from_message(msg: M) -> Self { 549 | ErrorData::from_message(msg).into() 550 | } 551 | 552 | /// Creates a [`StashedErrors`] variant of [`Error`]. 553 | pub fn from_stash(summary: M, errors: E, locations: L) -> Self 554 | where 555 | M: Display, 556 | E: Into>, 557 | L: Into>, 558 | { 559 | ErrorData::from_stash(summary, errors, locations).into() 560 | } 561 | 562 | /// Creates a [`WrappedError`] variant of [`Error`] 563 | /// from something that can be turned into an 564 | /// [_inner error type_ `I`](Error#inner-error-type-i). 565 | #[track_caller] 566 | pub fn wrap(err: E) -> Self 567 | where 568 | E: Into, 569 | { 570 | ErrorData::wrap(err).into() 571 | } 572 | 573 | /// Creates a [`WrappedError`] variant of [`Error`] 574 | /// from something that can be turned into an 575 | /// [_inner error type_ `I`](Error#inner-error-type-i) 576 | /// and annotates it with an informative message. 577 | #[track_caller] 578 | pub fn wrap_with(err: E, msg: M) -> Self 579 | where 580 | E: Into, 581 | M: Display, 582 | { 583 | ErrorData::wrap_with(err, msg).into() 584 | } 585 | } 586 | 587 | impl ErrorData { 588 | /// Creates an [`AdHocError`] variant of [`Error`] from a message. 589 | #[track_caller] 590 | pub fn from_message(msg: M) -> Self { 591 | let err = AdHocError::from_message(msg.to_string()); 592 | Self::AdHoc(err) 593 | } 594 | 595 | /// Creates a [`StashedErrors`] variant of [`Error`]. 596 | pub fn from_stash(summary: M, errors: E, locations: L) -> Self 597 | where 598 | M: Display, 599 | E: Into>, 600 | L: Into>, 601 | { 602 | let err = StashedErrors::from(summary, errors, locations); 603 | Self::Stashed(err) 604 | } 605 | 606 | /// Creates a [`WrappedError`] variant of [`Error`] 607 | /// from something that can be turned into an 608 | /// [_inner error type_ `I`](Error#inner-error-type-i). 609 | #[track_caller] 610 | pub fn wrap(err: E) -> Self 611 | where 612 | E: Into, 613 | { 614 | Self::Wrapped(WrappedError::wrap(err)) 615 | } 616 | 617 | /// Creates a [`WrappedError`] variant of [`Error`] 618 | /// from something that can be turned into an 619 | /// [_inner error type_ `I`](Error#inner-error-type-i) 620 | /// and annotates it with an informative message. 621 | #[track_caller] 622 | pub fn wrap_with(err: E, msg: M) -> Self 623 | where 624 | E: Into, 625 | M: Display, 626 | { 627 | Self::Wrapped(WrappedError::wrap_with(err, msg)) 628 | } 629 | 630 | /// Deprecated method that was renamed to 631 | /// [`children`](Self::children). 632 | #[deprecated(since = "0.6.0", note = "renamed to `children`")] 633 | pub fn childs(&self) -> &[I] { 634 | self.children() 635 | } 636 | 637 | /// Returns all errors that are direct children of this error. 638 | /// 639 | /// ``` 640 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 641 | /// use lazy_errors::prelude::*; 642 | /// 643 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 644 | /// use lazy_errors::surrogate_error_trait::prelude::*; 645 | /// 646 | /// let err = Error::from_message("Something went wrong"); 647 | /// assert!(err.children().is_empty()); 648 | /// 649 | /// let err = Error::wrap("A thing went wrong"); 650 | /// let e = match err.children() { 651 | /// [e] => e, 652 | /// _ => unreachable!(), 653 | /// }; 654 | /// assert_eq!(&format!("{e}"), "A thing went wrong"); 655 | /// 656 | /// let mut err = ErrorStash::new(|| "One or more things went wrong"); 657 | /// err.push("An error"); 658 | /// err.push("Another error"); 659 | /// 660 | /// let r: Result<(), Error> = err.into(); 661 | /// let err = r.unwrap_err(); 662 | /// let [e1, e2] = match err.children() { 663 | /// [e1, e2] => [e1, e2], 664 | /// _ => unreachable!(), 665 | /// }; 666 | /// 667 | /// assert_eq!(&format!("{e1}"), "An error"); 668 | /// assert_eq!(&format!("{e2}"), "Another error"); 669 | /// ``` 670 | /// 671 | /// Note that this method only returns _direct_ children. 672 | /// Each of those errors thus may have been created from 673 | /// an [`ErrorStash`](crate::ErrorStash), 674 | /// which stored another level of errors. 675 | /// Such transitive children will _not_ be returned from this method. 676 | pub fn children(&self) -> &[I] { 677 | match self { 678 | Self::AdHoc(_) => &[], 679 | Self::Wrapped(err) => core::slice::from_ref(err.inner()), 680 | Self::Stashed(errs) => errs.errors(), 681 | } 682 | } 683 | } 684 | 685 | impl StashedErrors { 686 | pub fn from(summary: M, errors: E, locations: L) -> Self 687 | where 688 | M: Display, 689 | E: Into>, 690 | L: Into>, 691 | { 692 | Self { 693 | summary: summary.to_string().into_boxed_str(), 694 | errors: errors.into(), 695 | locations: locations.into(), 696 | } 697 | } 698 | 699 | pub fn errors(&self) -> &[I] { 700 | &self.errors 701 | } 702 | } 703 | 704 | impl WrappedError { 705 | /// Creates a [`WrappedError`] 706 | /// from something that can be turned into an 707 | /// [_inner error type_ `I`](Error#inner-error-type-i). 708 | #[track_caller] 709 | pub fn wrap(err: E) -> Self 710 | where 711 | E: Into, 712 | { 713 | Self { 714 | context: None, 715 | inner: err.into(), 716 | location: location(), 717 | } 718 | } 719 | 720 | /// Creates a [`WrappedError`] 721 | /// from something that can be turned into an 722 | /// [_inner error type_ `I`](Error#inner-error-type-i) 723 | /// and annotates it with an informative message. 724 | #[track_caller] 725 | pub fn wrap_with(err: E, msg: M) -> Self 726 | where 727 | E: Into, 728 | M: Display, 729 | { 730 | Self { 731 | context: Some(msg.to_string().into_boxed_str()), 732 | inner: err.into(), 733 | location: location(), 734 | } 735 | } 736 | 737 | /// Return the error that was wrapped. 738 | pub fn inner(&self) -> &I { 739 | &self.inner 740 | } 741 | } 742 | 743 | impl AdHocError { 744 | /// Creates an [`AdHocError`] from a message. 745 | #[track_caller] 746 | pub fn from_message(msg: M) -> Self { 747 | Self { 748 | message: msg.to_string().into_boxed_str(), 749 | location: location(), 750 | } 751 | } 752 | } 753 | 754 | #[track_caller] 755 | pub fn location() -> Location { 756 | core::panic::Location::caller() 757 | } 758 | 759 | fn display_list_of_children( 760 | f: &mut fmt::Formatter<'_>, 761 | errs: &[I], 762 | locs: &[Location], 763 | ) -> fmt::Result { 764 | for (e, l) in errs.iter().zip(locs) { 765 | display_multiline(f, &e)?; 766 | display_location(f, " ", l)?; 767 | } 768 | Ok(()) 769 | } 770 | 771 | fn display_multiline( 772 | f: &mut fmt::Formatter<'_>, 773 | err: &I, 774 | ) -> fmt::Result { 775 | let mut prefix = "- "; 776 | for line in format!("{err:#}").lines() { 777 | writeln!(f)?; 778 | write!(f, "{prefix}{line}")?; 779 | prefix = " "; 780 | } 781 | Ok(()) 782 | } 783 | 784 | fn display_location( 785 | f: &mut fmt::Formatter<'_>, 786 | indent: &str, 787 | location: Location, 788 | ) -> fmt::Result { 789 | writeln!(f)?; 790 | write!(f, "{indent}at {location}") 791 | } 792 | 793 | #[cfg(test)] 794 | mod tests { 795 | #[test] 796 | #[cfg(any(feature = "rust-v1.81", feature = "std"))] 797 | fn error_is_small_std() { 798 | use crate::prelude::Stashable; 799 | assert_small::>(); 800 | } 801 | 802 | #[test] 803 | fn error_is_small_surrogate() { 804 | use crate::surrogate_error_trait::prelude::Stashable; 805 | assert_small::>(); 806 | } 807 | 808 | fn assert_small() { 809 | use core::mem::size_of; 810 | assert_eq!(size_of::(), size_of::()); 811 | } 812 | } 813 | -------------------------------------------------------------------------------- /lazy_errors/src/into_eyre.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Display; 2 | 3 | use crate::{ 4 | error::{AdHocError, Error, ErrorData, StashedErrors, WrappedError}, 5 | stash::{ErrorStash, StashWithErrors}, 6 | }; 7 | 8 | /// Adds the [`into_eyre_result`](Self::into_eyre_result) method 9 | /// on types that can be converted into `Result`, 10 | /// converting them into `Result` instead. 11 | /// 12 | /// Do not implement this trait. Importing the trait is sufficient 13 | /// due to blanket implementations. The trait is implemented on `R` 14 | /// if `R` can be converted into `Result<_, E>` and 15 | /// if `E` implements [`IntoEyreReport`]. 16 | pub trait IntoEyreResult 17 | where 18 | I: IntoEyreReport, 19 | { 20 | /// Lossy conversion to return some type, 21 | /// for example a list of errors that may be empty, 22 | /// from functions returning [`eyre::Result`], 23 | /// i.e. `Result<_, E>` where `E` is [`eyre::Report`]: 24 | /// 25 | /// ``` 26 | /// use eyre::WrapErr; 27 | /// use lazy_errors::prelude::*; 28 | /// 29 | /// fn parse(s: &str) -> eyre::Result { 30 | /// use core::str::FromStr; 31 | /// i32::from_str(s).wrap_err_with(|| format!("Not an i32: '{s}'")) 32 | /// } 33 | /// 34 | /// fn parse_all() -> eyre::Result<()> { 35 | /// let mut stash = ErrorStash::new(|| "Failed to parse"); 36 | /// 37 | /// parse("🙈").or_stash(&mut stash); 38 | /// parse("🙉").or_stash(&mut stash); 39 | /// parse("🙊").or_stash(&mut stash); 40 | /// 41 | /// stash.into_eyre_result() 42 | /// } 43 | /// 44 | /// let err: eyre::Report = parse_all().unwrap_err(); 45 | /// let msg = format!("{err}"); 46 | /// assert!(msg.contains("🙈")); 47 | /// assert!(msg.contains("🙉")); 48 | /// assert!(msg.contains("🙊")); 49 | /// ``` 50 | /// 51 | /// Note: This method discards information because [`IntoEyreReport`] 52 | /// flattens the type into a single string 53 | /// that is then passed to [`eyre::eyre!`]. 54 | /// 55 | /// In some cases, for example if you're using [`or_create_stash`], 56 | /// you may want to use [`IntoEyreReport`] instead. 57 | /// 58 | /// [`or_create_stash`]: 59 | /// crate::or_create_stash::OrCreateStash::or_create_stash 60 | fn into_eyre_result(self) -> Result; 61 | } 62 | 63 | /// Adds the [`into_eyre_report`](Self::into_eyre_report) method 64 | /// on various error and error builder types. 65 | /// 66 | /// Do not implement this trait. Importing the trait is sufficient. 67 | /// due to blanket implementations. The trait is implemented on 68 | /// [`StashWithErrors`] and on 69 | /// `E` if `E` implements `core::error::Error + Send + Sync + 'a`. 70 | pub trait IntoEyreReport { 71 | /// Lossy conversion to return some type, for example 72 | /// a non-empty list of one or more errors, 73 | /// from functions returning [`eyre::Result`], 74 | /// i.e. `Result<_, E>` where `E` is [`eyre::Report`]. 75 | /// 76 | /// ``` 77 | /// # use lazy_errors::doctest_line_num_helper as replace_line_numbers; 78 | /// use eyre::{bail, eyre}; 79 | /// use lazy_errors::prelude::*; 80 | /// 81 | /// fn adhoc_error() -> eyre::Result<()> { 82 | /// let err = Error::from_message("first() failed"); 83 | /// bail!(err.into_eyre_report()); 84 | /// } 85 | /// 86 | /// let err: eyre::Report = adhoc_error().unwrap_err(); 87 | /// let printed = format!("{err}"); // No pretty-printing required 88 | /// let printed = replace_line_numbers(&printed); 89 | /// assert_eq!(printed, indoc::indoc! {" 90 | /// first() failed 91 | /// at src/into_eyre.rs:1234:56"}); 92 | /// 93 | /// fn wrapped_report() -> eyre::Result<()> { 94 | /// let report = eyre!("This is an eyre::Report"); 95 | /// let err: Error = Error::wrap(report); 96 | /// bail!(err.into_eyre_report()); 97 | /// } 98 | /// 99 | /// let err: eyre::Report = wrapped_report().unwrap_err(); 100 | /// let printed = format!("{err}"); // No pretty-printing required 101 | /// let printed = replace_line_numbers(&printed); 102 | /// assert_eq!(printed, indoc::indoc! {" 103 | /// This is an eyre::Report 104 | /// at src/into_eyre.rs:1234:56"}); 105 | /// 106 | /// fn stashed_errors() -> eyre::Result<()> { 107 | /// let mut stash = ErrorStash::new(|| "One or more things failed"); 108 | /// 109 | /// adhoc_error().or_stash(&mut stash); 110 | /// wrapped_report().or_stash(&mut stash); 111 | /// 112 | /// stash.into_eyre_result() 113 | /// } 114 | /// 115 | /// let err: eyre::Report = stashed_errors().unwrap_err(); 116 | /// let printed = format!("{err}"); // No pretty-printing required 117 | /// let printed = replace_line_numbers(&printed); 118 | /// assert_eq!(printed, indoc::indoc! {" 119 | /// One or more things failed 120 | /// - first() failed 121 | /// at src/into_eyre.rs:1234:56 122 | /// at src/into_eyre.rs:1234:56 123 | /// - This is an eyre::Report 124 | /// at src/into_eyre.rs:1234:56 125 | /// at src/into_eyre.rs:1234:56"}); 126 | /// ``` 127 | /// 128 | /// Note: This method discards information because it 129 | /// flattens the type into a single string 130 | /// that is then passed to [`eyre::eyre!`]. 131 | /// 132 | /// In some cases, for example if you're using [`or_stash`], 133 | /// you may want to use [`IntoEyreResult`] instead. 134 | /// 135 | /// [`or_stash`]: crate::or_stash::OrStash::or_stash 136 | fn into_eyre_report(self) -> eyre::Report; 137 | } 138 | 139 | impl IntoEyreResult<(), Error> for ErrorStash 140 | where 141 | F: FnOnce() -> M, 142 | M: Display, 143 | Error: IntoEyreReport, 144 | { 145 | #[track_caller] 146 | fn into_eyre_result(self) -> Result<(), eyre::Report> { 147 | let result: Result<(), Error> = self.into(); 148 | result.map_err(IntoEyreReport::into_eyre_report) 149 | } 150 | } 151 | 152 | impl IntoEyreReport for StashWithErrors { 153 | /// Flattens the error hierarchy into a single string 154 | /// that is then passed to [`eyre::eyre!`]. 155 | /// 156 | /// TODO: Improve this adapter somehow, if this is even possible. 157 | /// `color_eyre::Section` adds `Report::error`, 158 | /// but that method is not suited for our purpose. 159 | /// Firstly, it takes the error by value. 160 | /// Secondly, there aren't any accessors for these errors. 161 | /// Thirdly, these errors are not printed when using `{:?}`, 162 | /// as opposed to the “regular” error causes added via `wrap_err`. 163 | /// If we used `Into>` 164 | /// and return the [`eyre::Report`] from `main`, eyre would 165 | /// display the error using the regular, non-pretty-printed 166 | /// form and we won't see the full list of errors. 167 | #[track_caller] 168 | fn into_eyre_report(self) -> eyre::Report { 169 | let err = Error::::from(self); 170 | eyre::eyre!(format!("{err:#}")) 171 | } 172 | } 173 | 174 | impl IntoEyreReport for Error { 175 | fn into_eyre_report(self) -> eyre::Report { 176 | match self.into() { 177 | ErrorData::Stashed(inner) => inner.into_eyre_report(), 178 | ErrorData::Wrapped(inner) => inner.into_eyre_report(), 179 | ErrorData::AdHoc(inner) => inner.into_eyre_report(), 180 | } 181 | } 182 | } 183 | 184 | impl IntoEyreReport for StashedErrors { 185 | fn into_eyre_report(self) -> eyre::Report { 186 | eyre::eyre!(format!("{self:#}")) 187 | } 188 | } 189 | 190 | impl IntoEyreReport for WrappedError { 191 | fn into_eyre_report(self) -> eyre::Report { 192 | eyre::eyre!(format!("{self:#}")) 193 | } 194 | } 195 | 196 | impl IntoEyreReport for AdHocError { 197 | fn into_eyre_report(self) -> eyre::Report { 198 | eyre::eyre!(format!("{self:#}")) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /lazy_errors/src/or_create_stash.rs: -------------------------------------------------------------------------------- 1 | use crate::StashWithErrors; 2 | 3 | /// Adds the [`or_create_stash`](Self::or_create_stash) method 4 | /// on `Result<_, E>`, 5 | /// if `E` implements [`Into`](crate::Error#inner-error-type-i). 6 | /// 7 | /// Do not implement this trait. 8 | /// Importing the trait is sufficient due to blanket implementations. 9 | /// The trait is implemented on `Result<_, E>` if `E` implements `Into`, 10 | /// where `I` is the [_inner error type_](crate::Error#inner-error-type-i), 11 | /// typically [`prelude::Stashable`]. 12 | #[cfg_attr( 13 | any(feature = "rust-v1.81", feature = "std"), 14 | doc = r##" 15 | 16 | [`prelude::Stashable`]: crate::prelude::Stashable 17 | "## 18 | )] 19 | #[cfg_attr( 20 | not(any(feature = "rust-v1.81", feature = "std")), 21 | doc = r##" 22 | 23 | [`prelude::Stashable`]: crate::surrogate_error_trait::prelude::Stashable 24 | "## 25 | )] 26 | pub trait OrCreateStash 27 | where 28 | F: FnOnce() -> M, 29 | M: core::fmt::Display, 30 | { 31 | /// If `self` is `Result::Ok(value)`, returns `Result::Ok(value)`; 32 | /// if `self` is `Result::Err(e)`, returns `Result::Err(errs)` 33 | /// where `errs` is a [`StashWithErrors`] that is described by 34 | /// the provided error summary message and that will contain 35 | /// `e` as its first element. 36 | /// 37 | /// Use this method to defer both handling errors as well as 38 | /// creating an [`ErrorStash`]. 39 | /// In case the `Result` is `Result::Err(e)`, `or_create_stash` will 40 | /// create a [`StashWithErrors`] that contains `e` as its sole element. 41 | /// You can turn this stash into [`Error`] later. 42 | /// Meanwhile, you can run additional fallible functions, 43 | /// for example fallible cleanup steps. 44 | /// If those cleanup steps return errors as well, you can add them to 45 | /// the current error list by calling [`or_stash`]. 46 | /// When you're done, you can return the entire error list in one go. 47 | /// 48 | /// ``` 49 | /// # use lazy_errors::doctest_line_num_helper as replace_line_numbers; 50 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 51 | /// use lazy_errors::prelude::*; 52 | /// 53 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 54 | /// use lazy_errors::surrogate_error_trait::prelude::*; 55 | /// 56 | /// fn write_or_cleanup(text: &str) -> Result<(), Error> { 57 | /// match write(text).or_create_stash(|| "Failed to write") { 58 | /// Ok(()) => Ok(()), 59 | /// Err(mut stash) => { 60 | /// write("Recovering...").or_stash(&mut stash); 61 | /// cleanup().or_stash(&mut stash); 62 | /// return Err(stash.into()); 63 | /// } 64 | /// } 65 | /// } 66 | /// 67 | /// fn write(text: &str) -> Result<(), Error> { 68 | /// if !text.is_ascii() { 69 | /// return Err(err!("Input is not ASCII: '{text}'")); 70 | /// } 71 | /// 72 | /// Ok(()) 73 | /// } 74 | /// 75 | /// fn cleanup() -> Result<(), Error> { 76 | /// Err(err!("Cleanup failed")) 77 | /// } 78 | /// 79 | /// fn main() { 80 | /// assert!(write_or_cleanup("ASCII text").is_ok()); 81 | /// 82 | /// let err = write_or_cleanup("❌").unwrap_err(); 83 | /// let printed = format!("{err:#}"); 84 | /// let printed = replace_line_numbers(&printed); 85 | /// assert_eq!(printed, indoc::indoc! {" 86 | /// Failed to write 87 | /// - Input is not ASCII: '❌' 88 | /// at src/or_create_stash.rs:1234:56 89 | /// at src/or_create_stash.rs:1234:56 90 | /// - Cleanup failed 91 | /// at src/or_create_stash.rs:1234:56 92 | /// at src/or_create_stash.rs:1234:56"}); 93 | /// } 94 | /// ``` 95 | /// 96 | /// Sometimes you want to create an empty [`ErrorStash`] beforehand, 97 | /// adding errors (if any) as you go. 98 | /// In that case, please take a look at [`ErrorStash`] and [`or_stash`]. 99 | /// 100 | /// [`Error`]: crate::Error 101 | /// [`ErrorStash`]: crate::ErrorStash 102 | /// [`or_stash`]: crate::OrStash::or_stash 103 | /// [`or_create_stash`]: Self::or_create_stash 104 | fn or_create_stash(self, f: F) -> Result> 105 | where 106 | E: Into; 107 | } 108 | 109 | impl OrCreateStash for Result 110 | where 111 | F: FnOnce() -> M, 112 | M: core::fmt::Display, 113 | { 114 | #[track_caller] 115 | fn or_create_stash(self, f: F) -> Result> 116 | where 117 | E: Into, 118 | { 119 | match self { 120 | Ok(v) => Ok(v), 121 | Err(err) => Err(StashWithErrors::from(f(), err)), 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lazy_errors/src/or_stash.rs: -------------------------------------------------------------------------------- 1 | use crate::{stash::ErrorSink, StashWithErrors}; 2 | 3 | /// Adds the [`or_stash`](Self::or_stash) method on `Result<_, E>`, 4 | /// if `E` implements [`Into`](crate::Error#inner-error-type-i). 5 | /// 6 | /// Do not implement this trait. 7 | /// Importing the trait is sufficient due to blanket implementations. 8 | /// The trait is implemented on `Result<_, E>` if `E` implements `Into`, 9 | /// where `I` is the [_inner error type_](crate::Error#inner-error-type-i), 10 | /// typically [`prelude::Stashable`]. 11 | #[cfg_attr( 12 | any(feature = "rust-v1.81", feature = "std"), 13 | doc = r##" 14 | 15 | [`prelude::Stashable`]: crate::prelude::Stashable 16 | "## 17 | )] 18 | #[cfg_attr( 19 | not(any(feature = "rust-v1.81", feature = "std")), 20 | doc = r##" 21 | 22 | [`prelude::Stashable`]: crate::surrogate_error_trait::prelude::Stashable 23 | "## 24 | )] 25 | pub trait OrStash { 26 | /// If `self` is `Result::Ok(value)`, 27 | /// returns the `Ok(value)` variant of [`StashedResult`]; 28 | /// if `self` is `Result::Err(e)`, 29 | /// adds `e` to the provided [`ErrorStash`] or [`StashWithErrors`] 30 | /// and returns the `Err` variant [`StashedResult`]. 31 | /// 32 | /// Use this method to collect an arbitrary number 33 | /// of `Result::Err` occurrences 34 | /// in an [`ErrorStash`] or a [`StashWithErrors`], 35 | /// deferring error handling to some later point in time, 36 | /// for example until additional `Err`s or have been collected 37 | /// or until some cleanup logic has been executed. 38 | /// At that point, [`StashWithErrors`] can be converted into 39 | /// [`Error`](crate::Error) or into `eyre::Report` 40 | /// while [`ErrorStash`] can be converted into 41 | /// `Result<(), Error>` or `eyre::Result<()>`. 42 | /// 43 | /// ``` 44 | /// # use lazy_errors::doctest_line_num_helper as replace_line_numbers; 45 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 46 | /// use lazy_errors::prelude::*; 47 | /// 48 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 49 | /// use lazy_errors::surrogate_error_trait::prelude::*; 50 | /// 51 | /// fn run() -> Result<(), Error> { 52 | /// let mut stash = ErrorStash::new(|| "Failed to run application"); 53 | /// 54 | /// print_if_ascii("❓").or_stash(&mut stash); 55 | /// print_if_ascii("❗").or_stash(&mut stash); 56 | /// print_if_ascii("42").or_stash(&mut stash); 57 | /// 58 | /// cleanup().or_stash(&mut stash); // Runs regardless of errors 59 | /// 60 | /// stash.into() // Ok(()) if the stash is still empty 61 | /// } 62 | /// 63 | /// fn print_if_ascii(text: &str) -> Result<(), Error> { 64 | /// if !text.is_ascii() { 65 | /// return Err(err!("Input is not ASCII: '{text}'")); 66 | /// } 67 | /// 68 | /// println!("{text}"); 69 | /// Ok(()) 70 | /// } 71 | /// 72 | /// fn cleanup() -> Result<(), Error> { 73 | /// Err(err!("Cleanup failed")) 74 | /// } 75 | /// 76 | /// fn main() { 77 | /// let err = run().unwrap_err(); 78 | /// let printed = format!("{err:#}"); 79 | /// let printed = replace_line_numbers(&printed); 80 | /// assert_eq!(printed, indoc::indoc! {" 81 | /// Failed to run application 82 | /// - Input is not ASCII: '❓' 83 | /// at src/or_stash.rs:1234:56 84 | /// at src/or_stash.rs:1234:56 85 | /// - Input is not ASCII: '❗' 86 | /// at src/or_stash.rs:1234:56 87 | /// at src/or_stash.rs:1234:56 88 | /// - Cleanup failed 89 | /// at src/or_stash.rs:1234:56 90 | /// at src/or_stash.rs:1234:56"}); 91 | /// } 92 | /// ``` 93 | /// 94 | /// The [`ErrorStash`] is created manually in the example above. 95 | /// Before the first `Err` is added, the [`ErrorStash`] is empty. 96 | /// Converting an empty [`ErrorStash`] to `Result` will produce `Ok(())`. 97 | /// When [`or_stash`](OrStash::or_stash) is called on `Result::Err(e)`, 98 | /// `e` will be moved into the [`ErrorStash`]. As soon as there is 99 | /// at least one error stored in the [`ErrorStash`], converting it 100 | /// will yield `Result::Err(Error)`. 101 | /// 102 | /// Sometimes you don't want to create an empty [`ErrorStash`] beforehand. 103 | /// In that case you can call [`or_create_stash`] on `Result` 104 | /// to create a non-empty container on-demand, whenever necessary. 105 | /// 106 | /// [`ErrorStash`]: crate::ErrorStash 107 | /// [`or_create_stash`]: crate::OrCreateStash::or_create_stash 108 | fn or_stash(self, stash: &mut S) -> StashedResult; 109 | } 110 | 111 | /// Similar to [`core::result::Result`], except that this type 112 | /// is deliberately _not_ `#[must_use]` 113 | /// and the `Err` type is more or less hardcoded. 114 | /// 115 | /// Note that the error variant is [`&mut StashWithErrors`][StashWithErrors]. 116 | /// When `StashedResult` is returned from [`or_stash`], 117 | /// it actually borrows the inner value from 118 | /// the [`&mut ErrorStash`][crate::ErrorStash] 119 | /// that was passed to [`or_stash`]. 120 | /// Thus, if you want to keep the results of multiple [`or_stash`] calls 121 | /// around at the same time, in order to extract their `Ok(t)` values later, 122 | /// you need to call [`StashedResult::ok`] on them. 123 | /// Otherwise you'll get ownership-related compilation errors. 124 | /// Check out [`StashedResult::ok`] for an example. 125 | /// 126 | /// The reason we're keeping a reference to the [`StashWithErrors`] is 127 | /// that it allows you to use the [`try2!`] macro 128 | /// (and will probably allow you use the `?` operator in the future 129 | /// when the `Try` trait is stabilized). 130 | /// 131 | /// `StashedResult` is returned from [`or_stash`]. 132 | /// There should be no need to create values of this type manually. 133 | /// 134 | /// [`ErrorStash`]: crate::ErrorStash 135 | /// [`try2!`]: crate::try2! 136 | /// [`or_stash`]: OrStash::or_stash 137 | #[derive(Debug)] 138 | pub enum StashedResult<'s, T, I> { 139 | Ok(T), 140 | Err(&'s mut StashWithErrors), 141 | } 142 | 143 | impl OrStash for Result 144 | where 145 | E: Into, 146 | S: ErrorSink, 147 | { 148 | #[track_caller] 149 | fn or_stash(self, stash: &mut S) -> StashedResult { 150 | match self { 151 | Ok(v) => StashedResult::Ok(v), 152 | Err(err) => StashedResult::Err(stash.stash(err)), 153 | } 154 | } 155 | } 156 | 157 | impl StashedResult<'_, T, E> { 158 | /// Returns `Some(t)` if `self` is `Ok(t)`, `None` otherwise. 159 | /// 160 | /// This method is useful to discard the `&mut` borrowing of the 161 | /// [`ErrorStash`]/[`StashWithErrors`] that was passed as parameter 162 | /// to [`or_stash`]. 163 | /// You may need to do this if you have multiple [`or_stash`] statements 164 | /// and want to extract the `Ok(t)` result from them later. 165 | /// For example, the following example would fail to compile 166 | /// without calling `ok` (due to borrowing `errs` mutably twice): 167 | /// 168 | /// ``` 169 | /// # use core::str::FromStr; 170 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 171 | /// use lazy_errors::{prelude::*, Result}; 172 | /// 173 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 174 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 175 | /// 176 | /// fn parse_version(major: &str, minor: &str) -> Result<(u32, u32)> { 177 | /// let mut errs = ErrorStash::new(|| "Invalid version number"); 178 | /// 179 | /// let major = u32::from_str(major) 180 | /// .or_stash(&mut errs) 181 | /// .ok(); 182 | /// 183 | /// let minor = u32::from_str(minor) 184 | /// .or_stash(&mut errs) 185 | /// .ok(); 186 | /// 187 | /// // Return _all_ errors if major, minor, or both were invalid. 188 | /// errs.into_result()?; 189 | /// 190 | /// // If the result above was `Ok`, all `ok()` calls returned `Some`. 191 | /// Ok((major.unwrap(), minor.unwrap())) 192 | /// } 193 | /// 194 | /// assert_eq!(parse_version("42", "1337").unwrap(), (42, 1337)); 195 | /// 196 | /// assert_eq!( 197 | /// parse_version("42", "-1") 198 | /// .unwrap_err() 199 | /// .children() 200 | /// .len(), 201 | /// 1 202 | /// ); 203 | /// 204 | /// assert_eq!( 205 | /// parse_version("-1", "-1") 206 | /// .unwrap_err() 207 | /// .children() 208 | /// .len(), 209 | /// 2 210 | /// ); 211 | /// ``` 212 | /// 213 | /// [`ErrorStash`]: crate::ErrorStash 214 | /// [`or_stash`]: OrStash::or_stash 215 | pub fn ok(self) -> Option { 216 | match self { 217 | StashedResult::Ok(t) => Some(t), 218 | StashedResult::Err(_) => None, 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /lazy_errors/src/or_wrap.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | 3 | /// Adds the [`or_wrap`](Self::or_wrap) method on `Result<_, E>`, 4 | /// if `E` implements [`Into`](crate::Error#inner-error-type-i). 5 | /// 6 | /// Do not implement this trait. 7 | /// Importing the trait is sufficient due to blanket implementations. 8 | /// The trait is implemented on `Result<_, E>` if `E` implements `Into`, 9 | /// where `I` is the [_inner error type_](crate::Error#inner-error-type-i), 10 | /// typically [`prelude::Stashable`]. 11 | #[cfg_attr( 12 | any(feature = "rust-v1.81", feature = "std"), 13 | doc = r##" 14 | 15 | [`prelude::Stashable`]: crate::prelude::Stashable 16 | "## 17 | )] 18 | #[cfg_attr( 19 | not(any(feature = "rust-v1.81", feature = "std")), 20 | doc = r##" 21 | 22 | [`prelude::Stashable`]: crate::surrogate_error_trait::prelude::Stashable 23 | "## 24 | )] 25 | pub trait OrWrap { 26 | /// If `self` is `Result::Ok(value)`, returns `Result::Ok(value)`; 27 | /// if `self` is `Result::Err(e1)`, returns `Result::Err(e2)` 28 | /// where `e2` is an [`Error`] containing a [`WrappedError`] 29 | /// that will hold the original `e1` value. 30 | /// 31 | /// Allows you to convert any `Result<_, E>` to `Result<_, Error>` 32 | /// if `E` implements `Into`, where `I` is the 33 | /// [_inner error type_ of `Error`](crate::Error#inner-error-type-i) 34 | /// typically [`prelude::Stashable`]. 35 | /// 36 | /// ``` 37 | /// # use lazy_errors::doctest_line_num_helper as replace_line_numbers; 38 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 39 | /// use lazy_errors::prelude::*; 40 | /// 41 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 42 | /// use lazy_errors::surrogate_error_trait::prelude::*; 43 | /// 44 | /// fn run(tokens: &[&str]) -> Result<(), Error> { 45 | /// all_ascii(tokens).or_wrap() 46 | /// } 47 | /// 48 | /// fn all_ascii(tokens: &[&str]) -> Result<(), String> { 49 | /// match tokens.iter().find(|s| !s.is_ascii()) { 50 | /// None => Ok(()), 51 | /// Some(not_ascii) => Err(not_ascii.to_string()), 52 | /// } 53 | /// } 54 | /// 55 | /// fn main() { 56 | /// assert!(run(&["foo", "bar"]).is_ok()); 57 | /// 58 | /// let err = run(&["foo", "❌", "bar"]).unwrap_err(); 59 | /// let printed = format!("{err:#}"); 60 | /// let printed = replace_line_numbers(&printed); 61 | /// assert_eq!(printed, indoc::indoc! {" 62 | /// ❌ 63 | /// at src/or_wrap.rs:1234:56"}); 64 | /// } 65 | /// ``` 66 | /// 67 | /// Please take a look at [`or_wrap_with`] if you'd like to 68 | /// provide some kind of context information when wrapping the error. 69 | /// 70 | /// [`WrappedError`]: crate::WrappedError 71 | /// [`or_wrap_with`]: crate::OrWrapWith::or_wrap_with 72 | #[cfg_attr( 73 | any(feature = "rust-v1.81", feature = "std"), 74 | doc = r##" 75 | [`prelude::Stashable`]: crate::prelude::Stashable 76 | "## 77 | )] 78 | #[cfg_attr( 79 | not(any(feature = "rust-v1.81", feature = "std")), 80 | doc = r##" 81 | [`prelude::Stashable`]: crate::surrogate_error_trait::prelude::Stashable 82 | "## 83 | )] 84 | fn or_wrap(self) -> Result> 85 | where 86 | E: Into; 87 | } 88 | 89 | impl OrWrap for Result { 90 | #[track_caller] 91 | fn or_wrap(self) -> Result> 92 | where 93 | E: Into, 94 | { 95 | match self { 96 | Ok(t) => Ok(t), 97 | Err(inner) => Err(Error::wrap(inner)), 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lazy_errors/src/or_wrap_with.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Display; 2 | 3 | use crate::Error; 4 | 5 | /// Adds the [`or_wrap_with`](Self::or_wrap_with) method on `Result<_, E>`, 6 | /// if `E` implements [`Into`](crate::Error#inner-error-type-i). 7 | /// 8 | /// Do not implement this trait. 9 | /// Importing the trait is sufficient due to blanket implementations. 10 | /// The trait is implemented on `Result<_, E>` if `E` implements `Into`, 11 | /// where `I` is the [_inner error type_](crate::Error#inner-error-type-i), 12 | /// typically [`prelude::Stashable`]. 13 | #[cfg_attr( 14 | any(feature = "rust-v1.81", feature = "std"), 15 | doc = r##" 16 | 17 | [`prelude::Stashable`]: crate::prelude::Stashable 18 | "## 19 | )] 20 | #[cfg_attr( 21 | not(any(feature = "rust-v1.81", feature = "std")), 22 | doc = r##" 23 | 24 | [`prelude::Stashable`]: crate::surrogate_error_trait::prelude::Stashable 25 | "## 26 | )] 27 | pub trait OrWrapWith 28 | where 29 | F: FnOnce() -> M, 30 | M: Display, 31 | { 32 | /// If `self` is `Result::Ok(value)`, returns `Result::Ok(value)`; 33 | /// if `self` is `Result::Err(e1)`, returns `Result::Err(e2)` 34 | /// where `e2` is an [`Error`] containing a [`WrappedError`] 35 | /// that will hold the original `e1` value 36 | /// and annotates it with the message provided by the user. 37 | /// 38 | /// This method behaves identically to [`or_wrap`] 39 | /// except that you can pass some information 40 | /// about the context of the error. 41 | /// 42 | /// ``` 43 | /// # use lazy_errors::doctest_line_num_helper as replace_line_numbers; 44 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 45 | /// use lazy_errors::prelude::*; 46 | /// 47 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 48 | /// use lazy_errors::surrogate_error_trait::prelude::*; 49 | /// 50 | /// fn run(tokens: &[&str]) -> Result<(), Error> { 51 | /// all_ascii(tokens).or_wrap_with(|| "Input is not ASCII") 52 | /// } 53 | /// 54 | /// fn all_ascii(tokens: &[&str]) -> Result<(), String> { 55 | /// match tokens.iter().find(|s| !s.is_ascii()) { 56 | /// None => Ok(()), 57 | /// Some(not_ascii) => Err(not_ascii.to_string()), 58 | /// } 59 | /// } 60 | /// 61 | /// fn main() { 62 | /// assert!(run(&["foo", "bar"]).is_ok()); 63 | /// 64 | /// let err = run(&["foo", "❌", "bar"]).unwrap_err(); 65 | /// let printed = format!("{err:#}"); 66 | /// let printed = replace_line_numbers(&printed); 67 | /// assert_eq!(printed, indoc::indoc! {" 68 | /// Input is not ASCII: ❌ 69 | /// at src/or_wrap_with.rs:1234:56"}); 70 | /// } 71 | /// ``` 72 | /// 73 | /// Please take a look at [`or_wrap`] if you do not want to supply 74 | /// the informative message. 75 | /// 76 | /// [`WrappedError`]: crate::WrappedError 77 | /// [`or_wrap`]: crate::OrWrap::or_wrap 78 | fn or_wrap_with(self, f: F) -> Result> 79 | where 80 | E: Into; 81 | } 82 | 83 | impl OrWrapWith for Result 84 | where 85 | F: FnOnce() -> M, 86 | M: Display, 87 | { 88 | #[track_caller] 89 | fn or_wrap_with(self, f: F) -> Result> 90 | where 91 | E: Into, 92 | { 93 | match self { 94 | Ok(t) => Ok(t), 95 | Err(inner) => Err(Error::wrap_with(inner, f())), 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lazy_errors/src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! Exports traits and _aliased_ types to support the most common use-cases. 2 | //! 3 | //! When using any container from `lazy_errors`, such as [`lazy_errors::Error`] 4 | //! or [`lazy_errors::ErrorStash`], you usually don't want to specify the 5 | //! [_inner error type_ `I`] explicitly. 6 | //! This prelude exports type aliases for all types that otherwise need 7 | //! the `I` parameter. The specific type used as `I` makes the aliased types 8 | //! from the prelude well suited for the common 9 | //! “just bail out now (or later)” use case. 10 | //! 11 | //! Usually, anything that you want to treat as an error can be boxed 12 | //! into a [`lazy_errors::Stashable`]. 13 | //! Also, using the `'static` bound for the trait object usually works fine. 14 | //! Thus, `Stashable<'static>` is the [_inner error type_ `I`] 15 | //! for all container type aliases exported by this prelude. 16 | //! We also define and export [`Stashable`] as an alias for 17 | //! `Stashable<'static>` for readability, ergonomics, and maintainability. 18 | //! 19 | //! If you want to use different inner error types, you can go ahead and use 20 | //! the container and wrapper types from this library directly. In that case, 21 | //! please check out [the example in the crate root documentation][CUSTOM]. 22 | //! 23 | //! If you're using a Rust version older than v1.81 _and_ 24 | //! don't enable the `std` feature, you need to use 25 | //! the [`surrogate_error_trait::prelude`] instead. 26 | //! 27 | //! [`lazy_errors::Error`]: crate::Error 28 | //! [`lazy_errors::ErrorStash`]: crate::ErrorStash 29 | //! [`lazy_errors::Stashable`]: crate::Stashable 30 | //! [`surrogate_error_trait::prelude`]: crate::surrogate_error_trait::prelude 31 | //! [_inner error type_ `I`]: crate::Error#inner-error-type-i 32 | //! [CUSTOM]: crate#example-custom-error-types 33 | 34 | pub use crate::{ 35 | err, try2, OrCreateStash, OrStash, OrWrap, OrWrapWith, StashErr, 36 | TryCollectOrStash, TryMapOrStash, 37 | }; 38 | 39 | #[cfg(feature = "eyre")] 40 | pub use crate::{IntoEyreReport, IntoEyreResult}; 41 | 42 | /// Type alias for [`crate::StashedResult`] 43 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 44 | /// as explained in [the module documentation](module@self). 45 | pub type StashedResult<'a, T> = crate::StashedResult<'a, T, Stashable>; 46 | 47 | /// Type alias for [`crate::ErrorStash`] 48 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 49 | /// as explained in [the module documentation](module@self). 50 | pub type ErrorStash = crate::ErrorStash; 51 | 52 | /// Type alias for [`crate::StashWithErrors`] 53 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 54 | /// as explained in [the module documentation](module@self). 55 | pub type StashWithErrors = crate::StashWithErrors; 56 | 57 | /// Type alias for [`crate::Error`] 58 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 59 | /// as explained in [the module documentation](module@self). 60 | pub type Error = crate::Error; 61 | 62 | /// Type alias for [`crate::ErrorData`] 63 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 64 | /// as explained in [the module documentation](module@self). 65 | pub type ErrorData = crate::ErrorData; 66 | 67 | /// Type alias for [`crate::StashedErrors`] 68 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 69 | /// as explained in [the module documentation](module@self). 70 | pub type StashedErrors = crate::StashedErrors; 71 | 72 | /// Type alias for [`crate::WrappedError`] 73 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 74 | /// as explained in [the module documentation](module@self). 75 | pub type WrappedError = crate::WrappedError; 76 | 77 | /// Type alias for [`crate::AdHocError`] to get access to all error types by 78 | /// importing [`lazy_errors::prelude::*`](module@self). 79 | pub type AdHocError = crate::AdHocError; 80 | 81 | /// Type alias for [`crate::Stashable`] 82 | /// to use a `'static` bound for the boxed 83 | /// [_inner error type_ `I`](crate::Error#inner-error-type-i) trait object, 84 | /// as explained in [the module documentation](module@self). 85 | pub type Stashable = crate::Stashable<'static>; 86 | -------------------------------------------------------------------------------- /lazy_errors/src/stash.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::{self, Debug, Display}; 2 | 3 | use alloc::{ 4 | boxed::Box, 5 | string::{String, ToString}, 6 | vec::Vec, 7 | }; 8 | 9 | use crate::{ 10 | err, 11 | error::{self, Location}, 12 | Error, StashedResult, 13 | }; 14 | 15 | /// Something to push (“stash”) errors into. 16 | /// 17 | /// This trait is implemented by [`ErrorStash`] and [`StashWithErrors`] 18 | /// and serves to deduplicate internal logic that needs to work with 19 | /// either of these types. 20 | pub trait ErrorSink 21 | where 22 | E: Into, 23 | { 24 | /// Appends an error to this list of errors. 25 | fn stash(&mut self, error: E) -> &mut StashWithErrors; 26 | } 27 | 28 | /// Something to read errors from. 29 | /// 30 | /// This trait is implemented by [`ErrorStash`] and [`StashWithErrors`] 31 | /// and serves to deduplicate internal logic that needs to work with 32 | /// either of these types. 33 | pub trait ErrorSource { 34 | /// Returns all errors that have been added to this list so far. 35 | fn errors(&self) -> &[I]; 36 | } 37 | 38 | /// Something that is/wraps a mutable, empty or non-empty list of errors, 39 | /// and can be forced to contain at least one error. 40 | /// 41 | /// This trait is implemented by [`ErrorStash`] and [`StashWithErrors`] 42 | /// and serves to deduplicate internal logic that needs to work with 43 | /// either of these types. 44 | /// 45 | /// Since [`enforce_errors`] returns a `&mut StashWithErrors`, 46 | /// this trait is trivially implemented by `StashWithErrors`. 47 | /// It's main purpose, however, is to “coerce” a `&mut ErrorStash` 48 | /// (which is either empty or wraps a `StashWithErrors`) 49 | /// into a `&mut StashWithErrors`. 50 | /// When deduplicating internal implementation details of this crate, 51 | /// we ran into some cases where we know that a given `ErrorStash` 52 | /// won't be empty, but the type system doesn't. 53 | /// While it may be tempting to use [`ErrorStash::ok`] instead, 54 | /// that method returns [`StashedResult<(), _>`] 55 | /// when what we need may be `StashedResult`. 56 | /// In those cases we usually don't have such a generic `T` value 57 | /// and can't create it either. 58 | /// While using `unreachable!()` for `T` would be possible, 59 | /// using [`enforce_errors`] instead ensures that the crate won't panic. 60 | /// 61 | /// This trait should _never_ be made part of the crate's API. 62 | /// 63 | /// [`enforce_errors`]: Self::enforce_errors 64 | pub trait EnforceErrors { 65 | /// If this list of errors is non-empty, 66 | /// coerces `&mut self` to [`&mut StashWithErrors`], 67 | /// otherwise an internal error will be added to the list first, 68 | /// ensuring that it won't be empty anymore. 69 | /// 70 | /// [`&mut StashWithErrors`]: StashWithErrors 71 | fn enforce_errors(&mut self) -> &mut StashWithErrors; 72 | } 73 | 74 | /// A builder for [`Error`] that keeps a list of errors 75 | /// which may still be empty, along with a message that summarizes 76 | /// all errors that end up in the list. 77 | /// 78 | /// The generic type parameter `F` is a function or closure that 79 | /// will create the error summary message lazily. 80 | /// It will be called when the first error is added. 81 | /// The generic type parameter `M` is the result returned from `F`, 82 | /// i.e. the type of the error summary message itself. 83 | /// The generic type parameter `I` is the 84 | /// [_inner error type_ of `Error`](Error#inner-error-type-i). 85 | /// 86 | /// Essentially, this type is a builder for something similar to 87 | /// `Result<(), Vec>`. Errors can be added by calling 88 | /// [`push`] or by calling [`or_stash`] on `Result`. 89 | /// When you're done collecting the errors, the [`ErrorStash`] can be 90 | /// transformed into `Result<(), Error>` (via [`From`]/[`Into`]), 91 | /// where [`Error`] basically wraps a `Vec` 92 | /// along with a message that summarizes all errors in that list. 93 | /// 94 | /// ``` 95 | /// # use lazy_errors::doctest_line_num_helper as replace_line_numbers; 96 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 97 | /// use lazy_errors::prelude::*; 98 | /// 99 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 100 | /// use lazy_errors::surrogate_error_trait::prelude::*; 101 | /// 102 | /// let errs = ErrorStash::new(|| "Something went wrong"); 103 | /// assert_eq!(&format!("{errs}"), "Stash of 0 errors currently"); 104 | /// let r: Result<(), Error> = errs.into(); 105 | /// assert!(r.is_ok()); 106 | /// 107 | /// let mut errs = ErrorStash::new(|| "Something went wrong"); 108 | /// errs.push("This is an error message"); 109 | /// assert_eq!(&format!("{errs}"), "Stash of 1 errors currently"); 110 | /// 111 | /// errs.push("Yet another error message"); 112 | /// assert_eq!(&format!("{errs}"), "Stash of 2 errors currently"); 113 | /// 114 | /// let r: Result<(), Error> = errs.into(); 115 | /// let err = r.unwrap_err(); 116 | /// 117 | /// assert_eq!(&format!("{err}"), "Something went wrong (2 errors)"); 118 | /// 119 | /// let printed = format!("{err:#}"); 120 | /// let printed = replace_line_numbers(&printed); 121 | /// assert_eq!(printed, indoc::indoc! {" 122 | /// Something went wrong 123 | /// - This is an error message 124 | /// at src/stash.rs:1234:56 125 | /// - Yet another error message 126 | /// at src/stash.rs:1234:56"}); 127 | /// ``` 128 | #[cfg_attr( 129 | feature = "eyre", 130 | doc = r##" 131 | 132 | There's also [`IntoEyreResult`](crate::IntoEyreResult) 133 | which performs a (lossy) conversion to 134 | [`eyre::Result`](eyre::Result). 135 | 136 | "## 137 | )] 138 | /// If you do not want to create an empty [`ErrorStash`] before adding errors, 139 | /// you can use [`or_create_stash`] which will 140 | /// create a [`StashWithErrors`] when an error actually occurs. 141 | /// 142 | /// [`or_stash`]: crate::OrStash::or_stash 143 | /// [`or_create_stash`]: crate::OrCreateStash::or_create_stash 144 | /// [`push`]: Self::push 145 | pub enum ErrorStash 146 | where 147 | F: FnOnce() -> M, 148 | M: Display, 149 | { 150 | Empty(F), 151 | WithErrors(StashWithErrors), 152 | } 153 | 154 | /// A builder for [`Error`] that keeps a list of one or more errors, 155 | /// along with a message that summarizes all errors that end up in the list. 156 | /// 157 | /// The generic type parameter `I` is the 158 | /// [_inner error type_ of `Error`](Error#inner-error-type-i). 159 | /// 160 | /// This type is similar to [`ErrorStash`] except that an [`ErrorStash`] 161 | /// may be empty. Since [`StashWithErrors`] contains at least one error, 162 | /// guaranteed by the type system at compile time, this type implements 163 | /// `Into`. 164 | #[cfg_attr( 165 | feature = "eyre", 166 | doc = r##" 167 | 168 | There's also [`IntoEyreReport`](crate::IntoEyreReport) 169 | which performs a (lossy) conversion to 170 | [`eyre::Report`](eyre::Report). 171 | "## 172 | )] 173 | #[derive(Debug)] 174 | pub struct StashWithErrors { 175 | summary: Box, 176 | errors: Vec, 177 | locations: Vec, 178 | } 179 | 180 | impl Debug for ErrorStash 181 | where 182 | F: FnOnce() -> M, 183 | M: Display, 184 | I: Debug, 185 | { 186 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 187 | match self { 188 | Self::Empty(_) => write!(f, "ErrorStash(Empty)"), 189 | Self::WithErrors(errs) => { 190 | write!(f, "ErrorStash(")?; 191 | Debug::fmt(errs, f)?; 192 | write!(f, ")")?; 193 | Ok(()) 194 | } 195 | } 196 | } 197 | } 198 | 199 | impl Display for ErrorStash 200 | where 201 | F: FnOnce() -> M, 202 | M: Display, 203 | { 204 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 205 | match self { 206 | Self::Empty(_) => display::(f, &[]), 207 | Self::WithErrors(errs) => Display::fmt(errs, f), 208 | } 209 | } 210 | } 211 | 212 | impl Display for StashWithErrors { 213 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 214 | display(f, self.errors()) 215 | } 216 | } 217 | 218 | impl ErrorSource for ErrorStash 219 | where 220 | F: FnOnce() -> M, 221 | M: Display, 222 | { 223 | fn errors(&self) -> &[I] { 224 | self.errors() 225 | } 226 | } 227 | 228 | impl ErrorSource for StashWithErrors { 229 | fn errors(&self) -> &[I] { 230 | self.errors() 231 | } 232 | } 233 | 234 | impl ErrorSink for ErrorStash 235 | where 236 | E: Into, 237 | F: FnOnce() -> M, 238 | M: Display, 239 | { 240 | #[track_caller] 241 | fn stash(&mut self, err: E) -> &mut StashWithErrors { 242 | self.push(err) 243 | } 244 | } 245 | 246 | impl ErrorSink for StashWithErrors 247 | where 248 | E: Into, 249 | { 250 | #[track_caller] 251 | fn stash(&mut self, err: E) -> &mut StashWithErrors { 252 | self.push(err) 253 | } 254 | } 255 | 256 | impl EnforceErrors for ErrorStash 257 | where 258 | F: FnOnce() -> M, 259 | M: Display, 260 | Error: Into, 261 | { 262 | #[track_caller] 263 | fn enforce_errors(&mut self) -> &mut StashWithErrors { 264 | match self { 265 | ErrorStash::Empty(_) => self.stash(err!("INTERNAL ERROR")), 266 | ErrorStash::WithErrors(stash) => stash, 267 | } 268 | } 269 | } 270 | 271 | impl EnforceErrors for StashWithErrors 272 | where 273 | Error: Into, 274 | { 275 | fn enforce_errors(&mut self) -> &mut StashWithErrors { 276 | self 277 | } 278 | } 279 | 280 | impl From> for Result<(), Error> 281 | where 282 | F: FnOnce() -> M, 283 | M: Display, 284 | { 285 | fn from(stash: ErrorStash) -> Self { 286 | match stash { 287 | ErrorStash::Empty(_) => Ok(()), 288 | ErrorStash::WithErrors(stash) => Err(stash.into()), 289 | } 290 | } 291 | } 292 | 293 | impl From> for Error { 294 | fn from(stash: StashWithErrors) -> Self { 295 | Error::from_stash(stash.summary, stash.errors, stash.locations) 296 | } 297 | } 298 | 299 | impl ErrorStash 300 | where 301 | F: FnOnce() -> M, 302 | M: Display, 303 | { 304 | /// Creates a new [`ErrorStash`] with a “lazy” error summary message 305 | /// that will be evaluated when the first error (if any) is added 306 | /// to the stash. 307 | pub fn new(f: F) -> Self { 308 | Self::Empty(f) 309 | } 310 | 311 | /// Adds an error to this stash. 312 | /// 313 | /// Since the stash is guaranteed to be non-empty afterwards, this method 314 | /// returns a mutable reference to the inner [`StashWithErrors`]. 315 | /// If you need to get that [`StashWithErrors`] by value, 316 | /// you can call [`push_and_convert`](Self::push_and_convert) instead. 317 | #[track_caller] 318 | pub fn push(&mut self, err: E) -> &mut StashWithErrors 319 | where 320 | E: Into, 321 | { 322 | // We need to move out of `&mut self` 323 | // because we want to call `f()` which is `FnOnce()`. 324 | 325 | let mut swap = Self::WithErrors(StashWithErrors { 326 | summary: String::new().into_boxed_str(), 327 | errors: vec![], 328 | locations: vec![], 329 | }); 330 | 331 | core::mem::swap(self, &mut swap); 332 | *self = ErrorStash::WithErrors(swap.push_and_convert(err)); 333 | match self { 334 | ErrorStash::Empty(_) => unreachable!(), 335 | ErrorStash::WithErrors(stash_with_errors) => stash_with_errors, 336 | } 337 | } 338 | 339 | /// Adds an error to this stash, 340 | /// consumes `self`, and returns the inner [`StashWithErrors`] by value. 341 | /// 342 | /// Usually, you'd want to call [`push`](Self::push) instead, 343 | /// which takes a `&mut self` instead of `self`. 344 | /// However, `push_and_convert` is more useful in some cases, 345 | /// for example if you want to return from a function 346 | /// after pushing a final error: 347 | /// 348 | /// ``` 349 | /// # use lazy_errors::doctest_line_num_helper as replace_line_numbers; 350 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 351 | /// use lazy_errors::{prelude::*, Result}; 352 | /// 353 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 354 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 355 | /// 356 | /// fn check(bytes: &[u8]) -> Result<()> { 357 | /// let mut errs = ErrorStash::new(|| "Something went wrong"); 358 | /// 359 | /// // ... Code that may or may not have added errors to `errs` ... 360 | /// 361 | /// match bytes { 362 | /// [] => Ok(()), 363 | /// [42] => Ok(()), 364 | /// [1, 3, 7] => Ok(()), 365 | /// _ => { 366 | /// let msg = format!("Invalid bytes: {bytes:?}"); 367 | /// let errs: StashWithErrors = errs.push_and_convert(msg); 368 | /// let errs: Error = errs.into(); 369 | /// Err(errs) 370 | /// } 371 | /// } 372 | /// } 373 | /// ``` 374 | #[track_caller] 375 | pub fn push_and_convert(self, err: E) -> StashWithErrors 376 | where 377 | E: Into, 378 | { 379 | match self { 380 | ErrorStash::Empty(f) => StashWithErrors::from(f(), err), 381 | ErrorStash::WithErrors(mut stash) => { 382 | stash.push(err); 383 | stash 384 | } 385 | } 386 | } 387 | 388 | /// Returns `true` if the stash is empty. 389 | /// 390 | /// ``` 391 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 392 | /// use lazy_errors::prelude::*; 393 | /// 394 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 395 | /// use lazy_errors::surrogate_error_trait::prelude::*; 396 | /// 397 | /// let mut errs = ErrorStash::new(|| "Summary message"); 398 | /// assert!(errs.is_empty()); 399 | /// 400 | /// errs.push("First error"); 401 | /// assert!(!errs.is_empty()); 402 | /// ``` 403 | pub fn is_empty(&self) -> bool { 404 | match self { 405 | ErrorStash::Empty(_) => true, 406 | ErrorStash::WithErrors(_) => false, 407 | } 408 | } 409 | 410 | /// Returns all errors that have been put into this stash so far. 411 | /// 412 | /// ``` 413 | /// type ErrorStash = lazy_errors::ErrorStash; 414 | /// 415 | /// let mut errs = ErrorStash::new(|| "Summary message"); 416 | /// assert_eq!(errs.errors(), &[]); 417 | /// 418 | /// errs.push(42); 419 | /// errs.push(-1); 420 | /// errs.push(1337); 421 | /// assert_eq!(errs.errors(), &[42, -1, 1337]); 422 | /// ``` 423 | /// 424 | /// Note that this method only returns errors that have been 425 | /// put into this stash _directly_. 426 | /// Each of those errors thus may have been created from 427 | /// an [`ErrorStash`](crate::ErrorStash), 428 | /// which stored another level of errors. 429 | /// Such transitive children will _not_ be returned from this method. 430 | pub fn errors(&self) -> &[I] { 431 | match self { 432 | ErrorStash::Empty(_) => &[], 433 | ErrorStash::WithErrors(stash) => stash.errors(), 434 | } 435 | } 436 | 437 | /// Returns `Ok(())` if the stash is empty, 438 | /// otherwise returns [`StashedResult::Err`]. 439 | /// 440 | /// This method basically allows you to use the `?` operator 441 | /// (currently implemented in the form of the [`try2!`] macro) 442 | /// on _all_ prior errors simultaneously. 443 | /// 444 | /// ``` 445 | /// use std::collections::HashMap; 446 | /// 447 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 448 | /// use lazy_errors::{prelude::*, Result}; 449 | /// 450 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 451 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 452 | /// 453 | /// // Always parses two configs, even if the first one contains an error. 454 | /// // All errors or groups of errors returned from this function 455 | /// // share the same error summary message. 456 | /// fn configure( 457 | /// path_to_config_a: &str, 458 | /// path_to_config_b: &str, 459 | /// ) -> Result> { 460 | /// let mut errs = ErrorStash::new(|| "Invalid app config"); 461 | /// 462 | /// let config_a = parse_config(path_to_config_a) 463 | /// .or_stash(&mut errs) 464 | /// .ok(); 465 | /// 466 | /// let config_b = parse_config(path_to_config_b) 467 | /// .or_stash(&mut errs) 468 | /// .ok(); 469 | /// 470 | /// // If there was any error, bail out now. 471 | /// // If there were no errors, both configs can be unwrapped. 472 | /// try2!(errs.ok()); 473 | /// let config_a = config_a.unwrap(); 474 | /// let config_b = config_b.unwrap(); 475 | /// 476 | /// Ok(try2!(merge(config_a, config_b).or_stash(&mut errs))) 477 | /// } 478 | /// 479 | /// fn parse_config(path: &str) -> Result> { 480 | /// if path == "bad.cfg" { 481 | /// Err(err!("Config file contains an error")) 482 | /// } else { 483 | /// // ... 484 | /// Ok(HashMap::new()) 485 | /// } 486 | /// } 487 | /// 488 | /// fn merge( 489 | /// _a: HashMap, 490 | /// _b: HashMap, 491 | /// ) -> Result> { 492 | /// // ... 493 | /// Ok(HashMap::new()) 494 | /// } 495 | /// 496 | /// let err = configure("bad.cfg", "bad.cfg").unwrap_err(); 497 | /// assert_eq!(err.children().len(), 2); 498 | /// 499 | /// let err = configure("good.cfg", "bad.cfg").unwrap_err(); 500 | /// assert_eq!(err.children().len(), 1); 501 | /// 502 | /// assert!(configure("good.cfg", "good.cfg").is_ok()); 503 | /// ``` 504 | /// 505 | /// This method is similar to [`ErrorStash::into_result`] or 506 | /// `ErrorStash::into`. As opposed to these other methods, however, 507 | /// [`ok`] does not consume `self`. It only borrows `self` mutably. 508 | /// This allows you to continue adding errors later, 509 | /// as soon as you have dropped the [`StashedResult`] 510 | /// or called [`StashedResult::ok`] to discard the borrowed reference. 511 | /// 512 | /// This method enables you to place “barriers” in your code. 513 | /// Before the “barrier”, you can collect multiple errors. 514 | /// Then, at some pivotal check, you'll either return all previous errors 515 | /// or keep going, knowing that no errors have occurred so far. 516 | /// 517 | /// [`ErrorData::Stashed`]: crate::ErrorData::Stashed 518 | /// [`StashedErrors`]: crate::StashedErrors 519 | /// [`ok`]: Self::ok 520 | /// [`try2!`]: crate::try2! 521 | pub fn ok(&mut self) -> StashedResult<(), I> { 522 | match self { 523 | ErrorStash::Empty(_) => StashedResult::Ok(()), 524 | ErrorStash::WithErrors(errs) => StashedResult::Err(errs), 525 | } 526 | } 527 | 528 | /// Returns `Ok(())` if the stash is empty, otherwise returns an `Err` 529 | /// containing all errors from this stash. 530 | /// 531 | /// You can usually call `into` instead of this method. 532 | /// This method actually does nothing else besides specifying 533 | /// the return type. In some cases, Rust cannot figure out 534 | /// which type you want to convert into. 535 | /// This method may be more readable than specifying the concrete types: 536 | /// 537 | /// ``` 538 | /// # use core::str::FromStr; 539 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 540 | /// use lazy_errors::{prelude::*, Result}; 541 | /// 542 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 543 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 544 | /// 545 | /// fn count_numbers(nums: &[&str]) -> Result { 546 | /// let mut errs = ErrorStash::new(|| "Something wasn't a number"); 547 | /// 548 | /// for n in nums { 549 | /// i32::from_str(n).or_stash(&mut errs); 550 | /// } 551 | /// 552 | /// // errs.into()?; // Does not compile 553 | /// // Result::<()>::from(errs)?; // Works but is hard to read and type 554 | /// errs.into_result()?; // Much nicer 555 | /// 556 | /// Ok(nums.len()) 557 | /// } 558 | /// 559 | /// assert_eq!(count_numbers(&["42"]).unwrap(), 1); 560 | /// assert!(count_numbers(&["42", ""]).is_err()); 561 | /// ``` 562 | /// 563 | /// In case there was at least one error in this stash, 564 | /// the [`Error`] will hold the [`ErrorData::Stashed`] variant 565 | /// which contains a [`StashedErrors`] object. 566 | /// 567 | /// [`ErrorData::Stashed`]: crate::ErrorData::Stashed 568 | /// [`StashedErrors`]: crate::StashedErrors 569 | pub fn into_result(self) -> Result<(), Error> { 570 | self.into() 571 | } 572 | } 573 | 574 | impl StashWithErrors { 575 | /// Creates a [`StashWithErrors`] that contains a single error so far; 576 | /// the supplied message shall summarize 577 | /// that error and all errors that will be added later. 578 | #[track_caller] 579 | pub fn from(summary: M, error: E) -> Self 580 | where 581 | M: Display, 582 | E: Into, 583 | { 584 | Self { 585 | summary: summary.to_string().into(), 586 | errors: vec![error.into()], 587 | locations: vec![error::location()], 588 | } 589 | } 590 | 591 | /// Adds an error into the stash. 592 | #[track_caller] 593 | pub fn push(&mut self, err: E) -> &mut StashWithErrors 594 | where 595 | E: Into, 596 | { 597 | self.errors.push(err.into()); 598 | self.locations.push(error::location()); 599 | self 600 | } 601 | 602 | /// Returns all errors that have been put into this stash so far. 603 | /// 604 | /// Note that this method only returns errors that have been 605 | /// put into this stash _directly_. 606 | /// Each of those errors thus may have been created from 607 | /// an [`ErrorStash`](crate::ErrorStash), 608 | /// which stored another level of errors. 609 | /// Such transitive children will _not_ be returned from this method. 610 | pub fn errors(&self) -> &[I] { 611 | &self.errors 612 | } 613 | 614 | /// ⚠️ Do not use this method! ⚠️ 615 | /// 616 | /// Returns a [`StashWithErrors`] that's identical to `self` 617 | /// by replacing the contents of `&mut self` with dummy values. 618 | /// 619 | /// Do not call this method. It must only be used for internal purposes. 620 | /// This method is basically a wrapper for [`core::mem::swap`] 621 | /// that also handles the `I` type parameter. 622 | /// 623 | /// For internal usage only. Even then: Take care when using this method. 624 | /// Even if you have a `&mut`, you or your callers may not expect 625 | /// the value to change “that much”. 626 | /// This method should only be used by the [`try2!`] macro. 627 | /// When the `Try` trait is stabilized, we can implement it 628 | /// and remove the [`try2!`] macro and this method. 629 | /// 630 | /// ⚠️ Do not use this method! ⚠️ 631 | /// 632 | /// [`try2!`]: crate::try2! 633 | #[doc(hidden)] 634 | pub fn take(&mut self) -> Self { 635 | // The dummy we'll be swapping into `self` should never “leak”, 636 | // if this type is used correctly. 637 | // But better print a specific error message in case it does. 638 | const WARNING: &str = "Internal error: Error info cleared by take()"; 639 | 640 | let mut swap_with = Self { 641 | summary: WARNING.to_string().into_boxed_str(), 642 | errors: vec![], 643 | locations: vec![], 644 | }; 645 | 646 | core::mem::swap(&mut swap_with, self); 647 | swap_with 648 | } 649 | } 650 | 651 | fn display(f: &mut fmt::Formatter<'_>, errors: &[I]) -> fmt::Result { 652 | let count = errors.len(); 653 | write!(f, "Stash of {count} errors currently") 654 | } 655 | 656 | #[cfg(test)] 657 | mod tests { 658 | #[cfg(any(feature = "rust-v1.81", feature = "std"))] 659 | use crate::prelude::*; 660 | 661 | #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 662 | use crate::surrogate_error_trait::prelude::*; 663 | 664 | use crate::stash::EnforceErrors; 665 | 666 | #[test] 667 | fn stash_debug_fmt_when_empty() { 668 | let errs = ErrorStash::new(|| "Mock message"); 669 | assert_eq!(format!("{errs:?}"), "ErrorStash(Empty)"); 670 | } 671 | 672 | #[test] 673 | fn stash_debug_fmt_with_errors() { 674 | let mut errs = ErrorStash::new(|| "Mock message"); 675 | errs.push("First error"); 676 | errs.push(Error::from_message("Second error")); 677 | 678 | let msg = format!("{errs:?}"); 679 | dbg!(&msg); 680 | 681 | assert!(msg.contains("ErrorStash")); 682 | assert!(msg.contains("StashWithErrors")); 683 | 684 | assert!(msg.contains("First error")); 685 | assert!(msg.contains("Second error")); 686 | 687 | assert!(msg.contains("lazy_errors")); 688 | assert!(msg.contains("stash.rs")); 689 | } 690 | 691 | #[cfg(feature = "eyre")] 692 | #[test] 693 | fn stash_debug_fmt_with_errors_eyre() { 694 | let mut errs = ErrorStash::new(|| "Mock message"); 695 | 696 | errs.push(eyre::eyre!("Eyre error")); 697 | 698 | let msg = format!("{errs:?}"); 699 | dbg!(&msg); 700 | 701 | assert!(msg.contains("Eyre error")); 702 | assert!(msg.contains("lazy_errors")); 703 | assert!(msg.contains("stash.rs")); 704 | } 705 | 706 | #[test] 707 | fn error_stash_enforce_errors_modifies_original_stash() { 708 | let mut error_stash = ErrorStash::new(|| "Failure"); 709 | assert_eq!(error_stash.errors().len(), 0); 710 | 711 | { 712 | let stash_with_errors = error_stash.enforce_errors(); 713 | assert_eq!(stash_with_errors.errors().len(), 1); 714 | // Drop the mutable borrow 715 | } 716 | 717 | assert_eq!(error_stash.errors().len(), 1); 718 | 719 | let err = error_stash.into_result().unwrap_err(); 720 | let msg = format!("{err}"); 721 | assert_eq!("Failure: INTERNAL ERROR", &msg); 722 | } 723 | 724 | #[test] 725 | fn error_stash_enforce_errors_modifies_only_once() { 726 | let mut error_stash = ErrorStash::new(|| "Failure"); 727 | assert_eq!(error_stash.errors().len(), 0); 728 | 729 | error_stash.enforce_errors(); 730 | assert_eq!(error_stash.errors().len(), 1); 731 | 732 | { 733 | let stash_with_errors = error_stash.enforce_errors(); 734 | assert_eq!(stash_with_errors.errors().len(), 1); 735 | // Drop the mutable borrow 736 | } 737 | 738 | assert_eq!(error_stash.errors().len(), 1); 739 | 740 | let err = error_stash.into_result().unwrap_err(); 741 | let msg = format!("{err}"); 742 | assert_eq!("Failure: INTERNAL ERROR", &msg); 743 | } 744 | 745 | #[test] 746 | fn error_stash_enforce_errors_does_not_modify_if_nonempty() { 747 | let mut error_stash = ErrorStash::new(|| "Failure"); 748 | assert_eq!(error_stash.errors().len(), 0); 749 | 750 | error_stash.push("External error"); 751 | assert_eq!(error_stash.errors().len(), 1); 752 | 753 | error_stash.enforce_errors(); 754 | assert_eq!(error_stash.errors().len(), 1); 755 | 756 | { 757 | let stash_with_errors = error_stash.enforce_errors(); 758 | assert_eq!(stash_with_errors.errors().len(), 1); 759 | // Drop the mutable borrow 760 | } 761 | 762 | assert_eq!(error_stash.errors().len(), 1); 763 | 764 | let err = error_stash.into_result().unwrap_err(); 765 | let msg = format!("{err}"); 766 | assert_eq!("Failure: External error", &msg); 767 | } 768 | 769 | #[test] 770 | fn stash_with_errors_enforce_errors_modifies_only_once() { 771 | let mut error_stash = ErrorStash::new(|| "Failure"); 772 | assert_eq!(error_stash.errors().len(), 0); 773 | 774 | error_stash.enforce_errors(); 775 | assert_eq!(error_stash.errors().len(), 1); 776 | 777 | { 778 | let stash_with_errors = error_stash.enforce_errors(); 779 | assert_eq!(stash_with_errors.errors().len(), 1); 780 | 781 | stash_with_errors.enforce_errors(); 782 | assert_eq!(stash_with_errors.errors().len(), 1); 783 | 784 | // Drop the mutable borrow 785 | } 786 | 787 | assert_eq!(error_stash.errors().len(), 1); 788 | } 789 | 790 | #[test] 791 | fn stash_with_errors_enforce_errors_does_not_modify() { 792 | let mut swe = StashWithErrors::from("Failure", "External error"); 793 | assert_eq!(swe.errors().len(), 1); 794 | 795 | swe.enforce_errors(); 796 | assert_eq!(swe.errors().len(), 1); 797 | 798 | let err: Error = swe.into(); 799 | let msg = format!("{err}"); 800 | assert_eq!("Failure: External error", &msg); 801 | } 802 | } 803 | -------------------------------------------------------------------------------- /lazy_errors/src/stash_err.rs: -------------------------------------------------------------------------------- 1 | use core::marker::PhantomData; 2 | 3 | use crate::{OrStash, StashedResult}; 4 | 5 | /// Adds the [`stash_err`](Self::stash_err) method on 6 | /// [`Iterator>`](Iterator) 7 | /// if `E` implements [`Into`](crate::Error#inner-error-type-i). 8 | /// 9 | /// Do not implement this trait. 10 | /// Importing the trait is sufficient due to blanket implementations. 11 | /// The trait is implemented automatically if `E` implements `Into`, 12 | /// where `I` is the [_inner error type_](crate::Error#inner-error-type-i), 13 | /// typically [`prelude::Stashable`]. 14 | #[cfg_attr( 15 | any(feature = "rust-v1.81", feature = "std"), 16 | doc = r##" 17 | 18 | [`prelude::Stashable`]: crate::prelude::Stashable 19 | "## 20 | )] 21 | #[cfg_attr( 22 | not(any(feature = "rust-v1.81", feature = "std")), 23 | doc = r##" 24 | 25 | [`prelude::Stashable`]: crate::surrogate_error_trait::prelude::Stashable 26 | "## 27 | )] 28 | pub trait StashErr: Iterator> 29 | where 30 | E: Into, 31 | { 32 | /// Turns an [`Iterator>`](Iterator) 33 | /// into an `Iterator` 34 | /// that will move any `E` item into an error stash 35 | /// as soon as it is encountered. 36 | /// 37 | /// ``` 38 | /// # use core::str::FromStr; 39 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 40 | /// use lazy_errors::{prelude::*, Result}; 41 | /// 42 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 43 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 44 | /// 45 | /// fn parse_each_u8(tokens: &[&str]) -> (Vec, usize) { 46 | /// let mut errs = ErrorStash::new(|| "There were one or more errors"); 47 | /// 48 | /// let numbers: Vec = tokens 49 | /// .iter() 50 | /// .map(|&s| u8::from_str(s)) 51 | /// .stash_err(&mut errs) 52 | /// .collect(); 53 | /// 54 | /// let errors = match errs.into_result() { 55 | /// Ok(()) => 0, 56 | /// Err(e) => e.children().len(), 57 | /// }; 58 | /// 59 | /// (numbers, errors) 60 | /// } 61 | /// 62 | /// assert_eq!(parse_each_u8(&[]), (vec![], 0)); 63 | /// assert_eq!(parse_each_u8(&["1", "42", "3"]), (vec![1, 42, 3], 0)); 64 | /// assert_eq!(parse_each_u8(&["1", "XX", "3"]), (vec![1, 3], 1)); 65 | /// assert_eq!(parse_each_u8(&["1", "XX", "Y"]), (vec![1], 2)); 66 | /// assert_eq!(parse_each_u8(&["X", "YY", "Z"]), (vec![], 3)); 67 | /// ``` 68 | /// 69 | /// [`stash_err`] is most useful for chaining another method, such as 70 | /// [`Iterator::filter`] or [`Iterator::map`], 71 | /// on the resulting `Iterator` before calling 72 | /// [`Iterator::collect`], [`Iterator::fold`], or a similar method. 73 | /// 74 | /// When using `stash_err` together with `collect`, 75 | /// there will be no indication of whether 76 | /// the iterator contained any `Err` items: 77 | /// all `Err` items will simply be moved into the error stash. 78 | /// If you don't need to chain any methods between calling 79 | /// `stash_err` and `collect`, or if 80 | /// you need `collect` to fail (lazily) if 81 | /// the iterator contained any `Err` items, 82 | /// you can call [`try_collect_or_stash`] 83 | /// on `Iterator>` instead. 84 | /// If you want to map elements of a fixed-size array, 85 | /// take a look at [`try_map_or_stash`]. 86 | /// 87 | /// [`stash_err`]: Self::stash_err 88 | /// [`try_collect_or_stash`]: 89 | /// crate::TryCollectOrStash::try_collect_or_stash 90 | /// [`try_map_or_stash`]: crate::TryMapOrStash::try_map_or_stash 91 | fn stash_err(self, stash: &mut S) -> StashErrIter 92 | where 93 | Self: Sized, 94 | { 95 | StashErrIter { 96 | iter: self, 97 | stash, 98 | _unused: PhantomData, 99 | } 100 | } 101 | } 102 | 103 | impl StashErr for Iter 104 | where 105 | Iter: Iterator>, 106 | E: Into, 107 | { 108 | } 109 | 110 | /// An iterator that will turn a sequence of [`Result`] items 111 | /// into a sequence of `T` items, 112 | /// moving any `Err` item into the supplied error stash. 113 | /// 114 | /// Values of this type can be created by calling [`stash_err`] on 115 | /// [`Iterator>`](Iterator). 116 | /// 117 | /// [`stash_err`]: StashErr::stash_err 118 | pub struct StashErrIter<'a, Iter, T, E, S, I> 119 | where 120 | Iter: Iterator>, 121 | { 122 | iter: Iter, 123 | stash: &'a mut S, 124 | _unused: PhantomData, 125 | } 126 | 127 | impl Iterator for StashErrIter<'_, Iter, T, E, S, I> 128 | where 129 | Iter: Iterator>, 130 | Iter::Item: OrStash, 131 | E: Into, 132 | { 133 | type Item = T; 134 | 135 | /// Moves all `Err` items of the underlying iterator into the error stash 136 | /// until an `Ok` value is encountered. 137 | /// As soon as `Ok(T)` is encountered, `Some(T)` will be returned. 138 | /// Returns `None` when the underlying iterator returns `None`. 139 | fn next(&mut self) -> Option { 140 | // This method has no `#[track_caller]` annotation. 141 | // Thus, the backtrace will show the name of this file and 142 | // the location of this method within that file, 143 | // instead of the location where `stash_err` 144 | // (or a method like `collect`) was called. 145 | // If this method had a `#[track_caller]` annotation, 146 | // the backtrace would point to internals of the Rust standard library 147 | // instead of this file, making it even harder to understand. 148 | loop { 149 | match self.iter.next() { 150 | Some(result) => match result.or_stash(self.stash) { 151 | StashedResult::Err(_) => continue, 152 | StashedResult::Ok(t) => return Some(t), 153 | }, 154 | None => return None, 155 | }; 156 | } 157 | } 158 | } 159 | 160 | #[cfg(test)] 161 | mod tests { 162 | use core::str::FromStr; 163 | 164 | use alloc::vec::Vec; 165 | 166 | #[cfg(any(feature = "rust-v1.81", feature = "std"))] 167 | use crate::prelude::*; 168 | 169 | #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 170 | use crate::surrogate_error_trait::prelude::*; 171 | 172 | /// Ensures that all relevant methods have the `#[track_caller]` annotation 173 | /// and we're not losing the backtrace due to, e.g., calling a closure 174 | /// as long as feature `closure_track_caller` (#87417) is unstable. 175 | /// Also ensures that the `#[track_caller]` is missing from methods 176 | /// that would create misleading backtraces. 177 | #[test] 178 | fn stash_err_has_correct_backtrace() { 179 | let mut errs = ErrorStash::new(|| "There were one or more errors"); 180 | 181 | let _: Vec = vec!["not a number"] 182 | .into_iter() 183 | .map(u8::from_str) 184 | .stash_err(&mut errs) 185 | .collect(); 186 | 187 | let err: Error = errs.into_result().unwrap_err(); 188 | let msg = crate::doctest_line_num_helper(&format!("{err:#}")); 189 | assert_eq!(&msg, indoc::indoc! {" 190 | There were one or more errors 191 | - invalid digit found in string 192 | at src/stash_err.rs:1234:56"}); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /lazy_errors/src/surrogate_error_trait/mod.rs: -------------------------------------------------------------------------------- 1 | //! Alternatives to types based on 2 | //! `std::error::Error`/`core::error::Error`. 3 | //! 4 | //! When you're using the Rust v1.81 (or later) 5 | //! or when you've enabled the `std` feature, 6 | //! there should be no need to use anything from this module. 7 | //! However, 8 | //! in Rust versions before 1.81, `core::error::Error` is not available. 9 | //! If you don't enable the `std` feature in that case, 10 | //! `std::error::Error` won't be either. 11 | //! Thus, you'd need an alternative to `lazy_errors::Stashable`. 12 | //! [`Reportable`] is a surrogate for `std::error::Error`/`core::error::Error` 13 | //! and [`lazy_errors::surrogate_error_trait::Stashable`] is for [`Reportable`] 14 | //! what `lazy_errors::Stashable` is for `core::error::Error`. 15 | //! 16 | //! It's usually sufficient to import 17 | //! [`lazy_errors::surrogate_error_trait::prelude::*`](prelude) and 18 | //! [`lazy_errors::surrogate_error_trait::Result`](Result). 19 | //! 20 | //! [`lazy_errors::Error`]: crate::Error 21 | //! [`lazy_errors::ErrorStash`]: crate::ErrorStash 22 | //! [`lazy_errors::surrogate_error_trait::Stashable`]: 23 | //! crate::surrogate_error_trait::Stashable 24 | 25 | pub mod prelude; 26 | 27 | use core::fmt::{Debug, Display}; 28 | 29 | use alloc::boxed::Box; 30 | 31 | use crate::{AdHocError, Error, ErrorData, StashedErrors, WrappedError}; 32 | 33 | /// Marker trait for types that can be put into [`ErrorStash`] 34 | /// and other containers of this crate 35 | /// when both `std` and `core::error::Error` are not available. 36 | /// 37 | /// By default, this trait is referenced in exactly one place: [`Stashable`]. 38 | /// By implementing this trait for your custom type, you will be able to 39 | /// put that type into [`ErrorStash`] or other containers 40 | /// (that use the boxed type aliases from the [`prelude`]), 41 | /// without having to specify some static type parameters. 42 | /// 43 | /// ``` 44 | /// use core::fmt::{Display, Formatter, Result}; 45 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Reportable}; 46 | /// 47 | /// #[derive(Debug)] 48 | /// struct MyType; 49 | /// 50 | /// impl Display for MyType { 51 | /// fn fmt(&self, f: &mut Formatter<'_>) -> Result { 52 | /// write!(f, "MyType") 53 | /// } 54 | /// } 55 | /// 56 | /// impl Reportable for MyType {} 57 | /// 58 | /// let mut errs = ErrorStash::new(|| "Error summary"); 59 | /// errs.push(MyType); 60 | /// ``` 61 | /// 62 | /// If you need a more complex conversion, you could instead implement 63 | /// `From` for `Box` or for [`Stashable`]. 64 | /// As long as `MyType` itself does not implement `Reportable` 65 | /// (there would be a conflicting implementation in that case), 66 | /// implementing `From` will make `lazy_errors` convert your type 67 | /// as expected when put into an [`ErrorStash`]. 68 | /// 69 | /// ``` 70 | /// use core::fmt::{Display, Formatter, Result}; 71 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Reportable}; 72 | /// 73 | /// struct MyExpensiveType; 74 | /// 75 | /// impl From for Stashable { 76 | /// fn from(val: MyExpensiveType) -> Stashable { 77 | /// Box::new(String::from("Summary of data in MyType")) 78 | /// // Drop MyExpensiveType now, e.g. to free memory 79 | /// } 80 | /// } 81 | /// 82 | /// let mut errs = ErrorStash::new(|| "Error summary"); 83 | /// errs.push(MyExpensiveType); 84 | /// ``` 85 | /// 86 | /// [`ErrorStash`]: prelude::ErrorStash 87 | /// [`Stashable`]: prelude::Stashable 88 | pub trait Reportable: Display + Debug {} 89 | 90 | /// Alias of the `Result` we all know, but uses 91 | /// [`lazy_errors::surrogate_error_trait::prelude::Error`] 92 | /// as default value for `E` if not specified explitly. 93 | /// 94 | /// [`lazy_errors::surrogate_error_trait::prelude::Error`]: prelude::Error 95 | pub type Result = core::result::Result; 96 | 97 | /// The “default” [_inner error type_ `I`](crate::Error#inner-error-type-i) 98 | /// used by the type aliases from the 99 | /// [`surrogate_error_trait::prelude`](prelude) 100 | /// _without_ `'static` lifetime. 101 | /// 102 | /// This type is only used when you're using the type aliases from the 103 | /// [`surrogate_error_trait::prelude`](prelude), which you probably 104 | /// should only do when both `std` and `core::error::Error` are not available. 105 | /// 106 | /// When both `std` and the `core::error::Error` trait are not available, 107 | /// we need to fall back on some other trait. 108 | /// We defined the [`Reportable`] trait for that purpose. 109 | /// If you want to use this crate to handle custom error types, 110 | /// you have to implement `Reportable` yourself (it's a one-liner). 111 | /// 112 | /// The [`Send`] trait bound 113 | /// [makes errors usable with `thread::spawn` and `task::spawn`][1]. 114 | /// 115 | /// The [`Sync`] trait bound is present because 116 | /// `Stashable` from the “regular” prelude (`lazy_errors::prelude`) 117 | /// needs the [`Sync`] bound itself. 118 | /// By making the these two types share the same auto-trait bounds, 119 | /// `lazy_errors` can be used identically in `std`/`no_std` configuration. 120 | /// Furthermore, it allows you to put `no_std` errors into `std` stashes, 121 | /// and vice-versa. 122 | /// 123 | /// [1]: https://github.com/dtolnay/anyhow/issues/81 124 | #[cfg_attr( 125 | any(feature = "rust-v1.81", feature = "std"), 126 | doc = r##" 127 | ``` 128 | use lazy_errors::prelude as lazy_errors_regular; 129 | use lazy_errors::surrogate_error_trait::prelude as lazy_errors_surrogate; 130 | 131 | let regular_error = lazy_errors_regular::Error::from_message(""); 132 | let surrogate_error = lazy_errors_surrogate::Error::from_message(""); 133 | let mut regular_stash = lazy_errors_regular::ErrorStash::new(|| ""); 134 | let mut surrogate_stash = lazy_errors_surrogate::ErrorStash::new(|| ""); 135 | 136 | regular_stash.push(surrogate_error); 137 | surrogate_stash.push(regular_error); 138 | ``` 139 | "## 140 | )] 141 | /// Note that you can always define your own type aliases 142 | /// that don't require your error types to be `Sync` or `Send`. 143 | pub type Stashable<'a> = 144 | alloc::boxed::Box; 145 | 146 | /// Makes all [`Reportable`]s implement 147 | /// `Into>`, 148 | /// so that they satisfy the `E: Into` constraint used throughout this crate. 149 | impl<'a, E> From for Box 150 | where 151 | E: Reportable + 'a, 152 | { 153 | fn from(val: E) -> Self { 154 | Box::new(val) 155 | } 156 | } 157 | 158 | /// Makes [`Reportable`]s implement 159 | /// `Into>` if possible, 160 | /// so that they satisfy the `E: Into` constraint used throughout this crate. 161 | impl<'a, E> From for Box 162 | where 163 | E: Reportable + Send + 'a, 164 | { 165 | fn from(val: E) -> Self { 166 | Box::new(val) 167 | } 168 | } 169 | 170 | /// Makes [`Reportable`]s implement 171 | /// `Into>` if possible, 172 | /// so that they satisfy the `E: Into` constraint used throughout this crate. 173 | impl<'a, E> From for Box 174 | where 175 | E: Reportable + Sync + 'a, 176 | { 177 | fn from(val: E) -> Self { 178 | Box::new(val) 179 | } 180 | } 181 | 182 | /// Makes [`Reportable`]s implement 183 | /// `Into>` if possible, 184 | /// so that they satisfy the `E: Into` constraint used throughout this crate. 185 | impl<'a, E> From for Box 186 | where 187 | E: Reportable + Send + Sync + 'a, 188 | { 189 | fn from(val: E) -> Self { 190 | Box::new(val) 191 | } 192 | } 193 | 194 | impl Reportable for Error where I: Display + Debug {} 195 | 196 | impl Reportable for ErrorData where I: Display + Debug {} 197 | 198 | impl Reportable for StashedErrors where I: Display + Debug {} 199 | 200 | impl Reportable for WrappedError where I: Display + Debug {} 201 | 202 | impl Reportable for AdHocError {} 203 | 204 | impl Reportable for alloc::string::String {} 205 | 206 | impl Reportable for &str {} 207 | 208 | impl Reportable for core::convert::Infallible {} 209 | 210 | impl Reportable for core::alloc::LayoutError {} 211 | 212 | impl Reportable for core::array::TryFromSliceError {} 213 | 214 | impl Reportable for core::cell::BorrowError {} 215 | 216 | impl Reportable for core::cell::BorrowMutError {} 217 | 218 | impl Reportable for core::char::CharTryFromError {} 219 | 220 | impl Reportable for core::char::DecodeUtf16Error {} 221 | 222 | impl Reportable for core::char::ParseCharError {} 223 | 224 | impl Reportable for core::char::TryFromCharError {} 225 | 226 | impl Reportable for alloc::collections::TryReserveError {} 227 | 228 | #[cfg(feature = "rust-v1.69")] 229 | impl Reportable for core::ffi::FromBytesUntilNulError {} 230 | 231 | #[cfg(feature = "rust-v1.64")] 232 | impl Reportable for core::ffi::FromBytesWithNulError {} 233 | 234 | #[cfg(feature = "rust-v1.64")] 235 | impl Reportable for alloc::ffi::FromVecWithNulError {} 236 | 237 | #[cfg(feature = "rust-v1.64")] 238 | impl Reportable for alloc::ffi::IntoStringError {} 239 | 240 | #[cfg(feature = "rust-v1.64")] 241 | impl Reportable for alloc::ffi::NulError {} 242 | 243 | impl Reportable for core::fmt::Error {} 244 | 245 | #[cfg(feature = "rust-v1.77")] 246 | impl Reportable for core::net::AddrParseError {} 247 | 248 | impl Reportable for core::num::ParseFloatError {} 249 | 250 | impl Reportable for core::num::ParseIntError {} 251 | 252 | impl Reportable for core::num::TryFromIntError {} 253 | 254 | impl Reportable for core::str::ParseBoolError {} 255 | 256 | impl Reportable for core::str::Utf8Error {} 257 | 258 | impl Reportable for alloc::string::FromUtf8Error {} 259 | 260 | impl Reportable for alloc::string::FromUtf16Error {} 261 | 262 | #[cfg(feature = "rust-v1.66")] 263 | impl Reportable for core::time::TryFromFloatSecsError {} 264 | -------------------------------------------------------------------------------- /lazy_errors/src/surrogate_error_trait/prelude.rs: -------------------------------------------------------------------------------- 1 | //! Exports traits and _aliased_ types to support the most common use-cases 2 | //! (when neither `core::error::Error` nor `std` are available). 3 | //! 4 | //! In Rust versions before v1.81, `core::error::Error` is not stable. 5 | //! In `#![no_std]` builds before Rust v1.81, 6 | //! `std::error::Error` is not available either. 7 | //! This prelude exports the surrogate error trait [`Reportable`] and 8 | //! type aliases that can be used in these cases. 9 | //! Consider using Rust v1.81 or newer, or (if that's not possible) 10 | //! consider enabling the `std` feature. Doing either makes 11 | //! the “regular” `lazy_errors::prelude::*` available. 12 | //! Types exported from the “regular” prelude 13 | //! are compatible with other crates. 14 | //! 15 | //! When using any container from `lazy_errors`, such as [`lazy_errors::Error`] 16 | //! or [`lazy_errors::ErrorStash`], you usually don't want to specify the 17 | //! [_inner error type_ `I`] explicitly. 18 | //! This prelude exports type aliases for all types that otherwise need 19 | //! the `I` parameter. The specific type used as `I` makes the aliased types 20 | //! from the prelude well suited for the common 21 | //! “just bail out now (or later)” use case. 22 | //! 23 | //! Usually, anything that you want to treat as an error can be boxed 24 | //! into a `lazy_errors::Stashable`. 25 | //! When `core::error::Error` is not available and 26 | //! you don't want to introduce a dependency on `std`, 27 | //! you need an alternative to `lazy_errors::Stashable`. 28 | //! [`Reportable`] is a surrogate for `std::error::Error`/`core::error::Error`. 29 | //! [`lazy_errors::surrogate_error_trait::Stashable`] is for 30 | //! [`Reportable`] what `lazy_errors::Stashable` is for `core::error::Error`. 31 | //! Also, using the `'static` bound for the trait object usually works fine. 32 | //! Thus, `Stashable<'static>` is the [_inner error type_ `I`] for all 33 | //! container type aliases exported by this prelude. We also define and export 34 | //! [`Stashable`] as an alias for `Stashable<'static>` for 35 | //! readability, ergonomics, and maintainability. 36 | //! 37 | //! If you want to use different inner error types, you can go ahead and use 38 | //! the container and wrapper types from this library directly. In that case, 39 | //! please check out [the example in the crate root documentation][CUSTOM]. 40 | //! 41 | //! [`lazy_errors::Error`]: crate::Error 42 | //! [`lazy_errors::ErrorStash`]: crate::ErrorStash 43 | //! [`lazy_errors::surrogate_error_trait::Stashable`]: 44 | //! crate::surrogate_error_trait::Stashable 45 | //! [`Reportable`]: crate::surrogate_error_trait::Reportable 46 | //! [_inner error type_ `I`]: crate::Error#inner-error-type-i 47 | //! [CUSTOM]: crate#example-custom-error-types 48 | 49 | pub use crate::{ 50 | err, try2, OrCreateStash, OrStash, OrWrap, OrWrapWith, StashErr, 51 | TryCollectOrStash, TryMapOrStash, 52 | }; 53 | 54 | /// Type alias for [`crate::StashedResult`] 55 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 56 | /// as explained in [the module documentation](module@self). 57 | pub type StashedResult<'a, T> = crate::StashedResult<'a, T, Stashable>; 58 | 59 | /// Type alias for [`crate::ErrorStash`] 60 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 61 | /// as explained in [the module documentation](module@self). 62 | pub type ErrorStash = crate::ErrorStash; 63 | 64 | /// Type alias for [`crate::StashWithErrors`] 65 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 66 | /// as explained in [the module documentation](module@self). 67 | pub type StashWithErrors = crate::StashWithErrors; 68 | 69 | /// Type alias for [`crate::Error`] 70 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 71 | /// as explained in [the module documentation](module@self). 72 | pub type Error = crate::Error; 73 | 74 | /// Type alias for [`crate::ErrorData`] 75 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 76 | /// as explained in [the module documentation](module@self). 77 | pub type ErrorData = crate::ErrorData; 78 | 79 | /// Type alias for [`crate::StashedErrors`] 80 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 81 | /// as explained in [the module documentation](module@self). 82 | pub type StashedErrors = crate::StashedErrors; 83 | 84 | /// Type alias for [`crate::WrappedError`] 85 | /// to use a boxed [_inner error type_ `I`](crate::Error#inner-error-type-i), 86 | /// as explained in [the module documentation](module@self). 87 | pub type WrappedError = crate::WrappedError; 88 | 89 | /// Type alias for [`crate::AdHocError`] to get access to all error types by 90 | /// importing [`lazy_errors::surrogate_error_trait::prelude::*`](module@self). 91 | pub type AdHocError = crate::AdHocError; 92 | 93 | /// Type alias for [`super::Stashable`] 94 | /// to use a `'static` bound for the boxed 95 | /// [_inner error type_ `I`](crate::Error#inner-error-type-i) trait object, 96 | /// as explained in [the module documentation](module@self). 97 | pub type Stashable = super::Stashable<'static>; 98 | -------------------------------------------------------------------------------- /lazy_errors/src/try2.rs: -------------------------------------------------------------------------------- 1 | /// Works like the `?` operator on [`StashedResult`] should. 2 | /// 3 | /// The [`try2!`] macro works well with [`or_stash`] and [`ErrorStash::ok`]: 4 | /// 5 | /// ``` 6 | /// # use core::str::FromStr; 7 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 8 | /// use lazy_errors::{prelude::*, Result}; 9 | /// 10 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 11 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 12 | /// 13 | /// fn parse_version(s: &str) -> Result<(u32, u32)> { 14 | /// let mut errs = ErrorStash::new(|| "Invalid version"); 15 | /// 16 | /// // If `parts` does not contain exactly two elements, return right now. 17 | /// let [major, minor]: [_; 2] = try2!(s 18 | /// .split('.') 19 | /// .collect::>() 20 | /// .try_into() 21 | /// .map_err(|_| Error::from_message("Must have two parts")) 22 | /// .or_stash(&mut errs)); 23 | /// 24 | /// // If we got exactly two parts, try to parse both of them, 25 | /// // even if the first part already contains an error. 26 | /// 27 | /// let major = u32::from_str(major) 28 | /// .or_stash(&mut errs) 29 | /// .ok(); 30 | /// 31 | /// let minor = u32::from_str(minor) 32 | /// .or_stash(&mut errs) 33 | /// .ok(); 34 | /// 35 | /// // Return _all_ errors if major, minor, or both were invalid. 36 | /// try2!(errs.ok()); 37 | /// 38 | /// // If the result above was `Ok`, all `ok()` calls returned `Some`. 39 | /// Ok((major.unwrap(), minor.unwrap())) 40 | /// } 41 | /// 42 | /// assert_eq!(parse_version("42.1337").unwrap(), (42, 1337)); 43 | /// 44 | /// let err = parse_version("-1.-2.-3").unwrap_err(); 45 | /// assert_eq!(err.to_string(), "Invalid version: Must have two parts"); 46 | /// 47 | /// let err = parse_version("-1.-2").unwrap_err(); 48 | /// assert_eq!(err.to_string(), "Invalid version (2 errors)"); 49 | /// ``` 50 | /// 51 | /// When the `Try` trait is stabilized, this method will probably be replaced 52 | /// by the `?` operator. 53 | /// 54 | /// Before Rust had the `?` operator, that behavior was implemented in 55 | /// the [`try!`] macro. Currently, the `?` operator is being made more 56 | /// generic: When the `Try` trait gets stabilized, we can implement 57 | /// that trait on any of our types and the `?` operator “should just work”. 58 | /// Meanwhile, this macro takes the place of the `?` operator 59 | /// (for [`StashedResult`] only). 60 | /// 61 | /// [`ErrorStash::ok`]: crate::ErrorStash::ok 62 | /// [`StashedResult`]: crate::StashedResult 63 | /// [`or_stash`]: crate::OrStash::or_stash 64 | /// [`try2!`]: crate::try2! 65 | #[macro_export] 66 | macro_rules! try2 { 67 | ($expr:expr $(,)?) => { 68 | match $expr { 69 | $crate::StashedResult::Ok(val) => val, 70 | $crate::StashedResult::Err(errs) => { 71 | return core::result::Result::Err(errs.take().into()); 72 | } 73 | } 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /lazy_errors/src/try_collect_or_stash.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | stash::{EnforceErrors, ErrorSource}, 3 | Error, OrStash, OrWrap, StashErr, StashedResult, 4 | }; 5 | 6 | /// Adds the [`try_collect_or_stash`](Self::try_collect_or_stash) method on 7 | /// [`Iterator>`](Iterator) 8 | /// if `E` implements [`Into`](crate::Error#inner-error-type-i). 9 | /// 10 | /// Do not implement this trait. 11 | /// Importing the trait is sufficient due to blanket implementations. 12 | /// The trait is implemented automatically if `E` implements `Into`, 13 | /// where `I` is the [_inner error type_](crate::Error#inner-error-type-i), 14 | /// typically [`prelude::Stashable`]. 15 | #[cfg_attr( 16 | any(feature = "rust-v1.81", feature = "std"), 17 | doc = r##" 18 | 19 | [`prelude::Stashable`]: crate::prelude::Stashable 20 | "## 21 | )] 22 | #[cfg_attr( 23 | not(any(feature = "rust-v1.81", feature = "std")), 24 | doc = r##" 25 | 26 | [`prelude::Stashable`]: crate::surrogate_error_trait::prelude::Stashable 27 | "## 28 | )] 29 | pub trait TryCollectOrStash 30 | where 31 | E: Into, 32 | { 33 | /// Counterpart to [`Iterator::try_collect`] from the Rust standard library 34 | /// that will _not_ short-circuit, 35 | /// but instead move all `Err` items into an error stash. 36 | /// 37 | /// This method evaluates _all_ items in the [`Iterator`]. 38 | /// Each time an `Err` value is encountered, 39 | /// it will be put into the supplied error stash 40 | /// and iteration will continue with the next item. 41 | /// 42 | /// This method will return a [`StashedResult::Ok`] 43 | /// containing a collection of all [`Result::Ok`] items. 44 | /// If there are one or more [`Result::Err`] items, 45 | /// all of them will be added to the supplied error stash, and 46 | /// this method will return a [`StashedResult::Err`] 47 | /// containing that error stash instead. 48 | /// 49 | /// ``` 50 | /// # use core::str::FromStr; 51 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 52 | /// use lazy_errors::{prelude::*, Result}; 53 | /// 54 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 55 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 56 | /// 57 | /// fn parse_each_u8(tokens: &[&str]) -> Result> { 58 | /// let mut errs = ErrorStash::new(|| "There were one or more errors"); 59 | /// 60 | /// let numbers: StashedResult> = tokens 61 | /// .iter() 62 | /// .map(|&s| u8::from_str(s)) 63 | /// .try_collect_or_stash(&mut errs); 64 | /// 65 | /// let numbers: Vec = try2!(numbers); 66 | /// Ok(numbers) 67 | /// } 68 | /// 69 | /// let empty = parse_each_u8(&[]).unwrap(); 70 | /// let numbers = parse_each_u8(&["1", "42", "3"]).unwrap(); 71 | /// let errors_1 = parse_each_u8(&["1", "X", "3"]).unwrap_err(); 72 | /// let errors_2 = parse_each_u8(&["1", "X", "Y"]).unwrap_err(); 73 | /// let errors_3 = parse_each_u8(&["X", "Y", "Z"]).unwrap_err(); 74 | /// 75 | /// assert_eq!(&empty, &[]); 76 | /// assert_eq!(&numbers, &[1, 42, 3]); 77 | /// assert_eq!(errors_1.children().len(), 1); 78 | /// assert_eq!(errors_2.children().len(), 2); 79 | /// assert_eq!(errors_3.children().len(), 3); 80 | /// ``` 81 | /// 82 | /// Note that `Err` will only be returned 83 | /// if the iterator contains an `Err` element. 84 | /// Errors that have been added to the error stash before 85 | /// calling `try_collect_or_stash` will not be considered. 86 | /// You can call [`ErrorStash::ok`] if you want to bail 87 | /// in case of earlier errors as well: 88 | /// 89 | /// ``` 90 | /// # use core::str::FromStr; 91 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 92 | /// use lazy_errors::{prelude::*, Result}; 93 | /// 94 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 95 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 96 | /// 97 | /// let mut errs = ErrorStash::new(|| "There were one or more errors"); 98 | /// 99 | /// errs.push("Earlier error"); // Ignored in `try_collect_or_stash` 100 | /// 101 | /// assert!(matches!(errs.ok(), StashedResult::Err(_))); 102 | /// 103 | /// let numbers = ["42"] 104 | /// .iter() 105 | /// .map(|&s| u8::from_str(s)) 106 | /// .try_collect_or_stash::>(&mut errs); 107 | /// 108 | /// assert!(matches!(&numbers, StashedResult::Ok(_))); 109 | /// 110 | /// let numbers = numbers.ok().unwrap(); 111 | /// assert_eq!(&numbers, &[42]); 112 | /// 113 | /// assert!(matches!(errs.ok(), StashedResult::Err(_))); 114 | /// ``` 115 | /// 116 | /// If you need to transform an [`Iterator>`](Iterator) 117 | /// into an `Iterator` and 118 | /// call a method _before_ collecting all `T` items, 119 | /// take a look at [`stash_err`](crate::StashErr::stash_err). 120 | /// 121 | /// If you want to map elements of a fixed-size array in a similar manner, 122 | /// take a look at [`try_map_or_stash`]. 123 | /// 124 | /// [`ErrorStash::ok`]: crate::ErrorStash::ok 125 | /// [`try_map_or_stash`]: crate::TryMapOrStash::try_map_or_stash 126 | fn try_collect_or_stash(self, stash: &mut S) -> StashedResult 127 | where 128 | C: FromIterator; 129 | } 130 | 131 | impl TryCollectOrStash for Iter 132 | where 133 | Iter: Iterator>, 134 | E: Into, 135 | S: ErrorSource, 136 | S: EnforceErrors, 137 | Error: Into, 138 | Result>: OrStash, 139 | { 140 | // This method has no `#[track_caller]` annotation 141 | // because `stash_err` doesn't either. 142 | // If this method had a `#[track_caller]` annotation, 143 | // the backtrace would point to internals of the Rust standard library 144 | // instead of this file, making it even harder to understand. 145 | fn try_collect_or_stash(self, stash: &mut S) -> StashedResult 146 | where 147 | C: FromIterator, 148 | Self: Sized, 149 | { 150 | let before = stash.errors().len(); 151 | 152 | // Show this method in backtrace even despite `stash_err` 153 | // not supporting backtraces properly. 154 | let iter = self.map(|r| r.or_wrap()); 155 | let result = iter.stash_err(stash).collect(); 156 | 157 | let after = stash.errors().len(); 158 | 159 | if before == after { 160 | StashedResult::Ok(result) 161 | } else { 162 | // The stash "cannot" be empty now... unless in case of 163 | // weird `std::mem::take` shenanigans or API violations. 164 | StashedResult::Err(stash.enforce_errors()) 165 | } 166 | } 167 | } 168 | 169 | #[cfg(test)] 170 | mod tests { 171 | use alloc::vec::Vec; 172 | use core::str::FromStr; 173 | 174 | #[cfg(any(feature = "rust-v1.81", feature = "std"))] 175 | use crate::{prelude::*, Result}; 176 | 177 | #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 178 | use crate::surrogate_error_trait::{prelude::*, Result}; 179 | 180 | /// Tests `try_collect_or_stash` with `StashWithErrors` as parameter. 181 | /// 182 | /// All other (doc) tests use `ErrorStash` as parameter instead 183 | /// because its the more common use-case. 184 | #[test] 185 | fn try_collect_or_stash_into_stash_with_errors() -> Result<()> { 186 | let mut errs = ErrorStash::new(|| "There were one or more errors"); 187 | errs.push("Earlier error"); // Ignored in `try_collect_or_stash` 188 | 189 | let errs: &mut StashWithErrors = match errs.ok() { 190 | crate::StashedResult::Ok(_) => unreachable!(), 191 | crate::StashedResult::Err(stash_with_errors) => stash_with_errors, 192 | }; 193 | 194 | let empty: Vec> = vec![]; 195 | let empty: Vec = try2!(empty 196 | .into_iter() 197 | .try_collect_or_stash(errs)); 198 | assert_eq!(empty, &[]); 199 | 200 | let ok: Vec> = vec![Ok(42)]; 201 | let ok: Vec = try2!(ok 202 | .into_iter() 203 | .try_collect_or_stash(errs)); 204 | assert_eq!(ok, &[42]); 205 | 206 | let err: Vec> = vec![Err(err!("not a number"))]; 207 | let err = err 208 | .into_iter() 209 | .try_collect_or_stash::>(errs); 210 | assert!(matches!(err, StashedResult::Err(_))); 211 | 212 | Ok(()) 213 | } 214 | 215 | /// Ensures that all relevant methods have the `#[track_caller]` annotation 216 | /// and we're not losing the backtrace due to, e.g., calling a closure 217 | /// as long as feature `closure_track_caller` (#87417) is unstable. 218 | /// Also ensures that the `#[track_caller]` is missing from methods 219 | /// that would create misleading backtraces. 220 | #[test] 221 | fn try_collect_or_stash_has_correct_backtrace() { 222 | let mut errs = ErrorStash::new(|| "There were one or more errors"); 223 | 224 | let _numbers = vec!["not a number"] 225 | .into_iter() 226 | .map(u8::from_str) 227 | .try_collect_or_stash::>(&mut errs); 228 | 229 | let err: Error = errs.into_result().unwrap_err(); 230 | let msg = crate::doctest_line_num_helper(&format!("{err:#}")); 231 | assert_eq!(&msg, indoc::indoc! {" 232 | There were one or more errors 233 | - invalid digit found in string 234 | at src/try_collect_or_stash.rs:1234:56 235 | at src/stash_err.rs:1234:56"}); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /lazy_errors/src/try_map_or_stash.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | 3 | use crate::{ 4 | err, 5 | stash::{EnforceErrors, ErrorSink}, 6 | Error, OrStash, StashedResult, 7 | }; 8 | 9 | /// Adds the [`try_map_or_stash`](Self::try_map_or_stash) method on 10 | /// [`[T; _]`](array) and 11 | /// [`[Result; _]`](array) 12 | /// if `E` implements [`Into`](crate::Error#inner-error-type-i). 13 | /// 14 | /// Do not implement this trait. 15 | /// Importing the trait is sufficient due to blanket implementations. 16 | /// The trait is implemented automatically if `E` implements `Into`, 17 | /// where `I` is the [_inner error type_](crate::Error#inner-error-type-i), 18 | /// typically [`prelude::Stashable`]. 19 | #[cfg_attr( 20 | any(feature = "rust-v1.81", feature = "std"), 21 | doc = r##" 22 | 23 | [`prelude::Stashable`]: crate::prelude::Stashable 24 | "## 25 | )] 26 | #[cfg_attr( 27 | not(any(feature = "rust-v1.81", feature = "std")), 28 | doc = r##" 29 | 30 | [`prelude::Stashable`]: crate::surrogate_error_trait::prelude::Stashable 31 | "## 32 | )] 33 | pub trait TryMapOrStash 34 | where 35 | E: Into, 36 | { 37 | /// Counterpart to [`array::try_map`] from the Rust standard library 38 | /// that will _not_ short-circuit, 39 | /// but instead move all `Err` elements/results into an error stash. 40 | /// 41 | /// This method will touch _all_ elements of arrays 42 | /// of type `[T; _]` or `[Result; _]`, 43 | /// mapping _each_ `T` or `Ok(T)` via the supplied mapping function. 44 | /// Each time a `Result::Err` element is encountered 45 | /// or an element is mapped to a `Result::Err` value, 46 | /// that error will be put into the supplied error stash. 47 | /// If there are one or more `Result::Err`s, 48 | /// this method will return a [`StashedResult::Err`] 49 | /// wrapping that error stash. 50 | /// Otherwise, this method will return a [`StashedResult::Ok`] 51 | /// containing an array of the mapped elements, in order. 52 | /// 53 | /// Here's an example using `[T; _]`: 54 | /// 55 | /// ``` 56 | /// # use core::str::FromStr; 57 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 58 | /// use lazy_errors::{prelude::*, Result}; 59 | /// 60 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 61 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 62 | /// 63 | /// fn parse_each_u8(input: [&str; 2]) -> Result<[u8; 2]> { 64 | /// let mut errs = ErrorStash::new(|| "Invalid input"); 65 | /// 66 | /// let numbers = input.try_map_or_stash(u8::from_str, &mut errs); 67 | /// let numbers: [u8; 2] = try2!(numbers); 68 | /// Ok(numbers) 69 | /// } 70 | /// 71 | /// let numbers = parse_each_u8(["42", "0"]).unwrap(); 72 | /// let errors_1 = parse_each_u8(["X", "0"]).unwrap_err(); 73 | /// let errors_2 = parse_each_u8(["X", "Y"]).unwrap_err(); 74 | /// 75 | /// assert_eq!(numbers, [42, 0]); 76 | /// assert_eq!(errors_1.children().len(), 1); 77 | /// assert_eq!(errors_2.children().len(), 2); 78 | /// ``` 79 | /// 80 | /// Here's a similar example using `[Result; _]` instead: 81 | /// 82 | /// ``` 83 | /// # use core::str::FromStr; 84 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 85 | /// use lazy_errors::{prelude::*, Result}; 86 | /// 87 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 88 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 89 | /// 90 | /// fn try_parse_each_u8( 91 | /// input: [Result<&'static str, &'static str>; 2], 92 | /// ) -> Result<[u8; 2]> { 93 | /// let mut errs = ErrorStash::new(|| "Invalid input"); 94 | /// 95 | /// let numbers = input.try_map_or_stash(u8::from_str, &mut errs); 96 | /// let numbers: [u8; 2] = try2!(numbers); 97 | /// Ok(numbers) 98 | /// } 99 | /// 100 | /// let numbers = try_parse_each_u8([Ok("42"), Ok("0")]).unwrap(); 101 | /// let errors_1 = try_parse_each_u8([Err("42"), Ok("0")]).unwrap_err(); 102 | /// let errors_2 = try_parse_each_u8([Err("42"), Ok("X")]).unwrap_err(); 103 | /// 104 | /// assert_eq!(numbers, [42, 0]); 105 | /// assert_eq!(errors_1.children().len(), 1); 106 | /// assert_eq!(errors_2.children().len(), 2); 107 | /// ``` 108 | /// 109 | /// Note that `Err` will only be returned 110 | /// if the array contains an `Err` element or 111 | /// if any element of the array gets mapped to an `Err` value. 112 | /// Errors that have been added to the error stash before 113 | /// calling `try_map_or_stash` will not be considered. 114 | /// You can call [`ErrorStash::ok`] if you want to bail 115 | /// in case of earlier errors as well: 116 | /// 117 | /// ``` 118 | /// # use core::str::FromStr; 119 | /// #[cfg(any(feature = "rust-v1.81", feature = "std"))] 120 | /// use lazy_errors::{prelude::*, Result}; 121 | /// 122 | /// #[cfg(not(any(feature = "rust-v1.81", feature = "std")))] 123 | /// use lazy_errors::surrogate_error_trait::{prelude::*, Result}; 124 | /// 125 | /// let mut errs = ErrorStash::new(|| "There were one or more errors"); 126 | /// 127 | /// errs.push("Earlier error"); // Ignored in `try_map_or_stash` 128 | /// 129 | /// assert!(matches!(errs.ok(), StashedResult::Err(_))); 130 | /// 131 | /// let numbers: [&str; 1] = ["42"]; 132 | /// let numbers = numbers.try_map_or_stash(u8::from_str, &mut errs); 133 | /// assert!(matches!(&numbers, StashedResult::Ok(_))); 134 | /// 135 | /// let numbers1 = numbers.ok().unwrap(); 136 | /// 137 | /// let numbers: [Result<_>; 1] = [Ok("24")]; 138 | /// let numbers = numbers.try_map_or_stash(u8::from_str, &mut errs); 139 | /// assert!(matches!(&numbers, StashedResult::Ok(_))); 140 | /// 141 | /// let numbers2 = numbers.ok().unwrap(); 142 | /// 143 | /// assert_eq!(&numbers1, &[42]); 144 | /// assert_eq!(&numbers2, &[24]); 145 | /// 146 | /// assert!(matches!(errs.ok(), StashedResult::Err(_))); 147 | /// ``` 148 | /// 149 | /// If you need to map and collect items of an 150 | /// [`Iterator>`](Iterator), 151 | /// take a look at [`try_collect_or_stash`] and [`stash_err`]. 152 | /// 153 | /// [`ErrorStash::ok`]: crate::ErrorStash::ok 154 | /// [`stash_err`]: crate::StashErr::stash_err 155 | /// [`try_collect_or_stash`]: 156 | /// crate::TryCollectOrStash::try_collect_or_stash 157 | fn try_map_or_stash( 158 | self, 159 | f: F, 160 | stash: &mut S, 161 | ) -> StashedResult<[U; N], I> 162 | where 163 | F: FnMut(T) -> Result; 164 | } 165 | 166 | impl TryMapOrStash for [T; N] 167 | where 168 | E: Into, 169 | S: ErrorSink, 170 | S: EnforceErrors, 171 | Error: Into, 172 | S: ErrorSink, I>, 173 | { 174 | // Note that the `#[track_caller]` annotation on this method does not work 175 | // as long as `closure_track_caller` (#87417) is unstable. 176 | #[track_caller] 177 | fn try_map_or_stash( 178 | self, 179 | f: F, 180 | stash: &mut S, 181 | ) -> StashedResult<[U; N], I> 182 | where 183 | F: FnMut(T) -> Result, 184 | Result: OrStash, 185 | { 186 | let vec = filter_map_or_stash(self, f, stash); 187 | 188 | if vec.len() != N { 189 | // The stash "cannot" be empty now... unless in case of 190 | // weird `std::mem::take` shenanigans or API violations. 191 | return StashedResult::Err(stash.enforce_errors()); 192 | } 193 | 194 | vec_try_into_or_stash(vec, stash) 195 | } 196 | } 197 | 198 | impl TryMapOrStash 199 | for [Result; N] 200 | where 201 | E1: Into, 202 | E2: Into, 203 | S: ErrorSink, 204 | S: ErrorSink, 205 | S: EnforceErrors, 206 | Error: Into, 207 | S: ErrorSink, I>, 208 | { 209 | // Note that the `#[track_caller]` annotation on this method does not work 210 | // as long as `closure_track_caller` (#87417) is unstable. 211 | #[track_caller] 212 | fn try_map_or_stash( 213 | self, 214 | f: F, 215 | stash: &mut S, 216 | ) -> StashedResult<[U; N], I> 217 | where 218 | F: FnMut(T) -> Result, 219 | Result: OrStash, 220 | { 221 | let vec = filter_map_ok_or_stash(self, f, stash); 222 | 223 | if vec.len() != N { 224 | // The stash "cannot" be empty now... unless in case of 225 | // weird `std::mem::take` shenanigans or API violations. 226 | return StashedResult::Err(stash.enforce_errors()); 227 | } 228 | 229 | vec_try_into_or_stash(vec, stash) 230 | } 231 | } 232 | 233 | // Note that the `#[track_caller]` annotation on this method does not work 234 | // as long as `closure_track_caller` (#87417) is unstable. 235 | #[track_caller] 236 | fn filter_map_or_stash( 237 | array: [T; N], 238 | mut f: F, 239 | stash: &mut S, 240 | ) -> Vec 241 | where 242 | F: FnMut(T) -> Result, 243 | Result: OrStash, 244 | { 245 | array 246 | .into_iter() 247 | .filter_map(|t| f(t).or_stash(stash).ok()) 248 | .collect() 249 | } 250 | 251 | // Note that the `#[track_caller]` annotation on this method does not work 252 | // as long as `closure_track_caller` (#87417) is unstable. 253 | #[track_caller] 254 | fn filter_map_ok_or_stash( 255 | array: [Result; N], 256 | mut f: F, 257 | stash: &mut S, 258 | ) -> Vec 259 | where 260 | E1: Into, 261 | F: FnMut(T) -> Result, 262 | Result: OrStash, 263 | S: ErrorSink, 264 | { 265 | array 266 | .into_iter() 267 | .filter_map(|r| match r { 268 | Ok(t) => f(t).or_stash(stash).ok(), 269 | Err(e) => { 270 | stash.stash(e); 271 | None 272 | } 273 | }) 274 | .collect() 275 | } 276 | 277 | fn vec_try_into_or_stash( 278 | vec: Vec, 279 | stash: &mut S, 280 | ) -> StashedResult<[T; N], I> 281 | where 282 | Result<[T; N], Error>: OrStash, 283 | { 284 | vec.try_into() 285 | .map_err(|_| err!("INTERNAL ERROR: Failed to convert vector to array")) 286 | .or_stash(stash) 287 | } 288 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | newline_style = "Unix" 3 | chain_width = 40 4 | wrap_comments = true 5 | format_code_in_doc_comments = true 6 | doc_comment_code_block_width = 80 7 | normalize_comments = true 8 | normalize_doc_attributes = true 9 | format_macro_matchers = true 10 | hex_literal_case = "Upper" 11 | imports_granularity = "Crate" 12 | reorder_impl_items = true 13 | overflow_delimited_expr = true 14 | # Try to align members of structs and enums neatly. Default: 0 (off). 15 | # Our number is just a guess what might probably work... 16 | struct_field_align_threshold = 4 17 | enum_discrim_align_threshold = 4 18 | match_arm_leading_pipes = "Preserve" 19 | edition = "2021" 20 | use_field_init_shorthand = true 21 | unstable_features = true 22 | error_on_line_overflow = true 23 | error_on_unformatted = true 24 | -------------------------------------------------------------------------------- /tarpaulin.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | target-dir = "target/tarpaulin" # Avoid recompilation 3 | skip-clean = true 4 | 5 | [report] 6 | out = ["Html"] 7 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | edition = "2021" 4 | authors = ["Andreas Waidler "] 5 | license = "MIT OR Apache-2.0" 6 | version = "0.0.0" 7 | publish = false 8 | 9 | [dependencies] 10 | clap = { version = "4.3.21", features = ["derive"] } 11 | 12 | # `clap` needs the `FromStr` implementations to return `std::error::Error`. 13 | # Furthermore, `xtask` needs to use several parts of `std` anyways, 14 | # so we could just enable the flag for `lazy_errors` as well, 15 | # making sure `lazy_errors` builds even on old Rust versions. 16 | lazy_errors = { path = "../lazy_errors", features = ["std"] } 17 | 18 | [dev-dependencies] 19 | test-case = "3.1.0" 20 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | //! Helper tool to run the CI pipeline locally (`cargo xtask ci`) or 4 | //! set the version based on `git describe` (`cargo xtask version`). 5 | //! 6 | //! The implementation of the `xtask` workspace and `cargo xtask` 7 | //! is based on [the blog post “Make Your Own Make” by matklad][MYOM] 8 | //! and the [`xtask` GitHub repo][xtask] by the same author. 9 | //! Additional ideas have been stolen from [Robbepop]. 10 | //! 11 | //! [MYOM]: https://matklad.github.io/2018/01/03/make-your-own-make.html 12 | //! [xtask]: https://github.com/matklad/cargo-xtask 13 | //! [Robbepop]: https://github.com/Robbepop 14 | 15 | mod ci; 16 | mod version; 17 | 18 | use core::str; 19 | 20 | use std::process::{self, ExitCode, Stdio}; 21 | 22 | use lazy_errors::{prelude::*, Result}; 23 | 24 | use ci::Ci; 25 | use version::Version; 26 | 27 | type CommandLine = Vec<&'static str>; 28 | 29 | #[derive(clap::Parser, Debug, Clone, PartialEq, Hash, Eq)] 30 | enum Xtask { 31 | /// Runs the CI quality gate or parts thereof 32 | /// in the workspace on your local machine. 33 | #[command(subcommand)] 34 | Ci(Ci), 35 | 36 | /// Manipulates the `version` attribute in `Cargo.toml` and `Cargo.lock`. 37 | #[command(subcommand)] 38 | Version(Version), 39 | } 40 | 41 | fn main() -> ExitCode { 42 | match run() { 43 | Ok(()) => ExitCode::SUCCESS, 44 | Err(err) => { 45 | eprintln!("{err:#}"); 46 | ExitCode::FAILURE 47 | } 48 | } 49 | } 50 | 51 | fn run() -> Result<()> { 52 | let command = parse_args_from_env()?; 53 | 54 | match command { 55 | Xtask::Ci(command) => ci::run(&command), 56 | Xtask::Version(command) => version::run(&command), 57 | } 58 | } 59 | 60 | fn parse_args_from_env() -> Result { 61 | parse_args(std::env::args_os()) 62 | } 63 | 64 | fn parse_args(args: IntoIter) -> Result 65 | where 66 | IntoIter: IntoIterator, 67 | T: Into + Clone, 68 | { 69 | use clap::Parser; 70 | 71 | let command = Xtask::try_parse_from(args).or_wrap()?; 72 | 73 | Ok(command) 74 | } 75 | 76 | fn exec_all(tasklist: &[L]) -> Result<()> 77 | where 78 | L: AsRef<[&'static str]>, 79 | { 80 | for task in tasklist { 81 | exec(task.as_ref())?; 82 | } 83 | 84 | Ok(()) 85 | } 86 | 87 | fn exec(command_with_args: &[&str]) -> Result<()> { 88 | exec_impl(command_with_args, false)?; 89 | Ok(()) 90 | } 91 | 92 | fn exec_and_capture(command_with_args: &[&str]) -> Result { 93 | exec_impl(command_with_args, true) 94 | } 95 | 96 | fn exec_impl(command_with_args: &[&str], capture: bool) -> Result { 97 | let (command, args) = match command_with_args { 98 | [head, tail @ ..] => (head, tail), 99 | _ => return Err(err!("No command passed.")), 100 | }; 101 | 102 | eprintln!("Starting '{}'...", command_with_args.join(" ")); 103 | 104 | let mut handle = process::Command::new(command); 105 | 106 | if capture { 107 | handle 108 | .stdout(Stdio::piped()) 109 | .stderr(Stdio::piped()); 110 | } 111 | 112 | let mut errs = 113 | ErrorStash::new(|| format!("Failed to run {command_with_args:?}")); 114 | 115 | let process = try2!(handle 116 | .args(args) 117 | .spawn() 118 | .or_wrap_with::(|| "Failed to start process") 119 | .and_then(|process| process.wait_with_output().or_wrap()) 120 | .or_stash(&mut errs)); 121 | 122 | match process.status.code() { 123 | Some(0) => (), 124 | Some(c) => { 125 | errs.push(format!("Status code was {c}")); 126 | } 127 | None => { 128 | errs.push("No status code (terminated by signal?)"); 129 | } 130 | }; 131 | 132 | let stdout = str_or_stash(&process.stdout, &mut errs); 133 | let stderr = str_or_stash(&process.stderr, &mut errs); 134 | 135 | if !errs.is_empty() && capture { 136 | if !stdout.is_empty() { 137 | errs.push(format!("STDOUT:\n{stdout}")); 138 | } 139 | 140 | if !stderr.is_empty() { 141 | errs.push(format!("STDERR:\n{stderr}")); 142 | } 143 | } 144 | 145 | errs.into_result()?; 146 | 147 | Ok(stdout.to_owned()) 148 | } 149 | 150 | fn str_or_stash<'a, F, M>( 151 | bytes: &'a [u8], 152 | errs: &mut ErrorStash, 153 | ) -> &'a str 154 | where 155 | F: FnOnce() -> M, 156 | M: core::fmt::Display, 157 | { 158 | str::from_utf8(bytes) 159 | .map(str::trim) 160 | .or_wrap_with::(|| "Cannot create string: Invalid byte(s)") 161 | .or_stash(errs) 162 | .ok() 163 | .unwrap_or_default() 164 | } 165 | 166 | #[cfg(test)] 167 | mod tests { 168 | use test_case::test_case; 169 | 170 | use super::*; 171 | 172 | #[test] 173 | fn exec_is_no_op_if_list_is_empty() -> Result<()> { 174 | let empty: &[&[&str]] = &[]; 175 | exec_all(empty) // no-op 176 | } 177 | 178 | #[test] 179 | fn exec_returns_error_if_command_is_empty() -> Result<()> { 180 | let err = exec_all(&[&[]]).unwrap_err(); 181 | assert_eq!(err.to_string(), "No command passed."); 182 | Ok(()) 183 | } 184 | 185 | #[test] 186 | #[cfg_attr(miri, ignore)] 187 | fn exec_can_invoke_cargo() -> Result<()> { 188 | exec_all(&[&["cargo", "version"]]) 189 | } 190 | 191 | #[test] 192 | #[cfg_attr(miri, ignore)] 193 | fn exec_returns_cargo_version() -> Result<()> { 194 | let version = exec_and_capture(&["cargo", "version"])?; 195 | dbg!(&version); 196 | 197 | // Loosely assert that we got some output from the process. 198 | assert!(version.starts_with("cargo")); 199 | assert!(version.contains('.')); 200 | 201 | Ok(()) 202 | } 203 | 204 | #[test_case( 205 | &[&["unexisting-program"]], 206 | r#"Failed to run ["unexisting-program"]: Failed to start process: "#)] 207 | #[test_case( 208 | &[&["cargo", "unexisting-subcommand"]], 209 | "Failed to run [\"cargo\", \"unexisting-subcommand\"]: \ 210 | Status code was 101")] 211 | #[cfg_attr(miri, ignore)] 212 | fn exec_propagates_process_failure( 213 | commands: &[&[&'static str]], 214 | expected_error: &str, 215 | ) { 216 | let err = exec_all(commands).unwrap_err(); 217 | let msg = &format!("{err}"); 218 | 219 | dbg!(msg, expected_error); 220 | assert!(msg.starts_with(expected_error)); 221 | } 222 | 223 | #[test] 224 | #[cfg_attr(miri, ignore)] 225 | fn exec_propagates_stderr() { 226 | let err = 227 | exec_and_capture(&["cargo", "unexisting-subcommand"]).unwrap_err(); 228 | 229 | let msg = &format!("{err:#}"); 230 | dbg!(msg); 231 | assert!(msg.contains("- STDERR:\n")); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /xtask/src/version.rs: -------------------------------------------------------------------------------- 1 | use core::{ 2 | fmt::{self, Display}, 3 | str::FromStr, 4 | }; 5 | 6 | use lazy_errors::{prelude::*, Result}; 7 | 8 | #[derive(clap::Subcommand, Debug, Clone, PartialEq, Hash, Eq)] 9 | pub enum Version { 10 | /// Extracts the version number from some source 11 | /// and writes it into the `Cargo.toml` and `Cargo.lock` files. 12 | Import(ImportArgs), 13 | } 14 | 15 | #[derive(clap::Args, Debug, Clone, PartialEq, Hash, Eq)] 16 | pub struct ImportArgs { 17 | /// Where to import the version number from. 18 | source: Source, 19 | 20 | /// Whitelists version number formats that are allowed to import. 21 | /// 22 | /// If missing, any version number is accepted. 23 | /// If one or more patterns are present, the version number from `source` 24 | /// will be imported if it matches at least one of the patterns. 25 | /// Otherwise, an error will be returned. 26 | #[clap(long, value_name = "PATTERN")] 27 | accept: Vec, 28 | } 29 | 30 | #[derive(clap::ValueEnum, Debug, Copy, Clone, PartialEq, Hash, Eq)] 31 | enum Source { 32 | /// Use the string returned from `git describe --dirty` as version number. 33 | GitDescribe, 34 | } 35 | 36 | #[derive(clap::ValueEnum, Debug, Copy, Clone, PartialEq, Hash, Eq)] 37 | enum Pattern { 38 | /// Matches a “regular” version number, 39 | /// i.e. `MAJOR.MINOR.PATCH` strings if all parts are decimal numbers. 40 | MajorMinorPatch, 41 | } 42 | 43 | #[derive(Debug, Clone, PartialEq, Hash, Eq)] 44 | enum VersionNumber { 45 | MajorMinorPatch(MajorMinorPatch), 46 | CustomVersion(CustomVersion), 47 | } 48 | 49 | #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)] 50 | struct MajorMinorPatch { 51 | major: u16, 52 | minor: u16, 53 | patch: u16, 54 | } 55 | 56 | #[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)] 57 | struct CustomVersion(String); 58 | 59 | impl FromStr for VersionNumber { 60 | type Err = Error; 61 | 62 | fn from_str(s: &str) -> Result { 63 | if let Ok(v) = MajorMinorPatch::from_str(s) { 64 | return Ok(VersionNumber::MajorMinorPatch(v)); 65 | } 66 | 67 | Ok(VersionNumber::CustomVersion(s.parse()?)) 68 | } 69 | } 70 | 71 | impl FromStr for MajorMinorPatch { 72 | type Err = Error; 73 | 74 | fn from_str(s: &str) -> Result { 75 | let mut errs = ErrorStash::new(|| { 76 | format!("Doesn't match MAJOR.MINOR.PATCH: '{s}'") 77 | }); 78 | 79 | let tokens: [&str; 3] = try2!(s 80 | .split('.') 81 | .collect::>() 82 | .try_into() 83 | .map_err(|_| -> Error { 84 | err!("Invalid number of parts separated by '.'") 85 | }) 86 | .or_stash(&mut errs)); 87 | 88 | let [major, minor, patch]: [u16; 3] = try2!(tokens.try_map_or_stash( 89 | |token| { 90 | u16::from_str(token) 91 | .map_err(|_| -> Error { err!("Invalid number: '{token}'") }) 92 | }, 93 | &mut errs 94 | )); 95 | 96 | Ok(Self { 97 | major, 98 | minor, 99 | patch, 100 | }) 101 | } 102 | } 103 | 104 | impl FromStr for CustomVersion { 105 | type Err = Error; 106 | 107 | fn from_str(s: &str) -> Result { 108 | if s.is_empty() { 109 | return Err(err!("Version number is empty")); 110 | } 111 | 112 | Ok(Self(s.to_owned())) 113 | } 114 | } 115 | 116 | impl Display for VersionNumber { 117 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 118 | match self { 119 | VersionNumber::MajorMinorPatch(v) => Display::fmt(v, f), 120 | VersionNumber::CustomVersion(v) => Display::fmt(v, f), 121 | } 122 | } 123 | } 124 | 125 | impl Display for MajorMinorPatch { 126 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 127 | let Self { 128 | major, 129 | minor, 130 | patch, 131 | } = self; 132 | 133 | write!(f, "{major}.{minor}.{patch}") 134 | } 135 | } 136 | 137 | impl Display for CustomVersion { 138 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 139 | write!(f, "{}", self.0) 140 | } 141 | } 142 | 143 | pub fn run(command: &Version) -> Result<()> { 144 | match command { 145 | Version::Import(args) => run_import(args), 146 | } 147 | } 148 | 149 | fn run_import(args: &ImportArgs) -> Result<()> { 150 | crate::exec_and_capture(&["git", "describe", "--dirty"]) 151 | .and_then(|stdout| parse_and_filter(&stdout, &args.accept)) 152 | .and_then(|v| crate::exec(&["cargo", "set-version", &v.to_string()])) 153 | .or_wrap_with(|| "Failed to set version number based on `git describe`") 154 | } 155 | 156 | fn parse_and_filter( 157 | git_output: &str, 158 | accept: &[Pattern], 159 | ) -> Result { 160 | let version = parse_git_describe_output(git_output)?; 161 | 162 | if !is_accepted(&version, accept) { 163 | return Err(err!( 164 | "Version '{version}' does not match any `accept` parameter" 165 | )); 166 | } 167 | 168 | Ok(version) 169 | } 170 | 171 | fn parse_git_describe_output(output: &str) -> Result { 172 | let output = output.trim(); 173 | 174 | let output = match output.strip_prefix('v') { 175 | Some(remainder) => remainder, 176 | None => output, 177 | }; 178 | 179 | output.parse() 180 | } 181 | 182 | fn is_accepted(version: &VersionNumber, accept: &[Pattern]) -> bool { 183 | accept.is_empty() 184 | || accept 185 | .iter() 186 | .any(|accept| match accept { 187 | Pattern::MajorMinorPatch => { 188 | matches!(version, VersionNumber::MajorMinorPatch(_)) 189 | } 190 | }) 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use test_case::test_case; 196 | 197 | use super::*; 198 | 199 | fn v(major: u16, minor: u16, patch: u16) -> VersionNumber { 200 | VersionNumber::MajorMinorPatch(MajorMinorPatch { 201 | major, 202 | minor, 203 | patch, 204 | }) 205 | } 206 | 207 | fn custom(s: &str) -> VersionNumber { 208 | VersionNumber::CustomVersion(CustomVersion(s.to_owned())) 209 | } 210 | 211 | #[test_case("1.2.3", &[], Ok(v(1, 2, 3)))] 212 | #[test_case("1.2.3", &[Pattern::MajorMinorPatch], Ok(v(1, 2, 3)))] 213 | #[test_case("v1.2.3", &[], Ok(v(1, 2, 3)))] 214 | #[test_case("v1.2.3", &[Pattern::MajorMinorPatch], Ok(v(1, 2, 3)))] 215 | #[test_case("0.5.0-2-ga712af5", &[], 216 | Ok(custom("0.5.0-2-ga712af5")))] 217 | #[test_case("0.5.0-2-ga712af5", &[Pattern::MajorMinorPatch], 218 | Err(String::from( 219 | "Version '0.5.0-2-ga712af5' does not match any `accept` parameter" 220 | )))] 221 | #[test_case("v0.5.0-2-ga712af5", &[], 222 | Ok(custom("0.5.0-2-ga712af5")))] 223 | #[test_case("v0.5.0-2-ga712af5", &[Pattern::MajorMinorPatch], 224 | Err(String::from( 225 | "Version '0.5.0-2-ga712af5' does not match any `accept` parameter" 226 | )))] 227 | fn parse_and_filter( 228 | input: &str, 229 | accept: &[Pattern], 230 | expectation: Result, 231 | ) { 232 | let actual = super::parse_and_filter(input, accept); 233 | 234 | match expectation { 235 | Ok(v) => assert_eq!(v, actual.unwrap()), 236 | Err(e) => { 237 | let actual = actual.unwrap_err(); 238 | dbg!(&actual); 239 | assert_eq!(actual.to_string(), e); 240 | } 241 | } 242 | } 243 | 244 | #[test] 245 | fn parse_major_minor_patch_multiple_err() { 246 | let err = super::MajorMinorPatch::from_str("-1.-2.-3").unwrap_err(); 247 | let msg = format!("{err:#}"); 248 | eprintln!("{}", msg); 249 | 250 | assert!(msg.starts_with("Doesn't match MAJOR.MINOR.PATCH: '-1.-2.-3'")); 251 | assert!(msg.contains("Invalid number: '-1'")); 252 | assert!(msg.contains("Invalid number: '-2'")); 253 | assert!(msg.contains("Invalid number: '-3'")); 254 | } 255 | 256 | #[test_case("0.0.0", v(0, 0, 0))] 257 | #[test_case("0.0.7", v(0, 0, 7))] 258 | #[test_case("0.7.0", v(0, 7, 0))] 259 | #[test_case("7.0.0", v(7, 0, 0))] 260 | #[test_case("1.2.3", v(1, 2, 3))] 261 | #[test_case("v0.0.0", v(0, 0, 0))] 262 | #[test_case("v0.0.7", v(0, 0, 7))] 263 | #[test_case("v0.7.0", v(0, 7, 0))] 264 | #[test_case("v7.0.0", v(7, 0, 0))] 265 | #[test_case("v1.2.3", v(1, 2, 3))] 266 | #[test_case("0.5.0-2-ga712af5", custom("0.5.0-2-ga712af5"))] 267 | #[test_case("v0.5.0-2-ga712af5", custom("0.5.0-2-ga712af5"))] 268 | #[test_case(" \n v0.5.0-2-ga712af5 \n ", custom("0.5.0-2-ga712af5"))] 269 | #[test_case("abcdef", custom("abcdef"))] 270 | #[test_case("foobar", custom("foobar"))] 271 | #[test_case("-1.-2.-3", custom("-1.-2.-3"))] 272 | fn parse_git_describe_output(input: &str, expectation: VersionNumber) { 273 | let actual = super::parse_git_describe_output(input).unwrap(); 274 | assert_eq!(actual, expectation); 275 | } 276 | 277 | #[test_case(""; "empty")] 278 | #[test_case(" \n\t\r"; "only whitespace")] 279 | fn parse_git_describe_output_err(input: &str) { 280 | assert!(super::parse_git_describe_output(input).is_err()); 281 | } 282 | 283 | #[test_case(v(0, 0, 0), "0.0.0")] 284 | #[test_case(v(0, 0, 7), "0.0.7")] 285 | #[test_case(v(0, 7, 0), "0.7.0")] 286 | #[test_case(v(7, 0, 0), "7.0.0")] 287 | #[test_case(v(1, 2, 3), "1.2.3")] 288 | #[test_case(custom("0.5.0-2-ga712af5"), "0.5.0-2-ga712af5")] 289 | #[test_case(custom("v0.5.0-2-ga712af5"), "v0.5.0-2-ga712af5")] 290 | fn display_version_number(input: VersionNumber, expectation: &str) { 291 | assert_eq!(&input.to_string(), expectation); 292 | } 293 | 294 | #[test_case(v(0, 0, 0), &[], true)] 295 | #[test_case(v(0, 0, 7), &[], true)] 296 | #[test_case(v(0, 7, 0), &[], true)] 297 | #[test_case(v(7, 0, 0), &[], true)] 298 | #[test_case(v(1, 2, 3), &[], true)] 299 | #[test_case(custom("0.5.0-2-ga712af5"), &[], true)] 300 | #[test_case(v(0, 0, 0), &[Pattern::MajorMinorPatch], true)] 301 | #[test_case(v(0, 0, 7), &[Pattern::MajorMinorPatch], true)] 302 | #[test_case(v(0, 7, 0), &[Pattern::MajorMinorPatch], true)] 303 | #[test_case(v(7, 0, 0), &[Pattern::MajorMinorPatch], true)] 304 | #[test_case(v(1, 2, 3), &[Pattern::MajorMinorPatch], true)] 305 | #[test_case(custom("0.5.0-2-ga712af5"), &[Pattern::MajorMinorPatch], false)] 306 | #[test_case( 307 | v(0, 0, 0), 308 | &[Pattern::MajorMinorPatch, Pattern::MajorMinorPatch], 309 | true)] 310 | #[test_case( 311 | v(0, 0, 7), 312 | &[Pattern::MajorMinorPatch, Pattern::MajorMinorPatch], 313 | true)] 314 | #[test_case( 315 | v(0, 7, 0), 316 | &[Pattern::MajorMinorPatch, Pattern::MajorMinorPatch], 317 | true)] 318 | #[test_case( 319 | v(7, 0, 0), 320 | &[Pattern::MajorMinorPatch, Pattern::MajorMinorPatch], 321 | true)] 322 | #[test_case( 323 | v(1, 2, 3), 324 | &[Pattern::MajorMinorPatch, Pattern::MajorMinorPatch], 325 | true)] 326 | #[test_case( 327 | custom("0.5.0-2-ga712af5"), 328 | &[Pattern::MajorMinorPatch, Pattern::MajorMinorPatch], 329 | false)] 330 | fn is_accepted(v: VersionNumber, accept: &[Pattern], expectation: bool) { 331 | let actual = super::is_accepted(&v, accept); 332 | assert_eq!(actual, expectation); 333 | } 334 | } 335 | --------------------------------------------------------------------------------