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    if message.contains("must be created by a service parent with scaling config") {
14        return Some(ErrorCode::PolicyReplicaRequiresServiceWithScaling);
15    }
16    if message.contains("must be created by a service parent with sharding config") {
17        return Some(ErrorCode::PolicyShardRequiresServiceWithSharding);
18    }
19    if message.contains("must be created by a service parent with directory config") {
20        return Some(ErrorCode::PolicyInstanceRequiresServiceWithDirectory);
21    }
22
23    None
24}
25
26fn internal_error_to_public(err: &InternalError) -> Error {
27    if let Some(public) = err.public_error() {
28        return public.clone();
29    }
30
31    let message = err.to_string();
32
33    match err.class() {
34        InternalErrorClass::Access => Error::unauthorized(message),
35
36        InternalErrorClass::Domain => match err.origin() {
37            InternalErrorOrigin::Config => Error::invalid(message),
38            _ => {
39                if let Some(code) = registry_policy_error_code(&message) {
40                    Error::policy(code, message)
41                } else {
42                    Error::conflict(message)
43                }
44            }
45        },
46
47        InternalErrorClass::Invariant => Error::invariant(message),
48
49        InternalErrorClass::Infra | InternalErrorClass::Ops | InternalErrorClass::Workflow => {
50            Error::internal(message)
51        }
52    }
53}
54
55impl From<&InternalError> for Error {
56    fn from(err: &InternalError) -> Self {
57        internal_error_to_public(err)
58    }
59}
60
61impl From<InternalError> for Error {
62    fn from(err: InternalError) -> Self {
63        internal_error_to_public(&err)
64    }
65}
66
67// -----------------------------------------------------------------------------
68// Tests
69// -----------------------------------------------------------------------------
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::{
75        access::AccessError,
76        cdk::types::Principal,
77        domain::policy::pure::topology::{TopologyPolicyError, registry::RegistryPolicyError},
78        ids::CanisterRole,
79    };
80
81    fn p(id: u8) -> Principal {
82        Principal::from_slice(&[id; 29])
83    }
84
85    #[test]
86    fn internal_error_mapping_matches_class_contract() {
87        let access: Error = InternalError::from(AccessError::Denied("denied".to_string())).into();
88        assert_eq!(access.code, ErrorCode::Unauthorized);
89
90        let domain_config: Error =
91            InternalError::domain(InternalErrorOrigin::Config, "bad config").into();
92        assert_eq!(domain_config.code, ErrorCode::InvalidInput);
93
94        let domain_other: Error =
95            InternalError::domain(InternalErrorOrigin::Domain, "conflict").into();
96        assert_eq!(domain_other.code, ErrorCode::Conflict);
97
98        let invariant: Error =
99            InternalError::invariant(InternalErrorOrigin::Ops, "broken invariant").into();
100        assert_eq!(invariant.code, ErrorCode::InvariantViolation);
101
102        let infra: Error = InternalError::infra(InternalErrorOrigin::Infra, "infra fail").into();
103        assert_eq!(infra.code, ErrorCode::Internal);
104
105        let ops: Error = InternalError::ops(InternalErrorOrigin::Ops, "ops fail").into();
106        assert_eq!(ops.code, ErrorCode::Internal);
107
108        let workflow: Error =
109            InternalError::workflow(InternalErrorOrigin::Workflow, "workflow fail").into();
110        assert_eq!(workflow.code, ErrorCode::Internal);
111    }
112
113    #[test]
114    fn public_error_is_preserved_without_remap() {
115        let public = Error::not_found("missing");
116        let remapped: Error = InternalError::public(public.clone()).into();
117        assert_eq!(remapped, public);
118    }
119
120    #[test]
121    fn registry_policy_errors_map_to_stable_public_policy_codes() {
122        let err = RegistryPolicyError::RoleAlreadyRegistered {
123            role: CanisterRole::new("app"),
124            pid: p(7),
125        };
126        let internal: InternalError = TopologyPolicyError::from(err).into();
127        let public: Error = internal.into();
128        assert_eq!(public.code, ErrorCode::PolicyRoleAlreadyRegistered);
129    }
130
131    #[test]
132    fn registry_policy_service_parent_errors_use_service_owned_public_codes() {
133        let cases = [
134            (
135                RegistryPolicyError::ReplicaRequiresServiceWithScaling {
136                    role: CanisterRole::new("replica"),
137                    parent_role: CanisterRole::new("plain_parent"),
138                },
139                ErrorCode::PolicyReplicaRequiresServiceWithScaling,
140            ),
141            (
142                RegistryPolicyError::ShardRequiresServiceWithSharding {
143                    role: CanisterRole::new("shard"),
144                    parent_role: CanisterRole::new("plain_parent"),
145                },
146                ErrorCode::PolicyShardRequiresServiceWithSharding,
147            ),
148            (
149                RegistryPolicyError::InstanceRequiresServiceWithDirectory {
150                    role: CanisterRole::new("instance"),
151                    parent_role: CanisterRole::new("plain_parent"),
152                },
153                ErrorCode::PolicyInstanceRequiresServiceWithDirectory,
154            ),
155        ];
156
157        for (err, expected) in cases {
158            let internal: InternalError = TopologyPolicyError::from(err).into();
159            let public: Error = internal.into();
160
161            assert_eq!(public.code, expected);
162        }
163    }
164}