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") {
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#[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}