battler_wamp_values/
lib.rs

1//! # battler-wamp-values
2//!
3//! **battler-wamp-values** is a utility crate for [`battler-wamp`](https://crates.io/crates/battler-wamp). It provides core value type definitions, as well as procedural macros for serializing Rust structs into WAMP lists and dictionaries.
4//!
5//! In WAMP, all parts of a message can be boiled into a [`Value`]. For a Rust type to be encoded
6//! into a WAMP message, it must be convertible to a [`Value`]. This behavior is covered by the
7//! [`WampSerialize`] and [`WampDeserialize`] traits.
8//!
9//! For convenience, the [`WampDictionary`] and [`WampList`] procedural macros can be used to
10//! automatically derive the [`WampSerialize`] and [`WampDeserialize`] traits for complex Rust
11//! types. Both of these macros assume that all struct fields also implement these traits.
12//!
13//! These macro also have additional optional attributes for struct fields:
14//!
15//! * `default` - If the field is missing during deserialization, the field is initialized to its
16//!   default value.
17//! * `skip_serializing_if` - Checks if the field should be skipped during serialization using the
18//!   function provided. For lists, all subsequent fields will also be skipped, regardless of their
19//!   value.
20//!
21//!
22//! ## Example
23//!
24//! ```
25//! use battler_wamp_values::{
26//!     Dictionary,
27//!     Integer,
28//!     List,
29//!     Value,
30//!     WampDeserialize,
31//!     WampDictionary,
32//!     WampList,
33//!     WampSerialize,
34//! };
35//!
36//! #[derive(Debug, PartialEq, Eq, WampDictionary)]
37//! struct Metadata {
38//!     version: Integer,
39//!     #[battler_wamp_values(default, skip_serializing_if = Option::is_none)]
40//!     feature_enabled: Option<bool>,
41//!     name: String,
42//! }
43//!
44//! #[derive(Debug, PartialEq, Eq, WampList)]
45//! struct Args(
46//!     Integer,
47//!     Integer,
48//!     #[battler_wamp_values(default, skip_serializing_if = List::is_empty)] List,
49//! );
50//!
51//! fn main() {
52//!     // Serialization.
53//!     assert_eq!(
54//!         Metadata {
55//!             version: 1,
56//!             feature_enabled: None,
57//!             name: "foo".to_owned(),
58//!         }
59//!         .wamp_serialize()
60//!         .unwrap(),
61//!         Value::Dictionary(Dictionary::from_iter([
62//!             ("version".to_owned(), Value::Integer(1)),
63//!             ("name".to_owned(), Value::String("foo".to_owned())),
64//!         ]))
65//!     );
66//!     assert_eq!(
67//!         Args(1, 2, Vec::from_iter((3..6).map(Value::Integer)))
68//!             .wamp_serialize()
69//!             .unwrap(),
70//!         Value::List(List::from_iter([
71//!             Value::Integer(1),
72//!             Value::Integer(2),
73//!             Value::List(List::from_iter([
74//!                 Value::Integer(3),
75//!                 Value::Integer(4),
76//!                 Value::Integer(5),
77//!             ])),
78//!         ]))
79//!     );
80//!
81//!     // Deserialization.
82//!     assert_eq!(
83//!         Metadata::wamp_deserialize(Value::Dictionary(Dictionary::from_iter([
84//!             ("version".to_owned(), Value::Integer(2)),
85//!             ("name".to_owned(), Value::String("bar".to_owned())),
86//!             ("feature_enabled".to_owned(), Value::Bool(false)),
87//!         ])))
88//!         .unwrap(),
89//!         Metadata {
90//!             version: 2,
91//!             name: "bar".to_owned(),
92//!             feature_enabled: Some(false),
93//!         }
94//!     );
95//!     assert_eq!(
96//!         Args::wamp_deserialize(Value::List(List::from_iter([
97//!             Value::Integer(7),
98//!             Value::Integer(8),
99//!         ])))
100//!         .unwrap(),
101//!         Args(7, 8, List::default())
102//!     );
103//! }
104//! ```
105
106use std::{
107    collections::HashSet,
108    hash::{
109        BuildHasher,
110        Hash,
111    },
112};
113
114pub use battler_wamp_values_proc_macro::{
115    WampDictionary,
116    WampList,
117};
118use serde::{
119    Deserialize,
120    Serialize,
121};
122use thiserror::Error;
123
124/// An integer type for WAMP messages.
125pub type Integer = u64;
126
127/// A dictionary of key-value pairs.
128pub type Dictionary = ahash::HashMap<String, Value>;
129
130/// A sequence of values.
131pub type List = Vec<Value>;
132
133/// A value for WAMP messages.
134///
135/// In WAMP, all parts of a message can be boiled into a [`Value`]. The [`WampSerialize`] and
136/// [`WampDeserialize`] traits handle the conversion of Rust types into WAMP values.
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(untagged)]
139pub enum Value {
140    Null,
141    Integer(Integer),
142    String(String),
143    Bool(bool),
144    Dictionary(Dictionary),
145    List(List),
146}
147
148impl Value {
149    /// The value as an [`Integer`].
150    pub fn integer(&self) -> Option<Integer> {
151        match self {
152            Self::Integer(val) => Some(*val),
153            _ => None,
154        }
155    }
156
157    /// The value as a [`str`].
158    pub fn string(&self) -> Option<&str> {
159        match self {
160            Self::String(val) => Some(val),
161            _ => None,
162        }
163    }
164
165    /// The value as a [`bool`].
166    pub fn bool(&self) -> Option<bool> {
167        match self {
168            Self::Bool(val) => Some(*val),
169            _ => None,
170        }
171    }
172
173    /// The value as a [`Dictionary`].
174    pub fn dictionary(&self) -> Option<&Dictionary> {
175        match self {
176            Self::Dictionary(val) => Some(val),
177            _ => None,
178        }
179    }
180
181    /// The value as a [`Dictionary`].
182    pub fn dictionary_mut(&mut self) -> Option<&mut Dictionary> {
183        match self {
184            Self::Dictionary(val) => Some(val),
185            _ => None,
186        }
187    }
188
189    /// The value as a [`List`].
190    pub fn list(&self) -> Option<&List> {
191        match self {
192            Self::List(val) => Some(val),
193            _ => None,
194        }
195    }
196
197    /// The value as a [`List`].
198    pub fn list_mut(&mut self) -> Option<&mut List> {
199        match self {
200            Self::List(val) => Some(val),
201            _ => None,
202        }
203    }
204}
205
206impl From<Integer> for Value {
207    fn from(value: Integer) -> Self {
208        Self::Integer(value)
209    }
210}
211
212impl From<String> for Value {
213    fn from(value: String) -> Self {
214        Self::String(value)
215    }
216}
217
218impl From<&str> for Value {
219    fn from(value: &str) -> Self {
220        Self::String(value.to_owned())
221    }
222}
223
224impl From<bool> for Value {
225    fn from(value: bool) -> Self {
226        Self::Bool(value)
227    }
228}
229
230impl From<Dictionary> for Value {
231    fn from(value: Dictionary) -> Self {
232        Self::Dictionary(value)
233    }
234}
235
236impl From<List> for Value {
237    fn from(value: List) -> Self {
238        Self::List(value)
239    }
240}
241
242impl<T> From<Option<T>> for Value
243where
244    T: Into<Value>,
245{
246    fn from(value: Option<T>) -> Self {
247        match value {
248            Some(value) => value.into(),
249            None => Value::Null,
250        }
251    }
252}
253
254/// An error resulting from serializing a Rust object into a WAMP value using the [`WampSerialize`]
255/// trait.
256#[derive(Debug, Error)]
257#[error("{msg}")]
258pub struct WampSerializeError {
259    msg: String,
260}
261
262impl WampSerializeError {
263    pub fn new<S>(msg: S) -> Self
264    where
265        S: Into<String>,
266    {
267        Self { msg: msg.into() }
268    }
269
270    pub fn annotate(&self, msg: String) -> Self {
271        Self::new(format!("{}; {msg}", self.msg))
272    }
273}
274
275/// An error resulting from deserializing a Rust object from a WAMP value using the
276/// [`WampDeserialize`] trait.
277#[derive(Debug, Error)]
278#[error("{msg}")]
279pub struct WampDeserializeError {
280    msg: String,
281}
282
283impl WampDeserializeError {
284    pub fn new<S>(msg: S) -> Self
285    where
286        S: Into<String>,
287    {
288        Self { msg: msg.into() }
289    }
290
291    pub fn annotate(&self, msg: String) -> Self {
292        Self::new(format!("{}; {msg}", self.msg))
293    }
294}
295
296/// Trait for serializing a Rust object into a WAMP value.
297pub trait WampSerialize {
298    /// Serializes the object into a WAMP value.
299    fn wamp_serialize(self) -> Result<Value, WampSerializeError>;
300}
301
302impl WampSerialize for Integer {
303    fn wamp_serialize(self) -> Result<Value, WampSerializeError> {
304        Ok(Value::Integer(self))
305    }
306}
307
308impl WampSerialize for String {
309    fn wamp_serialize(self) -> Result<Value, WampSerializeError> {
310        Ok(Value::String(self))
311    }
312}
313
314impl WampSerialize for bool {
315    fn wamp_serialize(self) -> Result<Value, WampSerializeError> {
316        Ok(Value::Bool(self))
317    }
318}
319
320impl WampSerialize for List {
321    fn wamp_serialize(self) -> Result<Value, WampSerializeError> {
322        Ok(Value::List(self))
323    }
324}
325
326impl WampSerialize for Dictionary {
327    fn wamp_serialize(self) -> Result<Value, WampSerializeError> {
328        Ok(Value::Dictionary(self))
329    }
330}
331
332impl<T> WampSerialize for Option<T>
333where
334    T: WampSerialize,
335{
336    fn wamp_serialize(self) -> Result<Value, WampSerializeError> {
337        match self {
338            Some(val) => val.wamp_serialize(),
339            None => Ok(Value::Null),
340        }
341    }
342}
343
344impl<T> WampSerialize for Vec<T>
345where
346    T: WampSerialize,
347{
348    fn wamp_serialize(self) -> Result<Value, WampSerializeError> {
349        Ok(Value::List(
350            self.into_iter()
351                .map(|val| val.wamp_serialize())
352                .collect::<Result<Vec<_>, _>>()?,
353        ))
354    }
355}
356
357impl<T, S> WampSerialize for std::collections::HashSet<T, S>
358where
359    T: WampSerialize,
360{
361    fn wamp_serialize(self) -> Result<Value, WampSerializeError> {
362        Ok(Value::List(
363            self.into_iter()
364                .map(|val| val.wamp_serialize())
365                .collect::<Result<Vec<_>, _>>()?,
366        ))
367    }
368}
369
370impl<T> WampSerialize for ahash::HashMap<String, T>
371where
372    T: WampSerialize,
373{
374    fn wamp_serialize(self) -> Result<Value, WampSerializeError> {
375        Ok(Value::Dictionary(
376            self.into_iter()
377                .map(|(key, val)| Ok((key, val.wamp_serialize()?)))
378                .collect::<Result<Dictionary, _>>()?,
379        ))
380    }
381}
382
383/// Trait for deserializing a Rust object from a WAMP value.
384pub trait WampDeserialize: Sized {
385    /// Deserializes the object from a WAMP value.
386    fn wamp_deserialize(value: Value) -> Result<Self, WampDeserializeError>;
387}
388
389impl WampDeserialize for Integer {
390    fn wamp_deserialize(value: Value) -> Result<Self, WampDeserializeError> {
391        match value {
392            Value::Integer(val) => Ok(val),
393            _ => Err(WampDeserializeError::new("value must be an integer")),
394        }
395    }
396}
397
398impl WampDeserialize for String {
399    fn wamp_deserialize(value: Value) -> Result<Self, WampDeserializeError> {
400        match value {
401            Value::String(val) => Ok(val),
402            _ => Err(WampDeserializeError::new("value must be a string")),
403        }
404    }
405}
406
407impl WampDeserialize for bool {
408    fn wamp_deserialize(value: Value) -> Result<Self, WampDeserializeError> {
409        match value {
410            Value::Bool(val) => Ok(val),
411            _ => Err(WampDeserializeError::new("value must be a bool")),
412        }
413    }
414}
415
416impl WampDeserialize for List {
417    fn wamp_deserialize(value: Value) -> Result<Self, WampDeserializeError> {
418        match value {
419            Value::List(val) => Ok(val),
420            _ => Err(WampDeserializeError::new("value must be a list")),
421        }
422    }
423}
424
425impl WampDeserialize for Dictionary {
426    fn wamp_deserialize(value: Value) -> Result<Self, WampDeserializeError> {
427        match value {
428            Value::Dictionary(val) => Ok(val),
429            _ => Err(WampDeserializeError::new("value must be a dictionary")),
430        }
431    }
432}
433
434impl<T> WampDeserialize for Option<T>
435where
436    T: WampDeserialize,
437{
438    fn wamp_deserialize(value: Value) -> Result<Self, WampDeserializeError> {
439        match value {
440            Value::Null => Ok(None),
441            value @ _ => Ok(Some(T::wamp_deserialize(value)?)),
442        }
443    }
444}
445
446impl<T> WampDeserialize for Vec<T>
447where
448    T: WampDeserialize,
449{
450    fn wamp_deserialize(value: Value) -> Result<Self, WampDeserializeError> {
451        match value {
452            Value::List(val) => val
453                .into_iter()
454                .map(|val| WampDeserialize::wamp_deserialize(val))
455                .collect::<Result<Vec<_>, _>>(),
456            _ => Err(WampDeserializeError::new("value must be a list")),
457        }
458    }
459}
460
461impl<T, S> WampDeserialize for HashSet<T, S>
462where
463    T: WampDeserialize + Eq + Hash,
464    S: BuildHasher + Default,
465{
466    fn wamp_deserialize(value: Value) -> Result<Self, WampDeserializeError> {
467        match value {
468            Value::List(val) => val
469                .into_iter()
470                .map(|val| WampDeserialize::wamp_deserialize(val))
471                .collect::<Result<HashSet<_, S>, _>>(),
472            _ => Err(WampDeserializeError::new("value must be a list")),
473        }
474    }
475}
476
477impl<T> WampDeserialize for ahash::HashMap<String, T>
478where
479    T: WampDeserialize,
480{
481    fn wamp_deserialize(value: Value) -> Result<Self, WampDeserializeError> {
482        match value {
483            Value::Dictionary(val) => val
484                .into_iter()
485                .map(|(key, val)| Ok((key, WampDeserialize::wamp_deserialize(val)?)))
486                .collect::<Result<ahash::HashMap<_, _>, _>>(),
487            _ => Err(WampDeserializeError::new("value must be a dictionary")),
488        }
489    }
490}