thin-status 0.1.5

Low-overhead, production-grade error status type for Rust, heavily inspired by Abseil's absl::Status
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
// Copyright 2026 <https://github.com/ppetr/>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[cfg(feature = "use_libc")]
use libc;
use std::fmt::Display;
use std::num::NonZeroI32;
use strum;

/// `errno` constants that the `libc` crate does not expose on every target
/// (many `E*` values are Linux-specific, and Windows only defines a small
/// subset).
///
/// Where the real constant is missing we substitute a unique negative sentinel.
/// Real `errno` values are positive, so the affected match arms in
/// [`ErrorCode::from_errno`] simply never match on those targets.
#[cfg(feature = "use_libc")]
mod compat_errno {
    use libc::c_int;

    /// Present only on Linux / Android.
    macro_rules! linux_errno {
        ($($name:ident = $sentinel:literal,)*) => {$(
            #[cfg(any(target_os = "linux", target_os = "android"))]
            pub const $name: c_int = libc::$name;
            #[cfg(not(any(target_os = "linux", target_os = "android")))]
            pub const $name: c_int = -$sentinel;
        )*};
    }

    /// Present on Unix-like targets but not on Windows.
    macro_rules! unix_errno {
        ($($name:ident = $sentinel:literal,)*) => {$(
            #[cfg(unix)]
            pub const $name: c_int = libc::$name;
            #[cfg(not(unix))]
            pub const $name: c_int = -$sentinel;
        )*};
    }

    linux_errno! {
        ENOMEDIUM = 1001,
        ENOTUNIQ = 1002,
        ENOKEY = 1003,
        EBADFD = 1004,
        EISNAM = 1005,
        EUNATCH = 1006,
        ECHRNG = 1007,
        ENOPKG = 1008,
        ECOMM = 1009,
        ENONET = 1010,
    }

    unix_errno! {
        ENOTBLK = 1101,
        ESHUTDOWN = 1102,
        EDQUOT = 1103,
        EUSERS = 1104,
        EPFNOSUPPORT = 1105,
        ESOCKTNOSUPPORT = 1106,
        EHOSTDOWN = 1107,
        ESTALE = 1108,
    }
}

/// Derived from <https://github.com/abseil/abseil-cpp/blob/master/absl/status/status.h>. See the
/// link for more information.
///
/// (Copyright 2019 The Abseil Authors.)
///
/// Unlike `absl::StatusCode`, this enum only allows representing non-OK values.
#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    strum::EnumString,
    strum::EnumIter,
    strum::FromRepr,
    strum::IntoStaticStr,
    strum::VariantArray,
)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[repr(i32)]
#[non_exhaustive]
pub enum ErrorCode {
    /// `Cancelled` (gRPC code "CANCELLED") indicates the operation was cancelled, typically by the
    /// caller.
    Cancelled = 1,

    /// `Unknown` (gRPC code "UNKNOWN") indicates an unknown error occurred. In general, more
    /// specific errors should be raised, if possible. Errors raised by APIs that do not return
    /// enough error information may be converted to this error.
    Unknown = 2,

    /// `InvalidArgument` (gRPC code "INVALID_ARGUMENT") indicates the caller specified an invalid
    /// argument, such as a malformed filename. Note that use of such errors should be narrowly
    /// limited to indicate the invalid nature of the arguments themselves. Errors with validly
    /// formed arguments that may cause errors with the state of the receiving system should be
    /// denoted with `FailedPrecondition` instead.
    InvalidArgument = 3,

    /// `DeadlineExceeded` (gRPC code "DEADLINE_EXCEEDED") indicates a deadline expired before the
    /// operation could complete. For operations that may change state within a system, this error
    /// may be returned even if the operation has completed successfully. For example, a successful
    /// response from a server could have been delayed long enough for the deadline to expire.
    DeadlineExceeded = 4,

    /// `NotFound` (gRPC code "NOT_FOUND") indicates some requested entity (such as a file or
    /// directory) was not found.
    ///
    /// `NotFound` is useful if a request should be denied for an entire class of users, such as
    /// during a gradual feature rollout or undocumented allow list. If a request should be denied
    /// for specific sets of users, such as through user-based access control, use
    /// `PermissionDenied` instead.
    NotFound = 5,

    /// `AlreadyExists` (gRPC code "ALREADY_EXISTS") indicates that the entity a caller attempted to
    /// create (such as a file or directory) is already present.
    AlreadyExists = 6,

    /// `PermissionDenied` (gRPC code "PERMISSION_DENIED") indicates that the caller does not have
    /// permission to execute the specified operation. Note that this error is different than an
    /// error due to an *un*authenticated user. This error code does not imply the request is valid
    /// or the requested entity exists or satisfies any other pre-conditions.
    ///
    /// `PermissionDenied` must not be used for rejections caused by exhausting some resource.
    /// Instead, use `ResourceExhausted` for those errors. `PermissionDenied` must not be used if
    /// the caller cannot be identified. Instead, use `Unauthenticated` for those errors.
    PermissionDenied = 7,

    /// `ResourceExhausted` (gRPC code "RESOURCE_EXHAUSTED") indicates some resource has been
    /// exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space.
    ResourceExhausted = 8,

    /// `FailedPrecondition` (gRPC code "FAILED_PRECONDITION") indicates that the operation was
    /// rejected because the system is not in a state required for the operation's execution. For
    /// example, a directory to be deleted may be non-empty, an "rmdir" operation is applied to a
    /// non-directory, etc.
    ///
    /// Some guidelines that may help a service implementer in deciding between
    /// `FailedPrecondition`, `Aborted`, and `Unavailable`:
    ///
    ///   1. Use `Unavailable` if the client can retry just the failing call.
    ///   2. Use `Aborted` if the client should retry at a higher transaction level (such as when a
    ///      client-specified test-and-set fails, indicating the client should restart a
    ///      read-modify-write sequence).
    ///   3. Use `FailedPrecondition` if the client should not retry until the system state has
    ///      been explicitly fixed. For example, if a "rmdir" fails because the directory is
    ///      non-empty, `FailedPrecondition` should be returned since the client should not retry
    ///      unless the files are deleted from the directory.
    FailedPrecondition = 9,

    /// `Aborted` (gRPC code "ABORTED") indicates the operation was aborted, typically due to a
    /// concurrency issue such as a sequencer check failure or a failed transaction.
    ///
    /// See the guidelines above for deciding between `FailedPrecondition`, `Aborted`, and
    /// `Unavailable`.
    Aborted = 10,

    /// `OutOfRange` (gRPC code "OUT_OF_RANGE") indicates the operation was attempted past the valid
    /// range, such as seeking or reading past an end-of-file.
    ///
    /// Unlike `InvalidArgument`, this error indicates a problem that may be fixed if the system
    /// state changes. For example, a 32-bit file system will generate `InvalidArgument` if asked
    /// to read at an offset that is not in the range [0,2^32-1], but it will generate `OutOfRange`
    /// if asked to read from an offset past the current file size.
    ///
    /// There is a fair bit of overlap between `FailedPrecondition` and `OutOfRange`.  We recommend
    /// using `OutOfRange` (the more specific error) when it applies so that callers who are
    /// iterating through a space can easily look for an `OutOfRange` error to detect when they are
    /// done.
    OutOfRange = 11,

    /// `Unimplemented` (gRPC code "UNIMPLEMENTED") indicates the operation is not implemented or
    /// supported in this service. In this case, the operation should not be re-attempted.
    Unimplemented = 12,

    /// `Internal` (gRPC code "INTERNAL") indicates an internal error has occurred and some
    /// invariants expected by the underlying system have not been satisfied. This error code is
    /// reserved for serious errors.
    Internal = 13,

    /// `Unavailable` (gRPC code "UNAVAILABLE") indicates the service is currently unavailable and
    /// that this is most likely a transient condition. An error such as this can be corrected by
    /// retrying with a backoff scheme. Note that it is not always safe to retry non-idempotent
    /// operations.
    ///
    /// See the guidelines above for deciding between `FailedPrecondition`, `Aborted`, and
    /// `Unavailable`.
    Unavailable = 14,

    /// `DataLoss` (gRPC code "DATA_LOSS") indicates that unrecoverable data loss or corruption has
    /// occurred. As this error is serious, proper alerting should be attached to errors such as
    /// this.
    DataLoss = 15,

    /// `Unauthenticated` (gRPC code "UNAUTHENTICATED") indicates that the request does not have
    /// valid authentication credentials for the operation. Correct the authentication and try
    /// again.
    Unauthenticated = 16,
}

impl ErrorCode {
    /// Adapted from `ErrnoToStatusCode` in
    /// <https://github.com/abseil/abseil-cpp/blob/master/absl/status/status.cc>
    #[cfg(feature = "use_libc")]
    pub fn from_errno(errno: i32) -> Option<ErrorCode> {
        match errno {
            0 => None,
            libc::EINVAL |        // Invalid argument
                libc::ENAMETOOLONG |  // Filename too long
                libc::E2BIG |         // Argument list too long
                libc::EDESTADDRREQ |  // Destination address required
                libc::EDOM |          // Mathematics argument out of domain of function
                libc::EFAULT |        // Bad address
                libc::EILSEQ |        // Illegal byte sequence
                libc::ENOPROTOOPT |   // Protocol not available
                libc::ENOTSOCK |      // Not a socket
                libc::ENOTTY |        // Inappropriate I/O control operation
                libc::EPROTOTYPE |    // Protocol wrong type for socket
                libc::ESPIPE =>       // Invalid seek
                Some(Self::InvalidArgument),
            libc::ETIMEDOUT => // Connection timed out
                Some(Self::DeadlineExceeded),
            libc::ENODEV |  // No such device
                libc::ENOENT |  // No such file or directory
                compat_errno::ENOMEDIUM |  // No medium found
                libc::ENXIO |  // No such device or address
                libc::ESRCH => // No such process
                Some(Self::NotFound),
            libc::EEXIST |         // File exists
                libc::EADDRNOTAVAIL |  // Address not available
                libc::EALREADY |       // Connection already in progress
                compat_errno::ENOTUNIQ => // Name not unique on network
                Some(Self::AlreadyExists),
            libc::EPERM |   // Operation not permitted
                libc::EACCES |  // Permission denied
                compat_errno::ENOKEY |  // Required key not available
                libc::EROFS => // Read only file system
                Some(Self::PermissionDenied),
            libc::ENOTEMPTY |   // Directory not empty
                libc::EISDIR |      // Is a directory
                libc::ENOTDIR |     // Not a directory
                libc::EADDRINUSE |  // Address already in use
                libc::EBADF |       // Invalid file descriptor
                compat_errno::EBADFD |  // File descriptor in bad state
                libc::EBUSY |    // Device or resource busy
                libc::ECHILD |   // No child processes
                libc::EISCONN |  // Socket is connected
                compat_errno::EISNAM |  // Is a named type file
                compat_errno::ENOTBLK |  // Block device required
                libc::ENOTCONN |  // The socket is not connected
                libc::EPIPE |     // Broken pipe
                compat_errno::ESHUTDOWN |  // Cannot send after transport endpoint shutdown
                libc::ETXTBSY |  // Text file busy
                compat_errno::EUNATCH => // Protocol driver not attached
                Some(Self::FailedPrecondition),
            libc::ENOSPC |  // No space left on device
                compat_errno::EDQUOT |  // Disk quota exceeded
                libc::EMFILE |   // Too many open files
                libc::EMLINK |   // Too many links
                libc::ENFILE |   // Too many open files in system
                libc::ENOBUFS |  // No buffer space available
                libc::ENOMEM |   // Not enough space
                compat_errno::EUSERS => // Too many users
                Some(Self::ResourceExhausted),
            compat_errno::ECHRNG |  // Channel number out of range
                libc::EFBIG |      // File too large
                libc::EOVERFLOW |  // Value too large to be stored in data type
                libc::ERANGE =>    // Result too large
                Some(Self::OutOfRange),
            compat_errno::ENOPKG |  // Package not installed
                libc::ENOSYS |        // Function not implemented
                libc::ENOTSUP |       // Operation not supported
                libc::EAFNOSUPPORT |  // Address family not supported
                compat_errno::EPFNOSUPPORT |  // Protocol family not supported
                libc::EPROTONOSUPPORT |  // Protocol not supported
                compat_errno::ESOCKTNOSUPPORT |  // Socket type not supported
                libc::EXDEV => // Improper link
                Some(Self::Unimplemented),
            libc::EAGAIN |  // Resource temporarily unavailable
                compat_errno::ECOMM |  // Communication error on send
                libc::ECONNREFUSED |  // Connection refused
                libc::ECONNABORTED |  // Connection aborted
                libc::ECONNRESET |    // Connection reset
                libc::EINTR |         // Interrupted function call
                compat_errno::EHOSTDOWN |  // Host is down
                libc::EHOSTUNREACH |  // Host is unreachable
                libc::ENETDOWN |      // Network is down
                libc::ENETRESET |     // Connection aborted by network
                libc::ENETUNREACH |   // Network unreachable
                libc::ENOLCK |        // No locks available
                libc::ENOLINK |       // Link has been severed
                compat_errno::ENONET => // Machine is not on the network
                Some(Self::Unavailable),
            libc::EDEADLK |  // Resource deadlock avoided
                compat_errno::ESTALE => // Stale file handle
                Some(Self::Aborted),
            libc::ECANCELED =>  // Operation cancelled
                Some(Self::Cancelled),
            _ => Some(Self::Unknown),
        }
    }

    pub fn from_error_kind(kind: std::io::ErrorKind) -> ErrorCode {
        use std::io::ErrorKind::*;
        match kind {
            NotFound => ErrorCode::NotFound,

            PermissionDenied => ErrorCode::PermissionDenied,

            AlreadyExists => ErrorCode::AlreadyExists,

            InvalidInput | ArgumentListTooLong => ErrorCode::InvalidArgument,

            TimedOut => ErrorCode::DeadlineExceeded,

            StorageFull | QuotaExceeded | OutOfMemory => ErrorCode::ResourceExhausted,

            DirectoryNotEmpty | IsADirectory | NotADirectory | NotConnected | AddrInUse
            | ResourceBusy | ExecutableFileBusy | ReadOnlyFilesystem // | FilesystemLoop
            | CrossesDevices | NotSeekable => ErrorCode::FailedPrecondition,

            FileTooLarge => ErrorCode::OutOfRange,

            Unsupported => ErrorCode::Unimplemented,

            WouldBlock | Interrupted | BrokenPipe | ConnectionRefused | ConnectionReset
            | ConnectionAborted | HostUnreachable | NetworkUnreachable | NetworkDown => {
                ErrorCode::Unavailable
            }

            Deadlock | StaleNetworkFileHandle => ErrorCode::Aborted,

            InvalidData | WriteZero | UnexpectedEof => ErrorCode::DataLoss,

            Other => ErrorCode::Unknown,

            _ => ErrorCode::Unknown,
        }
    }

    /// If `err` contains a `raw_os_error()`, returns it converted into an `ErrorCode`.
    /// Otherwise returns `None`.
    #[cfg(feature = "use_libc")]
    pub fn from_raw_os_error(err: &std::io::Error) -> Option<ErrorCode> {
        err.raw_os_error().and_then(Self::from_errno)
    }
}

impl From<ErrorCode> for NonZeroI32 {
    fn from(code: ErrorCode) -> Self {
        NonZeroI32::new(code as i32).expect(&format!(
            "The enum value of an ErrorCode must be nonzero, but got '{:?}'",
            code
        ))
    }
}

/// If a value matches one of the defined error codes, returns it as an `ErrorCode`, otherwise
/// returns a `()` error.
///
/// For converting from strings, use the `std::str::FromStr` instance.
impl TryFrom<i32> for ErrorCode {
    type Error = ();

    fn try_from(code: i32) -> Result<Self, ()> {
        Self::from_repr(code).ok_or(())
    }
}

/// If the `alternate` flag is set, prints the error code as a number, otherwise as text.
impl Display for ErrorCode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if f.alternate() {
            write!(f, "{}", *self as i32)
        } else {
            f.write_str(self.into())
        }
    }
}

#[cfg(test)]
mod thin_status_tests {
    use super::*;
    use anyhow;

    #[cfg(feature = "use_libc")]
    #[test]
    fn test_cloud_rpc_status_conversions() {
        assert_eq!(ErrorCode::from_errno(0), None);
        assert_eq!(
            ErrorCode::from_errno(libc::ENOENT),
            Some(ErrorCode::NotFound)
        );
    }

    #[test]
    fn test_to_string() {
        assert_eq!(format!("{}", ErrorCode::NotFound), "NOT_FOUND");
        assert_eq!(format!("{:#}", ErrorCode::NotFound), "5");
    }

    #[test]
    fn test_anyhow_context() {
        let err = anyhow::anyhow!("test error").context(ErrorCode::PermissionDenied);
        assert_eq!(
            err.downcast_ref::<ErrorCode>(),
            Some(&ErrorCode::PermissionDenied)
        );
    }
}