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
26impl From<ConfigSchemaError> for Error {
27 fn from(err: ConfigSchemaError) -> Self {
28 ConfigError::from(err).into()
29 }
30}
31
32pub trait Validate {
37 fn validate(&self) -> Result<(), ConfigSchemaError>;
38}
39
40#[derive(Clone, Debug, Default, Deserialize, Serialize)]
45#[serde(deny_unknown_fields)]
46pub struct ConfigModel {
47 #[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 #[must_use]
71 pub fn get_subnet(&self, ty: &SubnetRole) -> Option<SubnetConfig> {
72 self.subnets.get(ty).cloned()
73 }
74
75 #[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 #[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 #[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 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 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 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#[derive(Clone, Debug, Default, Deserialize, Serialize)]
135#[serde(deny_unknown_fields)]
136pub struct Whitelist {
137 #[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#[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}