Skip to main content

kui_core/
value.rs

1//! [`Value`]: the plain-data payload type for everything that crosses an
2//! event or binding boundary.
3//!
4//! A click payload, a drag event's fields, a readback for a script: all of
5//! them are a `Value`, so Rust, Lua, Node and C see one shape. A Rust app
6//! that wants an exhaustive `match` over its own payloads puts a typed
7//! enum on top with [`crate::message`].
8//!
9//! ```rust
10//! use kui_core::Value;
11//!
12//! let v = Value::map([
13//!     ("kind", Value::str("drag")),
14//!     ("x", Value::from(1.5f32)),
15//!     ("n", Value::from(4usize)),
16//!     ("on", true.into()),
17//! ]);
18//! assert_eq!(v.get_str("kind"), Some("drag"));
19//! assert_eq!(v.get_f32("x"), Some(1.5));
20//! assert_eq!(v.get_float("n"), Some(4.0)); // an int reads as a float
21//! assert_eq!(v.get_str("x"), None); // the wrong type is None
22//! assert_eq!(Value::from("save"), Value::Str("save".into()));
23//! ```
24
25/// A dynamically typed value: null, bool, int, float, string, list or an
26/// ordered string-keyed map. See the [module docs](self) for an example.
27#[derive(Clone, Debug, Default, PartialEq)]
28pub enum Value {
29    #[default]
30    Null,
31    Bool(bool),
32    Int(i64),
33    Float(f64),
34    Str(String),
35    List(Vec<Value>),
36    Map(Vec<(String, Value)>),
37}
38
39impl Value {
40    /// A string value.
41    pub fn str(s: impl Into<String>) -> Value {
42        Value::Str(s.into())
43    }
44
45    /// A map from `(key, value)` pairs, in the order given.
46    pub fn map(entries: impl IntoIterator<Item = (&'static str, Value)>) -> Value {
47        Value::Map(
48            entries
49                .into_iter()
50                .map(|(k, v)| (k.to_string(), v))
51                .collect(),
52        )
53    }
54
55    /// The entry under `key` of a map; `None` for a missing key or a
56    /// value that is not a map.
57    pub fn get(&self, key: &str) -> Option<&Value> {
58        match self {
59            Value::Map(entries) => entries.iter().find(|(k, _)| k == key).map(|(_, v)| v),
60            _ => None,
61        }
62    }
63
64    pub fn as_str(&self) -> Option<&str> {
65        match self {
66            Value::Str(s) => Some(s),
67            _ => None,
68        }
69    }
70
71    /// The number as an integer; a float is truncated.
72    pub fn as_int(&self) -> Option<i64> {
73        match self {
74            Value::Int(i) => Some(*i),
75            Value::Float(f) => Some(*f as i64),
76            _ => None,
77        }
78    }
79
80    pub fn as_list(&self) -> Option<&[Value]> {
81        match self {
82            Value::List(items) => Some(items),
83            _ => None,
84        }
85    }
86
87    /// The number as a float; an int converts.
88    pub fn as_float(&self) -> Option<f64> {
89        match self {
90            Value::Float(f) => Some(*f),
91            Value::Int(i) => Some(*i as f64),
92            _ => None,
93        }
94    }
95
96    /// What kind of value this is, for an error message.
97    pub fn type_name(&self) -> &'static str {
98        match self {
99            Value::Null => "nil",
100            Value::Bool(_) => "boolean",
101            Value::Int(_) | Value::Float(_) => "number",
102            Value::Str(_) => "string",
103            Value::List(_) => "list",
104            Value::Map(_) => "map",
105        }
106    }
107
108    pub fn as_bool(&self) -> Option<bool> {
109        match self {
110            Value::Bool(b) => Some(*b),
111            _ => None,
112        }
113    }
114
115    /// `get(key)` then `as_str`: a map entry read as a string.
116    #[inline]
117    pub fn get_str(&self, key: &str) -> Option<&str> {
118        self.get(key).and_then(Value::as_str)
119    }
120
121    /// `get(key)` then `as_int`.
122    #[inline]
123    pub fn get_int(&self, key: &str) -> Option<i64> {
124        self.get(key).and_then(Value::as_int)
125    }
126
127    /// `get(key)` then `as_float`.
128    #[inline]
129    pub fn get_float(&self, key: &str) -> Option<f64> {
130        self.get(key).and_then(Value::as_float)
131    }
132
133    /// `get(key)` then `as_float`, as the `f32` geometry is in — a drag's
134    /// `x`, a scroll's `dy`.
135    #[inline]
136    pub fn get_f32(&self, key: &str) -> Option<f32> {
137        self.get_float(key).map(|v| v as f32)
138    }
139
140    /// `get(key)` then `as_bool`.
141    #[inline]
142    pub fn get_bool(&self, key: &str) -> Option<bool> {
143        self.get(key).and_then(Value::as_bool)
144    }
145}
146
147/// How a binding spells the handles inside a readback (a node key, a
148/// resource id) when a shape crosses as a [`Value`]. A JS number cannot
149/// hold a 64-bit handle and a Lua integer can, so Node writes sixteen hex
150/// digits ([`Handles::HEX`]) and Lua the integer itself ([`Handles::INT`]).
151#[derive(Clone, Copy)]
152pub struct Handles {
153    pub key: fn(crate::key::Key) -> Value,
154    pub id: fn(u64) -> Value,
155}
156
157impl Handles {
158    /// A handle as the integer it is — Lua's spelling, and the one
159    /// `schema::ENV_FIELDS` uses for a focus key.
160    pub const INT: Handles = Handles {
161        key: |k| Value::Int(k.0 as i64),
162        id: |id| Value::Int(id as i64),
163    };
164    /// A handle as sixteen hex digits — Node's spelling.
165    pub const HEX: Handles = Handles {
166        key: |k| Value::Str(format!("{:016x}", k.0)),
167        id: |id| Value::Str(format!("{id:016x}")),
168    };
169
170    pub fn opt_key(&self, k: Option<crate::key::Key>) -> Value {
171        k.map_or(Value::Null, self.key)
172    }
173}
174
175impl Value {
176    /// A float, as a readback spells one.
177    pub fn float(v: f32) -> Value {
178        Value::Float(v as f64)
179    }
180
181    /// An `Option` as the value or `Null`: a readback keeps every key,
182    /// so a reader destructures a stable shape.
183    pub fn opt<T>(v: Option<T>, f: impl FnOnce(T) -> Value) -> Value {
184        v.map_or(Value::Null, f)
185    }
186
187    pub fn opt_str(s: &Option<String>) -> Value {
188        Value::opt(s.as_ref(), |s| Value::Str(s.clone()))
189    }
190
191    pub fn opt_float(v: Option<f32>) -> Value {
192        Value::opt(v, Value::float)
193    }
194
195    pub fn opt_usize(v: Option<usize>) -> Value {
196        Value::opt(v, |v| Value::Int(v as i64))
197    }
198
199    pub fn opt_bool(v: Option<bool>) -> Value {
200        Value::opt(v, Value::Bool)
201    }
202
203    pub fn list(items: impl IntoIterator<Item = Value>) -> Value {
204        Value::List(items.into_iter().collect())
205    }
206
207    pub fn floats(items: &[f32]) -> Value {
208        Value::list(items.iter().map(|v| Value::float(*v)))
209    }
210}
211
212impl From<&str> for Value {
213    fn from(s: &str) -> Self {
214        Value::Str(s.to_string())
215    }
216}
217
218impl From<String> for Value {
219    fn from(s: String) -> Self {
220        Value::Str(s)
221    }
222}
223
224impl From<i64> for Value {
225    fn from(v: i64) -> Self {
226        Value::Int(v)
227    }
228}
229
230impl From<i32> for Value {
231    fn from(v: i32) -> Self {
232        Value::Int(v.into())
233    }
234}
235
236impl From<u32> for Value {
237    fn from(v: u32) -> Self {
238        Value::Int(v.into())
239    }
240}
241
242/// An index or a count. One past `i64::MAX` cannot be a payload's number,
243/// so it saturates there rather than wrapping negative.
244impl From<usize> for Value {
245    fn from(v: usize) -> Self {
246        Value::Int(i64::try_from(v).unwrap_or(i64::MAX))
247    }
248}
249
250impl From<f32> for Value {
251    fn from(v: f32) -> Self {
252        Value::float(v)
253    }
254}
255
256impl From<f64> for Value {
257    fn from(v: f64) -> Self {
258        Value::Float(v)
259    }
260}
261
262impl From<bool> for Value {
263    fn from(v: bool) -> Self {
264        Value::Bool(v)
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn map_get_finds_entries() {
274        let v = Value::map([("kind", Value::str("inc")), ("by", Value::Int(2))]);
275        assert_eq!(v.get("kind").and_then(Value::as_str), Some("inc"));
276        assert_eq!(v.get("by").and_then(Value::as_int), Some(2));
277        assert_eq!(v.get("missing"), None);
278    }
279
280    #[test]
281    fn numeric_coercions() {
282        assert_eq!(Value::Int(3).as_float(), Some(3.0));
283        assert_eq!(Value::Float(3.7).as_int(), Some(3));
284        assert_eq!(Value::str("x").as_int(), None);
285    }
286}