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    pub enum NnsNeuronState {
185        /// Governance supplied the unspecified state code.
186        Unspecified = 0 => "unspecified",
187        /// Neuron is not dissolving.
188        NotDissolving = 1 => "not-dissolving",
189        /// Neuron is dissolving.
190        Dissolving = 2 => "dissolving",
191        /// Neuron has dissolved.
192        Dissolved = 3 => "dissolved",
193        /// Neuron is spawning.
194        Spawning = 4 => "spawning",
195        ; unknown Unknown(i32),
196    }
197}
198
199native_optional_code_classification! {
200    "visibility";
201    ///
202    /// NnsNeuronVisibility
203    ///
204    /// Native optional NNS Governance neuron visibility with unknown evidence retained.
205    ///
206    pub enum NnsNeuronVisibility {
207        /// Governance omitted the optional visibility code.
208        Unknown,
209        /// Governance supplied the unspecified visibility code.
210        Unspecified = 0 => "unspecified",
211        /// Neuron is private.
212        Private = 1 => "private",
213        /// Neuron is public.
214        Public = 2 => "public",
215        ; unknown UnknownCode(i32),
216    }
217}
218
219native_optional_code_classification! {
220    "type";
221    ///
222    /// NnsNeuronType
223    ///
224    /// Native optional NNS Governance neuron type with unknown evidence retained.
225    ///
226    pub enum NnsNeuronType {
227        /// Governance omitted the optional neuron-type code.
228        Unknown,
229        /// Governance supplied the unspecified neuron-type code.
230        Unspecified = 0 => "unspecified",
231        /// Seed neuron.
232        Seed = 1 => "seed",
233        /// Early-contributor-token neuron.
234        Ect = 2 => "ect",
235        ; unknown UnknownCode(i32),
236    }
237}
238
239native_code_classification! {
240    "vote";
241    ///
242    /// NnsNeuronVote
243    ///
244    /// Native NNS Governance neuron-ballot vote with unrecognized codes retained.
245    ///
246    pub enum NnsNeuronVote {
247        /// Governance supplied the unspecified vote code.
248        Unspecified = 0 => "unspecified",
249        /// Affirmative ballot.
250        Yes = 1 => "yes",
251        /// Negative ballot.
252        No = 2 => "no",
253        ; unknown Unknown(i32),
254    }
255}
256
257fn deserialize_code_label<'de, D, T>(
258    deserializer: D,
259    classification: &str,
260    known: fn(&str) -> Option<T>,
261    unknown: fn(i32) -> T,
262) -> Result<T, D::Error>
263where
264    D: serde::Deserializer<'de>,
265{
266    use serde::de::Error as _;
267
268    let label = <String as serde::Deserialize>::deserialize(deserializer)?;
269    if let Some(value) = known(&label) {
270        return Ok(value);
271    }
272    let code = parse_unknown_code(&label).ok_or_else(|| {
273        D::Error::custom(format!(
274            "invalid NNS neuron {classification} label {label:?}"
275        ))
276    })?;
277    Ok(unknown(code))
278}
279
280fn parse_unknown_code(label: &str) -> Option<i32> {
281    let code = label
282        .strip_prefix("unknown(")?
283        .strip_suffix(')')?
284        .parse::<i32>()
285        .ok()?;
286    (label == format!("unknown({code})")).then_some(code)
287}
288
289#[cfg(test)]
290mod tests {
291    use super::{NnsNeuronState, NnsNeuronType, NnsNeuronVisibility, NnsNeuronVote};
292
293    #[test]
294    fn required_classifications_preserve_codes_and_labels() {
295        for (code, state, label) in [
296            (0, NnsNeuronState::Unspecified, "unspecified"),
297            (1, NnsNeuronState::NotDissolving, "not-dissolving"),
298            (2, NnsNeuronState::Dissolving, "dissolving"),
299            (3, NnsNeuronState::Dissolved, "dissolved"),
300            (4, NnsNeuronState::Spawning, "spawning"),
301            (99, NnsNeuronState::Unknown(99), "unknown(99)"),
302        ] {
303            assert_eq!(NnsNeuronState::from_code(code), state);
304            assert_eq!(state.code(), code);
305            assert_eq!(state.to_string(), label);
306            assert_eq!(serde_json::to_value(state).expect("serialize state"), label);
307        }
308        for (code, vote, label) in [
309            (0, NnsNeuronVote::Unspecified, "unspecified"),
310            (1, NnsNeuronVote::Yes, "yes"),
311            (2, NnsNeuronVote::No, "no"),
312            (99, NnsNeuronVote::Unknown(99), "unknown(99)"),
313        ] {
314            assert_eq!(NnsNeuronVote::from_code(code), vote);
315            assert_eq!(vote.code(), code);
316            assert_eq!(vote.to_string(), label);
317            assert_eq!(serde_json::to_value(vote).expect("serialize vote"), label);
318        }
319    }
320
321    #[test]
322    fn optional_classifications_distinguish_absent_and_unknown_codes() {
323        for (code, visibility, label) in [
324            (None, NnsNeuronVisibility::Unknown, "unknown"),
325            (Some(0), NnsNeuronVisibility::Unspecified, "unspecified"),
326            (Some(1), NnsNeuronVisibility::Private, "private"),
327            (Some(2), NnsNeuronVisibility::Public, "public"),
328            (
329                Some(99),
330                NnsNeuronVisibility::UnknownCode(99),
331                "unknown(99)",
332            ),
333        ] {
334            assert_eq!(NnsNeuronVisibility::from_code(code), visibility);
335            assert_eq!(visibility.code(), code);
336            assert_eq!(visibility.to_string(), label);
337            assert_eq!(
338                serde_json::to_value(visibility).expect("serialize visibility"),
339                label
340            );
341        }
342        for (code, neuron_type, label) in [
343            (None, NnsNeuronType::Unknown, "unknown"),
344            (Some(0), NnsNeuronType::Unspecified, "unspecified"),
345            (Some(1), NnsNeuronType::Seed, "seed"),
346            (Some(2), NnsNeuronType::Ect, "ect"),
347            (Some(99), NnsNeuronType::UnknownCode(99), "unknown(99)"),
348        ] {
349            assert_eq!(NnsNeuronType::from_code(code), neuron_type);
350            assert_eq!(neuron_type.code(), code);
351            assert_eq!(neuron_type.to_string(), label);
352            assert_eq!(
353                serde_json::to_value(neuron_type).expect("serialize neuron type"),
354                label
355            );
356        }
357    }
358
359    #[test]
360    fn classifications_read_canonical_cache_labels_only() {
361        assert_eq!(
362            serde_json::from_str::<NnsNeuronState>("\"unknown(-9)\"")
363                .expect("deserialize unknown state"),
364            NnsNeuronState::Unknown(-9)
365        );
366        assert_eq!(
367            serde_json::from_str::<NnsNeuronVisibility>("\"unknown\"")
368                .expect("deserialize absent visibility"),
369            NnsNeuronVisibility::Unknown
370        );
371        assert_eq!(
372            serde_json::from_str::<NnsNeuronType>("\"unknown(9)\"")
373                .expect("deserialize unknown neuron type"),
374            NnsNeuronType::UnknownCode(9)
375        );
376        assert_eq!(
377            serde_json::from_str::<NnsNeuronVote>("\"yes\"").expect("deserialize known vote"),
378            NnsNeuronVote::Yes
379        );
380        assert!(serde_json::from_str::<NnsNeuronState>("\"unknown(+9)\"").is_err());
381        assert!(serde_json::from_str::<NnsNeuronVote>("\"maybe\"").is_err());
382    }
383}