canic/config/schema/
mod.rs1mod log;
2mod subnet;
3
4pub use log::*;
5pub use subnet::*;
6
7use crate::{
8 Error,
9 config::ConfigError,
10 types::{CanisterType, SubnetType},
11};
12use candid::Principal;
13use serde::{Deserialize, Serialize};
14use std::collections::{BTreeMap, BTreeSet};
15use thiserror::Error as ThisError;
16
17#[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
33pub trait Validate {
38 fn validate(&self) -> Result<(), ConfigSchemaError>;
39}
40
41#[derive(Clone, Debug, Default, Deserialize, Serialize)]
46#[serde(deny_unknown_fields)]
47pub struct ConfigModel {
48 #[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<CanisterType>,
61
62 #[serde(default)]
63 pub subnets: BTreeMap<SubnetType, SubnetConfig>,
64
65 #[serde(default)]
66 pub whitelist: Option<Whitelist>,
67}
68
69impl ConfigModel {
70 #[must_use]
72 pub fn get_subnet(&self, ty: &SubnetType) -> Option<SubnetConfig> {
73 self.subnets.get(ty).cloned()
74 }
75
76 #[must_use]
78 pub fn is_whitelisted(&self, principal: &Principal) -> bool {
79 self.whitelist
80 .as_ref()
81 .is_none_or(|w| w.principals.contains(&principal.to_string()))
82 }
83
84 #[must_use]
86 pub fn icrc21_enabled(&self) -> bool {
87 self.standards.as_ref().is_some_and(|s| s.icrc21)
88 }
89}
90
91impl Validate for ConfigModel {
92 fn validate(&self) -> Result<(), ConfigSchemaError> {
93 let prime = SubnetType::PRIME;
95 let prime_subnet = self
96 .subnets
97 .get(&prime)
98 .ok_or_else(|| ConfigSchemaError::ValidationError("prime subnet not found".into()))?;
99
100 for canister_ty in &self.app_directory {
102 if !prime_subnet.canisters.contains_key(canister_ty) {
103 return Err(ConfigSchemaError::ValidationError(format!(
104 "app directory canister '{canister_ty}' is not in prime subnet",
105 )));
106 }
107 }
108
109 if let Some(list) = &self.whitelist {
111 list.validate()?;
112 }
113 for subnet in self.subnets.values() {
114 subnet.validate()?;
115 }
116
117 Ok(())
118 }
119}
120
121#[derive(Clone, Debug, Default, Deserialize, Serialize)]
126#[serde(deny_unknown_fields)]
127pub struct Whitelist {
128 #[serde(default)]
132 pub principals: BTreeSet<String>,
133}
134
135impl Validate for Whitelist {
136 fn validate(&self) -> Result<(), ConfigSchemaError> {
137 for (i, s) in self.principals.iter().enumerate() {
138 if Principal::from_text(s).is_err() {
139 return Err(ConfigSchemaError::ValidationError(format!(
140 "principal #{i} {s} is invalid"
141 )));
142 }
143 }
144
145 Ok(())
146 }
147}
148
149#[derive(Clone, Debug, Default, Deserialize, Serialize)]
154#[serde(deny_unknown_fields)]
155pub struct Standards {
156 #[serde(default)]
157 pub icrc21: bool,
158
159 #[serde(default)]
160 pub icrc103: bool,
161}