canic_core/config/schema/
mod.rs

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