Skip to main content

canic_core/dto/
error.rs

1use crate::{InternalError, access::AccessError, dto::prelude::*};
2use std::fmt::{self, Display};
3
4///
5/// Error
6///
7/// Public-facing error DTO returned across the canister API boundary.
8/// Encodes a stable error code and a human-readable message.
9///
10
11#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
12pub struct Error {
13    pub code: ErrorCode,
14    pub message: String,
15}
16
17impl Display for Error {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        write!(f, "[{:?}] {}", self.code, self.message)
20    }
21}
22
23impl Error {
24    #[must_use]
25    pub const fn new(code: ErrorCode, message: String) -> Self {
26        Self { code, message }
27    }
28
29    /// 409 – Conflict with existing state or resource.
30    pub fn conflict(message: impl Into<String>) -> Self {
31        Self::new(ErrorCode::Conflict, message.into())
32    }
33
34    /// 409 – Policy violation with a stable policy-specific code.
35    pub fn policy(code: ErrorCode, message: impl Into<String>) -> Self {
36        Self::new(code, message.into())
37    }
38
39    /// 403 – Authenticated caller is not permitted to perform this action.
40    pub fn forbidden(message: impl Into<String>) -> Self {
41        Self::new(ErrorCode::Forbidden, message.into())
42    }
43
44    /// 500 – Internal or unexpected failure.
45    pub fn internal(message: impl Into<String>) -> Self {
46        Self::new(ErrorCode::Internal, message.into())
47    }
48
49    /// 400 – Invalid input or malformed request.
50    pub fn invalid(message: impl Into<String>) -> Self {
51        Self::new(ErrorCode::InvalidInput, message.into())
52    }
53
54    /// 500 – Broken invariant or impossible internal state.
55    pub fn invariant(message: impl Into<String>) -> Self {
56        Self::new(ErrorCode::InvariantViolation, message.into())
57    }
58
59    /// 429 / 507 – Resource, quota, or capacity exhaustion.
60    pub fn exhausted(message: impl Into<String>) -> Self {
61        Self::new(ErrorCode::ResourceExhausted, message.into())
62    }
63
64    /// 404 – Requested resource was not found.
65    pub fn not_found(message: impl Into<String>) -> Self {
66        Self::new(ErrorCode::NotFound, message.into())
67    }
68
69    /// 401 – Caller is unauthenticated or has an invalid identity.
70    pub fn unauthorized(message: impl Into<String>) -> Self {
71        Self::new(ErrorCode::Unauthorized, message.into())
72    }
73}
74
75impl From<AccessError> for Error {
76    fn from(err: AccessError) -> Self {
77        Self::from(InternalError::from(err))
78    }
79}
80
81///
82/// ErrorCode
83///
84/// Stable public error codes returned by the API.
85/// New variants may be added in the future; consumers must handle unknown values.
86///
87
88#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
89#[non_exhaustive]
90#[remain::sorted]
91pub enum ErrorCode {
92    Conflict,
93    Forbidden,
94    Internal,
95    InvalidInput,
96    InvariantViolation,
97    NotFound,
98    PolicyReplicaRequiresSingletonWithScaling,
99    PolicyRoleAlreadyRegistered,
100    PolicyShardRequiresSingletonWithSharding,
101    PolicySingletonAlreadyRegisteredUnderParent,
102    PolicyTenantRequiresSingletonParent,
103    ResourceExhausted,
104    Unauthorized,
105}