canic_core/config/schema/
mod.rs1mod log;
2mod subnet;
3
4pub use log::*;
5pub use subnet::*;
6
7use crate::{
8 Error, ThisError,
9 config::ConfigError,
10 ids::{CanisterRole, SubnetRole},
11};
12use candid::Principal;
13use serde::{Deserialize, Serialize};
14use std::collections::{BTreeMap, BTreeSet};
15
16#[derive(Debug, ThisError)]
21pub enum ConfigSchemaError {
22 #[error("validation error: {0}")]
23 ValidationError(String),
24}
25
26pub const NAME_MAX_BYTES: usize = 40;
27
28fn validate_canister_role_len(role: &CanisterRole, context: &str) -> Result<(), ConfigSchemaError> {
29 if role.as_ref().len() > NAME_MAX_BYTES {
30 return Err(ConfigSchemaError::ValidationError(format!(
31 "{context} '{role}' exceeds {NAME_MAX_BYTES} bytes",
32 )));
33 }
34
35 Ok(())
36}
37
38fn validate_subnet_role_len(role: &SubnetRole, context: &str) -> Result<(), ConfigSchemaError> {
39 if role.as_ref().len() > NAME_MAX_BYTES {
40 return Err(ConfigSchemaError::ValidationError(format!(
41 "{context} '{role}' exceeds {NAME_MAX_BYTES} bytes",
42 )));
43 }
44
45 Ok(())
46}
47
48impl From<ConfigSchemaError> for Error {
49 fn from(err: ConfigSchemaError) -> Self {
50 ConfigError::from(err).into()
51 }
52}
53
54pub trait Validate {
59 fn validate(&self) -> Result<(), ConfigSchemaError>;
60}
61
62#[derive(Clone, Debug, Default, Deserialize, Serialize)]
67#[serde(deny_unknown_fields)]
68pub struct ConfigModel {
69 #[serde(default)]
72 pub controllers: Vec<Principal>,
73
74 #[serde(default)]
75 pub standards: Option<Standards>,
76
77 #[serde(default)]
78 pub log: LogConfig,
79
80 #[serde(default)]
81 pub app_directory: BTreeSet<CanisterRole>,
82
83 #[serde(default)]
84 pub subnets: BTreeMap<SubnetRole, SubnetConfig>,
85
86 #[serde(default)]
87 pub whitelist: Option<Whitelist>,
88}
89
90impl ConfigModel {
91 #[must_use]
93 pub fn get_subnet(&self, ty: &SubnetRole) -> Option<SubnetConfig> {
94 self.subnets.get(ty).cloned()
95 }
96
97 #[cfg(test)]
99 #[must_use]
100 pub fn test_default() -> Self {
101 let mut cfg = Self::default();
102 cfg.subnets
103 .insert(SubnetRole::PRIME, SubnetConfig::default());
104 cfg
105 }
106
107 #[must_use]
109 pub fn is_whitelisted(&self, principal: &Principal) -> bool {
110 self.whitelist
111 .as_ref()
112 .is_none_or(|w| w.principals.contains(&principal.to_string()))
113 }
114
115 #[must_use]
117 pub fn icrc21_enabled(&self) -> bool {
118 self.standards.as_ref().is_some_and(|s| s.icrc21)
119 }
120}
121
122impl Validate for ConfigModel {
123 fn validate(&self) -> Result<(), ConfigSchemaError> {
124 for subnet_role in self.subnets.keys() {
125 validate_subnet_role_len(subnet_role, "subnet")?;
126 }
127
128 let prime = SubnetRole::PRIME;
130 let prime_subnet = self
131 .subnets
132 .get(&prime)
133 .ok_or_else(|| ConfigSchemaError::ValidationError("prime subnet not found".into()))?;
134
135 for canister_ty in &self.app_directory {
137 validate_canister_role_len(canister_ty, "app directory canister")?;
138 if !prime_subnet.canisters.contains_key(canister_ty) {
139 return Err(ConfigSchemaError::ValidationError(format!(
140 "app directory canister '{canister_ty}' is not in prime subnet",
141 )));
142 }
143 }
144
145 if let Some(list) = &self.whitelist {
147 list.validate()?;
148 }
149 for subnet in self.subnets.values() {
150 subnet.validate()?;
151 }
152
153 Ok(())
154 }
155}
156
157#[derive(Clone, Debug, Default, Deserialize, Serialize)]
162#[serde(deny_unknown_fields)]
163pub struct Whitelist {
164 #[serde(default)]
168 pub principals: BTreeSet<String>,
169}
170
171impl Validate for Whitelist {
172 fn validate(&self) -> Result<(), ConfigSchemaError> {
173 for (i, s) in self.principals.iter().enumerate() {
174 if Principal::from_text(s).is_err() {
175 return Err(ConfigSchemaError::ValidationError(format!(
176 "principal #{i} {s} is invalid"
177 )));
178 }
179 }
180
181 Ok(())
182 }
183}
184
185#[derive(Clone, Debug, Default, Deserialize, Serialize)]
190#[serde(deny_unknown_fields)]
191pub struct Standards {
192 #[serde(default)]
193 pub icrc21: bool,
194
195 #[serde(default)]
196 pub icrc103: bool,
197}