Skip to main content

privatus_analytics/
value.rs

1use std::borrow::Cow;
2
3/// One prop value: text, a number, true or false, or a list.
4///
5/// You rarely build one by hand. Anything that converts with `.into()` works:
6/// `&str`, `String`, `bool`, every integer type, `f32`, `f64`, `Option<T>`
7/// (`None` means "leave it out"), and `Vec<T>` or arrays for lists.
8#[derive(Clone, Debug, PartialEq)]
9pub enum Value {
10    /// No value. The prop is left out. In per-call page props it removes a
11    /// configured prop with the same key.
12    Null,
13    /// `true` or `false`.
14    Bool(bool),
15    /// A whole number.
16    Int(i64),
17    /// A whole number too big for `i64`.
18    UInt(u64),
19    /// A number with a fraction.
20    Float(f64),
21    /// Text.
22    Str(String),
23    /// A list of values.
24    List(Vec<Value>),
25}
26
27impl From<bool> for Value {
28    fn from(value: bool) -> Self {
29        Value::Bool(value)
30    }
31}
32
33macro_rules! from_signed {
34    ($($ty:ty),*) => {$(
35        impl From<$ty> for Value {
36            fn from(value: $ty) -> Self {
37                Value::Int(value as i64)
38            }
39        }
40    )*};
41}
42from_signed!(i8, i16, i32, i64, isize);
43
44macro_rules! from_unsigned {
45    ($($ty:ty),*) => {$(
46        impl From<$ty> for Value {
47            fn from(value: $ty) -> Self {
48                let value = value as u64;
49                if value <= i64::MAX as u64 {
50                    Value::Int(value as i64)
51                } else {
52                    Value::UInt(value)
53                }
54            }
55        }
56    )*};
57}
58from_unsigned!(u8, u16, u32, u64, usize);
59
60impl From<f64> for Value {
61    fn from(value: f64) -> Self {
62        Value::Float(value)
63    }
64}
65
66impl From<f32> for Value {
67    /// Goes through text so `1.1_f32` stays `1.1` and does not become `1.100000023841858`.
68    fn from(value: f32) -> Self {
69        Value::Float(value.to_string().parse().unwrap_or(value as f64))
70    }
71}
72
73impl From<&str> for Value {
74    fn from(value: &str) -> Self {
75        Value::Str(value.to_owned())
76    }
77}
78
79impl From<String> for Value {
80    fn from(value: String) -> Self {
81        Value::Str(value)
82    }
83}
84
85impl From<&String> for Value {
86    fn from(value: &String) -> Self {
87        Value::Str(value.clone())
88    }
89}
90
91impl From<Cow<'_, str>> for Value {
92    fn from(value: Cow<'_, str>) -> Self {
93        Value::Str(value.into_owned())
94    }
95}
96
97impl From<char> for Value {
98    fn from(value: char) -> Self {
99        Value::Str(value.to_string())
100    }
101}
102
103impl From<&Value> for Value {
104    fn from(value: &Value) -> Self {
105        value.clone()
106    }
107}
108
109impl<T: Into<Value>> From<Option<T>> for Value {
110    fn from(value: Option<T>) -> Self {
111        value.map_or(Value::Null, Into::into)
112    }
113}
114
115impl<T: Into<Value>> From<Vec<T>> for Value {
116    fn from(value: Vec<T>) -> Self {
117        Value::List(value.into_iter().map(Into::into).collect())
118    }
119}
120
121impl<T: Into<Value>, const N: usize> From<[T; N]> for Value {
122    fn from(value: [T; N]) -> Self {
123        Value::List(value.into_iter().map(Into::into).collect())
124    }
125}
126
127impl<T: Into<Value> + Clone> From<&[T]> for Value {
128    fn from(value: &[T]) -> Self {
129        Value::List(value.iter().cloned().map(Into::into).collect())
130    }
131}
132
133/// An ordered list of props: key and value pairs.
134///
135/// Build it with the [`props!`](crate::props) macro, with [`Props::with`], or
136/// collect it from an iterator of pairs. Setting a key that is already there
137/// replaces its value and keeps its place.
138///
139/// ```
140/// use privatus_analytics::{props, Props};
141///
142/// let a = props! { "plan" => "pro", "seats" => 5, "beta" => true };
143/// let b = Props::new().with("plan", "pro").with("seats", 5).with("beta", true);
144/// assert_eq!(a, b);
145/// ```
146#[derive(Clone, Debug, Default, PartialEq)]
147pub struct Props(Vec<(String, Value)>);
148
149impl Props {
150    /// No props.
151    pub fn new() -> Self {
152        Props(Vec::new())
153    }
154
155    /// Sets a prop and returns the props, for chaining.
156    pub fn with(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
157        self.insert(key, value);
158        self
159    }
160
161    /// Sets a prop. A key that is already there gets the new value.
162    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<Value>) {
163        let key = key.into();
164        let value = value.into();
165        match self.0.iter_mut().find(|(existing, _)| *existing == key) {
166            Some(slot) => slot.1 = value,
167            None => self.0.push((key, value)),
168        }
169    }
170
171    /// The value for a key, if there is one.
172    pub fn get(&self, key: &str) -> Option<&Value> {
173        self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v)
174    }
175
176    /// The number of props.
177    pub fn len(&self) -> usize {
178        self.0.len()
179    }
180
181    /// Whether there are no props.
182    pub fn is_empty(&self) -> bool {
183        self.0.is_empty()
184    }
185
186    /// The props in order.
187    pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
188        self.0.iter().map(|(k, v)| (k.as_str(), v))
189    }
190}
191
192impl<K: Into<String>, V: Into<Value>> FromIterator<(K, V)> for Props {
193    fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
194        let mut props = Props::new();
195        for (key, value) in iter {
196            props.insert(key, value);
197        }
198        props
199    }
200}
201
202impl<K: Into<String>, V: Into<Value>> Extend<(K, V)> for Props {
203    fn extend<I: IntoIterator<Item = (K, V)>>(&mut self, iter: I) {
204        for (key, value) in iter {
205            self.insert(key, value);
206        }
207    }
208}
209
210impl IntoIterator for Props {
211    type Item = (String, Value);
212    type IntoIter = std::vec::IntoIter<(String, Value)>;
213
214    fn into_iter(self) -> Self::IntoIter {
215        self.0.into_iter()
216    }
217}
218
219impl<'a> IntoIterator for &'a Props {
220    type Item = (&'a String, &'a Value);
221    type IntoIter = std::iter::Map<
222        std::slice::Iter<'a, (String, Value)>,
223        fn(&'a (String, Value)) -> (&'a String, &'a Value),
224    >;
225
226    fn into_iter(self) -> Self::IntoIter {
227        self.0.iter().map(|(k, v)| (k, v))
228    }
229}
230
231/// Builds [`Props`] from `key => value` pairs.
232///
233/// ```
234/// use privatus_analytics::props;
235///
236/// let props = props! { "plan" => "pro", "role" => "admin", "seats" => 5 };
237/// assert_eq!(props.len(), 3);
238/// let none = props! {};
239/// assert!(none.is_empty());
240/// ```
241#[macro_export]
242macro_rules! props {
243    () => { $crate::Props::new() };
244    ($($key:expr => $value:expr),+ $(,)?) => {{
245        let mut props = $crate::Props::new();
246        $( props.insert($key, $value); )+
247        props
248    }};
249}