Skip to main content

dpp/voting/vote_choices/resource_vote_choice/
mod.rs

1#[cfg(feature = "json-conversion")]
2use crate::serialization::JsonConvertible;
3#[cfg(feature = "value-conversion")]
4use crate::serialization::ValueConvertible;
5use crate::voting::vote_choices::resource_vote_choice::ResourceVoteChoice::{
6    Abstain, Lock, TowardsIdentity,
7};
8use crate::ProtocolError;
9use bincode::{Decode, DecodeUntrusted, Encode};
10use platform_value::Identifier;
11#[cfg(feature = "serde-conversion")]
12use serde::{Deserialize, Serialize};
13use std::fmt;
14
15/// A resource votes is a votes determining what we should do with a contested resource.
16/// For example Alice and Bob both want the username "Malaka"
17/// Some would vote for Alice to get it by putting in her Identifier.
18/// Some would vote for Bob to get it by putting in Bob's Identifier.
19/// Let's say someone voted, but is now not quite sure of their votes, they can abstain.
20/// Lock is there to signal that the shared resource should be given to no one.
21/// In this case Malaka might have a bad connotation in Greek, hence some might votes to Lock
22/// the name.
23///
24#[derive(
25    Debug, Clone, Copy, Encode, Decode, Ord, Eq, PartialOrd, PartialEq, Default, DecodeUntrusted,
26)]
27// Custom `Serialize` / `Deserialize` below — `derive(Serialize, Deserialize)`
28// can't produce the desired flat wire shape because the `TowardsIdentity`
29// variant wraps `Identifier` (a tuple struct that serializes as a base58
30// string, not a map), so internal tagging doesn't apply. The custom impl
31// emits a flat `{"$type": ..., "identity": ...}` shape with a synthesized
32// `identity` field name. Bincode `Encode` / `Decode` derives are untouched
33// (consensus binary format is unaffected).
34#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
35pub enum ResourceVoteChoice {
36    TowardsIdentity(Identifier),
37    #[default]
38    Abstain,
39    Lock,
40}
41
42#[cfg(feature = "serde-conversion")]
43impl Serialize for ResourceVoteChoice {
44    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
45        use serde::ser::SerializeMap;
46        match self {
47            ResourceVoteChoice::TowardsIdentity(id) => {
48                let mut m = serializer.serialize_map(Some(2))?;
49                m.serialize_entry("$type", "towardsIdentity")?;
50                m.serialize_entry("identity", id)?;
51                m.end()
52            }
53            ResourceVoteChoice::Abstain => {
54                let mut m = serializer.serialize_map(Some(1))?;
55                m.serialize_entry("$type", "abstain")?;
56                m.end()
57            }
58            ResourceVoteChoice::Lock => {
59                let mut m = serializer.serialize_map(Some(1))?;
60                m.serialize_entry("$type", "lock")?;
61                m.end()
62            }
63        }
64    }
65}
66
67#[cfg(feature = "serde-conversion")]
68impl<'de> Deserialize<'de> for ResourceVoteChoice {
69    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
70        use serde::de::{self, MapAccess, Visitor};
71
72        struct V;
73
74        impl<'de> Visitor<'de> for V {
75            type Value = ResourceVoteChoice;
76
77            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
78                f.write_str("ResourceVoteChoice as a map with `type` discriminator")
79            }
80
81            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
82                let mut variant: Option<String> = None;
83                let mut identity: Option<Identifier> = None;
84
85                while let Some(key) = map.next_key::<String>()? {
86                    match key.as_str() {
87                        "$type" => {
88                            if variant.is_some() {
89                                return Err(de::Error::duplicate_field("$type"));
90                            }
91                            variant = Some(map.next_value()?);
92                        }
93                        "identity" => {
94                            if identity.is_some() {
95                                return Err(de::Error::duplicate_field("identity"));
96                            }
97                            identity = Some(map.next_value()?);
98                        }
99                        _ => {
100                            let _: serde::de::IgnoredAny = map.next_value()?;
101                        }
102                    }
103                }
104
105                let variant = variant.ok_or_else(|| de::Error::missing_field("$type"))?;
106                match variant.as_str() {
107                    "towardsIdentity" => {
108                        let id = identity.ok_or_else(|| de::Error::missing_field("identity"))?;
109                        Ok(ResourceVoteChoice::TowardsIdentity(id))
110                    }
111                    "abstain" => Ok(ResourceVoteChoice::Abstain),
112                    "lock" => Ok(ResourceVoteChoice::Lock),
113                    other => Err(de::Error::unknown_variant(
114                        other,
115                        &["towardsIdentity", "abstain", "lock"],
116                    )),
117                }
118            }
119        }
120
121        deserializer.deserialize_map(V)
122    }
123}
124
125impl fmt::Display for ResourceVoteChoice {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match self {
128            ResourceVoteChoice::TowardsIdentity(identifier) => {
129                write!(f, "TowardsIdentity({})", identifier)
130            }
131            ResourceVoteChoice::Abstain => write!(f, "Abstain"),
132            ResourceVoteChoice::Lock => write!(f, "Lock"),
133        }
134    }
135}
136
137// Manual impl because ResourceVoteChoice is a flat enum (not versioned V0/V1).
138#[cfg(feature = "json-conversion")]
139impl JsonConvertible for ResourceVoteChoice {}
140
141#[cfg(all(test, feature = "json-conversion"))]
142mod tests {
143    use super::*;
144    use crate::serialization::JsonConvertible;
145
146    #[test]
147    fn resource_vote_choice_towards_identity_json_round_trip() {
148        let id = Identifier::from([0x42u8; 32]);
149        let choice = ResourceVoteChoice::TowardsIdentity(id);
150
151        let json = choice.to_json().expect("to_json should succeed");
152        let json_str = serde_json::to_string(&json).unwrap();
153        let expected_base58 = id.to_string(platform_value::string_encoding::Encoding::Base58);
154        assert!(
155            json_str.contains(&expected_base58),
156            "JSON should contain base58 identifier {}, got: {}",
157            expected_base58,
158            json_str
159        );
160
161        let restored = ResourceVoteChoice::from_json(json).expect("from_json should succeed");
162        assert_eq!(choice, restored);
163    }
164
165    #[test]
166    fn resource_vote_choice_abstain_json_round_trip() {
167        let choice = ResourceVoteChoice::Abstain;
168        let json = choice.to_json().expect("to_json should succeed");
169        let restored = ResourceVoteChoice::from_json(json).expect("from_json should succeed");
170        assert_eq!(choice, restored);
171    }
172
173    #[test]
174    fn resource_vote_choice_lock_json_round_trip() {
175        let choice = ResourceVoteChoice::Lock;
176        let json = choice.to_json().expect("to_json should succeed");
177        let restored = ResourceVoteChoice::from_json(json).expect("from_json should succeed");
178        assert_eq!(choice, restored);
179    }
180}
181
182impl TryFrom<(i32, Option<Vec<u8>>)> for ResourceVoteChoice {
183    type Error = ProtocolError;
184
185    fn try_from(value: (i32, Option<Vec<u8>>)) -> Result<Self, Self::Error> {
186        match value.0 {
187            0 => Ok(TowardsIdentity(value.1.ok_or(ProtocolError::DecodingError("identifier needed when trying to cast from an i32 to a resource vote choice".to_string()))?.try_into()?)),
188            1 => Ok(Abstain),
189            2 => Ok(Lock),
190            n => Err(ProtocolError::DecodingError(format!("identifier must be 0, 1, or 2, got {}", n)))
191        }
192    }
193}
194
195#[cfg(all(
196    test,
197    feature = "json-conversion",
198    feature = "value-conversion",
199    feature = "serde-conversion"
200))]
201mod json_convertible_tests_resourcevotechoice {
202    use super::*;
203
204    #[test]
205    fn json_round_trip_resourcevotechoice() {
206        use crate::serialization::JsonConvertible;
207        let original = ResourceVoteChoice::default();
208        let json = original.to_json().expect("to_json");
209        let recovered = ResourceVoteChoice::from_json(json).expect("from_json");
210        assert_eq!(original, recovered);
211    }
212
213    #[test]
214    fn value_round_trip_resourcevotechoice() {
215        use crate::serialization::ValueConvertible;
216        let original = ResourceVoteChoice::default();
217        let value = original.to_object().expect("to_object");
218        let recovered = ResourceVoteChoice::from_object(value).expect("from_object");
219        assert_eq!(original, recovered);
220    }
221}