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            $($(#[$variant_attr:meta])* $variant:ident = $code:literal => $label:literal,)*
15        }
16    ) => {
17        $(#[$enum_attr])*
18        pub enum $name {
19            $($(#[$variant_attr])* #[serde(rename = $label)] $variant,)*
20        }
21
22        impl $name {
23            /// Classify a native code, retaining the unspecified classification for unknown codes.
24            #[must_use]
25            pub const fn from_code(code: i32) -> Self {
26                match code {
27                    $($code => Self::$variant,)*
28                    _ => Self::Unspecified,
29                }
30            }
31
32            /// Return the canonical native code for this classification.
33            #[must_use]
34            pub const fn code(self) -> i32 {
35                match self { $(Self::$variant => $code,)* }
36            }
37
38            /// Return the stable JSON and text label.
39            #[must_use]
40            pub const fn as_str(self) -> &'static str {
41                match self { $(Self::$variant => $label,)* }
42            }
43        }
44    };
45}
46
47governance_code_enum! {
48    ///
49    /// NnsProposalStatus
50    ///
51    /// Native NNS Governance proposal decision status.
52    ///
53
54    #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
55    pub enum NnsProposalStatus {
56        /// Unspecified or unrecognized native status code.
57        Unspecified = 0 => "unspecified",
58        /// Proposal remains open for voting.
59        Open = 1 => "open",
60        /// Proposal was rejected.
61        Rejected = 2 => "rejected",
62        /// Proposal was adopted but has not completed execution.
63        Adopted = 3 => "adopted",
64        /// Proposal executed successfully.
65        Executed = 4 => "executed",
66        /// Proposal execution failed.
67        Failed = 5 => "failed",
68    }
69}
70
71governance_code_enum! {
72    ///
73    /// NnsProposalRewardStatus
74    ///
75    /// Native NNS Governance proposal reward-settlement status.
76    ///
77
78    #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
79    pub enum NnsProposalRewardStatus {
80        /// Unspecified or unrecognized native reward-status code.
81        Unspecified = 0 => "unspecified",
82        /// Proposal still accepts votes for reward purposes.
83        AcceptVotes = 1 => "accept-votes",
84        /// Proposal is ready for reward settlement.
85        ReadyToSettle = 2 => "ready-to-settle",
86        /// Proposal rewards have settled.
87        Settled = 3 => "settled",
88        /// Proposal is not eligible for voting rewards.
89        Ineligible = 4 => "ineligible",
90    }
91}
92
93governance_code_enum! {
94    ///
95    /// NnsProposalTopic
96    ///
97    /// Native NNS Governance proposal topic.
98    ///
99
100    #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
101    pub enum NnsProposalTopic {
102        /// Unspecified or unrecognized native topic code.
103        Unspecified = 0 => "unspecified",
104        /// Neuron management.
105        NeuronManagement = 1 => "neuron-management",
106        /// Exchange-rate management.
107        ExchangeRate = 2 => "exchange-rate",
108        /// Network economics.
109        NetworkEconomics = 3 => "network-economics",
110        /// Governance policy.
111        Governance = 4 => "governance",
112        /// Node administration.
113        NodeAdmin = 5 => "node-admin",
114        /// Participant management.
115        ParticipantManagement = 6 => "participant-management",
116        /// Subnet management.
117        SubnetManagement = 7 => "subnet-management",
118        /// Network-canister management.
119        NetworkCanisterManagement = 8 => "network-canister-management",
120        /// Know-your-customer policy.
121        Kyc = 9 => "kyc",
122        /// Node-provider rewards.
123        NodeProviderRewards = 10 => "node-provider-rewards",
124        /// IC OS version deployment.
125        IcOsVersionDeployment = 12 => "ic-os-version-deployment",
126        /// IC OS version election.
127        IcOsVersionElection = 13 => "ic-os-version-election",
128        /// SNS and Community Fund policy.
129        SnsAndCommunityFund = 14 => "sns-and-community-fund",
130        /// API boundary-node management.
131        ApiBoundaryNodeManagement = 15 => "api-boundary-node-management",
132        /// Subnet rental.
133        SubnetRental = 16 => "subnet-rental",
134        /// Application-canister management.
135        ApplicationCanisterManagement = 17 => "application-canister-management",
136        /// Protocol-canister management.
137        ProtocolCanisterManagement = 18 => "protocol-canister-management",
138    }
139}
140
141///
142/// NnsProposalListSort
143///
144/// Report-model sort selector for NNS proposal listings.
145///
146
147#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
148pub enum NnsProposalListSort {
149    /// Preserve the descending proposal-id order returned by Governance.
150    #[default]
151    Api,
152    /// Sort by proposal id.
153    Id,
154    /// Sort by the raw decision-status code.
155    Status,
156    /// Sort by the raw reward-settlement code.
157    RewardStatus,
158    /// Sort by the raw topic code.
159    Topic,
160    /// Sort by proposer neuron id.
161    Proposer,
162    /// Sort by proposal title.
163    Title,
164    /// Sort by the human-readable action classification.
165    Action,
166    /// Sort by affirmative voting power in the latest tally.
167    Yes,
168    /// Sort by negative voting power in the latest tally.
169    No,
170    /// Sort by total voting power in the latest tally.
171    TotalVotes,
172    /// Sort by the latest tally timestamp.
173    TallyTime,
174    /// Sort by total potential voting power.
175    VotingPower,
176    /// Sort by ballot count returned by Governance.
177    Ballots,
178    /// Sort by reject cost in ICP e8s.
179    RejectCost,
180    /// Sort by reward event round.
181    RewardRound,
182    /// Sort by proposal creation time.
183    Proposed,
184    /// Sort by voting deadline.
185    Deadline,
186    /// Sort by decision time.
187    Decided,
188    /// Sort by execution time.
189    Executed,
190    /// Sort by execution failure time.
191    Failed,
192}
193
194impl NnsProposalListSort {
195    /// Return the canonical filter or sort label used in reports.
196    #[must_use]
197    pub const fn as_str(self) -> &'static str {
198        match self {
199            Self::Api => "api",
200            Self::Id => "id",
201            Self::Status => "status",
202            Self::RewardStatus => "reward-status",
203            Self::Topic => "topic",
204            Self::Proposer => "proposer",
205            Self::Title => "title",
206            Self::Action => "action",
207            Self::Yes => "yes",
208            Self::No => "no",
209            Self::TotalVotes => "total-votes",
210            Self::TallyTime => "tally-time",
211            Self::VotingPower => "voting-power",
212            Self::Ballots => "ballots",
213            Self::RejectCost => "reject-cost",
214            Self::RewardRound => "reward-round",
215            Self::Proposed => "proposed",
216            Self::Deadline => "deadline",
217            Self::Decided => "decided",
218            Self::Executed => "executed",
219            Self::Failed => "failed",
220        }
221    }
222
223    /// Return the default direction for this sort key.
224    #[must_use]
225    pub const fn default_direction(self) -> SortDirection {
226        match self {
227            Self::Status
228            | Self::RewardStatus
229            | Self::Topic
230            | Self::Proposer
231            | Self::Title
232            | Self::Action => SortDirection::Asc,
233            _ => SortDirection::Desc,
234        }
235    }
236
237    /// Return whether this key applies a local sort direction.
238    #[must_use]
239    pub const fn uses_local_direction(self) -> bool {
240        !matches!(self, Self::Api)
241    }
242
243    /// Return `none` for API order or the selected local direction label.
244    #[must_use]
245    pub const fn direction_label(self, direction: SortDirection) -> &'static str {
246        match self {
247            Self::Api => "none",
248            _ => direction.as_str(),
249        }
250    }
251}
252
253///
254/// NnsProposalStatusFilter
255///
256/// Report-model status filter for bounded NNS proposal listings.
257///
258
259#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
260pub enum NnsProposalStatusFilter {
261    /// Include proposals without restricting this classification.
262    #[default]
263    Any,
264    /// Include only proposals still open for voting.
265    Open,
266    /// Include only rejected proposals.
267    Rejected,
268    /// Include only adopted proposals awaiting successful execution.
269    Adopted,
270    /// Include only successfully executed proposals.
271    Executed,
272    /// Include only proposals whose execution failed.
273    Failed,
274}
275
276///
277/// NnsProposalRewardStatusFilter
278///
279/// Report-model reward status filter for bounded NNS proposal listings.
280///
281
282#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
283pub enum NnsProposalRewardStatusFilter {
284    /// Include proposals without restricting this classification.
285    #[default]
286    Any,
287    /// Include only proposals accepting votes for rewards.
288    AcceptVotes,
289    /// Include only proposals ready for reward settlement.
290    ReadyToSettle,
291    /// Include only proposals with settled rewards.
292    Settled,
293    /// Include only proposals ineligible for voting rewards.
294    Ineligible,
295}
296
297impl NnsProposalRewardStatusFilter {
298    /// Return the canonical filter or sort label used in reports.
299    #[must_use]
300    pub const fn as_str(self) -> &'static str {
301        match self.classification() {
302            None => "any",
303            Some(value) => value.as_str(),
304        }
305    }
306
307    #[cfg(any(
308        feature = "nns-host",
309        all(feature = "canister", target_arch = "wasm32")
310    ))]
311    pub(in crate::nns) const fn governance_reward_status_code(self) -> Option<i32> {
312        match self.classification() {
313            None => None,
314            Some(value) => Some(value.code()),
315        }
316    }
317
318    const fn classification(self) -> Option<NnsProposalRewardStatus> {
319        match self {
320            Self::Any => None,
321            Self::AcceptVotes => Some(NnsProposalRewardStatus::AcceptVotes),
322            Self::ReadyToSettle => Some(NnsProposalRewardStatus::ReadyToSettle),
323            Self::Settled => Some(NnsProposalRewardStatus::Settled),
324            Self::Ineligible => Some(NnsProposalRewardStatus::Ineligible),
325        }
326    }
327}
328
329impl NnsProposalStatusFilter {
330    /// Return the canonical filter or sort label used in reports.
331    #[must_use]
332    pub const fn as_str(self) -> &'static str {
333        match self.classification() {
334            None => "any",
335            Some(value) => value.as_str(),
336        }
337    }
338
339    #[cfg(any(
340        feature = "nns-host",
341        all(feature = "canister", target_arch = "wasm32")
342    ))]
343    pub(in crate::nns) const fn governance_status_code(self) -> Option<i32> {
344        match self.classification() {
345            None => None,
346            Some(value) => Some(value.code()),
347        }
348    }
349
350    const fn classification(self) -> Option<NnsProposalStatus> {
351        match self {
352            Self::Any => None,
353            Self::Open => Some(NnsProposalStatus::Open),
354            Self::Rejected => Some(NnsProposalStatus::Rejected),
355            Self::Adopted => Some(NnsProposalStatus::Adopted),
356            Self::Executed => Some(NnsProposalStatus::Executed),
357            Self::Failed => Some(NnsProposalStatus::Failed),
358        }
359    }
360}
361
362///
363/// NnsProposalTopicFilter
364///
365/// Report-model topic filter for bounded NNS proposal listings.
366///
367
368#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
369pub enum NnsProposalTopicFilter {
370    /// Include proposals without restricting this classification.
371    #[default]
372    Any,
373    /// Include only proposals about neuron management.
374    NeuronManagement,
375    /// Include only proposals about exchange-rate management.
376    ExchangeRate,
377    /// Include only proposals about network economics.
378    NetworkEconomics,
379    /// Include only proposals about Governance policy.
380    Governance,
381    /// Include only proposals about node administration.
382    NodeAdmin,
383    /// Include only proposals about participant management.
384    ParticipantManagement,
385    /// Include only proposals about Subnet management.
386    SubnetManagement,
387    /// Include only proposals about network-canister management.
388    NetworkCanisterManagement,
389    /// Include only proposals about know-your-customer policy.
390    Kyc,
391    /// Include only proposals about node-provider rewards.
392    NodeProviderRewards,
393    /// Include only proposals about IC OS version deployment.
394    IcOsVersionDeployment,
395    /// Include only proposals about IC OS version election.
396    IcOsVersionElection,
397    /// Include only proposals about SNS and community-fund policy.
398    SnsAndCommunityFund,
399    /// Include only proposals about API boundary-node management.
400    ApiBoundaryNodeManagement,
401    /// Include only proposals about Subnet rental.
402    SubnetRental,
403    /// Include only proposals about application-canister management.
404    ApplicationCanisterManagement,
405    /// Include only proposals about protocol-canister management.
406    ProtocolCanisterManagement,
407}
408
409impl NnsProposalTopicFilter {
410    /// Return the canonical filter or sort label used in reports.
411    #[must_use]
412    pub const fn as_str(self) -> &'static str {
413        match self.classification() {
414            None => "any",
415            Some(value) => value.as_str(),
416        }
417    }
418
419    pub(in crate::nns) const fn topic_code(self) -> Option<i32> {
420        match self.classification() {
421            None => None,
422            Some(value) => Some(value.code()),
423        }
424    }
425
426    const fn classification(self) -> Option<NnsProposalTopic> {
427        match self {
428            Self::Any => None,
429            Self::NeuronManagement => Some(NnsProposalTopic::NeuronManagement),
430            Self::ExchangeRate => Some(NnsProposalTopic::ExchangeRate),
431            Self::NetworkEconomics => Some(NnsProposalTopic::NetworkEconomics),
432            Self::Governance => Some(NnsProposalTopic::Governance),
433            Self::NodeAdmin => Some(NnsProposalTopic::NodeAdmin),
434            Self::ParticipantManagement => Some(NnsProposalTopic::ParticipantManagement),
435            Self::SubnetManagement => Some(NnsProposalTopic::SubnetManagement),
436            Self::NetworkCanisterManagement => Some(NnsProposalTopic::NetworkCanisterManagement),
437            Self::Kyc => Some(NnsProposalTopic::Kyc),
438            Self::NodeProviderRewards => Some(NnsProposalTopic::NodeProviderRewards),
439            Self::IcOsVersionDeployment => Some(NnsProposalTopic::IcOsVersionDeployment),
440            Self::IcOsVersionElection => Some(NnsProposalTopic::IcOsVersionElection),
441            Self::SnsAndCommunityFund => Some(NnsProposalTopic::SnsAndCommunityFund),
442            Self::ApiBoundaryNodeManagement => Some(NnsProposalTopic::ApiBoundaryNodeManagement),
443            Self::SubnetRental => Some(NnsProposalTopic::SubnetRental),
444            Self::ApplicationCanisterManagement => {
445                Some(NnsProposalTopic::ApplicationCanisterManagement)
446            }
447            Self::ProtocolCanisterManagement => Some(NnsProposalTopic::ProtocolCanisterManagement),
448        }
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn proposal_status_codes_and_labels_round_trip() {
458        for (status, code, label) in [
459            (NnsProposalStatus::Unspecified, 0, "unspecified"),
460            (NnsProposalStatus::Open, 1, "open"),
461            (NnsProposalStatus::Rejected, 2, "rejected"),
462            (NnsProposalStatus::Adopted, 3, "adopted"),
463            (NnsProposalStatus::Executed, 4, "executed"),
464            (NnsProposalStatus::Failed, 5, "failed"),
465        ] {
466            assert_eq!(status.code(), code);
467            assert_eq!(NnsProposalStatus::from_code(code), status);
468            assert_json_label(status, label);
469        }
470        assert_eq!(
471            NnsProposalStatus::from_code(99),
472            NnsProposalStatus::Unspecified
473        );
474    }
475
476    #[test]
477    fn proposal_reward_status_codes_and_labels_round_trip() {
478        for (status, code, label) in [
479            (NnsProposalRewardStatus::Unspecified, 0, "unspecified"),
480            (NnsProposalRewardStatus::AcceptVotes, 1, "accept-votes"),
481            (NnsProposalRewardStatus::ReadyToSettle, 2, "ready-to-settle"),
482            (NnsProposalRewardStatus::Settled, 3, "settled"),
483            (NnsProposalRewardStatus::Ineligible, 4, "ineligible"),
484        ] {
485            assert_eq!(status.code(), code);
486            assert_eq!(NnsProposalRewardStatus::from_code(code), status);
487            assert_json_label(status, label);
488        }
489        assert_eq!(
490            NnsProposalRewardStatus::from_code(99),
491            NnsProposalRewardStatus::Unspecified
492        );
493    }
494
495    #[test]
496    fn proposal_topic_codes_and_labels_round_trip() {
497        for (topic, code, label) in [
498            (NnsProposalTopic::Unspecified, 0, "unspecified"),
499            (NnsProposalTopic::NeuronManagement, 1, "neuron-management"),
500            (NnsProposalTopic::ExchangeRate, 2, "exchange-rate"),
501            (NnsProposalTopic::NetworkEconomics, 3, "network-economics"),
502            (NnsProposalTopic::Governance, 4, "governance"),
503            (NnsProposalTopic::NodeAdmin, 5, "node-admin"),
504            (
505                NnsProposalTopic::ParticipantManagement,
506                6,
507                "participant-management",
508            ),
509            (NnsProposalTopic::SubnetManagement, 7, "subnet-management"),
510            (
511                NnsProposalTopic::NetworkCanisterManagement,
512                8,
513                "network-canister-management",
514            ),
515            (NnsProposalTopic::Kyc, 9, "kyc"),
516            (
517                NnsProposalTopic::NodeProviderRewards,
518                10,
519                "node-provider-rewards",
520            ),
521            (
522                NnsProposalTopic::IcOsVersionDeployment,
523                12,
524                "ic-os-version-deployment",
525            ),
526            (
527                NnsProposalTopic::IcOsVersionElection,
528                13,
529                "ic-os-version-election",
530            ),
531            (
532                NnsProposalTopic::SnsAndCommunityFund,
533                14,
534                "sns-and-community-fund",
535            ),
536            (
537                NnsProposalTopic::ApiBoundaryNodeManagement,
538                15,
539                "api-boundary-node-management",
540            ),
541            (NnsProposalTopic::SubnetRental, 16, "subnet-rental"),
542            (
543                NnsProposalTopic::ApplicationCanisterManagement,
544                17,
545                "application-canister-management",
546            ),
547            (
548                NnsProposalTopic::ProtocolCanisterManagement,
549                18,
550                "protocol-canister-management",
551            ),
552        ] {
553            assert_eq!(topic.code(), code);
554            assert_eq!(NnsProposalTopic::from_code(code), topic);
555            assert_json_label(topic, label);
556        }
557        assert_eq!(
558            NnsProposalTopic::from_code(11),
559            NnsProposalTopic::Unspecified
560        );
561        assert_eq!(
562            NnsProposalTopic::from_code(99),
563            NnsProposalTopic::Unspecified
564        );
565    }
566
567    fn assert_json_label<T>(value: T, label: &str)
568    where
569        T: Copy + std::fmt::Debug + Eq + Serialize + serde::de::DeserializeOwned,
570    {
571        assert_eq!(
572            serde_json::to_string(&value).unwrap(),
573            format!("\"{label}\"")
574        );
575        assert_eq!(
576            serde_json::from_str::<T>(&format!("\"{label}\"")).unwrap(),
577            value
578        );
579    }
580}