Skip to main content

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

1//! Module: sns::report::model::requests::proposals
2//!
3//! Responsibility: request DTOs for SNS proposal reports.
4//! Does not own: command option parsing, live proposal fetches, or rendering.
5//! Boundary: carries validated proposal inputs into SNS report builders.
6
7use super::lookup::SnsLookupRequest;
8use crate::report::SortDirection;
9use crate::sns::report::{
10    SnsProposalEligibilityFilter, SnsProposalStatusFilter, SnsProposalTopicFilter, SnsProposalsSort,
11};
12use std::path::{Path, PathBuf};
13use thiserror::Error as ThisError;
14
15///
16/// SnsProposalCollection
17///
18/// Acquisition route for a bounded live proposal view or a complete-cache view.
19///
20
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub enum SnsProposalCollection {
23    /// One bounded Governance response.
24    Live,
25    /// Complete snapshot, refreshing only when absent under the existing policy.
26    Cached {
27        /// Caller-selected managed cache root.
28        cache_root: PathBuf,
29    },
30}
31
32///
33/// SnsProposalViewError
34///
35/// A caller selected a filter requiring complete cached proposal evidence.
36///
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
39#[error("`--status decided` requires a complete proposal cache")]
40pub struct SnsProposalViewError;
41
42///
43/// SnsProposalRequest
44///
45/// Request accepted by the direct SNS proposal detail report builder.
46///
47
48#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct SnsProposalRequest {
50    /// SNS target, explicit endpoint, network and caller clock.
51    pub lookup: SnsLookupRequest,
52    pub proposal_id: u64,
53    pub cache_root: Option<PathBuf>,
54}
55
56impl SnsProposalRequest {
57    #[must_use]
58    pub const fn new(lookup: SnsLookupRequest, proposal_id: u64) -> Self {
59        Self {
60            lookup,
61            proposal_id,
62            cache_root: None,
63        }
64    }
65
66    #[must_use]
67    pub fn with_cache_root(mut self, cache_root: impl Into<PathBuf>) -> Self {
68        self.cache_root = Some(cache_root.into());
69        self
70    }
71}
72
73///
74/// SnsProposalsRequest
75///
76/// Request accepted by the bounded SNS proposal listing report builder.
77///
78
79#[derive(Clone, Debug, Eq, PartialEq)]
80pub struct SnsProposalsRequest {
81    /// SNS target, explicit endpoint, network and caller clock.
82    pub lookup: SnsLookupRequest,
83    pub limit: u32,
84    pub before_proposal_id: Option<u64>,
85    status: SnsProposalStatusFilter,
86    pub topic: SnsProposalTopicFilter,
87    pub eligibility: SnsProposalEligibilityFilter,
88    pub proposer_neuron_id: Option<String>,
89    pub query: Option<String>,
90    pub sort: SnsProposalsSort,
91    pub sort_direction: SortDirection,
92    collection: SnsProposalCollection,
93}
94
95impl SnsProposalsRequest {
96    #[must_use]
97    pub fn new(lookup: SnsLookupRequest, limit: u32) -> Self {
98        Self {
99            lookup,
100            limit,
101            before_proposal_id: None,
102            status: SnsProposalStatusFilter::default(),
103            topic: SnsProposalTopicFilter::default(),
104            eligibility: SnsProposalEligibilityFilter::default(),
105            proposer_neuron_id: None,
106            query: None,
107            sort: SnsProposalsSort::default(),
108            sort_direction: SortDirection::default(),
109            collection: SnsProposalCollection::Live,
110        }
111    }
112
113    #[must_use]
114    pub const fn with_before_proposal_id(mut self, before_proposal_id: u64) -> Self {
115        self.before_proposal_id = Some(before_proposal_id);
116        self
117    }
118
119    /// Admit a status filter for the selected acquisition route.
120    pub fn with_status(
121        mut self,
122        status: SnsProposalStatusFilter,
123    ) -> Result<Self, SnsProposalViewError> {
124        if status == SnsProposalStatusFilter::Decided
125            && matches!(self.collection, SnsProposalCollection::Live)
126        {
127            return Err(SnsProposalViewError);
128        }
129        self.status = status;
130        Ok(self)
131    }
132
133    #[must_use]
134    pub const fn with_topic(mut self, topic: SnsProposalTopicFilter) -> Self {
135        self.topic = topic;
136        self
137    }
138
139    #[must_use]
140    pub const fn with_eligibility(mut self, eligibility: SnsProposalEligibilityFilter) -> Self {
141        self.eligibility = eligibility;
142        self
143    }
144
145    #[must_use]
146    pub fn with_proposer_neuron_id(mut self, proposer_neuron_id: impl Into<String>) -> Self {
147        self.proposer_neuron_id = Some(proposer_neuron_id.into());
148        self
149    }
150
151    #[must_use]
152    pub fn with_query(mut self, query: impl Into<String>) -> Self {
153        self.query = Some(query.into());
154        self
155    }
156
157    #[must_use]
158    pub const fn with_sort(mut self, sort: SnsProposalsSort) -> Self {
159        self.sort = sort;
160        self.sort_direction = sort.default_direction();
161        self
162    }
163
164    #[must_use]
165    pub const fn with_sort_direction(mut self, sort_direction: SortDirection) -> Self {
166        self.sort_direction = sort_direction;
167        self
168    }
169
170    #[must_use]
171    pub fn with_cache_root(mut self, cache_root: impl Into<PathBuf>) -> Self {
172        self.collection = SnsProposalCollection::Cached {
173            cache_root: cache_root.into(),
174        };
175        self
176    }
177
178    /// Admitted acquisition route.
179    #[must_use]
180    pub const fn collection(&self) -> &SnsProposalCollection {
181        &self.collection
182    }
183
184    /// Caller-selected status admitted for this acquisition route.
185    #[must_use]
186    pub const fn status(&self) -> SnsProposalStatusFilter {
187        self.status
188    }
189
190    /// Root for complete-cache views, absent for bounded live acquisition.
191    #[must_use]
192    pub fn cache_root(&self) -> Option<&Path> {
193        match &self.collection {
194            SnsProposalCollection::Live => None,
195            SnsProposalCollection::Cached { cache_root } => Some(cache_root),
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn decided_filter_requires_an_explicit_complete_cache_route() {
206        let lookup = SnsLookupRequest::new("ic", "https://icp-api.io", 0, "aaaaa-aa").unwrap();
207        let live = SnsProposalsRequest::new(lookup.clone(), 10);
208        assert!(matches!(live.collection(), SnsProposalCollection::Live));
209        assert_eq!(
210            live.with_status(SnsProposalStatusFilter::Decided),
211            Err(SnsProposalViewError)
212        );
213        let cached = SnsProposalsRequest::new(lookup, 10)
214            .with_cache_root("cache")
215            .with_status(SnsProposalStatusFilter::Decided)
216            .unwrap();
217        assert!(
218            matches!(cached.collection(), SnsProposalCollection::Cached { cache_root } if cache_root == Path::new("cache"))
219        );
220        assert_eq!(cached.status(), SnsProposalStatusFilter::Decided);
221    }
222}