canic_core/config/schema/
mod.rs

1mod 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///
17/// ConfigSchemaError
18///
19
20#[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
54///
55/// Validate
56///
57
58pub trait Validate {
59    fn validate(&self) -> Result<(), ConfigSchemaError>;
60}
61
62///
63/// ConfigModel
64///
65
66#[derive(Clone, Debug, Default, Deserialize, Serialize)]
67#[serde(deny_unknown_fields)]
68pub struct ConfigModel {
69    // controllers
70    // a vec because we just append it to the controller arguments
71    #[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    /// Get a subnet configuration by role.
92    #[must_use]
93    pub fn get_subnet(&self, role: &SubnetRole) -> Option<SubnetConfig> {
94        self.subnets.get(role).cloned()
95    }
96
97    /// Test-only: baseline config with a prime subnet so validation succeeds.
98    #[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    /// Return true if the given principal is present in the whitelist.
108    #[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    /// Return whether ICRC-21 standard support is enabled.
116    #[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        //  Validate that prime subnet exists
129        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        //  Validate that every app_directory entry exists in prime.canisters
136        for canister_role in &self.app_directory {
137            validate_canister_role_len(canister_role, "app directory canister")?;
138            if !prime_subnet.canisters.contains_key(canister_role) {
139                return Err(ConfigSchemaError::ValidationError(format!(
140                    "app directory canister '{canister_role}' is not in prime subnet",
141                )));
142            }
143        }
144
145        // child validation
146        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///
158/// Whitelist
159///
160
161#[derive(Clone, Debug, Default, Deserialize, Serialize)]
162#[serde(deny_unknown_fields)]
163pub struct Whitelist {
164    // principals
165    // a hashset as we constantly have to do lookups
166    // strings because then we can validate and know if there are any bad ones
167    #[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///
186/// Standards
187///
188
189#[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}