File size: 5,006 Bytes
c01fd1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// Copyright (C) 2026 ERRANT DIGITAL INSTITUTE OF TECHNOLOGY (SEIT NGO) % SNAPKITTY COLLECTIVE
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// See LICENSE-AGPL and LICENSE-COMMERCIAL in this repository for license terms.

//! The shared error type returned by every `cloud-forge` crate.
//!
//! Rather than let each crate define its own error enum (and force
//! every caller composing several primitives to write a conversion
//! layer between them), every crate in this workspace returns
//! [`CloudError`] directly. Its variants are deliberately structural
//! (an operation kind plus the specific values involved) rather than
//! one variant per crate, so adding a new primitive crate never
//! requires adding a new variant here.
#![forbid(unsafe_code)]

use std::fmt;

/// A structured error shared across every `cloud-forge` crate.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CloudError {
    /// A value failed validation when constructing a typed wrapper
    /// (e.g. an `AccountId` that isn't 12 ASCII digits).
    InvalidFormat {
        what: &'static str,
        value: String,
        reason: String,
    },
    /// A state machine was asked to move from `from` to `to`, and that
    /// transition is not in its allowed set.
    InvalidTransition {
        what: &'static str,
        from: String,
        to: String,
    },
    /// A quota's limit would be exceeded by the requested amount.
    QuotaExceeded {
        resource: String,
        limit: u64,
        requested: u64,
    },
    /// A policy evaluation denied an action (either no rule allowed
    /// it, or an explicit deny matched).
    PolicyDenied {
        principal: String,
        action: String,
        resource: String,
    },
    /// A lookup by id found nothing.
    NotFound { what: &'static str, id: String },
    /// An operation conflicts with existing state (e.g. a duplicate
    /// resource id).
    Conflict { what: &'static str, id: String },
}

impl fmt::Display for CloudError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CloudError::InvalidFormat {
                what,
                value,
                reason,
            } => {
                write!(f, "invalid {what} {value:?}: {reason}")
            }
            CloudError::InvalidTransition { what, from, to } => {
                write!(f, "invalid {what} transition: {from} -> {to}")
            }
            CloudError::QuotaExceeded {
                resource,
                limit,
                requested,
            } => write!(
                f,
                "quota exceeded for {resource}: requested {requested}, limit {limit}"
            ),
            CloudError::PolicyDenied {
                principal,
                action,
                resource,
            } => write!(f, "policy denied: {principal} may not {action} {resource}"),
            CloudError::NotFound { what, id } => write!(f, "{what} not found: {id}"),
            CloudError::Conflict { what, id } => write!(f, "{what} conflict: {id}"),
        }
    }
}

impl std::error::Error for CloudError {}

pub type CloudResult<T> = Result<T, CloudError>;

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

    #[test]
    fn invalid_format_display_includes_what_value_and_reason() {
        let e = CloudError::InvalidFormat {
            what: "account id",
            value: "abc".to_string(),
            reason: "must be 12 ASCII digits".to_string(),
        };
        let s = e.to_string();
        assert!(s.contains("account id"));
        assert!(s.contains("abc"));
        assert!(s.contains("12 ASCII digits"));
    }

    #[test]
    fn quota_exceeded_display_includes_all_three_numbers() {
        let e = CloudError::QuotaExceeded {
            resource: "instances".to_string(),
            limit: 5,
            requested: 6,
        };
        let s = e.to_string();
        assert!(s.contains("instances"));
        assert!(s.contains('5'));
        assert!(s.contains('6'));
    }

    #[test]
    fn policy_denied_display_reads_as_a_sentence() {
        let e = CloudError::PolicyDenied {
            principal: "user/alice".to_string(),
            action: "delete".to_string(),
            resource: "instance/i-1".to_string(),
        };
        assert_eq!(
            e.to_string(),
            "policy denied: user/alice may not delete instance/i-1"
        );
    }

    #[test]
    fn errors_implement_std_error() {
        fn assert_is_error<E: std::error::Error>(_: &E) {}
        let e = CloudError::NotFound {
            what: "resource",
            id: "r-1".to_string(),
        };
        assert_is_error(&e);
    }

    #[test]
    fn equal_errors_compare_equal() {
        let a = CloudError::Conflict {
            what: "resource",
            id: "r-1".to_string(),
        };
        let b = CloudError::Conflict {
            what: "resource",
            id: "r-1".to_string(),
        };
        assert_eq!(a, b);
    }
}