├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── error_mancer ├── Cargo.toml ├── src │ └── lib.rs └── tests │ ├── anyhow.rs │ ├── async.rs │ ├── basic.rs │ ├── derive.rs │ ├── flatten.rs │ ├── name.rs │ ├── ui.rs │ └── ui │ ├── missing_return_type.rs │ ├── missing_return_type.stderr │ ├── no_errors_anyhow.rs │ ├── no_errors_anyhow.stderr │ ├── not_fn.rs │ └── not_fn.stderr ├── error_mancer_macros ├── .gitignore ├── Cargo.toml └── src │ └── lib.rs ├── justfile └── rustfmt.toml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: vivax3794 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v3 16 | 17 | - name: Use nightly 18 | run: rustup default nightly 19 | 20 | - uses: Swatinem/rust-cache@v2 21 | 22 | - name: Run Tests 23 | run: cargo test --all --verbose 24 | 25 | fmt: 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - name: Check out code 30 | uses: actions/checkout@v3 31 | 32 | - name: Use nightly 33 | run: rustup default nightly 34 | 35 | - name: Install Rustfmt 36 | run: rustup component add rustfmt 37 | 38 | - uses: Swatinem/rust-cache@v2 39 | 40 | - name: Check Formatting 41 | run: cargo fmt --all -- --check 42 | 43 | clippy: 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - name: Check out code 48 | uses: actions/checkout@v3 49 | 50 | - name: Install Clippy and Rustfmt 51 | run: rustup component add rustfmt 52 | 53 | - uses: Swatinem/rust-cache@v2 54 | 55 | - name: Run Clippy 56 | run: cargo clippy 57 | 58 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Crates 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Use nightly 18 | run: rustup default nightly 19 | 20 | - uses: Swatinem/rust-cache@v2 21 | 22 | - name: Publish Workspace 23 | env: 24 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 25 | run: | 26 | cargo -Z package-workspace publish --workspace --token $CARGO_REGISTRY_TOKEN 27 | 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.4.3 2 | * **feature:** Now correctly works on async functions. 3 | 4 | # 0.4.2 5 | * **feature:** You can now set an explicit enum name by providing a ident instead of `_` as the error type in the signature. 6 | * **Fix**: Some part of the generated code didnt have `::...` for a stdlib reference, which in theory could leave it open to name shadowing and hence breaking. 7 | * **Cleanup**: Make docs nicer 8 | 9 | # 0.4.1 10 | * Added support for annotating error types with `#[derive]` attribute to derive extra traits. 11 | 12 | # 0.4.0 13 | * Fixed variant name construction: Did not actually consider each path segment as a proper word. 14 | * Cleaned up variant names: Now strips "Error" suffix if present. 15 | 16 | | path | 0.3.1 | 0.4.0 | 17 | | --- | --- | --- | 18 | | `std::io::Error` | `StdioError` | `StdIo` | 19 | | `io::Error` | `IoError` | `Io` | 20 | -------------------------------------------------------------------------------- /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 = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.93" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" 25 | 26 | [[package]] 27 | name = "backtrace" 28 | version = "0.3.74" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 31 | dependencies = [ 32 | "addr2line", 33 | "cfg-if", 34 | "libc", 35 | "miniz_oxide", 36 | "object", 37 | "rustc-demangle", 38 | "windows-targets", 39 | ] 40 | 41 | [[package]] 42 | name = "cfg-if" 43 | version = "1.0.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 46 | 47 | [[package]] 48 | name = "convert_case" 49 | version = "0.6.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" 52 | dependencies = [ 53 | "unicode-segmentation", 54 | ] 55 | 56 | [[package]] 57 | name = "equivalent" 58 | version = "1.0.1" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 61 | 62 | [[package]] 63 | name = "error_mancer" 64 | version = "0.4.3" 65 | dependencies = [ 66 | "anyhow", 67 | "error_mancer_macros", 68 | "thiserror", 69 | "tokio", 70 | "trybuild", 71 | ] 72 | 73 | [[package]] 74 | name = "error_mancer_macros" 75 | version = "0.4.3" 76 | dependencies = [ 77 | "convert_case", 78 | "proc-macro2", 79 | "quote", 80 | "syn", 81 | ] 82 | 83 | [[package]] 84 | name = "gimli" 85 | version = "0.31.1" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 88 | 89 | [[package]] 90 | name = "glob" 91 | version = "0.3.1" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 94 | 95 | [[package]] 96 | name = "hashbrown" 97 | version = "0.15.1" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" 100 | 101 | [[package]] 102 | name = "indexmap" 103 | version = "2.6.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 106 | dependencies = [ 107 | "equivalent", 108 | "hashbrown", 109 | ] 110 | 111 | [[package]] 112 | name = "itoa" 113 | version = "1.0.11" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 116 | 117 | [[package]] 118 | name = "libc" 119 | version = "0.2.169" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 122 | 123 | [[package]] 124 | name = "memchr" 125 | version = "2.7.4" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 128 | 129 | [[package]] 130 | name = "miniz_oxide" 131 | version = "0.8.4" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" 134 | dependencies = [ 135 | "adler2", 136 | ] 137 | 138 | [[package]] 139 | name = "object" 140 | version = "0.36.7" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 143 | dependencies = [ 144 | "memchr", 145 | ] 146 | 147 | [[package]] 148 | name = "pin-project-lite" 149 | version = "0.2.16" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 152 | 153 | [[package]] 154 | name = "proc-macro2" 155 | version = "1.0.89" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 158 | dependencies = [ 159 | "unicode-ident", 160 | ] 161 | 162 | [[package]] 163 | name = "quote" 164 | version = "1.0.37" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 167 | dependencies = [ 168 | "proc-macro2", 169 | ] 170 | 171 | [[package]] 172 | name = "rustc-demangle" 173 | version = "0.1.24" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 176 | 177 | [[package]] 178 | name = "ryu" 179 | version = "1.0.18" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 182 | 183 | [[package]] 184 | name = "serde" 185 | version = "1.0.214" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" 188 | dependencies = [ 189 | "serde_derive", 190 | ] 191 | 192 | [[package]] 193 | name = "serde_derive" 194 | version = "1.0.214" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" 197 | dependencies = [ 198 | "proc-macro2", 199 | "quote", 200 | "syn", 201 | ] 202 | 203 | [[package]] 204 | name = "serde_json" 205 | version = "1.0.132" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" 208 | dependencies = [ 209 | "itoa", 210 | "memchr", 211 | "ryu", 212 | "serde", 213 | ] 214 | 215 | [[package]] 216 | name = "serde_spanned" 217 | version = "0.6.8" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 220 | dependencies = [ 221 | "serde", 222 | ] 223 | 224 | [[package]] 225 | name = "syn" 226 | version = "2.0.87" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 229 | dependencies = [ 230 | "proc-macro2", 231 | "quote", 232 | "unicode-ident", 233 | ] 234 | 235 | [[package]] 236 | name = "target-triple" 237 | version = "0.1.3" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" 240 | 241 | [[package]] 242 | name = "termcolor" 243 | version = "1.4.1" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 246 | dependencies = [ 247 | "winapi-util", 248 | ] 249 | 250 | [[package]] 251 | name = "thiserror" 252 | version = "2.0.3" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" 255 | dependencies = [ 256 | "thiserror-impl", 257 | ] 258 | 259 | [[package]] 260 | name = "thiserror-impl" 261 | version = "2.0.3" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" 264 | dependencies = [ 265 | "proc-macro2", 266 | "quote", 267 | "syn", 268 | ] 269 | 270 | [[package]] 271 | name = "tokio" 272 | version = "1.43.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 275 | dependencies = [ 276 | "backtrace", 277 | "pin-project-lite", 278 | "tokio-macros", 279 | ] 280 | 281 | [[package]] 282 | name = "tokio-macros" 283 | version = "2.5.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 286 | dependencies = [ 287 | "proc-macro2", 288 | "quote", 289 | "syn", 290 | ] 291 | 292 | [[package]] 293 | name = "toml" 294 | version = "0.8.19" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 297 | dependencies = [ 298 | "serde", 299 | "serde_spanned", 300 | "toml_datetime", 301 | "toml_edit", 302 | ] 303 | 304 | [[package]] 305 | name = "toml_datetime" 306 | version = "0.6.8" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 309 | dependencies = [ 310 | "serde", 311 | ] 312 | 313 | [[package]] 314 | name = "toml_edit" 315 | version = "0.22.22" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 318 | dependencies = [ 319 | "indexmap", 320 | "serde", 321 | "serde_spanned", 322 | "toml_datetime", 323 | "winnow", 324 | ] 325 | 326 | [[package]] 327 | name = "trybuild" 328 | version = "1.0.101" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "8dcd332a5496c026f1e14b7f3d2b7bd98e509660c04239c58b0ba38a12daded4" 331 | dependencies = [ 332 | "glob", 333 | "serde", 334 | "serde_derive", 335 | "serde_json", 336 | "target-triple", 337 | "termcolor", 338 | "toml", 339 | ] 340 | 341 | [[package]] 342 | name = "unicode-ident" 343 | version = "1.0.13" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 346 | 347 | [[package]] 348 | name = "unicode-segmentation" 349 | version = "1.12.0" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 352 | 353 | [[package]] 354 | name = "winapi-util" 355 | version = "0.1.9" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 358 | dependencies = [ 359 | "windows-sys", 360 | ] 361 | 362 | [[package]] 363 | name = "windows-sys" 364 | version = "0.59.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 367 | dependencies = [ 368 | "windows-targets", 369 | ] 370 | 371 | [[package]] 372 | name = "windows-targets" 373 | version = "0.52.6" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 376 | dependencies = [ 377 | "windows_aarch64_gnullvm", 378 | "windows_aarch64_msvc", 379 | "windows_i686_gnu", 380 | "windows_i686_gnullvm", 381 | "windows_i686_msvc", 382 | "windows_x86_64_gnu", 383 | "windows_x86_64_gnullvm", 384 | "windows_x86_64_msvc", 385 | ] 386 | 387 | [[package]] 388 | name = "windows_aarch64_gnullvm" 389 | version = "0.52.6" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 392 | 393 | [[package]] 394 | name = "windows_aarch64_msvc" 395 | version = "0.52.6" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 398 | 399 | [[package]] 400 | name = "windows_i686_gnu" 401 | version = "0.52.6" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 404 | 405 | [[package]] 406 | name = "windows_i686_gnullvm" 407 | version = "0.52.6" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 410 | 411 | [[package]] 412 | name = "windows_i686_msvc" 413 | version = "0.52.6" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 416 | 417 | [[package]] 418 | name = "windows_x86_64_gnu" 419 | version = "0.52.6" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 422 | 423 | [[package]] 424 | name = "windows_x86_64_gnullvm" 425 | version = "0.52.6" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 428 | 429 | [[package]] 430 | name = "windows_x86_64_msvc" 431 | version = "0.52.6" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 434 | 435 | [[package]] 436 | name = "winnow" 437 | version = "0.6.20" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 440 | dependencies = [ 441 | "memchr", 442 | ] 443 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["error_mancer", "error_mancer_macros"] 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2024 viv 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # error_mancer 2 | 3 | ## Overview 4 | 5 | The `error_mancer` crate adds a `#[errors]` attribute that allows you to easily define and restrict error types in functions. This approach makes error handling more concise and keeps the error definitions close to the relevant methods, simplifying maintenance and modification. 6 | 7 | ## Example Usage 8 | 9 | For example in the following code the `#[errors]` macro automatically defines a `OpenFileError` based on the macro and substitudes in the error type in the signature. 10 | 11 | ```rs 12 | use std::io; 13 | 14 | use error_mancer::*; 15 | 16 | #[errors(io::Error, serde_json::Error)] 17 | fn open_file() -> Result { 18 | let file = std::fs::File::open("hello.json")?; 19 | let data = serde_json::from_reader(file)?; 20 | 21 | Ok(data) 22 | } 23 | 24 | fn main() { 25 | match open_file() { 26 | Err(OpenFileError::Io(err)) => { /* Handle I/O error */ }, 27 | Err(OpenFileError::SerdeJson(err)) => { /* Handle JSON parsing error */ }, 28 | Ok(data) => { /* Use data */ } 29 | } 30 | } 31 | ``` 32 | 33 | The main benefit of this approach is that it moves the error enum definition much closer to the method, making it easier to modify. 34 | 35 | ## `anyhow` support 36 | Additionally, it supports generic error results like `anyhow`. In these cases, the return type is not modified, but the allowed return values are still restricted. This is particularly useful when implementing traits that require an `anyhow::Result`. 37 | 38 | ```rs 39 | use error_mancer::*; 40 | 41 | #[errors] 42 | impl other_crate::Trait for MyStruct { 43 | #[errors] 44 | fn some_method(&self) -> anyhow::Result<()> { 45 | // This is a compiler error now! 46 | std::fs::open("hello.txt")?; 47 | } 48 | } 49 | ``` 50 | 51 | ## Design Goals 52 | 53 | - **Simplified Error Wrapper Enums**: This crate aims to make defining trivial error wrapper enums much easier and more convenient. 54 | - **Enforcing Error Restrictions**: It aims to allow you to enforce error restrictions on `anyhow::Result` and similar `Result` types. 55 | - **Compatibility with `thiserror`**: This crate does **not** aim to replace `thiserror` or similar libraries. They are designed for public-facing errors where control over details is important. In contrast, this library is focused on minimizing boilerplate as much as possible, providing less control but offering sensible defaults for internal error enums. 56 | 57 | In other words, what if `anyhow` was strongly typed on the possible errors? 58 | 59 | -------------------------------------------------------------------------------- /error_mancer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "error_mancer" 3 | version = "0.4.3" 4 | edition = "2021" 5 | description = "Quickly define custom error enums for a function." 6 | license = "MIT" 7 | authors = ["vivax3794@protonmail.com"] 8 | readme = "../README.md" 9 | repository = "https://github.com/vivax3794/error_mancer" 10 | categories = ["rust-patterns"] 11 | keywords = ["macro", "attribute-macro", "error", "error-handling", "no-std"] 12 | 13 | [dependencies] 14 | error_mancer_macros = {path = "../error_mancer_macros", version="0.4.2"} 15 | 16 | [dev-dependencies] 17 | trybuild = "1" 18 | anyhow = "1" 19 | thiserror = "2" 20 | tokio = { version = "1.43.0", features = ["macros", "rt"] } 21 | -------------------------------------------------------------------------------- /error_mancer/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The primary macro in this crate is `errors`, designed to simplify error handling by allowing developers to define and restrict error types directly within functions. This reduces boilerplate code and improves readability. 2 | //! 3 | //! # Usage 4 | //! 5 | //! ## Basic Example 6 | //! 7 | //! Below is a basic example of how to use the `errors` macro: 8 | //! 9 | //! ```rust 10 | //! # use error_mancer::prelude::*; 11 | //! 12 | //! #[errors(std::io::Error)] 13 | //! fn foo() -> Result { 14 | //! std::fs::File::open("hello.txt")?; 15 | //! Ok(10) 16 | //! } 17 | //! 18 | //! fn bar() { 19 | //! match foo() { 20 | //! Err(FooError::StdIo(_)) => {/* Handle error */}, 21 | //! Ok(_) => {/* Handle success */} 22 | //! } 23 | //! } 24 | //! ``` 25 | //! 26 | //! This macro automatically generates an enum resembling the following and sets the `Result` error type to it, so developers do not need to manually define it: 27 | //! 28 | //! ```rust,ignore 29 | //! // Auto generated code from `#[errors]` 30 | //! #[derive(Debug)] 31 | //! enum FooError { 32 | //! StdIo(std::io::Error), 33 | //! } 34 | //! 35 | //! impl From for FooError { ... } 36 | //! impl Display for FooError { ... } 37 | //! impl Error for FooError {} 38 | //! ``` 39 | //! 40 | //! Defining no errors also works, which will generate an enum with no variants, enforcing that no errors are returned. This is useful for functions that are guaranteed not to fail but still require a `Result<...>` return type, such as in trait implementations. It provides extra safety by ensuring that no error paths are possible. 41 | //! 42 | //! ## Enum name 43 | //! You can also explicitly set the enum name by using a Ident instead of `_` in the signature 44 | //! ```rust 45 | //! # use error_mancer::prelude::*; 46 | //! 47 | //! #[errors(std::io::Error)] 48 | //! fn foo() -> Result { 49 | //! std::fs::File::open("hello.txt")?; 50 | //! Ok(10) 51 | //! } 52 | //! 53 | //! fn bar() { 54 | //! match foo() { 55 | //! Err(MyErrorEnum::StdIo(_)) => {/* ... */}, 56 | //! Ok(_) => {/* ... */} 57 | //! } 58 | //! } 59 | //! ``` 60 | //! 61 | //! ## Usage in `impl` Blocks 62 | //! 63 | //! To use the macro within an `impl` block, the block must also be annotated: 64 | //! 65 | //! ```rust 66 | //! # use error_mancer::prelude::*; 67 | //! # struct MyStruct; 68 | //! 69 | //! #[errors] 70 | //! impl MyStruct { 71 | //! #[errors(std::io::Error)] 72 | //! fn method(&self) -> Result<(), _> { 73 | //! Ok(()) 74 | //! } 75 | //! } 76 | //! ``` 77 | //! 78 | //! ## Usage with `anyhow::Result` 79 | //! 80 | //! The macro can also be used without overwriting an error type and is fully compatible with `anyhow::Result` and similar types. This is especially useful for developers who prefer using `anyhow` for general error handling but want to benefit from additional error type restrictions when needed, particularly in trait implementations: 81 | //! 82 | //! ```rust,compile_fail 83 | //! # use error_mancer::prelude::*; 84 | //! 85 | //! #[errors] 86 | //! fn foo() -> anyhow::Result<()> { 87 | //! // ❌ Compiler error: `std::fs::File::open(...)` returns `std::io::Error`, 88 | //! // but `#[errors]` enforces that no errors are allowed. 89 | //! std::fs::File::open("hello.txt")?; 90 | //! Ok(()) 91 | //! } 92 | //! ``` 93 | //! 94 | //! ## Upcasting types 95 | //! ```rust 96 | //! # use error_mancer::prelude::*; 97 | //! # use thiserror::Error; 98 | //! # #[derive(Error, Debug)] 99 | //! # #[error("1")] 100 | //! # struct Err1; 101 | //! # #[derive(Error, Debug)] 102 | //! # #[error("2")] 103 | //! # struct Err2; 104 | //! # #[derive(Error, Debug)] 105 | //! # #[error("3")] 106 | //! # struct Err3; 107 | //! 108 | //! #[errors(Err1, Err2)] 109 | //! fn foo() -> Result { 110 | //! // ... 111 | //! # todo!() 112 | //! } 113 | //! 114 | //! #[errors(Err1, Err2, Err3)] 115 | //! fn bar() -> Result { 116 | //! let result = foo().into_super_error::()?; 117 | //! Ok(result) 118 | //! } 119 | //! ``` 120 | //! 121 | //! ## Deriving traits for generated enum 122 | //! You can annotate the function with `#[derive]` to derive traits for the generated enum. 123 | //! Note that the `#[derive]` macro must be used after the `errors` macro. (technically in `impl` 124 | //! blocks the order doesnt matter, but we recommend using `#[derive]` after `errors` for consistency.) 125 | //! ```rust 126 | //! # use error_mancer::prelude::*; 127 | //! # use thiserror::Error; 128 | //! # #[derive(Error, Debug, Clone)] 129 | //! # #[error("1")] 130 | //! # struct Err1; 131 | //! 132 | //! #[errors(Err1)] 133 | //! #[derive(Clone)] 134 | //! fn foo() -> Result<(), _> { 135 | //! Ok(()) 136 | //! } 137 | //! 138 | //! fn bar() { 139 | //! let _ = foo().clone(); 140 | //! } 141 | //! ``` 142 | //! 143 | //! # Specifics and Implementation Details 144 | //! 145 | //! ## Error Type Overwriting 146 | //! 147 | //! The macro looks for a type named `Result` in the root of the return type. If the second generic argument is `_`, it replaces it with the appropriate error type. See the examples below: 148 | //! 149 | //! | Original | Modified | 150 | //! | --------------------------- | ------------------------------------------------ | 151 | //! | `Result` | `Result` | 152 | //! | `Result` | `Result` | 153 | //! | `Result>` | `Result>` | 154 | //! | `std::result::Result` | `std::result::Result` | 155 | //! | `anyhow::Result` | `anyhow::Result` | 156 | //! | `Vec>` | ❌ compiler error, nested types arent replaced | 157 | //! 158 | //! ## Enum Visibility 159 | //! 160 | //! The enum can either be emitted inside the function body, or alongside the function (in which 161 | //! case it takes on the functions visibility) based on the 162 | //! following conditions: 163 | //! * If the second generic argument in `Result` is `_` its emitted outside the function. 164 | //! * If the second generic argument in `Result` is a simple identifier its emitted outside. 165 | //! * Otherwise, the enum is generated inside the function body and cannot be accessed externally. 166 | //! 167 | //! (in the following example the enums are include to illustrate where the macro would 168 | //! generate them.) 169 | //! ```rust,ignore 170 | //! #[errors] 171 | //! fn foo() -> Result<(), _> { ... } 172 | //! 173 | //! // enum FooError {...} 174 | //! 175 | //! #[errors] 176 | //! pub fn bar() -> Result<(), _> { ... } 177 | //! 178 | //! // pub enum FooError {...} 179 | //! 180 | //! #[errors] 181 | //! pub fn baz() -> Result<(), CustomName> { ... } 182 | //! 183 | //! // pub enum CustomName {...} 184 | //! 185 | //! #[errors] 186 | //! pub fn secret() -> anyhow::Result<()> { 187 | //! // enum SecretError { ... } 188 | //! // ... 189 | //! } 190 | //! ``` 191 | //! 192 | //! ## Naming Conventions 193 | //! 194 | //! The enum name is derived from the function name, converted to Pascal case using the `case_fold` crate to conform to Rust naming conventions for types and enums. Similarly, variant names are derived from the path segments of the types, with the "Error" suffix removed if present. For example, `std::io::Error` would produce a variant called `StdIo`, while `io::Error` would produce `Io`. 195 | //! 196 | //! ## Display Implementation 197 | //! 198 | //! The `Display` implementation simply delegates to each contained error, ensuring consistent and readable error messages. 199 | //! 200 | //! ## `into_super_error` 201 | //! This function uses the `FlattenInto` trait which is automatically implemented by the macro for 202 | //! its errors, for all target types which implemnt `From<...>` for each of the errors variants. i.e a generated 203 | //! implementation might look like: 204 | //! ```rust 205 | //! # use error_mancer::{FlattenInto, errors}; 206 | //! # use thiserror::Error; 207 | //! # #[derive(Error, Debug)] 208 | //! # #[error("1")] 209 | //! # struct Err1; 210 | //! # #[derive(Error, Debug)] 211 | //! # #[error("2")] 212 | //! # struct Err2; 213 | //! # enum OurError { 214 | //! # Err1(Err1), 215 | //! # Err2(Err2) 216 | //! # } 217 | //! 218 | //! impl FlattenInto for OurError 219 | //! where T: From + From { 220 | //! fn flatten(self) -> T { 221 | //! match self { 222 | //! Self::Err1(err) => T::from(err), 223 | //! Self::Err2(err) => T::from(err), 224 | //! } 225 | //! } 226 | //! } 227 | //! ``` 228 | #![no_std] 229 | 230 | pub use error_mancer_macros::errors; 231 | 232 | pub mod prelude { 233 | pub use error_mancer_macros::errors; 234 | 235 | pub use super::ResultExt; 236 | } 237 | 238 | #[doc(hidden)] 239 | #[diagnostic::on_unimplemented( 240 | message = "Error `{T}` not allowed to be returned from this function.", 241 | label = "`{T}` is not listed in `#[errors]` attribute", 242 | note = "Add `{T}` to `#[errors]` attribute or handle this error locally." 243 | )] 244 | pub trait ErrorMancerFrom { 245 | fn from(value: T) -> Self; 246 | } 247 | 248 | /// This trait allows a error to be flattened into another one and is automatically implemented by 249 | /// the `#[errors]` macro for all super errors that implement `From<...>` for each of its fields. 250 | pub trait FlattenInto { 251 | fn flatten(self) -> T; 252 | } 253 | 254 | /// This trait extends `Result` with an additional method to upcast a error enum. 255 | pub trait ResultExt { 256 | /// This will convert from the current `E` into the specified super error. 257 | fn into_super_error(self) -> Result 258 | where 259 | E: FlattenInto; 260 | } 261 | 262 | impl ResultExt for Result { 263 | #[inline(always)] 264 | fn into_super_error(self) -> Result 265 | where 266 | E: FlattenInto, 267 | { 268 | self.map_err(|err| err.flatten()) 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /error_mancer/tests/anyhow.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use error_mancer::prelude::*; 3 | 4 | #[errors] 5 | fn no_errors_foo() -> Result { 6 | Ok(10) 7 | } 8 | 9 | #[test] 10 | fn no_errors() { 11 | assert_eq!(no_errors_foo().unwrap(), 10); 12 | } 13 | 14 | #[errors] 15 | fn double(x: i32) -> Result { 16 | Ok(x * 2) 17 | } 18 | 19 | #[test] 20 | fn arguments() { 21 | assert_eq!(double(10).unwrap(), 20); 22 | } 23 | 24 | trait Foo { 25 | fn foo(&self) -> Result; 26 | } 27 | 28 | struct IShouldntError; 29 | 30 | #[errors] 31 | impl Foo for IShouldntError { 32 | #[errors] 33 | fn foo(&self) -> Result { 34 | Ok(10) 35 | } 36 | } 37 | 38 | #[test] 39 | fn traits() { 40 | assert_eq!(IShouldntError.foo().unwrap(), 10); 41 | } 42 | -------------------------------------------------------------------------------- /error_mancer/tests/async.rs: -------------------------------------------------------------------------------- 1 | use error_mancer::prelude::*; 2 | 3 | async fn foo() -> i32 { 4 | 10 5 | } 6 | 7 | #[errors] 8 | #[derive(PartialEq, Eq)] 9 | async fn async_works() -> Result { 10 | Ok(foo().await) 11 | } 12 | 13 | #[tokio::test] 14 | async fn test_async() { 15 | assert_eq!(async_works().await, Ok(10)); 16 | } 17 | -------------------------------------------------------------------------------- /error_mancer/tests/basic.rs: -------------------------------------------------------------------------------- 1 | #![feature(assert_matches)] 2 | #![no_std] 3 | 4 | use core::assert_matches::assert_matches; 5 | use core::num::{ParseIntError, TryFromIntError}; 6 | 7 | use error_mancer::prelude::*; 8 | 9 | #[errors] 10 | fn foo() -> Result { 11 | Ok(10) 12 | } 13 | 14 | #[test] 15 | fn no_errors() { 16 | assert_eq!(foo().unwrap(), 10); 17 | } 18 | 19 | #[errors(ParseIntError, TryFromIntError)] 20 | fn bar(x: &str) -> Result { 21 | let result: i16 = x.parse()?; 22 | let result = result.try_into()?; 23 | Ok(result) 24 | } 25 | 26 | #[test] 27 | fn specify_error_tests() { 28 | assert!(bar("10").is_ok()); 29 | assert_matches!(bar("abc"), Err(BarError::ParseInt(_))); 30 | assert_matches!(bar("300"), Err(BarError::TryFromInt(_))); 31 | } 32 | 33 | mod module { 34 | use super::*; 35 | 36 | #[errors(TryFromIntError)] 37 | pub fn in_module() -> Result<(), _> { 38 | let _: u8 = 300_u16.try_into()?; 39 | Ok(()) 40 | } 41 | } 42 | 43 | #[test] 44 | fn pub_works() { 45 | assert_matches!( 46 | module::in_module(), 47 | Err(module::InModuleError::TryFromInt(_)) 48 | ); 49 | } 50 | 51 | struct Test; 52 | 53 | #[errors] 54 | impl Test { 55 | #[errors(TryFromIntError)] 56 | fn method(&self, x: u16) -> Result { 57 | let x = x.try_into()?; 58 | Ok(x) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /error_mancer/tests/derive.rs: -------------------------------------------------------------------------------- 1 | use error_mancer::errors; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Clone, Debug)] 5 | #[error("err1")] 6 | struct Err1; 7 | 8 | #[errors(Err1)] 9 | #[derive(Clone)] 10 | fn foo() -> Result<(), _> { 11 | Ok(()) 12 | } 13 | 14 | fn bar() { 15 | let _ = foo().clone(); 16 | } 17 | 18 | struct Bar; 19 | 20 | #[errors] 21 | impl Bar { 22 | #[errors(Err1)] 23 | #[derive(Clone)] 24 | fn bar(&self) -> Result<(), _> { 25 | Ok(()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /error_mancer/tests/flatten.rs: -------------------------------------------------------------------------------- 1 | #![feature(assert_matches)] 2 | #![no_std] 3 | 4 | use core::assert_matches::assert_matches; 5 | 6 | use error_mancer::prelude::*; 7 | use thiserror::Error; 8 | 9 | #[derive(Error, Debug)] 10 | #[error("error 1")] 11 | struct Err1; 12 | 13 | #[derive(Error, Debug)] 14 | #[error("error 2")] 15 | struct Err2; 16 | 17 | #[derive(Error, Debug)] 18 | #[error("error 3")] 19 | struct Err3; 20 | 21 | #[errors(Err1, Err2, Err3)] 22 | fn foo(x: i32) -> Result<(), _> { 23 | match x { 24 | 0 => Ok(()), 25 | 1 => Err(Err1.into()), 26 | 2 => Err(Err2.into()), 27 | _ => Err(Err3.into()), 28 | } 29 | } 30 | 31 | #[errors(FooError)] 32 | fn wrapped(x: i32) -> Result { 33 | let result = foo(x); 34 | let result = match result { 35 | Err(FooError::Err3(_)) => 20, 36 | Ok(_) => 10, 37 | Err(err) => return Err(err.into()), 38 | }; 39 | Ok(result) 40 | } 41 | 42 | #[errors(Err1, Err2, Err3)] 43 | fn unwrapped(x: i32) -> Result { 44 | foo(x).into_super_error::()?; 45 | Ok(10) 46 | } 47 | 48 | #[test] 49 | fn test_unwrapped() { 50 | assert_matches!(unwrapped(0), Ok(10)); 51 | assert_matches!(unwrapped(1), Err(UnwrappedError::Err1(Err1))); 52 | assert_matches!(unwrapped(2), Err(UnwrappedError::Err2(Err2))); 53 | assert_matches!(unwrapped(3), Err(UnwrappedError::Err3(Err3))); 54 | } 55 | -------------------------------------------------------------------------------- /error_mancer/tests/name.rs: -------------------------------------------------------------------------------- 1 | use error_mancer::errors; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Clone, Debug)] 5 | #[error("err1")] 6 | struct Err1; 7 | 8 | #[errors(Err1)] 9 | fn foo() -> Result<(), BarError> { 10 | Ok(()) 11 | } 12 | 13 | #[errors(BarError)] 14 | fn test() -> Result<(), _> { 15 | foo()?; 16 | Ok(()) 17 | } 18 | 19 | #[errors(Err1)] 20 | fn bar() -> Result<(), Box> { 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /error_mancer/tests/ui.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn ui() { 3 | let t = trybuild::TestCases::new(); 4 | t.compile_fail("tests/ui/*.rs"); 5 | } 6 | -------------------------------------------------------------------------------- /error_mancer/tests/ui/missing_return_type.rs: -------------------------------------------------------------------------------- 1 | use error_mancer::prelude::*; 2 | 3 | #[errors] 4 | fn foo() {} 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /error_mancer/tests/ui/missing_return_type.stderr: -------------------------------------------------------------------------------- 1 | error: Function must have a return type of Result 2 | --> tests/ui/missing_return_type.rs:3:1 3 | | 4 | 3 | #[errors] 5 | | ^^^^^^^^^ 6 | | 7 | = note: this error originates in the attribute macro `errors` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | -------------------------------------------------------------------------------- /error_mancer/tests/ui/no_errors_anyhow.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use error_mancer::prelude::*; 3 | 4 | #[errors] 5 | fn foo() -> Result { 6 | let _ = std::fs::File::open("hello.txt")?; 7 | Ok(10) 8 | } 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /error_mancer/tests/ui/no_errors_anyhow.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: Error `std::io::Error` not allowed to be returned from this function. 2 | --> tests/ui/no_errors_anyhow.rs:6:45 3 | | 4 | 6 | let _ = std::fs::File::open("hello.txt")?; 5 | | ^ `std::io::Error` is not listed in `#[errors]` attribute 6 | | 7 | = help: the trait `ErrorMancerFrom` is not implemented for `FooError` 8 | = note: Add `std::io::Error` to `#[errors]` attribute or handle this error locally. 9 | = help: the trait `FromResidual>` is implemented for `Result` 10 | note: required for `FooError` to implement `From` 11 | --> tests/ui/no_errors_anyhow.rs:4:1 12 | | 13 | 4 | #[errors] 14 | | ^^^^^^^^^ 15 | = note: required for `Result` to implement `FromResidual>` 16 | = note: this error originates in the attribute macro `errors` (in Nightly builds, run with -Z macro-backtrace for more info) 17 | -------------------------------------------------------------------------------- /error_mancer/tests/ui/not_fn.rs: -------------------------------------------------------------------------------- 1 | use error_mancer::prelude::*; 2 | 3 | #[errors] 4 | struct NotAFunction; 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /error_mancer/tests/ui/not_fn.stderr: -------------------------------------------------------------------------------- 1 | error: Expected function or impl block 2 | --> tests/ui/not_fn.rs:4:1 3 | | 4 | 4 | struct NotAFunction; 5 | | ^^^^^^^^^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /error_mancer_macros/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /error_mancer_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "error_mancer_macros" 3 | version = "0.4.3" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "proc macro for error_mancer" 7 | 8 | [dependencies] 9 | syn = {version = "2.0", features = ["full", "extra-traits"]} 10 | quote = "1.0" 11 | proc-macro2 = "1.0" 12 | convert_case = "0.6" 13 | 14 | [lib] 15 | proc-macro = true 16 | -------------------------------------------------------------------------------- /error_mancer_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use convert_case::{Case, Casing}; 2 | use proc_macro2::TokenStream; 3 | use quote::{format_ident, quote, ToTokens}; 4 | use syn::parse::Parser; 5 | use syn::punctuated::Punctuated; 6 | use syn::spanned::Spanned; 7 | use syn::{ 8 | self, 9 | parse2, 10 | parse_macro_input, 11 | parse_quote, 12 | GenericArgument, 13 | Path, 14 | PathArguments, 15 | ReturnType, 16 | Token, 17 | Type, 18 | TypePath, 19 | }; 20 | 21 | #[proc_macro_attribute] 22 | pub fn errors( 23 | attr: proc_macro::TokenStream, 24 | item: proc_macro::TokenStream, 25 | ) -> proc_macro::TokenStream { 26 | let attr = parse_macro_input!(attr); 27 | let item = parse_macro_input!(item); 28 | match errors_impl(attr, item) { 29 | Ok(result) => result.into(), 30 | Err(err) => err.into_compile_error().into(), 31 | } 32 | } 33 | 34 | fn errors_impl(attr: TokenStream, item: TokenStream) -> syn::Result { 35 | if let Ok(function) = syn::parse2(item.clone()) { 36 | do_free_function(function, attr) 37 | } else if let Ok(impl_block) = syn::parse2(item.clone()) { 38 | do_impl_block(impl_block) 39 | } else { 40 | Err(syn::Error::new( 41 | item.span(), 42 | "Expected function or impl block", 43 | )) 44 | } 45 | } 46 | 47 | fn do_impl_block(mut impl_block: syn::ItemImpl) -> syn::Result { 48 | let mut enums = Vec::new(); 49 | for item in &mut impl_block.items { 50 | if let syn::ImplItem::Fn(method) = item { 51 | if let Some(attr) = method 52 | .attrs 53 | .iter() 54 | .find(|&attr| attr.path().is_ident("errors")) 55 | { 56 | match attr.meta.clone() { 57 | syn::Meta::List(list) => { 58 | let arguments = list.tokens; 59 | let function = method.into_token_stream(); 60 | let function = parse2(function)?; 61 | let (enum_decl, function) = create_function(function, arguments)?; 62 | enums.push(enum_decl); 63 | 64 | let function = function.into_token_stream(); 65 | let function = parse2(function)?; 66 | *method = function; 67 | } 68 | syn::Meta::Path(_) => { 69 | let arguments = quote!(); 70 | let function = method.into_token_stream(); 71 | let function = parse2(function)?; 72 | let (enum_decl, function) = create_function(function, arguments)?; 73 | enums.push(enum_decl); 74 | 75 | let function = function.into_token_stream(); 76 | let function = parse2(function)?; 77 | *method = function; 78 | } 79 | _ => { 80 | return Err(syn::Error::new( 81 | attr.span(), 82 | "Expected list or simple `#[errors]`", 83 | )) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | Ok(quote! { 91 | #(#enums)* 92 | #impl_block 93 | }) 94 | } 95 | 96 | fn do_free_function(function: syn::ItemFn, attr: TokenStream) -> Result { 97 | let (enum_decl, new_function) = create_function(function, attr)?; 98 | Ok(quote! { 99 | #enum_decl 100 | #new_function 101 | }) 102 | } 103 | 104 | fn create_function( 105 | function: syn::ItemFn, 106 | attr: TokenStream, 107 | ) -> Result<(TokenStream, TokenStream), syn::Error> { 108 | let derives = match function 109 | .attrs 110 | .iter() 111 | .find(|attr| attr.path().is_ident("derive")) 112 | { 113 | Some(attr) => attr.to_token_stream(), 114 | None => quote!(), 115 | }; 116 | 117 | let vis = function.vis; 118 | let mut signature = function.sig; 119 | let body = function.block; 120 | 121 | let (ok_return_type, explicit_error_name) = get_return_generics(&signature.output)?; 122 | let (error_enum, error_return_type) = generate_error_type( 123 | attr, 124 | signature.ident.to_string(), 125 | vis.clone(), 126 | derives, 127 | explicit_error_name.clone(), 128 | )?; 129 | 130 | let inner_type: syn::ReturnType = 131 | parse_quote!(-> ::core::result::Result<#ok_return_type, #error_return_type>); 132 | 133 | let replaced = replace_error_value(&mut signature.output, error_return_type); 134 | 135 | let emit_enum_outside = replaced || explicit_error_name.is_some(); 136 | 137 | let maybe_async = if signature.asyncness.is_some() { 138 | quote!(async) 139 | } else { 140 | quote!() 141 | }; 142 | let maybe_await = if signature.asyncness.is_some() { 143 | quote!(.await) 144 | } else { 145 | quote!() 146 | }; 147 | 148 | if emit_enum_outside { 149 | let new_func = quote! { 150 | #[allow(clippy::needless_question_mark)] 151 | #vis #signature { 152 | Ok((#maybe_async move || #inner_type { #body })()#maybe_await?) 153 | } 154 | }; 155 | Ok((error_enum, new_func)) 156 | } else { 157 | let new_func = quote! { 158 | #[allow(clippy::needless_question_mark)] 159 | #vis #signature { 160 | #error_enum 161 | Ok((#maybe_async move || #inner_type { #body })()#maybe_await?) 162 | } 163 | }; 164 | Ok((quote!(), new_func)) 165 | } 166 | } 167 | 168 | fn get_return_generics(return_type: &ReturnType) -> syn::Result<(&Type, Option)> { 169 | match return_type { 170 | ReturnType::Default => Err(syn::Error::new( 171 | return_type.span(), 172 | "Function must have a return type of Result", 173 | )), 174 | ReturnType::Type(_, ty) => { 175 | // Ensure the return type is a Path type 176 | let type_path = match ty.as_ref() { 177 | Type::Path(TypePath { path, .. }) => path, 178 | _ => { 179 | return Err(syn::Error::new( 180 | ty.span(), 181 | "Expected return type to be a path, such as Result", 182 | )) 183 | } 184 | }; 185 | 186 | // Check if the last segment is 'Result' 187 | let last_segment = type_path.segments.last().ok_or_else(|| { 188 | syn::Error::new(type_path.span(), "Expected a path segment for Result") 189 | })?; 190 | 191 | if last_segment.ident != "Result" { 192 | return Err(syn::Error::new( 193 | last_segment.ident.span(), 194 | "Expected return type to be Result<...>", 195 | )); 196 | } 197 | 198 | // Ensure that Result has exactly two generic arguments 199 | let generic_args = match &last_segment.arguments { 200 | PathArguments::AngleBracketed(args) => &args.args, 201 | _ => { 202 | return Err(syn::Error::new( 203 | last_segment.span(), 204 | "Expected angle-bracketed generic arguments, like Result", 205 | )) 206 | } 207 | }; 208 | 209 | // Extract the first generic argument (Ok type) 210 | let ok_arg = generic_args.first().ok_or_else(|| { 211 | syn::Error::new( 212 | generic_args.span(), 213 | "Expected at least one generic argument for Result", 214 | ) 215 | })?; 216 | let ok_type = match ok_arg { 217 | GenericArgument::Type(ok_type) => ok_type, 218 | _ => { 219 | return Err(syn::Error::new( 220 | ok_arg.span(), 221 | "Expected the first generic argument of Result to be a type", 222 | )) 223 | } 224 | }; 225 | 226 | // Extract the second generic argument (Ok type) 227 | let err_arg = generic_args.get(1); 228 | let enum_name = match err_arg { 229 | Some(GenericArgument::Type(Type::Infer(_))) => None, 230 | Some(GenericArgument::Type(Type::Path(TypePath { 231 | path: Path { segments, .. }, 232 | qself: None, 233 | }))) => { 234 | if segments.len() != 1 { 235 | return Ok((ok_type, None)); 236 | } 237 | let segment = &segments[0]; 238 | 239 | if !segment.arguments.is_empty() { 240 | return Ok((ok_type, None)); 241 | } 242 | 243 | Some(segment.ident.clone()) 244 | } 245 | _ => None, 246 | }; 247 | 248 | Ok((ok_type, enum_name)) 249 | } 250 | } 251 | } 252 | 253 | fn replace_error_value(return_type: &mut ReturnType, error_type: syn::Type) -> bool { 254 | let ReturnType::Type(_, return_type) = return_type else { 255 | return false; 256 | }; 257 | 258 | let syn::Type::Path(return_type) = return_type.as_mut() else { 259 | return false; 260 | }; 261 | 262 | let Some(last) = return_type.path.segments.last_mut() else { 263 | return false; 264 | }; 265 | 266 | if last.ident != "Result" { 267 | return false; 268 | } 269 | 270 | let syn::PathArguments::AngleBracketed(arguments) = &mut last.arguments else { 271 | return false; 272 | }; 273 | 274 | if arguments.args.len() < 2 { 275 | return false; 276 | } 277 | 278 | if let syn::GenericArgument::Type(syn::Type::Infer(_)) = arguments.args[1] { 279 | arguments.args[1] = syn::GenericArgument::Type(error_type); 280 | return true; 281 | } 282 | false 283 | } 284 | 285 | fn generate_error_type( 286 | args: TokenStream, 287 | function_name: String, 288 | vis: syn::Visibility, 289 | derives: TokenStream, 290 | enum_name: Option, 291 | ) -> syn::Result<(TokenStream, Type)> { 292 | let enum_name = if let Some(enum_name) = enum_name { 293 | enum_name 294 | } else { 295 | let enum_name = function_name.to_case(Case::Pascal); 296 | format_ident!("{enum_name}Error") 297 | }; 298 | 299 | let error_types = Punctuated::::parse_terminated.parse2(args)?; 300 | let error_types_clone = error_types.clone().into_iter().collect::>(); 301 | let (fields, from_impls): (Vec<_>, Vec<_>) = error_types 302 | .iter() 303 | .map(|path| { 304 | let name = path 305 | .segments 306 | .iter() 307 | .map(|segment| segment.ident.to_string() + "_") 308 | .collect::() 309 | .to_case(Case::Pascal); 310 | let name = name.trim_end_matches("Error"); 311 | let name = format_ident!("{name}"); 312 | 313 | ( 314 | ( 315 | name.clone(), 316 | quote!( 317 | #name(#path) 318 | ), 319 | ), 320 | quote!( 321 | impl ::error_mancer::ErrorMancerFrom<#path> for #enum_name { 322 | fn from(value: #path) -> Self { 323 | Self::#name(value) 324 | } 325 | } 326 | ), 327 | ) 328 | }) 329 | .unzip(); 330 | let (names, fields): (Vec<_>, Vec<_>) = fields.into_iter().unzip(); 331 | 332 | let enum_stream = quote! { 333 | #[derive(::core::fmt::Debug)] 334 | #derives 335 | #vis enum #enum_name { 336 | #(#fields),* 337 | } 338 | 339 | #(#from_impls)* 340 | 341 | impl ::core::convert::From for #enum_name where Self: ::error_mancer::ErrorMancerFrom { 342 | fn from(value: T) -> Self { 343 | ::error_mancer::ErrorMancerFrom::from(value) 344 | } 345 | } 346 | 347 | impl ::error_mancer::FlattenInto for #enum_name 348 | where T: #(::error_mancer::ErrorMancerFrom<#error_types_clone>)+* { 349 | fn flatten(self) -> T { 350 | match self { 351 | #(Self::#names(err) => T::from(err),)* 352 | _ => unreachable!() 353 | } 354 | } 355 | } 356 | 357 | impl ::core::fmt::Display for #enum_name { 358 | fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { 359 | match self { 360 | #(Self::#names(err) => err.fmt(f),)* 361 | _ => unreachable!() 362 | } 363 | } 364 | } 365 | 366 | impl ::core::error::Error for #enum_name {} 367 | }; 368 | let enum_type = parse_quote!(#enum_name); 369 | 370 | Ok((enum_stream, enum_type)) 371 | } 372 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | test: 2 | cargo nextest run 3 | cargo test --doc 4 | 5 | update_ui: 6 | TRYBUILD=overwrite cargo nextest run ui 7 | 8 | docs: 9 | cargo doc --open 10 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | format_macro_matchers = true 3 | hard_tabs = false 4 | imports_layout = "HorizontalVertical" 5 | imports_granularity = "Module" 6 | group_imports = "StdExternalCrate" 7 | --------------------------------------------------------------------------------