Skip to main content

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