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