Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 81 additions & 2 deletions errlanes-derive/src/rejection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,17 @@ fn doc_summary(attrs: &[syn::Attribute]) -> Option<String> {
(!lines.is_empty()).then(|| lines.join(" "))
}

struct SharedCode {
code: String,
level: TokenStream,
description: TokenStream,
}

impl RejectionVariant {
fn takes_own_code(&self) -> bool {
self.code.is_some() || self.delegate || self.code_and_level_from.is_some()
}

fn is_delegating(&self) -> bool {
self.delegate
}
Expand Down Expand Up @@ -296,6 +306,49 @@ pub fn derive(ast: &syn::DeriveInput) -> darling::Result<TokenStream> {
let schema = crate::composition::schema(ast).map_err(darling::Error::from)?;
let error_manual = input.error_manual()?;

let shared = match &input.code {
None => None,
Some(code) => {
if input.code_prefix.is_some() {
return Err(darling::Error::custom(
"`code_prefix` and an enum-level `code` conflict: the prefix is for \
per-variant generated codes, a shared code is spelled in full",
)
.with_span(&input.ident));
}
if variants.is_empty() {
return Err(darling::Error::custom(
"an enum-level `code` on an enum without variants: an empty family has no \
rejection to report",
)
.with_span(&input.ident));
}
if variants.iter().all(|v| v.takes_own_code()) {
return Err(darling::Error::custom(
"the enum-level `code` is unused: every variant declares its own `code`, \
`code_and_level_from` or `delegate`",
)
.with_span(&input.ident));
}
if let Some(clash) = variants
.iter()
.find(|v| v.takes_own_code() && v.ident == *ident)
{
return Err(darling::Error::custom(
"a variant that declares its own code cannot be named like its enum: the \
shared code's variant in the code type is named after the enum",
)
.with_span(&clash.ident));
}
Comment thread
cursor[bot] marked this conversation as resolved.
Some(SharedCode {
code: code.clone(),
level: level_expr(&input.level, ident)?,
description: description_tokens(&leaf_description(&input.description, &ast.attrs)),
})
}
};
let mut shared_code_variant_emitted = false;

let mut code_variants = Vec::new();
let mut into_str_arms = Vec::new();
let mut code_match_arms = Vec::new();
Expand Down Expand Up @@ -416,7 +469,8 @@ pub fn derive(ast: &syn::DeriveInput) -> darling::Result<TokenStream> {
let mut whole_value_source: Option<Path> = None;
// Lift owns conversion generation. Its mapping also identifies the
// default metadata source; no Lift implementation is required here.
if code_and_level_from.is_none() && v.code.is_none() && !v.delegate {
let takes_shared = shared.is_some() && !v.takes_own_code();
if code_and_level_from.is_none() && v.code.is_none() && !v.delegate && !takes_shared {
let mappings: Vec<_> = raw
.attrs
.iter()
Expand Down Expand Up @@ -473,7 +527,32 @@ pub fn derive(ast: &syn::DeriveInput) -> darling::Result<TokenStream> {
))
.with_span(&v.ident)
};
let (code, level, variant_codes) = if let Some(mut source) = code_and_level_from {
let (code, level, variant_codes) = if let (true, Some(shared)) = (takes_shared, &shared) {
if v.description.is_some() {
return Err(darling::Error::custom(
"one code, one description: this variant takes the enum's shared `code`, \
so its description is the enum-level one",
)
.with_span(&v.ident));
}
if !shared_code_variant_emitted {
shared_code_variant_emitted = true;
code_variants.push(quote!(#ident));
let shared_code = &shared.code;
into_str_arms.push(quote!(#code_ident::#ident => #shared_code));
}
let shared_code = &shared.code;
let description = &shared.description;
(
quote!(#code_ident::#ident),
if v.level.is_some() {
v.level_expr()
} else {
shared.level.clone()
},
quote!(&[errlanes::CodeInfo { code: #shared_code, description: #description }]),
)
} else if let Some(mut source) = code_and_level_from {
if v.description.is_some() {
return Err(forwards_to(&quote!(#source)));
}
Expand Down
5 changes: 5 additions & 0 deletions errlanes-derive/tests/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ fn ui() {
t.pass("tests/trybuild/rejection_pass.rs");
t.pass("tests/trybuild/rejection_multi_lift.rs");
t.pass("tests/trybuild/error_format_args_pass.rs");
t.compile_fail("tests/trybuild/shared_code_unused.rs");
t.compile_fail("tests/trybuild/shared_code_variant_description.rs");
t.compile_fail("tests/trybuild/shared_code_with_prefix.rs");
t.compile_fail("tests/trybuild/shared_code_empty_enum.rs");
t.compile_fail("tests/trybuild/shared_code_own_code_named_like_enum.rs");
t.compile_fail("tests/trybuild/error_format_args_unknown_field.rs");
t.compile_fail("tests/trybuild/error_format_args_arbitrary_expr.rs");
t.compile_fail("tests/trybuild/error_format_args_bare_auto_index_still_rejected.rs");
Expand Down
5 changes: 5 additions & 0 deletions errlanes-derive/tests/trybuild/shared_code_empty_enum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[derive(errlanes::Rejection)]
#[rejection(code = "SHARED")]
enum Empty {}

fn main() {}
5 changes: 5 additions & 0 deletions errlanes-derive/tests/trybuild/shared_code_empty_enum.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: an enum-level `code` on an enum without variants: an empty family has no rejection to report
--> tests/trybuild/shared_code_empty_enum.rs:3:6
|
3 | enum Empty {}
| ^^^^^
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#[derive(Debug, errlanes::Rejection)]
#[rejection(code = "SHARED")]
enum Clash {
#[rejection(code = "OWN")]
Clash,
Other,
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: a variant that declares its own code cannot be named like its enum: the shared code's variant in the code type is named after the enum
--> tests/trybuild/shared_code_own_code_named_like_enum.rs:5:5
|
5 | Clash,
| ^^^^^
10 changes: 10 additions & 0 deletions errlanes-derive/tests/trybuild/shared_code_unused.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#[derive(Debug, errlanes::Rejection)]
#[rejection(code = "SHARED")]
enum Unused {
#[rejection(code = "A")]
A,
#[rejection(code = "B")]
B,
}

fn main() {}
5 changes: 5 additions & 0 deletions errlanes-derive/tests/trybuild/shared_code_unused.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: the enum-level `code` is unused: every variant declares its own `code`, `code_and_level_from` or `delegate`
--> tests/trybuild/shared_code_unused.rs:3:6
|
3 | enum Unused {
| ^^^^^^
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#[derive(Debug, errlanes::Rejection)]
#[rejection(code = "SHARED", description = "shared")]
enum Described {
#[rejection(description = "mine")]
A,
B,
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: one code, one description: this variant takes the enum's shared `code`, so its description is the enum-level one
--> tests/trybuild/shared_code_variant_description.rs:5:5
|
5 | A,
| ^
8 changes: 8 additions & 0 deletions errlanes-derive/tests/trybuild/shared_code_with_prefix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[derive(Debug, errlanes::Rejection)]
#[rejection(code = "SHARED", code_prefix = "P_")]
enum Prefixed {
A,
B,
}

fn main() {}
5 changes: 5 additions & 0 deletions errlanes-derive/tests/trybuild/shared_code_with_prefix.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: `code_prefix` and an enum-level `code` conflict: the prefix is for per-variant generated codes, a shared code is spelled in full
--> tests/trybuild/shared_code_with_prefix.rs:3:6
|
3 | enum Prefixed {
| ^^^^^^^^
98 changes: 98 additions & 0 deletions errlanes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,104 @@ assert!(matches!(Settlement::lift(exceeded), Ok(Settlement::BelowFloor(3))));
assert!(Settlement::lift(Batch::Apply(Apply::Locked)).is_err());
```

### One code, several causes

`#[rejection(code = "..", description = "..")]` on an enum is the code (and
description, and `level` if given) of every variant that declares neither its
own `code` nor `delegate`. A cause that the client cannot distinguish is a
variant under the family's code, not a wrapper around the foreign enum.
`#[error]`, `#[source]`, `#[rejection(from)]` and `#[lift]` work on each
variant as before, and a `#[lift]` arm takes the enum's code instead of
forwarding the source case's. The generated code type has one variant per
distinct code, and `CODES` lists each code once.

```rust
use errlanes::{Fail, Rejection, RejectionCode, ResultExt, lanes};

#[derive(Debug, errlanes::Rejection)]
enum Cala {
#[rejection(code = "CALA_DUPLICATE_ID")]
DuplicateId(u64),
#[rejection(code = "CALA_CODE_ALREADY_EXISTS")]
CodeAlreadyExists(String),
#[rejection(code = "CALA_UNRELATED")]
Unrelated,
}

#[derive(Debug, errlanes::Rejection, errlanes::Lift)]
#[rejection(
code = "HOLDING_ACCOUNT_ALREADY_EXISTS",
description = "A holding account with this id or reference already exists."
)]
#[lift(Cala, unhandled = fatal)]
enum CreateAccountRejection {
#[error("holding account id {0} already exists")]
#[lift(Cala::DuplicateId)]
DuplicateId(u64),
#[error("holding account code {0} already exists")]
#[lift(Cala::CodeAlreadyExists)]
CodeAlreadyExists(String),
}

assert_eq!(CreateAccountRejectionCode::CODES.len(), 1);

let duplicate: Result<(), Fail<CreateAccountRejection, lanes!(Fatal)>> =
Err::<(), _>(Cala::CodeAlreadyExists("H1".into())).lift();
let rejected = duplicate.unwrap_err().rejected().unwrap();
assert_eq!(
Into::<&str>::into(rejected.code()),
"HOLDING_ACCOUNT_ALREADY_EXISTS"
);
assert_eq!(rejected.to_string(), "holding account code H1 already exists");
```

Variants that still declare their own `code`, or `delegate`, keep it, so a
family can report one code for most causes and a distinct one for a few. One
code has one description: a variant that takes the shared code cannot declare
its own `description`. An enum-level `code` is a compile error when every
variant declares its own code or delegates, when the enum has no variants, or
next to `code_prefix`.

```rust
use errlanes::{Rejection, RejectionCode};

#[derive(Debug, errlanes::Rejection)]
#[rejection(code = "PARTNER_NOT_ACTIVE")]
#[error("the partner is not active")]
struct PartnerNotActive;

#[derive(Debug, errlanes::Rejection)]
#[rejection(
code = "PARTNER_API_KEY_CREDENTIALS_REJECTED",
description = "The presented API key credentials are not usable."
)]
enum ApiKeyCredentialsRejected {
#[rejection(from)]
#[error("the presented secret is not validly encoded")]
InvalidEncoding(#[source] std::str::Utf8Error),
#[rejection(from)]
#[error("the presented secret has the wrong length")]
InvalidLength(#[source] std::array::TryFromSliceError),
#[error("no api key matches the presented secret")]
Unknown,
#[error("the api key is not usable")]
Inactive,
#[rejection(delegate, from)]
#[error("{0}")]
PartnerNotActive(PartnerNotActive),
}

assert_eq!(ApiKeyCredentialsRejectedCode::CODES.len(), 2);
assert_eq!(
Into::<&str>::into(ApiKeyCredentialsRejected::Unknown.code()),
"PARTNER_API_KEY_CREDENTIALS_REJECTED"
);
assert_eq!(
Into::<&str>::into(ApiKeyCredentialsRejected::from(PartnerNotActive).code()),
"PARTNER_NOT_ACTIVE"
);
```

## Errors from other crates

Every local error type says, through one trait, how it enters the lanes:
Expand Down
Loading
Loading