Skip to main content

kui_core/
value.rs

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