Skip to main content

ic_query/nns/neuron/report/
classification.rs

1//! Module: nns::neuron::report::classification
2//!
3//! Responsibility: define native NNS neuron classifications and their stable labels.
4//! Does not own: raw Governance DTOs, report assembly, or text layout.
5//! Boundary: retains unrecognized numeric codes instead of collapsing protocol evidence.
6
7use std::fmt;
8
9macro_rules! native_code_classification {
10    (
11        $classification:literal;
12        $(#[$enum_meta:meta])*
13        pub enum $name:ident {
14            $(
15                $(#[$variant_meta:meta])*
16                $variant:ident = $code:literal => $label:literal,
17            )+
18            ; unknown $unknown:ident(i32),
19        }
20    ) => {
21        $(#[$enum_meta])*
22        #[derive(Clone, Copy, Debug, Eq, PartialEq)]
23        pub enum $name {
24            $(
25                $(#[$variant_meta])*
26                $variant,
27            )+
28            /// Governance supplied an unrecognized native code.
29            $unknown(i32),
30        }
31
32        impl $name {
33            /// Classify one raw native code without discarding unknown evidence.
34            #[must_use]
35            pub const fn from_code(code: i32) -> Self {
36                match code {
37                    $($code => Self::$variant,)+
38                    code => Self::$unknown(code),
39                }
40            }
41
42            /// Return the exact native code represented by this classification.
43            #[must_use]
44            pub const fn code(self) -> i32 {
45                match self {
46                    $(Self::$variant => $code,)+
47                    Self::$unknown(code) => code,
48                }
49            }
50        }
51
52        impl fmt::Display for $name {
53            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54                match self {
55                    $(Self::$variant => formatter.write_str($label),)+
56                    Self::$unknown(code) => write!(formatter, "unknown({code})"),
57                }
58            }
59        }
60
61        impl serde::Serialize for $name {
62            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
63            where
64                S: serde::Serializer,
65            {
66                serializer.collect_str(self)
67            }
68        }
69
70        impl<'de> serde::Deserialize<'de> for $name {
71            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72            where
73                D: serde::Deserializer<'de>,
74            {
75                deserialize_code_label(
76                    deserializer,
77                    $classification,
78                    |label| match label {
79                        $($label => Some(Self::$variant),)+
80                        _ => None,
81                    },
82                    Self::$unknown,
83                )
84            }
85        }
86    };
87}
88
89macro_rules! native_optional_code_classification {
90    (
91        $classification:literal;
92        $(#[$enum_meta:meta])*
93        pub enum $name:ident {
94            $(#[$absent_meta:meta])*
95            $absent:ident,
96            $(
97                $(#[$variant_meta:meta])*
98                $variant:ident = $code:literal => $label:literal,
99            )+
100            ; unknown $unknown:ident(i32),
101        }
102    ) => {
103        $(#[$enum_meta])*
104        #[derive(Clone, Copy, Debug, Eq, PartialEq)]
105        pub enum $name {
106            $(#[$absent_meta])*
107            $absent,
108            $(
109                $(#[$variant_meta])*
110                $variant,
111            )+
112            /// Governance supplied an unrecognized native code.
113            $unknown(i32),
114        }
115
116        impl $name {
117            /// Classify one optional raw native code without discarding unknown evidence.
118            #[must_use]
119            pub const fn from_code(code: Option<i32>) -> Self {
120                match code {
121                    None => Self::$absent,
122                    $(Some($code) => Self::$variant,)+
123                    Some(code) => Self::$unknown(code),
124                }
125            }
126
127            /// Return the exact optional native code represented by this classification.
128            #[must_use]
129            pub const fn code(self) -> Option<i32> {
130                match self {
131                    Self::$absent => None,
132                    $(Self::$variant => Some($code),)+
133                    Self::$unknown(code) => Some(code),
134                }
135            }
136        }
137
138        impl fmt::Display for $name {
139            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140                match self {
141                    Self::$absent => formatter.write_str("unknown"),
142                    $(Self::$variant => formatter.write_str($label),)+
143                    Self::$unknown(code) => write!(formatter, "unknown({code})"),
144                }
145            }
146        }
147
148        impl serde::Serialize for $name {
149            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150            where
151                S: serde::Serializer,
152            {
153                serializer.collect_str(self)
154            }
155        }
156
157        impl<'de> serde::Deserialize<'de> for $name {
158            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159            where
160                D: serde::Deserializer<'de>,
161            {
162                deserialize_code_label(
163                    deserializer,
164                    $classification,
165                    |label| match label {
166                        "unknown" => Some(Self::$absent),
167                        $($label => Some(Self::$variant),)+
168                        _ => None,
169                    },
170                    Self::$unknown,
171                )
172            }
173        }
174    };
175}
176
177native_code_classification! {
178    "state";
179    ///
180    /// NnsNeuronState
181    ///
182    /// Native NNS Governance neuron state with unrecognized codes retained.
183    ///
184
185    pub enum NnsNeuronState {
186        /// Governance supplied the unspecified state code.
187        Unspecified = 0 => "unspecified",
188        /// Neuron is not dissolving.
189        NotDissolving = 1 => "not-dissolving",
190        /// Neuron is dissolving.
191        Dissolving = 2 => "dissolving",
192        /// Neuron has dissolved.
193        Dissolved = 3 => "dissolved",
194        /// Neuron is spawning.
195        Spawning = 4 => "spawning",
196        ; unknown Unknown(i32),
197    }
198}
199
200native_optional_code_classification! {
201    "visibility";
202    ///
203    /// NnsNeuronVisibility
204    ///
205    /// Native optional NNS Governance neuron visibility with unknown evidence retained.
206    ///
207
208    pub enum NnsNeuronVisibility {
209        /// Governance omitted the optional visibility code.
210        Unknown,
211        /// Governance supplied the unspecified visibility code.
212        Unspecified = 0 => "unspecified",
213        /// Neuron is private.
214        Private = 1 => "private",
215        /// Neuron is public.
216        Public = 2 => "public",
217        ; unknown UnknownCode(i32),
218    }
219}
220
221native_optional_code_classification! {
222    "type";
223    ///
224    /// NnsNeuronType
225    ///
226    /// Native optional NNS Governance neuron type with unknown evidence retained.
227    ///
228
229    pub enum NnsNeuronType {
230        /// Governance omitted the optional neuron-type code.
231        Unknown,
232        /// Governance supplied the unspecified neuron-type code.
233        Unspecified = 0 => "unspecified",
234        /// Seed neuron.
235        Seed = 1 => "seed",
236        /// Early-contributor-token neuron.
237        Ect = 2 => "ect",
238        ; unknown UnknownCode(i32),
239    }
240}
241
242native_code_classification! {
243    "vote";
244    ///
245    /// NnsNeuronVote
246    ///
247    /// Native NNS Governance neuron-ballot vote with unrecognized codes retained.
248    ///
249
250    pub enum NnsNeuronVote {
251        /// Governance supplied the unspecified vote code.
252        Unspecified = 0 => "unspecified",
253        /// Affirmative ballot.
254        Yes = 1 => "yes",
255        /// Negative ballot.
256        No = 2 => "no",
257        ; unknown Unknown(i32),
258    }
259}
260
261fn deserialize_code_label<'de, D, T>(
262    deserializer: D,
263    classification: &str,
264    known: fn(&str) -> Option<T>,
265    unknown: fn(i32) -> T,
266) -> Result<T, D::Error>
267where
268    D: serde::Deserializer<'de>,
269{
270    use serde::de::Error as _;
271
272    let label = <String as serde::Deserialize>::deserialize(deserializer)?;
273    if let Some(value) = known(&label) {
274        return Ok(value);
275    }
276    let code = parse_unknown_code(&label).ok_or_else(|| {
277        D::Error::custom(format!(
278            "invalid NNS neuron {classification} label {label:?}"
279        ))
280    })?;
281    Ok(unknown(code))
282}
283
284fn parse_unknown_code(label: &str) -> Option<i32> {
285    let code = label
286        .strip_prefix("unknown(")?
287        .strip_suffix(')')?
288        .parse::<i32>()
289        .ok()?;
290    (label == format!("unknown({code})")).then_some(code)
291}
292
293#[cfg(test)]
294mod tests {
295    use super::{NnsNeuronState, NnsNeuronType, NnsNeuronVisibility, NnsNeuronVote};
296
297    #[test]
298    fn required_classifications_preserve_codes_and_labels() {
299        for (code, state, label) in [
300            (0, NnsNeuronState::Unspecified, "unspecified"),
301            (1, NnsNeuronState::NotDissolving, "not-dissolving"),
302            (2, NnsNeuronState::Dissolving, "dissolving"),
303            (3, NnsNeuronState::Dissolved, "dissolved"),
304            (4, NnsNeuronState::Spawning, "spawning"),
305            (99, NnsNeuronState::Unknown(99), "unknown(99)"),
306        ] {
307            assert_eq!(NnsNeuronState::from_code(code), state);
308            assert_eq!(state.code(), code);
309            assert_eq!(state.to_string(), label);
310            assert_eq!(serde_json::to_value(state).expect("serialize state"), label);
311        }
312        for (code, vote, label) in [
313            (0, NnsNeuronVote::Unspecified, "unspecified"),
314            (1, NnsNeuronVote::Yes, "yes"),
315            (2, NnsNeuronVote::No, "no"),
316            (99, NnsNeuronVote::Unknown(99), "unknown(99)"),
317        ] {
318            assert_eq!(NnsNeuronVote::from_code(code), vote);
319            assert_eq!(vote.code(), code);
320            assert_eq!(vote.to_string(), label);
321            assert_eq!(serde_json::to_value(vote).expect("serialize vote"), label);
322        }
323    }
324
325    #[test]
326    fn optional_classifications_distinguish_absent_and_unknown_codes() {
327        for (code, visibility, label) in [
328            (None, NnsNeuronVisibility::Unknown, "unknown"),
329            (Some(0), NnsNeuronVisibility::Unspecified, "unspecified"),
330            (Some(1), NnsNeuronVisibility::Private, "private"),
331            (Some(2), NnsNeuronVisibility::Public, "public"),
332            (
333                Some(99),
334                NnsNeuronVisibility::UnknownCode(99),
335                "unknown(99)",
336            ),
337        ] {
338            assert_eq!(NnsNeuronVisibility::from_code(code), visibility);
339            assert_eq!(visibility.code(), code);
340            assert_eq!(visibility.to_string(), label);
341            assert_eq!(
342                serde_json::to_value(visibility).expect("serialize visibility"),
343                label
344            );
345        }
346        for (code, neuron_type, label) in [
347            (None, NnsNeuronType::Unknown, "unknown"),
348            (Some(0), NnsNeuronType::Unspecified, "unspecified"),
349            (Some(1), NnsNeuronType::Seed, "seed"),
350            (Some(2), NnsNeuronType::Ect, "ect"),
351            (Some(99), NnsNeuronType::UnknownCode(99), "unknown(99)"),
352        ] {
353            assert_eq!(NnsNeuronType::from_code(code), neuron_type);
354            assert_eq!(neuron_type.code(), code);
355            assert_eq!(neuron_type.to_string(), label);
356            assert_eq!(
357                serde_json::to_value(neuron_type).expect("serialize neuron type"),
358                label
359            );
360        }
361    }
362
363    #[test]
364    fn classifications_read_canonical_cache_labels_only() {
365        assert_eq!(
366            serde_json::from_str::<NnsNeuronState>("\"unknown(-9)\"")
367                .expect("deserialize unknown state"),
368            NnsNeuronState::Unknown(-9)
369        );
370        assert_eq!(
371            serde_json::from_str::<NnsNeuronVisibility>("\"unknown\"")
372                .expect("deserialize absent visibility"),
373            NnsNeuronVisibility::Unknown
374        );
375        assert_eq!(
376            serde_json::from_str::<NnsNeuronType>("\"unknown(9)\"")
377                .expect("deserialize unknown neuron type"),
378            NnsNeuronType::UnknownCode(9)
379        );
380        assert_eq!(
381            serde_json::from_str::<NnsNeuronVote>("\"yes\"").expect("deserialize known vote"),
382            NnsNeuronVote::Yes
383        );
384        assert!(serde_json::from_str::<NnsNeuronState>("\"unknown(+9)\"").is_err());
385        assert!(serde_json::from_str::<NnsNeuronVote>("\"maybe\"").is_err());
386    }
387}