├── .gitignore ├── .rustfmt.toml ├── rust-toolchain.toml ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── README.md ├── tests └── integration_test.rs ├── Cargo.lock └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | tab_spaces = 2 3 | edition = "2021" 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.87.0" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "boxed_error" 3 | description = "Macro for easily boxing an error" 4 | version = "0.2.3" 5 | edition = "2021" 6 | authors = ["the Deno authors"] 7 | license = "MIT" 8 | repository = "https://github.com/denoland/boxed_error" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | quote = "1.0.37" 15 | syn = "2.0.87" 16 | 17 | [dev-dependencies] 18 | thiserror = "2.0" 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2018-2024 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseKind: 7 | description: "Kind of release" 8 | default: "minor" 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | required: true 14 | 15 | jobs: 16 | rust: 17 | name: release 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 30 20 | 21 | steps: 22 | - name: Clone repository 23 | uses: actions/checkout@v3 24 | with: 25 | token: ${{ secrets.DENOBOT_PAT }} 26 | 27 | - uses: denoland/setup-deno@v1 28 | with: 29 | deno-version: canary 30 | - uses: dsherret/rust-toolchain-file@v1 31 | 32 | - name: Tag and release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.DENOBOT_PAT }} 35 | GH_WORKFLOW_ACTOR: ${{ github.actor }} 36 | run: | 37 | git config user.email "denobot@users.noreply.github.com" 38 | git config user.name "denobot" 39 | deno run -A https://raw.githubusercontent.com/denoland/automation/0.14.2/tasks/publish_release.ts --${{github.event.inputs.releaseKind}} 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # boxed_error 2 | 3 | Experimental opinionated way to provide helper methods for use with boxing errors. 4 | 5 | Before: 6 | 7 | ```rs 8 | use thiserror::Error; 9 | 10 | #[derive(Error, Debug)] 11 | #[error(transparent)] 12 | pub struct DenoResolveError(pub Box); 13 | 14 | impl DenoResolveError { 15 | pub fn as_kind(&self) -> &DenoResolveErrorKind { 16 | &self.0 17 | } 18 | 19 | pub fn into_kind(self) -> DenoResolveErrorKind { 20 | *self.0 21 | } 22 | } 23 | 24 | impl From for DenoResolveError 25 | where 26 | DenoResolveErrorKind: From, 27 | { 28 | fn from(err: E) -> Self { 29 | DenoResolveError(Box::new(DenoResolveErrorKind::from(err))) 30 | } 31 | } 32 | 33 | #[derive(Debug, Error)] 34 | pub enum DenoResolveErrorKind { 35 | #[error("Importing ...")] 36 | InvalidVendorFolderImport, 37 | #[error(transparent)] 38 | MappedResolution(#[from] MappedResolutionError), 39 | // ... 40 | } 41 | 42 | impl DenoResolveErrorKind { 43 | pub fn into_box(self) -> DenoResolveError { 44 | DenoResolveError(Box::new(self)) 45 | } 46 | } 47 | ``` 48 | 49 | After: 50 | 51 | ```rs 52 | use boxed_error::Boxed; 53 | use thiserror::Error; 54 | 55 | #[derive(Debug, Boxed)] 56 | pub struct DenoResolveError(pub Box); 57 | 58 | #[derive(Debug, Error)] 59 | pub enum DenoResolveErrorKind { 60 | #[error("Importing ...")] 61 | InvalidVendorFolderImport, 62 | #[error(transparent)] 63 | MappedResolution(#[from] MappedResolutionError), 64 | // ... 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use std::io::ErrorKind; 2 | 3 | use boxed_error::Boxed; 4 | use thiserror::Error; 5 | 6 | #[test] 7 | fn test_boxed_enum_error() { 8 | #[derive(Debug, Boxed)] 9 | pub struct MyError(pub Box); 10 | 11 | #[allow(dead_code)] 12 | #[derive(Debug, Error)] 13 | pub enum MyErrorKind { 14 | #[error(transparent)] 15 | Io(std::io::Error), 16 | #[error(transparent)] 17 | ParseInt(std::num::ParseIntError), 18 | } 19 | 20 | let error = 21 | MyErrorKind::Io(std::io::Error::new(ErrorKind::NotFound, "File not found")) 22 | .into_box(); 23 | assert_eq!(error.to_string(), "File not found"); 24 | assert_eq!( 25 | std::any::type_name_of_val(error.as_kind()), 26 | "integration_test::test_boxed_enum_error::MyErrorKind" 27 | ); 28 | assert_eq!( 29 | std::any::type_name_of_val(&*error), 30 | "integration_test::test_boxed_enum_error::MyErrorKind" 31 | ); 32 | } 33 | 34 | #[test] 35 | fn test_boxed_struct_error() { 36 | #[derive(Debug, Boxed)] 37 | pub struct MyError(pub Box); 38 | 39 | #[derive(Debug)] 40 | pub struct MyErrorData { 41 | name: String, 42 | } 43 | 44 | impl std::fmt::Display for MyErrorData { 45 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 46 | write!(f, "error: {}", self.name) 47 | } 48 | } 49 | 50 | impl std::error::Error for MyErrorData {} 51 | 52 | let error = MyErrorData { 53 | name: "My error".to_string(), 54 | } 55 | .into_box(); 56 | assert_eq!(error.to_string(), "error: My error"); 57 | assert_eq!( 58 | std::any::type_name_of_val(error.as_data()), 59 | "integration_test::test_boxed_struct_error::MyErrorData" 60 | ); 61 | assert_eq!( 62 | std::any::type_name_of_val(&*error), 63 | "integration_test::test_boxed_struct_error::MyErrorData" 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "boxed_error" 7 | version = "0.2.3" 8 | dependencies = [ 9 | "quote", 10 | "syn", 11 | "thiserror", 12 | ] 13 | 14 | [[package]] 15 | name = "proc-macro2" 16 | version = "1.0.89" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 19 | dependencies = [ 20 | "unicode-ident", 21 | ] 22 | 23 | [[package]] 24 | name = "quote" 25 | version = "1.0.37" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 28 | dependencies = [ 29 | "proc-macro2", 30 | ] 31 | 32 | [[package]] 33 | name = "syn" 34 | version = "2.0.87" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 37 | dependencies = [ 38 | "proc-macro2", 39 | "quote", 40 | "unicode-ident", 41 | ] 42 | 43 | [[package]] 44 | name = "thiserror" 45 | version = "2.0.3" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" 48 | dependencies = [ 49 | "thiserror-impl", 50 | ] 51 | 52 | [[package]] 53 | name = "thiserror-impl" 54 | version = "2.0.3" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" 57 | dependencies = [ 58 | "proc-macro2", 59 | "quote", 60 | "syn", 61 | ] 62 | 63 | [[package]] 64 | name = "unicode-ident" 65 | version = "1.0.13" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 68 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | rust: 7 | name: boxed_error-${{ matrix.os }} 8 | if: | 9 | (github.event_name == 'push' || !startsWith(github.event.pull_request.head.label, 'denoland:')) 10 | && !startsWith(github.ref, 'refs/tags/deno/') 11 | runs-on: ${{ matrix.os }} 12 | permissions: 13 | contents: read 14 | id-token: write 15 | timeout-minutes: 30 16 | strategy: 17 | matrix: 18 | os: [macOS-latest, ubuntu-latest, windows-latest] 19 | 20 | env: 21 | CARGO_INCREMENTAL: 0 22 | RUST_BACKTRACE: full 23 | RUSTFLAGS: -D warnings 24 | 25 | steps: 26 | - name: Clone repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Install rust 30 | uses: dsherret/rust-toolchain-file@v1 31 | 32 | - uses: Swatinem/rust-cache@v2 33 | with: 34 | save-if: ${{ github.ref == 'refs/heads/main' }} 35 | 36 | - name: Format 37 | if: contains(matrix.os, 'ubuntu') 38 | run: | 39 | cargo fmt -- --check 40 | 41 | - name: Clippy 42 | if: contains(matrix.os, 'ubuntu') 43 | run: cargo clippy --locked --all-features --all-targets -- -D clippy::all 44 | 45 | - name: Cargo Build 46 | run: cargo build --locked --all-features --all-targets 47 | 48 | - name: Cargo Test 49 | run: cargo test --locked --all-features --all-targets 50 | 51 | # ensure we build with no default features, but only bother testing on linux 52 | - name: Cargo Build (no-default-features) 53 | if: contains(matrix.os, 'ubuntu') 54 | run: cargo build --locked --no-default-features 55 | 56 | - name: Cargo publish 57 | if: | 58 | contains(matrix.os, 'ubuntu') && 59 | github.repository == 'denoland/boxed_error' && 60 | startsWith(github.ref, 'refs/tags/') 61 | env: 62 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 63 | run: cargo publish 64 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. MIT license. 2 | 3 | #![deny(clippy::print_stderr)] 4 | #![deny(clippy::print_stdout)] 5 | 6 | use proc_macro::TokenStream; 7 | use quote::quote; 8 | use syn::parse_macro_input; 9 | use syn::spanned::Spanned; 10 | use syn::Data; 11 | use syn::DeriveInput; 12 | use syn::Error; 13 | use syn::Ident; 14 | use syn::Type; 15 | 16 | #[proc_macro_derive(Boxed)] 17 | pub fn derive_boxed(item: TokenStream) -> TokenStream { 18 | let input = parse_macro_input!(item as DeriveInput); 19 | let error_name = &input.ident; 20 | let field_type = match &input.data { 21 | Data::Struct(data_struct) => { 22 | // Check if the struct has exactly one field 23 | if data_struct.fields.len() != 1 { 24 | let error = Error::new( 25 | error_name.span(), 26 | "Struct must have exactly one field of type `Box`", 27 | ); 28 | return TokenStream::from(error.to_compile_error()); 29 | } 30 | 31 | // Extract the type of the single field 32 | let field = data_struct.fields.iter().next().unwrap(); 33 | &field.ty 34 | } 35 | _ => { 36 | let error = Error::new( 37 | error_name.span(), 38 | "Boxed can only be derived on structs with a single field", 39 | ); 40 | return TokenStream::from(error.to_compile_error()); 41 | } 42 | }; 43 | let inner_type = match field_type { 44 | Type::Path(type_path) => { 45 | if type_path.path.segments.len() == 1 46 | && type_path.path.segments[0].ident == "Box" 47 | { 48 | // Extract the inner type from `Box` 49 | match &type_path.path.segments[0].arguments { 50 | syn::PathArguments::AngleBracketed(args) => { 51 | if args.args.len() == 1 { 52 | if let syn::GenericArgument::Type(inner) = &args.args[0] { 53 | inner 54 | } else { 55 | let error = Error::new( 56 | field_type.span(), 57 | "Expected a single generic argument in `Box`", 58 | ); 59 | return TokenStream::from(error.to_compile_error()); 60 | } 61 | } else { 62 | let error = Error::new( 63 | field_type.span(), 64 | "Expected a single generic argument in `Box`", 65 | ); 66 | return TokenStream::from(error.to_compile_error()); 67 | } 68 | } 69 | _ => { 70 | let error = Error::new( 71 | field_type.span(), 72 | "Expected angle-bracketed arguments in `Box`", 73 | ); 74 | return TokenStream::from(error.to_compile_error()); 75 | } 76 | } 77 | } else { 78 | let error = 79 | Error::new(field_type.span(), "Field must be of type `Box`"); 80 | return TokenStream::from(error.to_compile_error()); 81 | } 82 | } 83 | _ => { 84 | let error = 85 | Error::new(field_type.span(), "Field must be of type `Box`"); 86 | return TokenStream::from(error.to_compile_error()); 87 | } 88 | }; 89 | let inner_name = match inner_type { 90 | Type::Path(type_path) => &type_path.path.segments[0].ident, 91 | _ => { 92 | let error = 93 | Error::new(inner_type.span(), "Expected an identifier in `Box`"); 94 | return TokenStream::from(error.to_compile_error()); 95 | } 96 | }; 97 | let inner_name_str = inner_name.to_string(); 98 | let expected_suffix = if inner_name_str.ends_with("Kind") { 99 | "kind" 100 | } else if inner_name_str.ends_with("Data") { 101 | "data" 102 | } else { 103 | "inner" 104 | }; 105 | 106 | let as_name = 107 | Ident::new(&format!("as_{}", expected_suffix), error_name.span()); 108 | let into_name = 109 | Ident::new(&format!("into_{}", expected_suffix), error_name.span()); 110 | 111 | // generate the code for the wrapper struct and implementations 112 | let expanded = quote! { 113 | impl #inner_name { 114 | pub fn into_box(self) -> #error_name { 115 | #error_name(Box::new(self)) 116 | } 117 | } 118 | 119 | impl std::fmt::Display for #error_name { 120 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 121 | std::fmt::Display::fmt(&self.0, f) 122 | } 123 | } 124 | 125 | impl std::error::Error for #error_name { 126 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 127 | self.0.source() 128 | } 129 | } 130 | 131 | impl std::ops::Deref for #error_name { 132 | type Target = #inner_name; 133 | 134 | fn deref(&self) -> &Self::Target { 135 | &self.0 136 | } 137 | } 138 | 139 | impl #error_name { 140 | pub fn #as_name(&self) -> &#inner_name { 141 | &self.0 142 | } 143 | 144 | pub fn #into_name(self) -> #inner_name { 145 | *self.0 146 | } 147 | } 148 | 149 | // implement conversion from other errors into the boxed error 150 | impl From for #error_name 151 | where 152 | #inner_name: From, 153 | { 154 | fn from(err: E) -> Self { 155 | #error_name(Box::new(#inner_name::from(err))) 156 | } 157 | } 158 | }; 159 | 160 | TokenStream::from(expanded) 161 | } 162 | --------------------------------------------------------------------------------