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
26impl From<ConfigSchemaError> for Error {
27    fn from(err: ConfigSchemaError) -> Self {
28        ConfigError::from(err).into()
29    }
30}
31
32///
33/// Validate
34///
35
36pub trait Validate {
37    fn validate(&self) -> Result<(), ConfigSchemaError>;
38}
39
40///
41/// ConfigModel
42///
43
44#[derive(Clone, Debug, Default, Deserialize, Serialize)]
45#[serde(deny_unknown_fields)]
46pub struct ConfigModel {
47    // controllers
48    // a vec because we just append it to the controller arguments
49    #[serde(default)]
50    pub controllers: Vec<Principal>,
51
52    #[serde(default)]
53    pub standards: Option<Standards>,
54
55    #[serde(default)]
56    pub log: LogConfig,
57
58    #[serde(default)]
59    pub app_directory: BTreeSet<CanisterRole>,
60
61    #[serde(default)]
62    pub subnets: BTreeMap<SubnetRole, SubnetConfig>,
63
64    #[serde(default)]
65    pub whitelist: Option<Whitelist>,
66}
67
68impl ConfigModel {
69    /// Get a subnet configuration by type.
70    #[must_use]
71    pub fn get_subnet(&self, ty: &SubnetRole) -> Option<SubnetConfig> {
72        self.subnets.get(ty).cloned()
73    }
74
75    /// Test-only: baseline config with a prime subnet so validation succeeds.
76    #[cfg(test)]
77    #[must_use]
78    pub fn test_default() -> Self {
79        let mut cfg = Self::default();
80        cfg.subnets
81            .insert(SubnetRole::PRIME, SubnetConfig::default());
82        cfg
83    }
84
85    /// Return true if the given principal is present in the whitelist.
86    #[must_use]
87    pub fn is_whitelisted(&self, principal: &Principal) -> bool {
88        self.whitelist
89            .as_ref()
90            .is_none_or(|w| w.principals.contains(&principal.to_string()))
91    }
92
93    /// Return whether ICRC-21 standard support is enabled.
94    #[must_use]
95    pub fn icrc21_enabled(&self) -> bool {
96        self.standards.as_ref().is_some_and(|s| s.icrc21)
97    }
98}
99
100impl Validate for ConfigModel {
101    fn validate(&self) -> Result<(), ConfigSchemaError> {
102        //  Validate that prime subnet exists
103        let prime = SubnetRole::PRIME;
104        let prime_subnet = self
105            .subnets
106            .get(&prime)
107            .ok_or_else(|| ConfigSchemaError::ValidationError("prime subnet not found".into()))?;
108
109        //  Validate that every app_directory entry exists in prime.canisters
110        for canister_ty in &self.app_directory {
111            if !prime_subnet.canisters.contains_key(canister_ty) {
112                return Err(ConfigSchemaError::ValidationError(format!(
113                    "app directory canister '{canister_ty}' is not in prime subnet",
114                )));
115            }
116        }
117
118        // child validation
119        if let Some(list) = &self.whitelist {
120            list.validate()?;
121        }
122        for subnet in self.subnets.values() {
123            subnet.validate()?;
124        }
125
126        Ok(())
127    }
128}
129
130///
131/// Whitelist
132///
133
134#[derive(Clone, Debug, Default, Deserialize, Serialize)]
135#[serde(deny_unknown_fields)]
136pub struct Whitelist {
137    // principals
138    // a hashset as we constantly have to do lookups
139    // strings because then we can validate and know if there are any bad ones
140    #[serde(default)]
141    pub principals: BTreeSet<String>,
142}
143
144impl Validate for Whitelist {
145    fn validate(&self) -> Result<(), ConfigSchemaError> {
146        for (i, s) in self.principals.iter().enumerate() {
147            if Principal::from_text(s).is_err() {
148                return Err(ConfigSchemaError::ValidationError(format!(
149                    "principal #{i} {s} is invalid"
150                )));
151            }
152        }
153
154        Ok(())
155    }
156}
157
158///
159/// Standards
160///
161
162#[derive(Clone, Debug, Default, Deserialize, Serialize)]
163#[serde(deny_unknown_fields)]
164pub struct Standards {
165    #[serde(default)]
166    pub icrc21: bool,
167
168    #[serde(default)]
169    pub icrc103: bool,
170}