Skip to main content

canic_core/api/
error.rs

1use crate::{
2    InternalError, InternalErrorClass, InternalErrorOrigin,
3    dto::error::{Error, ErrorCode},
4};
5
6fn registry_policy_error_code(message: &str) -> Option<ErrorCode> {
7    if message.contains("already registered to") {
8        return Some(ErrorCode::PolicyRoleAlreadyRegistered);
9    }
10    if message.contains("already registered under parent") {
11        return Some(ErrorCode::PolicySingletonAlreadyRegisteredUnderParent);
12    }
13    // These wire codes keep their pre-0.64 names for compatibility; the
14    // policy messages now describe service-owned manager pools.
15    if message.contains("must be created by a service parent with scaling config") {
16        return Some(ErrorCode::PolicyReplicaRequiresSingletonWithScaling);
17    }
18    if message.contains("must be created by a service parent with sharding config") {
19        return Some(ErrorCode::PolicyShardRequiresSingletonWithSharding);
20    }
21    if message.contains("must be created by a service parent with directory config") {
22        return Some(ErrorCode::PolicyInstanceRequiresSingletonWithDirectory);
23    }
24
25    None
26}
27
28fn internal_error_to_public(err: &InternalError) -> Error {
29    if let Some(public) = err.public_error() {
30        return public.clone();
31    }
32
33    let message = err.to_string();
34
35    match err.class() {
36        InternalErrorClass::Access => Error::unauthorized(message),
37
38        InternalErrorClass::Domain => match err.origin() {
39            InternalErrorOrigin::Config => Error::invalid(message),
40            _ => {
41                if let Some(code) = registry_policy_error_code(&message) {
42                    Error::policy(code, message)
43                } else {
44                    Error::conflict(message)
45                }
46            }
47        },
48
49        InternalErrorClass::Invariant => Error::invariant(message),
50
51        InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
52            Error::internal(message)
53        }
54    }
55}
56
57impl From<&InternalError> for Error {
58    fn from(err: &InternalError) -> Self {
59        internal_error_to_public(err)
60    }
61}
62
63impl From<InternalError> for Error {
64    fn from(err: InternalError) -> Self {
65        internal_error_to_public(&err)
66    }
67}
68
69///
70/// TESTS
71///
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::{
77        access::AccessError,
78        cdk::types::Principal,
79        domain::policy::topology::{TopologyPolicyError, registry::RegistryPolicyError},
80        ids::CanisterRole,
81    };
82
83    fn p(id: u8) -> Principal {
84        Principal::from_slice(&[id; 29])
85    }
86
87    #[test]
88    fn internal_error_mapping_matches_class_contract() {
89        let access: Error = InternalError::from(AccessError::Denied("denied".to_string())).into();
90        assert_eq!(access.code, ErrorCode::Unauthorized);
91
92        let domain_config: Error =
93            InternalError::domain(InternalErrorOrigin::Config, "bad config").into();
94        assert_eq!(domain_config.code, ErrorCode::InvalidInput);
95
96        let domain_other: Error =
97            InternalError::domain(InternalErrorOrigin::Domain, "conflict").into();
98        assert_eq!(domain_other.code, ErrorCode::Conflict);
99
100        let invariant: Error =
101            InternalError::invariant(InternalErrorOrigin::Ops, "broken invariant").into();
102        assert_eq!(invariant.code, ErrorCode::InvariantViolation);
103
104        let infra: Error = InternalError::infra(InternalErrorOrigin::Infra, "infra fail").into();
105        assert_eq!(infra.code, ErrorCode::Internal);
106
107        let ops: Error = InternalError::ops(InternalErrorOrigin::Ops, "ops fail").into();
108        assert_eq!(ops.code, ErrorCode::Internal);
109
110        let workflow: Error =
111            InternalError::workflow(InternalErrorOrigin::Workflow, "workflow fail").into();
112        assert_eq!(workflow.code, ErrorCode::Internal);
113    }
114
115    #[test]
116    fn public_error_is_preserved_without_remap() {
117        let public = Error::not_found("missing");
118        let remapped: Error = InternalError::public(public.clone()).into();
119        assert_eq!(remapped, public);
120    }
121
122    #[test]
123    fn registry_policy_errors_map_to_stable_public_policy_codes() {
124        let err = RegistryPolicyError::RoleAlreadyRegistered {
125            role: CanisterRole::new("app"),
126            pid: p(7),
127        };
128        let internal: InternalError = TopologyPolicyError::from(err).into();
129        let public: Error = internal.into();
130        assert_eq!(public.code, ErrorCode::PolicyRoleAlreadyRegistered);
131    }
132}