Skip to content

Commit a664897

Browse files
authored
feat(rust): add thiserror policy lint for error types (#381)
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent a690ea4 commit a664897

26 files changed

Lines changed: 334 additions & 439 deletions

File tree

rsworkspace/Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rsworkspace/crates/a2a-auth-callout/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ rustls-webpki = { workspace = true }
3434
serde = { workspace = true, features = ["derive"] }
3535
serde_json = { workspace = true }
3636
sha2 = { workspace = true }
37+
thiserror = { workspace = true }
3738
time = { workspace = true }
3839
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "sync"] }
3940
tracing = { workspace = true }

rsworkspace/crates/a2a-auth-callout/src/account_resolver.rs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use std::collections::BTreeSet;
2-
use std::fmt;
32
use std::sync::Arc;
43

54
use crate::error::AuthCalloutError;
@@ -22,23 +21,14 @@ impl RequestedAccount {
2221
}
2322
}
2423

25-
#[derive(Debug)]
24+
#[derive(Debug, thiserror::Error)]
2625
pub enum AccountResolverError {
26+
#[error("requested account must be non-empty")]
2727
EmptyRequest,
28+
#[error("requested account {0:?} not allowlisted")]
2829
Unknown(String),
2930
}
3031

31-
impl fmt::Display for AccountResolverError {
32-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33-
match self {
34-
Self::EmptyRequest => f.write_str("requested account must be non-empty"),
35-
Self::Unknown(name) => write!(f, "requested account {name:?} not allowlisted"),
36-
}
37-
}
38-
}
39-
40-
impl std::error::Error for AccountResolverError {}
41-
4232
impl From<AccountResolverError> for AuthCalloutError {
4333
fn from(value: AccountResolverError) -> Self {
4434
// Variant-to-variant routing — no string matching on the failure

rsworkspace/crates/a2a-auth-callout/src/credentials/api_key.rs

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use std::collections::HashMap;
2-
use std::fmt;
32
use std::sync::Arc;
43

54
use hmac::{Hmac, Mac};
@@ -8,42 +7,20 @@ use sha2::Sha256;
87
use crate::error::AuthCalloutError;
98
use crate::jwt::{AudienceAccount, ExternalSubject, JwtError, SpiceDbPrincipal, UserJwtClaims, derive_caller_id};
109

11-
#[derive(Debug)]
10+
#[derive(Debug, thiserror::Error)]
1211
pub enum ApiKeyError {
12+
#[error("API key must not be empty")]
1313
Empty,
14+
#[error("API key not found in registry")]
1415
Unknown,
1516
/// `derive_caller_id` rejected the registry entry's external_subject —
1617
/// preserves the upstream JwtError instead of stringifying it.
17-
CallerIdDerivation(JwtError),
18+
#[error("API key caller_id derivation failed")]
19+
CallerIdDerivation(#[source] JwtError),
1820
/// The caller-supplied audience didn't match the registry entry's
1921
/// audience for this API key.
20-
AudienceMismatch {
21-
requested: String,
22-
registered: String,
23-
},
24-
}
25-
26-
impl fmt::Display for ApiKeyError {
27-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28-
match self {
29-
Self::Empty => f.write_str("API key must not be empty"),
30-
Self::Unknown => f.write_str("API key not found in registry"),
31-
Self::CallerIdDerivation(_) => f.write_str("API key caller_id derivation failed"),
32-
Self::AudienceMismatch { requested, registered } => write!(
33-
f,
34-
"API key audience mismatch: requested={requested:?} registered={registered:?}"
35-
),
36-
}
37-
}
38-
}
39-
40-
impl std::error::Error for ApiKeyError {
41-
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
42-
match self {
43-
Self::CallerIdDerivation(e) => Some(e),
44-
_ => None,
45-
}
46-
}
22+
#[error("API key audience mismatch: requested={requested:?} registered={registered:?}")]
23+
AudienceMismatch { requested: String, registered: String },
4724
}
4825

4926
impl From<ApiKeyError> for AuthCalloutError {

rsworkspace/crates/a2a-auth-callout/src/denial_claims.rs

Lines changed: 12 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::fmt;
21
use std::time::{Duration, SystemTime, UNIX_EPOCH};
32

43
use jsonwebtoken::{Algorithm, Header, encode};
@@ -12,21 +11,12 @@ use crate::jwt::SigningKey;
1211
#[derive(Debug, Clone, PartialEq, Eq)]
1312
pub struct CalloutIssuer(String);
1413

15-
#[derive(Debug, PartialEq, Eq)]
14+
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
1615
pub enum CalloutIssuerError {
16+
#[error("callout issuer must be non-empty")]
1717
Empty,
1818
}
1919

20-
impl fmt::Display for CalloutIssuerError {
21-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22-
match self {
23-
Self::Empty => f.write_str("callout issuer must be non-empty"),
24-
}
25-
}
26-
}
27-
28-
impl std::error::Error for CalloutIssuerError {}
29-
3020
impl CalloutIssuer {
3121
pub fn new(issuer: impl Into<String>) -> Result<Self, CalloutIssuerError> {
3222
let s = issuer.into();
@@ -44,21 +34,12 @@ impl CalloutIssuer {
4434
#[derive(Debug, Clone, PartialEq, Eq)]
4535
pub struct ServerAudience(String);
4636

47-
#[derive(Debug, PartialEq, Eq)]
37+
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
4838
pub enum ServerAudienceError {
39+
#[error("server audience must be non-empty")]
4940
Empty,
5041
}
5142

52-
impl fmt::Display for ServerAudienceError {
53-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54-
match self {
55-
Self::Empty => f.write_str("server audience must be non-empty"),
56-
}
57-
}
58-
}
59-
60-
impl std::error::Error for ServerAudienceError {}
61-
6243
impl ServerAudience {
6344
pub fn new(audience: impl Into<String>) -> Result<Self, ServerAudienceError> {
6445
let s = audience.into();
@@ -76,21 +57,12 @@ impl ServerAudience {
7657
#[derive(Debug, Clone, PartialEq, Eq)]
7758
pub struct UserNkeySubject(String);
7859

79-
#[derive(Debug, PartialEq, Eq)]
60+
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
8061
pub enum UserNkeySubjectError {
62+
#[error("user nkey subject must be non-empty")]
8163
Empty,
8264
}
8365

84-
impl fmt::Display for UserNkeySubjectError {
85-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86-
match self {
87-
Self::Empty => f.write_str("user nkey subject must be non-empty"),
88-
}
89-
}
90-
}
91-
92-
impl std::error::Error for UserNkeySubjectError {}
93-
9466
impl UserNkeySubject {
9567
pub fn new(subject: impl Into<String>) -> Result<Self, UserNkeySubjectError> {
9668
let s = subject.into();
@@ -114,33 +86,16 @@ pub struct DenialClaims {
11486
pub request_jti: Option<String>,
11587
}
11688

117-
#[derive(Debug)]
89+
#[derive(Debug, thiserror::Error)]
11890
pub enum DenialClaimsError {
119-
Encode(jsonwebtoken::errors::Error),
120-
SystemTime(std::time::SystemTimeError),
91+
#[error("denial JWT encode error: {0}")]
92+
Encode(#[source] jsonwebtoken::errors::Error),
93+
#[error("system time error: {0}")]
94+
SystemTime(#[source] std::time::SystemTimeError),
95+
#[error("issued-at timestamp out of portable range")]
12196
IssuedAtOutOfRange,
12297
}
12398

124-
impl fmt::Display for DenialClaimsError {
125-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126-
match self {
127-
Self::Encode(e) => write!(f, "denial JWT encode error: {e}"),
128-
Self::SystemTime(e) => write!(f, "system time error: {e}"),
129-
Self::IssuedAtOutOfRange => f.write_str("issued-at timestamp out of portable range"),
130-
}
131-
}
132-
}
133-
134-
impl std::error::Error for DenialClaimsError {
135-
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
136-
match self {
137-
Self::Encode(e) => Some(e),
138-
Self::SystemTime(e) => Some(e),
139-
_ => None,
140-
}
141-
}
142-
}
143-
14499
impl From<DenialClaimsError> for AuthCalloutError {
145100
fn from(value: DenialClaimsError) -> Self {
146101
// Denial-claims minting failures aren't JwtError — they're this

rsworkspace/crates/a2a-auth-callout/src/denial_reason.rs

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::fmt;
2-
31
use crate::denial_category::DenialCategory;
42

53
const MAX_LEN: usize = 256;
@@ -8,23 +6,14 @@ const MAX_LEN: usize = 256;
86
#[derive(Debug, Clone, PartialEq, Eq)]
97
pub struct DenialReason(String);
108

11-
#[derive(Debug, PartialEq, Eq)]
9+
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
1210
pub enum DenialReasonError {
11+
#[error("denial reason must be non-empty")]
1312
Empty,
13+
#[error("denial reason must be at most {MAX_LEN} characters")]
1414
TooLong,
1515
}
1616

17-
impl fmt::Display for DenialReasonError {
18-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19-
match self {
20-
Self::Empty => f.write_str("denial reason must be non-empty"),
21-
Self::TooLong => write!(f, "denial reason must be at most {MAX_LEN} characters"),
22-
}
23-
}
24-
}
25-
26-
impl std::error::Error for DenialReasonError {}
27-
2817
impl DenialReason {
2918
pub fn new(category: DenialCategory) -> Result<Self, DenialReasonError> {
3019
Self::from_wire(category.as_str())

0 commit comments

Comments
 (0)