canic_subnet_catalog/model/
mod.rs1use crate::{
2 CATALOG_SCHEMA_VERSION, CatalogError, parse_principal, principal_bytes,
3 resolver::routing_range_sorts_after,
4};
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeSet;
7use std::str::FromStr;
8
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
13pub struct SubnetCatalog {
14 pub catalog_schema_version: u32,
15 pub network: String,
16 pub registry_canister_id: String,
17 pub registry_version: u64,
18 pub fetched_at: String,
19 pub fetched_by: String,
20 pub source_endpoint: String,
21 pub resolver_backend: String,
22 pub subnets: Vec<SubnetInfo>,
23 pub routing_ranges: Vec<RoutingRange>,
24}
25
26#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
30pub struct SubnetInfo {
31 pub subnet_principal: String,
32 pub subnet_kind: SubnetKind,
33 pub subnet_kind_source: ClassificationSource,
34 pub subnet_specialization: SubnetSpecialization,
35 pub subnet_specialization_source: ClassificationSource,
36 pub geographic_scope: GeographicScope,
37 pub geographic_scope_source: ClassificationSource,
38 pub subnet_label: String,
39 pub subnet_label_source: ClassificationSource,
40 pub node_count: Option<u32>,
41 pub charges_apply_by_default: bool,
42}
43
44#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
48pub struct RoutingRange {
49 pub start_canister_id: String,
50 pub end_canister_id: String,
51 pub subnet_principal: String,
52}
53
54#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
58#[serde(rename_all = "snake_case")]
59pub enum SubnetKind {
60 Application,
61 CloudEngine,
62 System,
63 Unknown,
64}
65
66impl SubnetKind {
67 #[must_use]
68 pub const fn as_str(self) -> &'static str {
69 match self {
70 Self::Application => "application",
71 Self::CloudEngine => "cloud_engine",
72 Self::System => "system",
73 Self::Unknown => "unknown",
74 }
75 }
76
77 #[must_use]
78 pub const fn charges_apply_by_default(self) -> bool {
79 matches!(self, Self::Application | Self::CloudEngine)
80 }
81}
82
83impl FromStr for SubnetKind {
84 type Err = String;
85
86 fn from_str(value: &str) -> Result<Self, Self::Err> {
87 match value {
88 "application" => Ok(Self::Application),
89 "cloud_engine" => Ok(Self::CloudEngine),
90 "system" => Ok(Self::System),
91 "unknown" => Ok(Self::Unknown),
92 other => Err(format!(
93 "invalid value {other}; use application, cloud_engine, system, or unknown"
94 )),
95 }
96 }
97}
98
99#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
103#[serde(rename_all = "snake_case")]
104pub enum SubnetSpecialization {
105 None,
106 Fiduciary,
107 European,
108 Unknown,
109}
110
111impl SubnetSpecialization {
112 #[must_use]
113 pub const fn as_str(self) -> &'static str {
114 match self {
115 Self::None => "none",
116 Self::Fiduciary => "fiduciary",
117 Self::European => "european",
118 Self::Unknown => "unknown",
119 }
120 }
121}
122
123impl FromStr for SubnetSpecialization {
124 type Err = String;
125
126 fn from_str(value: &str) -> Result<Self, Self::Err> {
127 match value {
128 "none" => Ok(Self::None),
129 "fiduciary" => Ok(Self::Fiduciary),
130 "european" => Ok(Self::European),
131 "unknown" => Ok(Self::Unknown),
132 other => Err(format!(
133 "invalid value {other}; use none, fiduciary, european, or unknown"
134 )),
135 }
136 }
137}
138
139#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
143#[serde(rename_all = "snake_case")]
144pub enum GeographicScope {
145 Global,
146 Europe,
147 Unknown,
148}
149
150impl GeographicScope {
151 #[must_use]
152 pub const fn as_str(self) -> &'static str {
153 match self {
154 Self::Global => "global",
155 Self::Europe => "europe",
156 Self::Unknown => "unknown",
157 }
158 }
159}
160
161impl FromStr for GeographicScope {
162 type Err = String;
163
164 fn from_str(value: &str) -> Result<Self, Self::Err> {
165 match value {
166 "global" => Ok(Self::Global),
167 "europe" => Ok(Self::Europe),
168 "unknown" => Ok(Self::Unknown),
169 other => Err(format!(
170 "invalid value {other}; use global, europe, or unknown"
171 )),
172 }
173 }
174}
175
176#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
180#[serde(rename_all = "snake_case")]
181pub enum ClassificationSource {
182 Registry,
183 Curated,
184 Computed,
185 Unknown,
186}
187
188impl ClassificationSource {
189 #[must_use]
190 pub const fn as_str(self) -> &'static str {
191 match self {
192 Self::Registry => "registry",
193 Self::Curated => "curated",
194 Self::Computed => "computed",
195 Self::Unknown => "unknown",
196 }
197 }
198}
199
200impl SubnetCatalog {
201 pub fn validate(&self) -> Result<(), CatalogError> {
203 if self.catalog_schema_version != CATALOG_SCHEMA_VERSION {
204 return Err(CatalogError::UnsupportedSchemaVersion {
205 found: self.catalog_schema_version,
206 supported: CATALOG_SCHEMA_VERSION,
207 });
208 }
209 if self.subnets.is_empty() {
210 return Err(CatalogError::EmptySubnets);
211 }
212 if self.routing_ranges.is_empty() {
213 return Err(CatalogError::EmptyRoutingRanges);
214 }
215 parse_principal(&self.registry_canister_id, "registry_canister_id")?;
216
217 let mut subnet_principals = BTreeSet::new();
218 for subnet in &self.subnets {
219 parse_principal(&subnet.subnet_principal, "subnet_principal")?;
220 if !subnet_principals.insert(subnet.subnet_principal.clone()) {
221 return Err(CatalogError::DuplicateSubnet {
222 subnet_principal: subnet.subnet_principal.clone(),
223 });
224 }
225 }
226
227 for range in &self.routing_ranges {
228 if !subnet_principals.contains(&range.subnet_principal) {
229 return Err(CatalogError::UnknownRoutingSubnet {
230 subnet_principal: range.subnet_principal.clone(),
231 });
232 }
233 let start = principal_bytes(&range.start_canister_id, "start_canister_id")?;
234 let end = principal_bytes(&range.end_canister_id, "end_canister_id")?;
235 parse_principal(&range.subnet_principal, "routing_range.subnet_principal")?;
236 if routing_range_sorts_after(&start, &end) {
237 return Err(CatalogError::InvalidRoutingRange {
238 subnet_principal: range.subnet_principal.clone(),
239 start_canister_id: range.start_canister_id.clone(),
240 end_canister_id: range.end_canister_id.clone(),
241 });
242 }
243 }
244
245 Ok(())
246 }
247
248 #[must_use]
249 pub fn subnet_by_principal(&self, subnet_principal: &str) -> Option<&SubnetInfo> {
250 self.subnets
251 .iter()
252 .find(|subnet| subnet.subnet_principal == subnet_principal)
253 }
254
255 #[must_use]
256 pub fn routing_ranges_for_subnet(&self, subnet_principal: &str) -> Vec<&RoutingRange> {
257 self.routing_ranges
258 .iter()
259 .filter(|range| range.subnet_principal == subnet_principal)
260 .collect()
261 }
262}