twurst_error/
lib.rs

1#![doc = include_str!("../README.md")]
2#![doc(
3    test(attr(deny(warnings))),
4    html_favicon_url = "https://raw.githubusercontent.com/helsing-ai/twurst/main/docs/img/twurst.png",
5    html_logo_url = "https://raw.githubusercontent.com/helsing-ai/twurst/main/docs/img/twurst.png"
6)]
7#![cfg_attr(docsrs, feature(doc_auto_cfg))]
8
9use std::collections::HashMap;
10use std::error::Error;
11use std::fmt;
12use std::sync::Arc;
13
14/// A Twirp [error](https://twitchtv.github.io/twirp/docs/spec_v7.html#errors)
15///
16/// It is composed of three elements:
17/// - An error `code` that is member of a fixed list [`TwirpErrorCode`]
18/// - A human error `message` describing the error as a string
19/// - A set of "`meta`" key-value pairs as strings holding arbitrary metadata describing the error.
20///
21/// ```
22/// # use twurst_error::{TwirpError, TwirpErrorCode};
23/// let error = TwirpError::not_found("Object foo not found").with_meta("id", "foo");
24/// assert_eq!(error.code(), TwirpErrorCode::NotFound);
25/// assert_eq!(error.message(), "Object foo not found");
26/// assert_eq!(error.meta("id"), Some("foo"));
27/// ```
28#[derive(Clone, Debug)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct TwirpError {
31    /// The [error code](https://twitchtv.github.io/twirp/docs/spec_v7.html#error-codes)
32    code: TwirpErrorCode,
33    /// The error message (human description of the error)
34    msg: String,
35    /// Some metadata describing the error
36    #[cfg_attr(
37        feature = "serde",
38        serde(default, skip_serializing_if = "HashMap::is_empty")
39    )]
40    meta: HashMap<String, String>,
41    #[cfg_attr(feature = "serde", serde(default, skip))]
42    source: Option<Arc<dyn Error + Send + Sync>>,
43}
44
45impl TwirpError {
46    #[inline]
47    pub fn code(&self) -> TwirpErrorCode {
48        self.code
49    }
50
51    #[inline]
52    pub fn message(&self) -> &str {
53        &self.msg
54    }
55
56    #[inline]
57    pub fn into_message(self) -> String {
58        self.msg
59    }
60
61    /// Get an associated metadata
62    #[inline]
63    pub fn meta(&self, key: &str) -> Option<&str> {
64        self.meta.get(key).map(|s| s.as_str())
65    }
66
67    /// Get all associated metadata
68    #[inline]
69    pub fn meta_iter(&self) -> impl Iterator<Item = (&str, &str)> {
70        self.meta.iter().map(|(k, v)| (k.as_str(), v.as_str()))
71    }
72
73    #[inline]
74    pub fn new(code: TwirpErrorCode, msg: impl Into<String>) -> Self {
75        Self {
76            code,
77            msg: msg.into(),
78            meta: HashMap::new(),
79            source: None,
80        }
81    }
82
83    #[inline]
84    pub fn wrap(
85        code: TwirpErrorCode,
86        msg: impl Into<String>,
87        e: impl Error + Send + Sync + 'static,
88    ) -> Self {
89        Self {
90            code,
91            msg: msg.into(),
92            meta: HashMap::new(),
93            source: Some(Arc::new(e)),
94        }
95    }
96
97    /// Set an associated metadata
98    #[inline]
99    pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
100        self.meta.insert(key.into(), value.into());
101        self
102    }
103
104    #[inline]
105    pub fn aborted(msg: impl Into<String>) -> Self {
106        Self::new(TwirpErrorCode::Aborted, msg)
107    }
108
109    #[inline]
110    pub fn already_exists(msg: impl Into<String>) -> Self {
111        Self::new(TwirpErrorCode::AlreadyExists, msg)
112    }
113
114    #[inline]
115    pub fn canceled(msg: impl Into<String>) -> Self {
116        Self::new(TwirpErrorCode::Canceled, msg)
117    }
118
119    #[inline]
120    pub fn dataloss(msg: impl Into<String>) -> Self {
121        Self::new(TwirpErrorCode::Dataloss, msg)
122    }
123
124    #[inline]
125    pub fn invalid_argument(msg: impl Into<String>) -> Self {
126        Self::new(TwirpErrorCode::InvalidArgument, msg)
127    }
128
129    #[inline]
130    pub fn internal(msg: impl Into<String>) -> Self {
131        Self::new(TwirpErrorCode::Internal, msg)
132    }
133
134    #[inline]
135    pub fn deadline_exceeded(msg: impl Into<String>) -> Self {
136        Self::new(TwirpErrorCode::DeadlineExceeded, msg)
137    }
138
139    #[inline]
140    pub fn failed_precondition(msg: impl Into<String>) -> Self {
141        Self::new(TwirpErrorCode::FailedPrecondition, msg)
142    }
143
144    #[inline]
145    pub fn malformed(msg: impl Into<String>) -> Self {
146        Self::new(TwirpErrorCode::Malformed, msg)
147    }
148
149    #[inline]
150    pub fn not_found(msg: impl Into<String>) -> Self {
151        Self::new(TwirpErrorCode::NotFound, msg)
152    }
153
154    #[inline]
155    pub fn out_of_range(msg: impl Into<String>) -> Self {
156        Self::new(TwirpErrorCode::OutOfRange, msg)
157    }
158
159    #[inline]
160    pub fn permission_denied(msg: impl Into<String>) -> Self {
161        Self::new(TwirpErrorCode::PermissionDenied, msg)
162    }
163
164    #[inline]
165    pub fn required_argument(msg: impl Into<String>) -> Self {
166        Self::invalid_argument(msg)
167    }
168
169    #[inline]
170    pub fn resource_exhausted(msg: impl Into<String>) -> Self {
171        Self::new(TwirpErrorCode::ResourceExhausted, msg)
172    }
173
174    #[inline]
175    pub fn unauthenticated(msg: impl Into<String>) -> Self {
176        Self::new(TwirpErrorCode::Unauthenticated, msg)
177    }
178
179    #[inline]
180    pub fn unavailable(msg: impl Into<String>) -> Self {
181        Self::new(TwirpErrorCode::Unavailable, msg)
182    }
183
184    #[inline]
185    pub fn unimplemented(msg: impl Into<String>) -> Self {
186        Self::new(TwirpErrorCode::Unimplemented, msg)
187    }
188}
189
190impl fmt::Display for TwirpError {
191    #[inline]
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        write!(f, "Twirp {:?} error: {}", self.code, self.msg)
194    }
195}
196
197impl Error for TwirpError {
198    #[inline]
199    fn source(&self) -> Option<&(dyn Error + 'static)> {
200        Some(self.source.as_ref()?)
201    }
202}
203
204impl PartialEq for TwirpError {
205    #[inline]
206    fn eq(&self, other: &Self) -> bool {
207        self.code == other.code && self.msg == other.msg && self.meta == other.meta
208    }
209}
210
211impl Eq for TwirpError {}
212
213/// A Twirp [error code](https://twitchtv.github.io/twirp/docs/spec_v7.html#error-codes)
214#[derive(Clone, Copy, Debug, PartialEq, Eq)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
216#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
217pub enum TwirpErrorCode {
218    /// The operation was cancelled.
219    Canceled,
220    /// An unknown error occurred. For example, this can be used when handling errors raised by APIs that do not return any error information.
221    Unknown,
222    /// The client specified an invalid argument. This indicates arguments that are invalid regardless of the state of the system (i.e. a malformed file name, required argument, number out of range, etc.).
223    InvalidArgument,
224    /// The client sent a message which could not be decoded. This may mean that the message was encoded improperly or that the client and server have incompatible message definitions.
225    Malformed,
226    /// Operation expired before completion. For operations that change the state of the system, this error may be returned even if the operation has completed successfully (timeout).
227    DeadlineExceeded,
228    /// Some requested entity was not found.
229    NotFound,
230    /// The requested URL path wasn't routable to a Twirp service and method. This is returned by generated server code and should not be returned by application code (use "not_found" or "unimplemented" instead).
231    BadRoute,
232    /// An attempt to create an entity failed because one already exists.
233    AlreadyExists,
234    /// The caller does not have permission to execute the specified operation. It must not be used if the caller cannot be identified (use "unauthenticated" instead).
235    PermissionDenied,
236    /// The request does not have valid authentication credentials for the operation.
237    Unauthenticated,
238    /// Some resource has been exhausted or rate-limited, perhaps a per-user quota, or perhaps the entire file system is out of space.
239    ResourceExhausted,
240    /// The operation was rejected because the system is not in a state required for the operation's execution. For example, doing an rmdir operation on a directory that is non-empty, or on a non-directory object, or when having conflicting read-modify-write on the same resource.
241    FailedPrecondition,
242    /// The operation was aborted, typically due to a concurrency issue like sequencer check failures, transaction aborts, etc.
243    Aborted,
244    /// The operation was attempted past the valid range. For example, seeking or reading past end of a paginated collection. Unlike "invalid_argument", this error indicates a problem that may be fixed if the system state changes (i.e. adding more items to the collection). There is a fair bit of overlap between "failed_precondition" and "out_of_range". We recommend using "out_of_range" (the more specific error) when it applies so that callers who are iterating through a space can easily look for an "out_of_range" error to detect when they are done.
245    OutOfRange,
246    /// The operation is not implemented or not supported/enabled in this service.
247    Unimplemented,
248    /// When some invariants expected by the underlying system have been broken. In other words, something bad happened in the library or backend service. Twirp specific issues like wire and serialization problems are also reported as "internal" errors.
249    Internal,
250    /// The service is currently unavailable. This is most likely a transient condition and may be corrected by retrying with a backoff.
251    Unavailable,
252    /// The operation resulted in unrecoverable data loss or corruption.
253    Dataloss,
254}
255
256/// Applies the mapping defined in [Twirp spec](https://twitchtv.github.io/twirp/docs/spec_v7.html#error-codes)
257#[cfg(feature = "http")]
258impl From<TwirpErrorCode> for http::StatusCode {
259    #[inline]
260    fn from(code: TwirpErrorCode) -> Self {
261        match code {
262            TwirpErrorCode::Canceled => Self::REQUEST_TIMEOUT,
263            TwirpErrorCode::Unknown => Self::INTERNAL_SERVER_ERROR,
264            TwirpErrorCode::InvalidArgument => Self::BAD_REQUEST,
265            TwirpErrorCode::Malformed => Self::BAD_REQUEST,
266            TwirpErrorCode::DeadlineExceeded => Self::REQUEST_TIMEOUT,
267            TwirpErrorCode::NotFound => Self::NOT_FOUND,
268            TwirpErrorCode::BadRoute => Self::NOT_FOUND,
269            TwirpErrorCode::AlreadyExists => Self::CONFLICT,
270            TwirpErrorCode::PermissionDenied => Self::FORBIDDEN,
271            TwirpErrorCode::Unauthenticated => Self::UNAUTHORIZED,
272            TwirpErrorCode::ResourceExhausted => Self::TOO_MANY_REQUESTS,
273            TwirpErrorCode::FailedPrecondition => Self::PRECONDITION_FAILED,
274            TwirpErrorCode::Aborted => Self::CONFLICT,
275            TwirpErrorCode::OutOfRange => Self::BAD_REQUEST,
276            TwirpErrorCode::Unimplemented => Self::NOT_IMPLEMENTED,
277            TwirpErrorCode::Internal => Self::INTERNAL_SERVER_ERROR,
278            TwirpErrorCode::Unavailable => Self::SERVICE_UNAVAILABLE,
279            TwirpErrorCode::Dataloss => Self::SERVICE_UNAVAILABLE,
280        }
281    }
282}
283
284#[cfg(feature = "http")]
285impl<B: From<String>> From<TwirpError> for http::Response<B> {
286    fn from(error: TwirpError) -> Self {
287        let json = serde_json::to_string(&error).unwrap();
288        http::Response::builder()
289            .status(error.code)
290            .header(http::header::CONTENT_TYPE, "application/json")
291            .extension(error)
292            .body(json.into())
293            .unwrap()
294    }
295}
296
297#[cfg(feature = "http")]
298impl<B: AsRef<[u8]>> From<http::Response<B>> for TwirpError {
299    fn from(response: http::Response<B>) -> Self {
300        if let Some(error) = response.extensions().get::<Self>() {
301            // We got a ready to use error in the extensions, let's use it
302            return error.clone();
303        }
304        // We are lenient here, a bad error is better than no error at all
305        let status = response.status();
306        let body = response.into_body();
307        if let Ok(error) = serde_json::from_slice::<TwirpError>(body.as_ref()) {
308            // The body is an error, we use it
309            return error;
310        }
311        // We don't have a Twirp error, we build a fallback
312        let code = if status == http::StatusCode::REQUEST_TIMEOUT {
313            TwirpErrorCode::DeadlineExceeded
314        } else if status == http::StatusCode::FORBIDDEN {
315            TwirpErrorCode::PermissionDenied
316        } else if status == http::StatusCode::UNAUTHORIZED {
317            TwirpErrorCode::Unauthenticated
318        } else if status == http::StatusCode::TOO_MANY_REQUESTS {
319            TwirpErrorCode::ResourceExhausted
320        } else if status == http::StatusCode::PRECONDITION_FAILED {
321            TwirpErrorCode::FailedPrecondition
322        } else if status == http::StatusCode::NOT_IMPLEMENTED {
323            TwirpErrorCode::Unimplemented
324        } else if status == http::StatusCode::TOO_MANY_REQUESTS
325            || status == http::StatusCode::BAD_GATEWAY
326            || status == http::StatusCode::SERVICE_UNAVAILABLE
327            || status == http::StatusCode::GATEWAY_TIMEOUT
328        {
329            TwirpErrorCode::Unavailable
330        } else if status == http::StatusCode::NOT_FOUND {
331            TwirpErrorCode::NotFound
332        } else if status.is_server_error() {
333            TwirpErrorCode::Internal
334        } else if status.is_client_error() {
335            TwirpErrorCode::Malformed
336        } else {
337            TwirpErrorCode::Unknown
338        };
339        TwirpError::new(code, String::from_utf8_lossy(body.as_ref()))
340    }
341}
342
343#[cfg(feature = "axum-08")]
344impl axum_core_05::response::IntoResponse for TwirpError {
345    #[inline]
346    fn into_response(self) -> axum_core_05::response::Response {
347        self.into()
348    }
349}
350
351#[cfg(feature = "tonic-012")]
352impl From<TwirpErrorCode> for tonic_012::Code {
353    #[inline]
354    fn from(code: TwirpErrorCode) -> Self {
355        match code {
356            TwirpErrorCode::Canceled => Self::Cancelled,
357            TwirpErrorCode::Unknown => Self::Unknown,
358            TwirpErrorCode::InvalidArgument => Self::InvalidArgument,
359            TwirpErrorCode::Malformed => Self::InvalidArgument,
360            TwirpErrorCode::DeadlineExceeded => Self::DeadlineExceeded,
361            TwirpErrorCode::NotFound => Self::NotFound,
362            TwirpErrorCode::BadRoute => Self::NotFound,
363            TwirpErrorCode::AlreadyExists => Self::AlreadyExists,
364            TwirpErrorCode::PermissionDenied => Self::PermissionDenied,
365            TwirpErrorCode::Unauthenticated => Self::Unauthenticated,
366            TwirpErrorCode::ResourceExhausted => Self::ResourceExhausted,
367            TwirpErrorCode::FailedPrecondition => Self::FailedPrecondition,
368            TwirpErrorCode::Aborted => Self::Aborted,
369            TwirpErrorCode::OutOfRange => Self::OutOfRange,
370            TwirpErrorCode::Unimplemented => Self::Unimplemented,
371            TwirpErrorCode::Internal => Self::Internal,
372            TwirpErrorCode::Unavailable => Self::Unavailable,
373            TwirpErrorCode::Dataloss => Self::DataLoss,
374        }
375    }
376}
377
378#[cfg(feature = "tonic-012")]
379impl From<TwirpError> for tonic_012::Status {
380    #[inline]
381    fn from(error: TwirpError) -> Self {
382        if let Some(source) = &error.source {
383            if let Some(status) = source.downcast_ref::<tonic_012::Status>() {
384                if status.code() == error.code().into() && status.message() == error.message() {
385                    // This is a status wrapped as a Twirp error, we reuse the status to keep the details
386                    return status.clone();
387                }
388            }
389        }
390        Self::new(error.code().into(), error.into_message())
391    }
392}
393
394#[cfg(feature = "tonic-012")]
395impl From<tonic_012::Code> for TwirpErrorCode {
396    #[inline]
397    fn from(code: tonic_012::Code) -> TwirpErrorCode {
398        match code {
399            tonic_012::Code::Cancelled => Self::Canceled,
400            tonic_012::Code::Unknown => Self::Unknown,
401            tonic_012::Code::InvalidArgument => Self::InvalidArgument,
402            tonic_012::Code::DeadlineExceeded => Self::DeadlineExceeded,
403            tonic_012::Code::NotFound => Self::NotFound,
404            tonic_012::Code::AlreadyExists => Self::AlreadyExists,
405            tonic_012::Code::PermissionDenied => Self::PermissionDenied,
406            tonic_012::Code::Unauthenticated => Self::Unauthenticated,
407            tonic_012::Code::ResourceExhausted => Self::ResourceExhausted,
408            tonic_012::Code::FailedPrecondition => Self::FailedPrecondition,
409            tonic_012::Code::Aborted => Self::Aborted,
410            tonic_012::Code::OutOfRange => Self::OutOfRange,
411            tonic_012::Code::Unimplemented => Self::Unimplemented,
412            tonic_012::Code::Internal => Self::Internal,
413            tonic_012::Code::Unavailable => Self::Unavailable,
414            tonic_012::Code::DataLoss => Self::Dataloss,
415            tonic_012::Code::Ok => Self::Unknown,
416        }
417    }
418}
419
420#[cfg(feature = "tonic-012")]
421impl From<tonic_012::Status> for TwirpError {
422    #[inline]
423    fn from(status: tonic_012::Status) -> TwirpError {
424        Self::wrap(status.code().into(), status.message().to_string(), status)
425    }
426}
427
428#[cfg(feature = "tonic-013")]
429impl From<TwirpErrorCode> for tonic_013::Code {
430    #[inline]
431    fn from(code: TwirpErrorCode) -> Self {
432        match code {
433            TwirpErrorCode::Canceled => Self::Cancelled,
434            TwirpErrorCode::Unknown => Self::Unknown,
435            TwirpErrorCode::InvalidArgument => Self::InvalidArgument,
436            TwirpErrorCode::Malformed => Self::InvalidArgument,
437            TwirpErrorCode::DeadlineExceeded => Self::DeadlineExceeded,
438            TwirpErrorCode::NotFound => Self::NotFound,
439            TwirpErrorCode::BadRoute => Self::NotFound,
440            TwirpErrorCode::AlreadyExists => Self::AlreadyExists,
441            TwirpErrorCode::PermissionDenied => Self::PermissionDenied,
442            TwirpErrorCode::Unauthenticated => Self::Unauthenticated,
443            TwirpErrorCode::ResourceExhausted => Self::ResourceExhausted,
444            TwirpErrorCode::FailedPrecondition => Self::FailedPrecondition,
445            TwirpErrorCode::Aborted => Self::Aborted,
446            TwirpErrorCode::OutOfRange => Self::OutOfRange,
447            TwirpErrorCode::Unimplemented => Self::Unimplemented,
448            TwirpErrorCode::Internal => Self::Internal,
449            TwirpErrorCode::Unavailable => Self::Unavailable,
450            TwirpErrorCode::Dataloss => Self::DataLoss,
451        }
452    }
453}
454
455#[cfg(feature = "tonic-013")]
456impl From<TwirpError> for tonic_013::Status {
457    #[inline]
458    fn from(error: TwirpError) -> Self {
459        if let Some(source) = &error.source {
460            if let Some(status) = source.downcast_ref::<tonic_013::Status>() {
461                if status.code() == error.code().into() && status.message() == error.message() {
462                    // This is a status wrapped as a Twirp error, we reuse the status to keep the details
463                    return status.clone();
464                }
465            }
466        }
467        Self::new(error.code().into(), error.into_message())
468    }
469}
470
471#[cfg(feature = "tonic-013")]
472impl From<tonic_013::Code> for TwirpErrorCode {
473    #[inline]
474    fn from(code: tonic_013::Code) -> TwirpErrorCode {
475        match code {
476            tonic_013::Code::Cancelled => Self::Canceled,
477            tonic_013::Code::Unknown => Self::Unknown,
478            tonic_013::Code::InvalidArgument => Self::InvalidArgument,
479            tonic_013::Code::DeadlineExceeded => Self::DeadlineExceeded,
480            tonic_013::Code::NotFound => Self::NotFound,
481            tonic_013::Code::AlreadyExists => Self::AlreadyExists,
482            tonic_013::Code::PermissionDenied => Self::PermissionDenied,
483            tonic_013::Code::Unauthenticated => Self::Unauthenticated,
484            tonic_013::Code::ResourceExhausted => Self::ResourceExhausted,
485            tonic_013::Code::FailedPrecondition => Self::FailedPrecondition,
486            tonic_013::Code::Aborted => Self::Aborted,
487            tonic_013::Code::OutOfRange => Self::OutOfRange,
488            tonic_013::Code::Unimplemented => Self::Unimplemented,
489            tonic_013::Code::Internal => Self::Internal,
490            tonic_013::Code::Unavailable => Self::Unavailable,
491            tonic_013::Code::DataLoss => Self::Dataloss,
492            tonic_013::Code::Ok => Self::Unknown,
493        }
494    }
495}
496
497#[cfg(feature = "tonic-013")]
498impl From<tonic_013::Status> for TwirpError {
499    #[inline]
500    fn from(status: tonic_013::Status) -> TwirpError {
501        Self::wrap(status.code().into(), status.message().to_string(), status)
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    #[cfg(feature = "http")]
509    use std::error::Error;
510
511    #[test]
512    fn test_accessors() {
513        let error = TwirpError::invalid_argument("foo is wrong").with_meta("foo", "bar");
514        assert_eq!(error.code(), TwirpErrorCode::InvalidArgument);
515        assert_eq!(error.message(), "foo is wrong");
516        assert_eq!(error.meta("foo"), Some("bar"));
517    }
518
519    #[cfg(feature = "http")]
520    #[test]
521    fn test_to_response() -> Result<(), Box<dyn Error>> {
522        let object =
523            TwirpError::permission_denied("Thou shall not pass").with_meta("target", "Balrog");
524        let response = http::Response::<Vec<u8>>::from(object);
525        assert_eq!(response.status(), http::StatusCode::FORBIDDEN);
526        assert_eq!(
527            response.headers().get(http::header::CONTENT_TYPE),
528            Some(&http::HeaderValue::from_static("application/json"))
529        );
530        assert_eq!(
531            response.into_body(), b"{\"code\":\"permission_denied\",\"msg\":\"Thou shall not pass\",\"meta\":{\"target\":\"Balrog\"}}"
532        );
533        Ok(())
534    }
535
536    #[cfg(feature = "http")]
537    #[test]
538    fn test_from_valid_response() -> Result<(), Box<dyn Error>> {
539        let response = http::Response::builder()
540            .header(http::header::CONTENT_TYPE, "application/json")
541            .body("{\"code\":\"permission_denied\",\"msg\":\"Thou shall not pass\",\"meta\":{\"target\":\"Balrog\"}}")?;
542        assert_eq!(
543            TwirpError::from(response),
544            TwirpError::permission_denied("Thou shall not pass").with_meta("target", "Balrog")
545        );
546        Ok(())
547    }
548
549    #[cfg(feature = "http")]
550    #[test]
551    fn test_from_plain_response() -> Result<(), Box<dyn Error>> {
552        let response = http::Response::builder()
553            .status(http::StatusCode::FORBIDDEN)
554            .body("Thou shall not pass")?;
555        assert_eq!(
556            TwirpError::from(response),
557            TwirpError::permission_denied("Thou shall not pass")
558        );
559        Ok(())
560    }
561
562    #[cfg(feature = "tonic-012")]
563    #[test]
564    fn test_from_tonic_012_status_simple() {
565        assert_eq!(
566            TwirpError::from(tonic_012::Status::not_found("Not found")),
567            TwirpError::not_found("Not found")
568        );
569    }
570
571    #[cfg(feature = "tonic-012")]
572    #[test]
573    fn test_to_tonic_012_status_simple() {
574        let error = TwirpError::not_found("Not found");
575        let status = tonic_012::Status::from(error);
576        assert_eq!(status.code(), tonic_012::Code::NotFound);
577        assert_eq!(status.message(), "Not found");
578    }
579
580    #[cfg(feature = "tonic-012")]
581    #[test]
582    fn test_from_to_tonic_012_status_roundtrip() {
583        let status = tonic_012::Status::with_details(
584            tonic_012::Code::NotFound,
585            "Not found",
586            b"some_dummy_details".to_vec().into(),
587        );
588        let new_status = tonic_012::Status::from(TwirpError::from(status.clone()));
589        assert_eq!(status.code(), new_status.code());
590        assert_eq!(status.message(), new_status.message());
591        assert_eq!(status.details(), new_status.details());
592    }
593
594    #[cfg(feature = "tonic-013")]
595    #[test]
596    fn test_from_tonic_013_status_simple() {
597        assert_eq!(
598            TwirpError::from(tonic_013::Status::not_found("Not found")),
599            TwirpError::not_found("Not found")
600        );
601    }
602
603    #[cfg(feature = "tonic-013")]
604    #[test]
605    fn test_to_tonic_013_status_simple() {
606        let error = TwirpError::not_found("Not found");
607        let status = tonic_013::Status::from(error);
608        assert_eq!(status.code(), tonic_013::Code::NotFound);
609        assert_eq!(status.message(), "Not found");
610    }
611
612    #[cfg(feature = "tonic-013")]
613    #[test]
614    fn test_from_to_tonic_013_status_roundtrip() {
615        let status = tonic_013::Status::with_details(
616            tonic_013::Code::NotFound,
617            "Not found",
618            b"some_dummy_details".to_vec().into(),
619        );
620        let new_status = tonic_013::Status::from(TwirpError::from(status.clone()));
621        assert_eq!(status.code(), new_status.code());
622        assert_eq!(status.message(), new_status.message());
623        assert_eq!(status.details(), new_status.details());
624    }
625}