Skip to main content

ic_query/sns/report/model/requests/
lookup.rs

1//! Module: sns::report::model::requests::lookup
2//!
3//! Responsibility: shared request DTO for direct SNS lookup reports.
4//! Does not own: command option parsing, source resolution, or rendering.
5//! Boundary: carries validated lookup inputs into one SNS report builder.
6
7use ic_platform::candid::contract::Principal;
8use thiserror::Error as ThisError;
9
10///
11/// SnsLookupError
12///
13/// Caller input is neither a numeric list position nor a root principal.
14///
15
16#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
17#[error("invalid SNS root principal: {input}")]
18pub struct SnsLookupError {
19    /// Original rejected selector.
20    pub input: String,
21}
22
23#[derive(Clone, Debug, Eq, PartialEq)]
24enum SnsLookupTarget {
25    Id(usize),
26    Root(Principal),
27}
28
29///
30/// SnsLookupRequest
31///
32/// Shared request accepted by direct SNS info, token, params, swap, and upgrade builders.
33///
34
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct SnsLookupRequest {
37    pub network: String,
38    pub source_endpoint: String,
39    pub now_unix_secs: u64,
40    input: String,
41    target: SnsLookupTarget,
42}
43
44impl SnsLookupRequest {
45    /// Parse the selector once while retaining the original caller input.
46    pub fn new(
47        network: impl Into<String>,
48        source_endpoint: impl Into<String>,
49        now_unix_secs: u64,
50        input: impl Into<String>,
51    ) -> Result<Self, SnsLookupError> {
52        let input = input.into();
53        let target = match input.parse::<usize>() {
54            Ok(id) => SnsLookupTarget::Id(id),
55            Err(_) => {
56                SnsLookupTarget::Root(Principal::from_text(&input).map_err(|_| SnsLookupError {
57                    input: input.clone(),
58                })?)
59            }
60        };
61        Ok(Self {
62            network: network.into(),
63            source_endpoint: source_endpoint.into(),
64            now_unix_secs,
65            input,
66            target,
67        })
68    }
69
70    /// Original caller selector, retained independently of canonical principal text.
71    #[must_use]
72    pub fn input(&self) -> &str {
73        &self.input
74    }
75
76    /// Numeric SNS-W list position when the caller selected one.
77    #[must_use]
78    pub const fn id(&self) -> Option<usize> {
79        match self.target {
80            SnsLookupTarget::Id(id) => Some(id),
81            SnsLookupTarget::Root(_) => None,
82        }
83    }
84
85    /// Parsed root principal when the caller selected a stable SNS identity.
86    #[must_use]
87    pub const fn root_canister_id(&self) -> Option<&Principal> {
88        match &self.target {
89            SnsLookupTarget::Root(root) => Some(root),
90            SnsLookupTarget::Id(_) => None,
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn selectors_retain_caller_text_and_parse_identity_once() {
101        let numeric = SnsLookupRequest::new("ic", "https://icp-api.io", 0, "0002").unwrap();
102        assert_eq!(numeric.input(), "0002");
103        assert_eq!(numeric.id(), Some(2));
104        assert_eq!(numeric.root_canister_id(), None);
105        let root = "be2us-64aaa-aaaaa-qaabq-cai";
106        let principal = SnsLookupRequest::new("ic", "https://icp-api.io", 0, root).unwrap();
107        assert_eq!(principal.input(), root);
108        assert_eq!(principal.id(), None);
109        assert_eq!(principal.root_canister_id().unwrap().to_text(), root);
110        let error =
111            SnsLookupRequest::new("ic", "https://icp-api.io", 0, "not-a-principal").unwrap_err();
112        assert_eq!(error.input, "not-a-principal");
113    }
114}