Skip to main content

cortexkit_bus_trait/
error.rs

1use std::fmt;
2use std::time::Duration;
3
4use async_trait::async_trait;
5
6pub type BusResult<T> = Result<T, BusError>;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum BusError {
10    Unavailable {
11        reason: String,
12        exhausted_limit: Option<String>,
13    },
14    Denied {
15        subject: String,
16        reason: String,
17    },
18    Absent {
19        resource: String,
20    },
21    Clamped {
22        clamp: String,
23        retry_after: Option<Duration>,
24    },
25}
26
27impl BusError {
28    pub fn unavailable(reason: impl Into<String>) -> Self {
29        Self::Unavailable {
30            reason: reason.into(),
31            exhausted_limit: None,
32        }
33    }
34
35    pub fn unavailable_limit(limit: impl Into<String>, reason: impl Into<String>) -> Self {
36        Self::Unavailable {
37            reason: reason.into(),
38            exhausted_limit: Some(limit.into()),
39        }
40    }
41
42    pub fn denied(subject: impl Into<String>, reason: impl Into<String>) -> Self {
43        Self::Denied {
44            subject: subject.into(),
45            reason: reason.into(),
46        }
47    }
48
49    pub fn absent(resource: impl Into<String>) -> Self {
50        Self::Absent {
51            resource: resource.into(),
52        }
53    }
54
55    pub fn clamped(clamp: impl Into<String>, retry_after: Option<Duration>) -> Self {
56        Self::Clamped {
57            clamp: clamp.into(),
58            retry_after,
59        }
60    }
61}
62
63impl fmt::Display for BusError {
64    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            Self::Unavailable {
67                reason,
68                exhausted_limit,
69            } => match exhausted_limit {
70                Some(limit) => write!(formatter, "unavailable ({limit} exhausted): {reason}"),
71                None => write!(formatter, "unavailable: {reason}"),
72            },
73            Self::Denied { subject, reason } => {
74                write!(formatter, "denied for subject {subject}: {reason}")
75            }
76            Self::Absent { resource } => write!(formatter, "absent: {resource}"),
77            Self::Clamped { clamp, retry_after } => match retry_after {
78                Some(delay) => write!(formatter, "clamped ({clamp}); retry after {delay:?}"),
79                None => write!(formatter, "clamped: {clamp}"),
80            },
81        }
82    }
83}
84
85impl std::error::Error for BusError {}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum ConnectionFailure {
89    ServerClosedThenReauthenticationRefused { subject: String, reason: String },
90    LostWithoutRefusal { reason: String },
91    WriteOnSeveredSocket,
92}
93
94pub fn map_connection_failure(failure: ConnectionFailure) -> BusError {
95    match failure {
96        ConnectionFailure::ServerClosedThenReauthenticationRefused { subject, reason } => {
97            BusError::denied(subject, reason)
98        }
99        ConnectionFailure::LostWithoutRefusal { reason } => BusError::unavailable(reason),
100        ConnectionFailure::WriteOnSeveredSocket => {
101            BusError::unavailable("write attempted on a severed connection")
102        }
103    }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct AsyncPermissionViolation {
108    pub subject: String,
109    pub reason: String,
110}
111
112impl AsyncPermissionViolation {
113    pub fn as_denied(&self) -> BusError {
114        BusError::denied(self.subject.clone(), self.reason.clone())
115    }
116}
117
118/// Reports a permission violation observed during the request instead of its less-specific timeout.
119pub fn attribute_sentinel_failure(
120    requester_error: BusError,
121    permission_violation: Option<&AsyncPermissionViolation>,
122) -> BusError {
123    permission_violation
124        .map(AsyncPermissionViolation::as_denied)
125        .unwrap_or(requester_error)
126}
127
128#[async_trait]
129pub trait PermissionViolationSource: Send {
130    async fn next_violation(&mut self) -> BusResult<Option<AsyncPermissionViolation>>;
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum CredentialSignFailure {
135    RecordMissing { record: String },
136    AuthorizationRejected { record: String, reason: String },
137}
138
139pub fn map_credential_sign_failure(failure: CredentialSignFailure) -> BusError {
140    match failure {
141        CredentialSignFailure::RecordMissing { record } => {
142            BusError::absent(format!("credential signing record {record}"))
143        }
144        CredentialSignFailure::AuthorizationRejected { record, reason } => {
145            BusError::denied(record, reason)
146        }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn connection_boundary_uses_following_refusal_not_socket_timing() {
156        assert!(matches!(
157            map_connection_failure(ConnectionFailure::ServerClosedThenReauthenticationRefused {
158                subject: "ck.a.room".into(),
159                reason: "grant revoked".into(),
160            }),
161            BusError::Denied { .. }
162        ));
163        assert!(matches!(
164            map_connection_failure(ConnectionFailure::LostWithoutRefusal {
165                reason: "connection reset".into(),
166            }),
167            BusError::Unavailable { .. }
168        ));
169        assert!(matches!(
170            map_connection_failure(ConnectionFailure::WriteOnSeveredSocket),
171            BusError::Unavailable { .. }
172        ));
173    }
174
175    #[test]
176    fn asynchronous_permission_violation_names_offending_subject() {
177        let error = AsyncPermissionViolation {
178            subject: "ck.account.denied".into(),
179            reason: "publish permission violation".into(),
180        }
181        .as_denied();
182
183        assert!(matches!(
184            error,
185            BusError::Denied { subject, .. } if subject == "ck.account.denied"
186        ));
187    }
188
189    #[test]
190    fn signing_against_deleted_record_is_absent() {
191        let error = map_credential_sign_failure(CredentialSignFailure::RecordMissing {
192            record: "spawn/7".into(),
193        });
194
195        assert!(matches!(error, BusError::Absent { .. }));
196    }
197
198    #[test]
199    fn sentinel_permission_violation_outranks_request_timeout() {
200        let violation = AsyncPermissionViolation {
201            subject: "_INBOX.UABC.reply".into(),
202            reason: "publish permission violation".into(),
203        };
204        let attributed = attribute_sentinel_failure(
205            BusError::unavailable("request timed out"),
206            Some(&violation),
207        );
208
209        assert!(matches!(
210            attributed,
211            BusError::Denied { subject, .. } if subject == "_INBOX.UABC.reply"
212        ));
213        assert!(matches!(
214            attribute_sentinel_failure(BusError::unavailable("request timed out"), None),
215            BusError::Unavailable { .. }
216        ));
217    }
218}