Skip to main content

ic_query/nns/proposals/report/model/
selection.rs

1//! Module: nns::proposals::report::model::selection
2//!
3//! Responsibility: NNS proposal filter, sort, and protocol vocabulary.
4//! Does not own: request DTOs, serialized report DTOs, or report rendering.
5//! Boundary: keeps public selection types and their canonical labels/codes together.
6
7use crate::report::SortDirection;
8use serde::{Deserialize, Serialize};
9
10macro_rules! governance_code_enum {
11    (
12        $(#[$enum_attr:meta])*
13        pub enum $name:ident {
14            $(#[$unspecified_attr:meta])* Unspecified = 0 => "unspecified",
15            $($(#[$variant_attr:meta])* $variant:ident = $code:literal => $label:literal,)*
16        }
17        filter $filter:ident;
18    ) => {
19        $(#[$enum_attr])*
20        pub enum $name {
21            $(#[$unspecified_attr])* #[serde(rename = "unspecified")] Unspecified,
22            $($(#[$variant_attr])* #[serde(rename = $label)] $variant,)*
23        }
24
25        impl $name {
26            /// Classify a native code, retaining the unspecified classification for unknown codes.
27            #[must_use]
28            pub const fn from_code(code: i32) -> Self {
29                match code {
30                    $($code => Self::$variant,)*
31                    _ => Self::Unspecified,
32                }
33            }
34
35            /// Return the canonical native code for this classification.
36            #[must_use]
37            pub const fn code(self) -> i32 {
38                match self { Self::Unspecified => 0, $(Self::$variant => $code,)* }
39            }
40
41            /// Return the stable JSON and text label.
42            #[must_use]
43            pub const fn as_str(self) -> &'static str {
44                match self { Self::Unspecified => "unspecified", $(Self::$variant => $label,)* }
45            }
46        }
47        ///
48        #[doc = stringify!($filter)]
49        ///
50        /// Selection of one known proposal classification or all proposals.
51        ///
52
53        #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
54        pub enum $filter {
55            /// Do not restrict this classification.
56            #[default]
57            Any,
58            $($(#[$variant_attr])* $variant,)*
59        }
60
61        impl $filter {
62            /// All supported filter choices in presentation order.
63            pub const ALL: &'static [Self] = &[Self::Any, $(Self::$variant,)*];
64
65            /// Return the canonical filter label.
66            #[must_use]
67            pub const fn as_str(self) -> &'static str {
68                match self.classification() { None => "any", Some(value) => value.as_str() }
69            }
70
71            const fn classification(self) -> Option<$name> {
72                match self { Self::Any => None, $(Self::$variant => Some($name::$variant),)* }
73            }
74        }
75    };
76}
77
78governance_code_enum! {
79    ///
80    /// NnsProposalStatus
81    ///
82    /// Native NNS Governance proposal decision status.
83    ///
84
85    #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
86    pub enum NnsProposalStatus {
87        /// Unspecified or unrecognized native status code.
88        Unspecified = 0 => "unspecified",
89        /// Proposal remains open for voting.
90        Open = 1 => "open",
91        /// Proposal was rejected.
92        Rejected = 2 => "rejected",
93        /// Proposal was adopted but has not completed execution.
94        Adopted = 3 => "adopted",
95        /// Proposal executed successfully.
96        Executed = 4 => "executed",
97        /// Proposal execution failed.
98        Failed = 5 => "failed",
99    }
100    filter NnsProposalStatusFilter;
101}
102
103governance_code_enum! {
104    ///
105    /// NnsProposalRewardStatus
106    ///
107    /// Native NNS Governance proposal reward-settlement status.
108    ///
109
110    #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
111    pub enum NnsProposalRewardStatus {
112        /// Unspecified or unrecognized native reward-status code.
113        Unspecified = 0 => "unspecified",
114        /// Proposal still accepts votes for reward purposes.
115        AcceptVotes = 1 => "accept-votes",
116        /// Proposal is ready for reward settlement.
117        ReadyToSettle = 2 => "ready-to-settle",
118        /// Proposal rewards have settled.
119        Settled = 3 => "settled",
120        /// Proposal is not eligible for voting rewards.
121        Ineligible = 4 => "ineligible",
122    }
123    filter NnsProposalRewardStatusFilter;
124}
125
126governance_code_enum! {
127    ///
128    /// NnsProposalTopic
129    ///
130    /// Native NNS Governance proposal topic.
131    ///
132
133    #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
134    pub enum NnsProposalTopic {
135        /// Unspecified or unrecognized native topic code.
136        Unspecified = 0 => "unspecified",
137        /// Neuron management.
138        NeuronManagement = 1 => "neuron-management",
139        /// Exchange-rate management.
140        ExchangeRate = 2 => "exchange-rate",
141        /// Network economics.
142        NetworkEconomics = 3 => "network-economics",
143        /// Governance policy.
144        Governance = 4 => "governance",
145        /// Node administration.
146        NodeAdmin = 5 => "node-admin",
147        /// Participant management.
148        ParticipantManagement = 6 => "participant-management",
149        /// Subnet management.
150        SubnetManagement = 7 => "subnet-management",
151        /// Network-canister management.
152        NetworkCanisterManagement = 8 => "network-canister-management",
153        /// Know-your-customer policy.
154        Kyc = 9 => "kyc",
155        /// Node-provider rewards.
156        NodeProviderRewards = 10 => "node-provider-rewards",
157        /// IC OS version deployment.
158        IcOsVersionDeployment = 12 => "ic-os-version-deployment",
159        /// IC OS version election.
160        IcOsVersionElection = 13 => "ic-os-version-election",
161        /// SNS and Community Fund policy.
162        SnsAndCommunityFund = 14 => "sns-and-community-fund",
163        /// API boundary-node management.
164        ApiBoundaryNodeManagement = 15 => "api-boundary-node-management",
165        /// Subnet rental.
166        SubnetRental = 16 => "subnet-rental",
167        /// Application-canister management.
168        ApplicationCanisterManagement = 17 => "application-canister-management",
169        /// Protocol-canister management.
170        ProtocolCanisterManagement = 18 => "protocol-canister-management",
171    }
172    filter NnsProposalTopicFilter;
173}
174
175macro_rules! proposal_sort {
176    ($($(#[$attr:meta])* $variant:ident => $label:literal,)*) => {
177        ///
178        /// NnsProposalListSort
179        ///
180        /// Report-model sort selector for NNS proposal listings.
181        ///
182
183        #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
184        pub enum NnsProposalListSort { $($(#[$attr])* $variant,)* }
185
186        impl NnsProposalListSort {
187            /// All supported sort choices in presentation order.
188            pub const ALL: &'static [Self] = &[$(Self::$variant,)*];
189
190            /// Return the canonical sort label.
191            #[must_use]
192            pub const fn as_str(self) -> &'static str {
193                match self { $(Self::$variant => $label,)* }
194            }
195        }
196    };
197}
198
199proposal_sort! {
200    /// Preserve the descending proposal-id order returned by Governance.
201    #[default]
202    Api => "api",
203    /// Sort by proposal id.
204    Id => "id",
205    /// Sort by the raw decision-status code.
206    Status => "status",
207    /// Sort by the raw reward-settlement code.
208    RewardStatus => "reward-status",
209    /// Sort by the raw topic code.
210    Topic => "topic",
211    /// Sort by proposer neuron id.
212    Proposer => "proposer",
213    /// Sort by proposal title.
214    Title => "title",
215    /// Sort by the human-readable action classification.
216    Action => "action",
217    /// Sort by affirmative voting power in the latest tally.
218    Yes => "yes",
219    /// Sort by negative voting power in the latest tally.
220    No => "no",
221    /// Sort by total voting power in the latest tally.
222    TotalVotes => "total-votes",
223    /// Sort by the latest tally timestamp.
224    TallyTime => "tally-time",
225    /// Sort by total potential voting power.
226    VotingPower => "voting-power",
227    /// Sort by ballot count returned by Governance.
228    Ballots => "ballots",
229    /// Sort by reject cost in ICP e8s.
230    RejectCost => "reject-cost",
231    /// Sort by reward event round.
232    RewardRound => "reward-round",
233    /// Sort by proposal creation time.
234    Proposed => "proposed",
235    /// Sort by voting deadline.
236    Deadline => "deadline",
237    /// Sort by decision time.
238    Decided => "decided",
239    /// Sort by execution time.
240    Executed => "executed",
241    /// Sort by execution failure time.
242    Failed => "failed",
243}
244
245impl NnsProposalListSort {
246    /// Return the default direction for this sort key.
247    #[must_use]
248    pub const fn default_direction(self) -> SortDirection {
249        match self {
250            Self::Status
251            | Self::RewardStatus
252            | Self::Topic
253            | Self::Proposer
254            | Self::Title
255            | Self::Action => SortDirection::Asc,
256            _ => SortDirection::Desc,
257        }
258    }
259
260    /// Return whether this key applies a local sort direction.
261    #[must_use]
262    pub const fn uses_local_direction(self) -> bool {
263        !matches!(self, Self::Api)
264    }
265
266    /// Return `none` for API order or the selected local direction label.
267    #[must_use]
268    pub const fn direction_label(self, direction: SortDirection) -> &'static str {
269        match self {
270            Self::Api => "none",
271            _ => direction.as_str(),
272        }
273    }
274}
275
276impl NnsProposalStatusFilter {
277    #[cfg(any(
278        feature = "nns-host",
279        all(feature = "canister", target_arch = "wasm32")
280    ))]
281    pub(in crate::nns) const fn governance_status_code(self) -> Option<i32> {
282        match self.classification() {
283            None => None,
284            Some(value) => Some(value.code()),
285        }
286    }
287}
288
289impl NnsProposalRewardStatusFilter {
290    #[cfg(any(
291        feature = "nns-host",
292        all(feature = "canister", target_arch = "wasm32")
293    ))]
294    pub(in crate::nns) const fn governance_reward_status_code(self) -> Option<i32> {
295        match self.classification() {
296            None => None,
297            Some(value) => Some(value.code()),
298        }
299    }
300}
301
302impl NnsProposalTopicFilter {
303    pub(in crate::nns) const fn topic_code(self) -> Option<i32> {
304        match self.classification() {
305            None => None,
306            Some(value) => Some(value.code()),
307        }
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn proposal_status_codes_and_labels_round_trip() {
317        for (status, code, label) in [
318            (NnsProposalStatus::Unspecified, 0, "unspecified"),
319            (NnsProposalStatus::Open, 1, "open"),
320            (NnsProposalStatus::Rejected, 2, "rejected"),
321            (NnsProposalStatus::Adopted, 3, "adopted"),
322            (NnsProposalStatus::Executed, 4, "executed"),
323            (NnsProposalStatus::Failed, 5, "failed"),
324        ] {
325            assert_eq!(status.code(), code);
326            assert_eq!(NnsProposalStatus::from_code(code), status);
327            assert_json_label(status, label);
328        }
329        assert_eq!(
330            NnsProposalStatus::from_code(99),
331            NnsProposalStatus::Unspecified
332        );
333    }
334
335    #[test]
336    fn proposal_reward_status_codes_and_labels_round_trip() {
337        for (status, code, label) in [
338            (NnsProposalRewardStatus::Unspecified, 0, "unspecified"),
339            (NnsProposalRewardStatus::AcceptVotes, 1, "accept-votes"),
340            (NnsProposalRewardStatus::ReadyToSettle, 2, "ready-to-settle"),
341            (NnsProposalRewardStatus::Settled, 3, "settled"),
342            (NnsProposalRewardStatus::Ineligible, 4, "ineligible"),
343        ] {
344            assert_eq!(status.code(), code);
345            assert_eq!(NnsProposalRewardStatus::from_code(code), status);
346            assert_json_label(status, label);
347        }
348        assert_eq!(
349            NnsProposalRewardStatus::from_code(99),
350            NnsProposalRewardStatus::Unspecified
351        );
352    }
353
354    #[test]
355    fn proposal_topic_codes_and_labels_round_trip() {
356        for (topic, code, label) in [
357            (NnsProposalTopic::Unspecified, 0, "unspecified"),
358            (NnsProposalTopic::NeuronManagement, 1, "neuron-management"),
359            (NnsProposalTopic::ExchangeRate, 2, "exchange-rate"),
360            (NnsProposalTopic::NetworkEconomics, 3, "network-economics"),
361            (NnsProposalTopic::Governance, 4, "governance"),
362            (NnsProposalTopic::NodeAdmin, 5, "node-admin"),
363            (
364                NnsProposalTopic::ParticipantManagement,
365                6,
366                "participant-management",
367            ),
368            (NnsProposalTopic::SubnetManagement, 7, "subnet-management"),
369            (
370                NnsProposalTopic::NetworkCanisterManagement,
371                8,
372                "network-canister-management",
373            ),
374            (NnsProposalTopic::Kyc, 9, "kyc"),
375            (
376                NnsProposalTopic::NodeProviderRewards,
377                10,
378                "node-provider-rewards",
379            ),
380            (
381                NnsProposalTopic::IcOsVersionDeployment,
382                12,
383                "ic-os-version-deployment",
384            ),
385            (
386                NnsProposalTopic::IcOsVersionElection,
387                13,
388                "ic-os-version-election",
389            ),
390            (
391                NnsProposalTopic::SnsAndCommunityFund,
392                14,
393                "sns-and-community-fund",
394            ),
395            (
396                NnsProposalTopic::ApiBoundaryNodeManagement,
397                15,
398                "api-boundary-node-management",
399            ),
400            (NnsProposalTopic::SubnetRental, 16, "subnet-rental"),
401            (
402                NnsProposalTopic::ApplicationCanisterManagement,
403                17,
404                "application-canister-management",
405            ),
406            (
407                NnsProposalTopic::ProtocolCanisterManagement,
408                18,
409                "protocol-canister-management",
410            ),
411        ] {
412            assert_eq!(topic.code(), code);
413            assert_eq!(NnsProposalTopic::from_code(code), topic);
414            assert_json_label(topic, label);
415        }
416        assert_eq!(
417            NnsProposalTopic::from_code(11),
418            NnsProposalTopic::Unspecified
419        );
420        assert_eq!(
421            NnsProposalTopic::from_code(99),
422            NnsProposalTopic::Unspecified
423        );
424    }
425
426    fn assert_json_label<T>(value: T, label: &str)
427    where
428        T: Copy + std::fmt::Debug + Eq + Serialize + serde::de::DeserializeOwned,
429    {
430        assert_eq!(
431            serde_json::to_string(&value).unwrap(),
432            format!("\"{label}\"")
433        );
434        assert_eq!(
435            serde_json::from_str::<T>(&format!("\"{label}\"")).unwrap(),
436            value
437        );
438    }
439}