Skip to main content

openstranded_common_wasmcontract/
value.rs

1use std::collections::HashMap;
2
3use crate::ServiceError;
4
5/// Dynamic type for Service API arguments and return values.
6///
7/// Allows passing data between plugins without knowing concrete types.
8/// Serialisable via serde for WASM boundary crossing.
9#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
10pub enum Value {
11    /// No value / null.
12    Null,
13
14    /// Boolean.
15    Bool(bool),
16
17    /// Signed 32-bit integer.
18    I32(i32),
19
20    /// Unsigned 32-bit integer (`item_id`, `type_id`).
21    U32(u32),
22
23    /// 64-bit float (weight, distance).
24    F64(f64),
25
26    /// String (name, material, class).
27    String(String),
28
29    /// Raw bytes (serialised ECS components, .ron files, etc.).
30    Bytes(Vec<u8>),
31
32    /// Array of values.
33    Array(Vec<Value>),
34
35    /// Associative map / object with named fields.
36    Map(HashMap<String, Value>),
37}
38
39// ── Type conversions ───────────────────────────────────────────────
40
41impl Value {
42    /// Extract as `u32`.
43    ///
44    /// Accepts `U32`, `I32` (if >= 0), `F64` (if integer and in range), and
45    /// parseable `String`.
46    pub fn as_u32(&self) -> Result<u32, ServiceError> {
47        match self {
48            Value::U32(n) => Ok(*n),
49            Value::I32(n) if *n >= 0 => Ok(*n as u32),
50            Value::F64(n) if *n >= 0.0 && *n <= f64::from(u32::MAX) => Ok(*n as u32),
51            Value::String(s) => s.parse::<u32>().map_err(|e| {
52                ServiceError::TypeMismatch {
53                    expected: "u32".into(),
54                    found: format!("string \"{s}\" ({e})"),
55                }
56            }),
57            other => Err(ServiceError::TypeMismatch {
58                expected: "u32".into(),
59                found: format!("{other:?}"),
60            }),
61        }
62    }
63
64    /// Extract as `i32`.
65    pub fn as_i32(&self) -> Result<i32, ServiceError> {
66        match self {
67            Value::I32(n) => Ok(*n),
68            Value::U32(n) if i32::try_from(*n).is_ok() => Ok(*n as i32),
69            Value::F64(n) if *n >= f64::from(i32::MIN) && *n <= f64::from(i32::MAX) => {
70                Ok(*n as i32)
71            }
72            Value::String(s) => s.parse::<i32>().map_err(|e| {
73                ServiceError::TypeMismatch {
74                    expected: "i32".into(),
75                    found: format!("string \"{s}\" ({e})"),
76                }
77            }),
78            other => Err(Self::type_mismatch("i32", other)),
79        }
80    }
81
82    /// Extract as `f64`.
83    pub fn as_f64(&self) -> Result<f64, ServiceError> {
84        match self {
85            Value::F64(n) => Ok(*n),
86            Value::I32(n) => Ok(f64::from(*n)),
87            Value::U32(n) => Ok(f64::from(*n)),
88            Value::String(s) => s.parse::<f64>().map_err(|e| {
89                ServiceError::TypeMismatch {
90                    expected: "f64".into(),
91                    found: format!("string \"{s}\" ({e})"),
92                }
93            }),
94            other => Err(Self::type_mismatch("f64", other)),
95        }
96    }
97
98    /// Extract as `&str`.
99    pub fn as_str(&self) -> Result<&str, ServiceError> {
100        match self {
101            Value::String(s) => Ok(s.as_str()),
102            other => Err(Self::type_mismatch("string", other)),
103        }
104    }
105
106    /// Extract as `bool`.
107    pub fn as_bool(&self) -> Result<bool, ServiceError> {
108        match self {
109            Value::Bool(b) => Ok(*b),
110            Value::U32(n) => Ok(*n != 0),
111            Value::I32(n) => Ok(*n != 0),
112            Value::String(s) if s == "true" || s == "1" => Ok(true),
113            Value::String(s) if s == "false" || s == "0" => Ok(false),
114            other => Err(Self::type_mismatch("bool", other)),
115        }
116    }
117
118    /// Extract as `&[u8]`.
119    pub fn as_bytes(&self) -> Result<&[u8], ServiceError> {
120        match self {
121            Value::Bytes(b) => Ok(b.as_slice()),
122            other => Err(Self::type_mismatch("bytes", other)),
123        }
124    }
125
126    /// Extract as `&[Value]`.
127    pub fn as_array(&self) -> Result<&[Value], ServiceError> {
128        match self {
129            Value::Array(arr) => Ok(arr.as_slice()),
130            other => Err(Self::type_mismatch("array", other)),
131        }
132    }
133
134    /// Extract as `&HashMap<String, Value>`.
135    pub fn as_map(&self) -> Result<&HashMap<String, Value>, ServiceError> {
136        match self {
137            Value::Map(map) => Ok(map),
138            other => Err(Self::type_mismatch("map", other)),
139        }
140    }
141
142    /// Get a value by key from a Map.
143    pub fn get(&self, key: &str) -> Result<&Value, ServiceError> {
144        match self {
145            Value::Map(map) => {
146                map.get(key).ok_or_else(|| ServiceError::TypeMismatch {
147                    expected: format!("map with key \"{key}\""),
148                    found: format!("{self:?}"),
149                })
150            }
151            other => Err(Self::type_mismatch("map", other)),
152        }
153    }
154
155    /// Helper to create a `TypeMismatch` error with automatic formatting.
156    pub(crate) fn type_mismatch(expected: &str, found: &Self) -> ServiceError {
157        ServiceError::TypeMismatch {
158            expected: expected.into(),
159            found: format!("{found:?}"),
160        }
161    }
162}
163
164// ── Conversions from primitives ────────────────────────────────────
165
166impl From<u32> for Value {
167    fn from(v: u32) -> Self {
168        Value::U32(v)
169    }
170}
171
172impl From<i32> for Value {
173    fn from(v: i32) -> Self {
174        Value::I32(v)
175    }
176}
177
178impl From<f64> for Value {
179    fn from(v: f64) -> Self {
180        Value::F64(v)
181    }
182}
183
184impl From<bool> for Value {
185    fn from(v: bool) -> Self {
186        Value::Bool(v)
187    }
188}
189
190impl From<String> for Value {
191    fn from(v: String) -> Self {
192        Value::String(v)
193    }
194}
195
196impl From<&str> for Value {
197    fn from(v: &str) -> Self {
198        Value::String(v.to_owned())
199    }
200}
201
202impl From<Vec<u8>> for Value {
203    fn from(v: Vec<u8>) -> Self {
204        Value::Bytes(v)
205    }
206}
207
208impl From<Vec<Value>> for Value {
209    fn from(v: Vec<Value>) -> Self {
210        Value::Array(v)
211    }
212}
213
214impl<K, V> From<HashMap<K, V>> for Value
215where
216    K: Into<String>,
217    V: Into<Value>,
218{
219    fn from(map: HashMap<K, V>) -> Self {
220        Value::Map(map.into_iter().map(|(k, v)| (k.into(), v.into())).collect())
221    }
222}
223
224// ── Tests ──────────────────────────────────────────────────────────
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_as_u32_from_u32() {
232        assert_eq!(Value::U32(42).as_u32().unwrap(), 42);
233    }
234
235    #[test]
236    fn test_as_u32_from_i32_positive() {
237        assert_eq!(Value::I32(42).as_u32().unwrap(), 42);
238    }
239
240    #[test]
241    fn test_as_u32_from_i32_negative() {
242        assert!(Value::I32(-1).as_u32().is_err());
243    }
244
245    #[test]
246    fn test_as_u32_from_string() {
247        assert_eq!(Value::String("42".into()).as_u32().unwrap(), 42);
248    }
249
250    #[test]
251    fn test_as_u32_from_string_invalid() {
252        assert!(Value::String("hello".into()).as_u32().is_err());
253    }
254
255    #[test]
256    fn test_as_u32_type_mismatch() {
257        let err = Value::Bool(true).as_u32().unwrap_err();
258        assert!(matches!(err, ServiceError::TypeMismatch { .. }));
259    }
260
261    #[test]
262    fn test_as_str_ok() {
263        assert_eq!(Value::String("hello".into()).as_str().unwrap(), "hello");
264    }
265
266    #[test]
267    fn test_as_str_type_mismatch() {
268        assert!(Value::U32(42).as_str().is_err());
269    }
270
271    #[test]
272    fn test_as_array_ok() {
273        let arr = Value::Array(vec![Value::U32(1), Value::U32(2)]);
274        assert_eq!(arr.as_array().unwrap().len(), 2);
275    }
276
277    #[test]
278    fn test_as_array_empty() {
279        let arr = Value::Array(vec![]);
280        assert!(arr.as_array().unwrap().is_empty());
281    }
282
283    #[test]
284    fn test_get_from_map() {
285        let mut map = HashMap::new();
286        map.insert("id".into(), Value::U32(42));
287        let val = Value::Map(map);
288        assert_eq!(val.get("id").unwrap().as_u32().unwrap(), 42);
289    }
290
291    #[test]
292    fn test_get_missing_key() {
293        let map = Value::Map(HashMap::new());
294        assert!(map.get("missing").is_err());
295    }
296
297    #[test]
298    fn test_get_from_non_map() {
299        assert!(Value::U32(42).get("id").is_err());
300    }
301
302    #[test]
303    fn test_from_primitives() {
304        assert_eq!(Value::from(42u32), Value::U32(42));
305        assert_eq!(Value::from(-1i32), Value::I32(-1));
306        assert_eq!(Value::from(3.14f64), Value::F64(3.14));
307        assert_eq!(Value::from(true), Value::Bool(true));
308        assert_eq!(Value::from("hello"), Value::String("hello".into()));
309    }
310
311    #[test]
312    fn test_as_i32_from_i32() {
313        assert_eq!(Value::I32(42).as_i32().unwrap(), 42);
314        assert_eq!(Value::I32(-5).as_i32().unwrap(), -5);
315    }
316
317    #[test]
318    fn test_as_i32_from_u32() {
319        assert_eq!(Value::U32(42).as_i32().unwrap(), 42);
320        assert!(Value::U32(u32::MAX).as_i32().is_err());
321    }
322
323    #[test]
324    fn test_as_f64_from_f64() {
325        assert!((Value::F64(3.14).as_f64().unwrap() - 3.14).abs() < 1e-10);
326    }
327
328    #[test]
329    fn test_as_f64_from_i32() {
330        assert!((Value::I32(5).as_f64().unwrap() - 5.0).abs() < 1e-10);
331    }
332
333    #[test]
334    fn test_as_bytes() {
335        let bytes = vec![1u8, 2, 3];
336        assert_eq!(Value::Bytes(bytes).as_bytes().unwrap(), &[1, 2, 3]);
337        assert!(Value::U32(42).as_bytes().is_err());
338    }
339
340    #[test]
341    fn test_as_map_ok() {
342        let mut map = HashMap::new();
343        map.insert("a".into(), Value::U32(1));
344        map.insert("b".into(), Value::U32(2));
345        let val = Value::Map(map);
346        let m = val.as_map().unwrap();
347        assert!(m.contains_key("a"));
348        assert!(m.contains_key("b"));
349    }
350
351    #[test]
352    fn test_as_map_type_mismatch() {
353        assert!(Value::U32(42).as_map().is_err());
354    }
355
356    #[test]
357    fn test_as_bool_from_bool() {
358        assert_eq!(Value::Bool(true).as_bool().unwrap(), true);
359    }
360
361    #[test]
362    fn test_as_bool_from_u32() {
363        assert_eq!(Value::U32(1).as_bool().unwrap(), true);
364        assert_eq!(Value::U32(0).as_bool().unwrap(), false);
365    }
366
367    #[test]
368    fn test_as_bool_from_string() {
369        assert_eq!(Value::String("true".into()).as_bool().unwrap(), true);
370        assert_eq!(Value::String("false".into()).as_bool().unwrap(), false);
371    }
372}