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