Skip to main content

canic_host/registry/
mod.rs

1//! Module: registry
2//!
3//! Responsibility: project typed subnet-registry responses into host registry entries.
4//! Does not own: registry persistence, ICP command execution, or deployment topology policy.
5//! Boundary: decodes the canonical ICP envelope and validates redundant registry identity fields.
6
7#[cfg(test)]
8mod tests;
9
10use crate::icp::{IcpJsonResponseError, decode_json_result_response};
11use canic_core::{
12    cdk::utils::hash::hex_bytes,
13    dto::topology::{SubnetRegistryEntry, SubnetRegistryResponse},
14};
15use thiserror::Error as ThisError;
16
17///
18/// RegistryEntry
19///
20
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct RegistryEntry {
23    pub pid: String,
24    pub role: Option<String>,
25    pub parent_pid: Option<String>,
26    pub module_hash: Option<String>,
27}
28
29///
30/// RegistryParseError
31///
32
33#[derive(Debug, ThisError)]
34pub enum RegistryParseError {
35    #[error("registry entry principal mismatch: entry={entry_pid} record={record_pid}")]
36    PrincipalMismatch {
37        entry_pid: String,
38        record_pid: String,
39    },
40
41    #[error(transparent)]
42    Response(#[from] IcpJsonResponseError),
43
44    #[error("registry entry role mismatch for {pid}: entry={entry_role} record={record_role}")]
45    RoleMismatch {
46        pid: String,
47        entry_role: String,
48        record_role: String,
49    },
50}
51
52/// Decode and validate one canonical ICP subnet-registry response.
53pub fn parse_registry_entries(
54    registry_json: &str,
55) -> Result<Vec<RegistryEntry>, RegistryParseError> {
56    let response = decode_json_result_response::<SubnetRegistryResponse>(registry_json)?;
57    registry_entries_from_response(response)
58}
59
60/// Validate and project one typed subnet-registry response.
61pub(crate) fn registry_entries_from_response(
62    response: SubnetRegistryResponse,
63) -> Result<Vec<RegistryEntry>, RegistryParseError> {
64    response.0.into_iter().map(registry_entry).collect()
65}
66
67fn registry_entry(entry: SubnetRegistryEntry) -> Result<RegistryEntry, RegistryParseError> {
68    let pid = entry.pid.to_text();
69    let record_pid = entry.record.pid.to_text();
70    if pid != record_pid {
71        return Err(RegistryParseError::PrincipalMismatch {
72            entry_pid: pid,
73            record_pid,
74        });
75    }
76
77    let role = entry.role.into_string();
78    let record_role = entry.record.role.into_string();
79    if role != record_role {
80        return Err(RegistryParseError::RoleMismatch {
81            pid,
82            entry_role: role,
83            record_role,
84        });
85    }
86
87    Ok(RegistryEntry {
88        pid,
89        role: Some(role),
90        parent_pid: entry.record.parent_pid.map(|pid| pid.to_text()),
91        module_hash: entry.record.module_hash.map(hex_bytes),
92    })
93}