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        #[cfg(feature = "host")]
71        impl<'de> serde::Deserialize<'de> for $name {
72            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
73            where
74                D: serde::Deserializer<'de>,
75            {
76                deserialize_code_label(
77                    deserializer,
78                    $classification,
79                    |label| match label {
80                        $($label => Some(Self::$variant),)+
81                        _ => None,
82                    },
83                    Self::$unknown,
84                )
85            }
86        }
87    };
88}
89
90macro_rules! native_optional_code_classification {
91    (
92        $classification:literal;
93        $(#[$enum_meta:meta])*
94        pub enum $name:ident {
95            $(#[$absent_meta:meta])*
96            $absent:ident,
97            $(
98                $(#[$variant_meta:meta])*
99                $variant:ident = $code:literal => $label:literal,
100            )+
101            ; unknown $unknown:ident(i32),
102        }
103    ) => {
104        $(#[$enum_meta])*
105        #[derive(Clone, Copy, Debug, Eq, PartialEq)]
106        pub enum $name {
107            $(#[$absent_meta])*
108            $absent,
109            $(
110                $(#[$variant_meta])*
111                $variant,
112            )+
113            /// Governance supplied an unrecognized native code.
114            $unknown(i32),
115        }
116
117        impl $name {
118            /// Classify one optional raw native code without discarding unknown evidence.
119            #[must_use]
120            pub const fn from_code(code: Option<i32>) -> Self {
121                match code {
122                    None => Self::$absent,
123                    $(Some($code) => Self::$variant,)+
124                    Some(code) => Self::$unknown(code),
125                }
126            }
127
128            /// Return the exact optional native code represented by this classification.
129            #[must_use]
130            pub const fn code(self) -> Option<i32> {
131                match self {
132                    Self::$absent => None,
133                    $(Self::$variant => Some($code),)+
134                    Self::$unknown(code) => Some(code),
135                }
136            }
137        }
138
139        impl fmt::Display for $name {
140            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141                match self {
142                    Self::$absent => formatter.write_str("unknown"),
143                    $(Self::$variant => formatter.write_str($label),)+
144                    Self::$unknown(code) => write!(formatter, "unknown({code})"),
145                }
146            }
147        }
148
149        impl serde::Serialize for $name {
150            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
151            where
152                S: serde::Serializer,
153            {
154                serializer.collect_str(self)
155            }
156        }
157
158        #[cfg(feature = "host")]
159        impl<'de> serde::Deserialize<'de> for $name {
160            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161            where
162                D: serde::Deserializer<'de>,
163            {
164                deserialize_code_label(
165                    deserializer,
166                    $classification,
167                    |label| match label {
168                        "unknown" => Some(Self::$absent),
169                        $($label => Some(Self::$variant),)+
170                        _ => None,
171                    },
172                    Self::$unknown,
173                )
174            }
175        }
176    };
177}
178
179native_code_classification! {
180    "state";
181    ///
182    /// NnsNeuronState
183    ///
184    /// Native NNS Governance neuron state with unrecognized codes retained.
185    ///
186    pub enum NnsNeuronState {
187        /// Governance supplied the unspecified state code.
188        Unspecified = 0 => "unspecified",
189        /// Neuron is not dissolving.
190        NotDissolving = 1 => "not-dissolving",
191        /// Neuron is dissolving.
192        Dissolving = 2 => "dissolving",
193        /// Neuron has dissolved.
194        Dissolved = 3 => "dissolved",
195        /// Neuron is spawning.
196        Spawning = 4 => "spawning",
197        ; unknown Unknown(i32),
198    }
199}
200
201native_optional_code_classification! {
202    "visibility";
203    ///
204    /// NnsNeuronVisibility
205    ///
206    /// Native optional NNS Governance neuron visibility with unknown evidence retained.
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    pub enum NnsNeuronType {
229        /// Governance omitted the optional neuron-type code.
230        Unknown,
231        /// Governance supplied the unspecified neuron-type code.
232        Unspecified = 0 => "unspecified",
233        /// Seed neuron.
234        Seed = 1 => "seed",
235        /// Early-contributor-token neuron.
236        Ect = 2 => "ect",
237        ; unknown UnknownCode(i32),
238    }
239}
240
241native_code_classification! {
242    "vote";
243    ///
244    /// NnsNeuronVote
245    ///
246    /// Native NNS Governance neuron-ballot vote with unrecognized codes retained.
247    ///
248    pub enum NnsNeuronVote {
249        /// Governance supplied the unspecified vote code.
250        Unspecified = 0 => "unspecified",
251        /// Affirmative ballot.
252        Yes = 1 => "yes",
253        /// Negative ballot.
254        No = 2 => "no",
255        ; unknown Unknown(i32),
256    }
257}
258
259#[cfg(feature = "host")]
260fn deserialize_code_label<'de, D, T>(
261    deserializer: D,
262    classification: &str,
263    known: fn(&str) -> Option<T>,
264    unknown: fn(i32) -> T,
265) -> Result<T, D::Error>
266where
267    D: serde::Deserializer<'de>,
268{
269    use serde::de::Error as _;
270
271    let label = <String as serde::Deserialize>::deserialize(deserializer)?;
272    if let Some(value) = known(&label) {
273        return Ok(value);
274    }
275    let code = parse_unknown_code(&label).ok_or_else(|| {
276        D::Error::custom(format!(
277            "invalid NNS neuron {classification} label {label:?}"
278        ))
279    })?;
280    Ok(unknown(code))
281}
282
283#[cfg(feature = "host")]
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    #[cfg(feature = "host")]
364    #[test]
365    fn classifications_read_canonical_cache_labels_only() {
366        assert_eq!(
367            serde_json::from_str::<NnsNeuronState>("\"unknown(-9)\"")
368                .expect("deserialize unknown state"),
369            NnsNeuronState::Unknown(-9)
370        );
371        assert_eq!(
372            serde_json::from_str::<NnsNeuronVisibility>("\"unknown\"")
373                .expect("deserialize absent visibility"),
374            NnsNeuronVisibility::Unknown
375        );
376        assert_eq!(
377            serde_json::from_str::<NnsNeuronType>("\"unknown(9)\"")
378                .expect("deserialize unknown neuron type"),
379            NnsNeuronType::UnknownCode(9)
380        );
381        assert_eq!(
382            serde_json::from_str::<NnsNeuronVote>("\"yes\"").expect("deserialize known vote"),
383            NnsNeuronVote::Yes
384        );
385        assert!(serde_json::from_str::<NnsNeuronState>("\"unknown(+9)\"").is_err());
386        assert!(serde_json::from_str::<NnsNeuronVote>("\"maybe\"").is_err());
387    }
388}