Skip to main content

gpui_rhai/
value.rs

1use std::collections::BTreeMap;
2
3use rhai::{Array, CustomType, Dynamic, FLOAT, INT, ImmutableString, Map, TypeBuilder};
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
8#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
9pub enum UiValuePathSegment {
10    Key(String),
11    Index(usize),
12    /// Select one map inside an array by a stable string field.
13    Item {
14        key_field: String,
15        key: String,
16    },
17}
18
19#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
20pub struct UiValuePath(Vec<UiValuePathSegment>);
21
22impl<'de> Deserialize<'de> for UiValuePath {
23    fn deserialize<Deserializer>(deserializer: Deserializer) -> Result<Self, Deserializer::Error>
24    where
25        Deserializer: serde::Deserializer<'de>,
26    {
27        let segments = Vec::<UiValuePathSegment>::deserialize(deserializer)?;
28        Self::new(segments).map_err(serde::de::Error::custom)
29    }
30}
31
32impl UiValuePath {
33    /// Construct a bounded nested value path.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`UiValuePathError::InvalidPath`] for more than 64 segments or
38    /// empty/oversized map keys.
39    pub fn new(segments: Vec<UiValuePathSegment>) -> Result<Self, UiValuePathError> {
40        if segments.len() > 64
41            || segments.iter().any(|segment| match segment {
42                UiValuePathSegment::Key(key) => key.is_empty() || key.len() > 256,
43                UiValuePathSegment::Index(_) => false,
44                UiValuePathSegment::Item { key_field, key } => {
45                    key_field.is_empty()
46                        || key_field.len() > 256
47                        || key.is_empty()
48                        || key.len() > 256
49                }
50            })
51        {
52            return Err(UiValuePathError::InvalidPath);
53        }
54        Ok(Self(segments))
55    }
56
57    #[must_use]
58    pub fn segments(&self) -> &[UiValuePathSegment] {
59        &self.0
60    }
61
62    #[must_use]
63    pub fn is_empty(&self) -> bool {
64        self.0.is_empty()
65    }
66}
67
68/// A typed reference to a Rust-owned resource that must not be copied into Rhai.
69#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
70pub struct OpaqueHandle {
71    kind: String,
72    id: u64,
73}
74
75impl OpaqueHandle {
76    #[must_use]
77    pub fn new(kind: impl Into<String>, id: u64) -> Self {
78        Self {
79            kind: kind.into(),
80            id,
81        }
82    }
83
84    #[must_use]
85    pub fn kind(&self) -> &str {
86        &self.kind
87    }
88
89    #[must_use]
90    pub fn id(&self) -> u64 {
91        self.id
92    }
93}
94
95impl CustomType for OpaqueHandle {
96    fn build(mut builder: TypeBuilder<Self>) {
97        builder
98            .with_name("OpaqueHandle")
99            .with_get("kind", |handle: &mut Self| {
100                ImmutableString::from(handle.kind.clone())
101            })
102            .with_fn("to_string", |handle: &mut Self| {
103                format!("{}#{}", handle.kind, handle.id)
104            });
105    }
106}
107
108/// Data allowed to cross a capability or semantic-event boundary.
109#[derive(Clone, Debug, PartialEq)]
110pub enum UiValue {
111    Null,
112    Bool(bool),
113    Integer(i64),
114    Float(f64),
115    String(String),
116    Array(Vec<Self>),
117    Map(BTreeMap<String, Self>),
118    Handle(OpaqueHandle),
119}
120
121#[derive(Serialize)]
122#[serde(tag = "type", content = "value", rename_all = "snake_case")]
123enum UiValueRef<'a> {
124    Null,
125    Bool(bool),
126    Integer(i64),
127    Float(f64),
128    String(&'a str),
129    Array(&'a [UiValue]),
130    Map(&'a BTreeMap<String, UiValue>),
131    Handle(&'a OpaqueHandle),
132}
133
134#[derive(Deserialize)]
135#[serde(tag = "type", content = "value", rename_all = "snake_case")]
136enum UiValueOwned {
137    Null,
138    Bool(bool),
139    Integer(i64),
140    Float(f64),
141    String(String),
142    Array(Vec<UiValue>),
143    Map(BTreeMap<String, UiValue>),
144    Handle(OpaqueHandle),
145}
146
147impl Serialize for UiValue {
148    fn serialize<Serializer>(
149        &self,
150        serializer: Serializer,
151    ) -> Result<Serializer::Ok, Serializer::Error>
152    where
153        Serializer: serde::Serializer,
154    {
155        self.validate().map_err(serde::ser::Error::custom)?;
156        match self {
157            Self::Null => UiValueRef::Null,
158            Self::Bool(value) => UiValueRef::Bool(*value),
159            Self::Integer(value) => UiValueRef::Integer(*value),
160            Self::Float(value) => UiValueRef::Float(*value),
161            Self::String(value) => UiValueRef::String(value),
162            Self::Array(value) => UiValueRef::Array(value),
163            Self::Map(value) => UiValueRef::Map(value),
164            Self::Handle(value) => UiValueRef::Handle(value),
165        }
166        .serialize(serializer)
167    }
168}
169
170impl<'de> Deserialize<'de> for UiValue {
171    fn deserialize<Deserializer>(deserializer: Deserializer) -> Result<Self, Deserializer::Error>
172    where
173        Deserializer: serde::Deserializer<'de>,
174    {
175        let value = match UiValueOwned::deserialize(deserializer)? {
176            UiValueOwned::Null => Self::Null,
177            UiValueOwned::Bool(value) => Self::Bool(value),
178            UiValueOwned::Integer(value) => Self::Integer(value),
179            UiValueOwned::Float(value) => Self::Float(value),
180            UiValueOwned::String(value) => Self::String(value),
181            UiValueOwned::Array(value) => Self::Array(value),
182            UiValueOwned::Map(value) => Self::Map(value),
183            UiValueOwned::Handle(value) => Self::Handle(value),
184        };
185        value.validate().map_err(serde::de::Error::custom)?;
186        Ok(value)
187    }
188}
189
190impl UiValue {
191    const MAX_DEPTH: usize = 64;
192    const MAX_ITEMS: usize = 1_000_000;
193    const MAX_BYTES: usize = 16 * 1024 * 1024;
194
195    /// Validate the complete durable value domain, including values constructed
196    /// directly by Rust or deserialized without passing through Rhai.
197    ///
198    /// # Errors
199    ///
200    /// Returns a path-aware non-finite or resource-limit violation.
201    pub fn validate(&self) -> Result<(), UiValueError> {
202        let mut items = 0usize;
203        let mut bytes = 0usize;
204        self.validate_at("$", 0, &mut items, &mut bytes)
205    }
206
207    fn validate_at(
208        &self,
209        path: &str,
210        depth: usize,
211        items: &mut usize,
212        bytes: &mut usize,
213    ) -> Result<(), UiValueError> {
214        if depth > Self::MAX_DEPTH {
215            return Err(UiValueError::Limit {
216                path: path.to_owned(),
217                resource: "depth",
218                limit: Self::MAX_DEPTH,
219            });
220        }
221        *items = items.saturating_add(1);
222        if *items > Self::MAX_ITEMS {
223            return Err(UiValueError::Limit {
224                path: path.to_owned(),
225                resource: "items",
226                limit: Self::MAX_ITEMS,
227            });
228        }
229        match self {
230            Self::Float(value) if !value.is_finite() => {
231                return Err(UiValueError::NonFiniteFloat {
232                    path: path.to_owned(),
233                });
234            }
235            Self::String(value) => *bytes = bytes.saturating_add(value.len()),
236            Self::Array(values) => {
237                for (index, value) in values.iter().enumerate() {
238                    value.validate_at(&format!("{path}[{index}]"), depth + 1, items, bytes)?;
239                }
240            }
241            Self::Map(values) => {
242                for (key, value) in values {
243                    *bytes = bytes.saturating_add(key.len());
244                    value.validate_at(&format!("{path}.{key}"), depth + 1, items, bytes)?;
245                }
246            }
247            Self::Null | Self::Bool(_) | Self::Integer(_) | Self::Float(_) | Self::Handle(_) => {}
248        }
249        if *bytes > Self::MAX_BYTES {
250            return Err(UiValueError::Limit {
251                path: path.to_owned(),
252                resource: "bytes",
253                limit: Self::MAX_BYTES,
254            });
255        }
256        Ok(())
257    }
258
259    /// Read one nested map/array path.
260    ///
261    /// # Errors
262    ///
263    /// Returns a precise type, missing-key, or out-of-bounds path error.
264    pub fn get_path(&self, path: &UiValuePath) -> Result<&Self, UiValuePathError> {
265        let mut current = self;
266        for (depth, segment) in path.segments().iter().enumerate() {
267            current = match (current, segment) {
268                (Self::Map(values), UiValuePathSegment::Key(key)) => {
269                    values
270                        .get(key)
271                        .ok_or_else(|| UiValuePathError::MissingKey {
272                            depth,
273                            key: key.clone(),
274                        })?
275                }
276                (Self::Array(values), UiValuePathSegment::Index(index)) => values
277                    .get(*index)
278                    .ok_or(UiValuePathError::IndexOutOfBounds {
279                        depth,
280                        index: *index,
281                        len: values.len(),
282                    })?,
283                (Self::Array(values), UiValuePathSegment::Item { key_field, key }) => {
284                    &values[Self::keyed_item_index(values, depth, key_field, key)?]
285                }
286                (value, segment) => {
287                    return Err(UiValuePathError::TypeMismatch {
288                        depth,
289                        expected: match segment {
290                            UiValuePathSegment::Key(_) => "map",
291                            UiValuePathSegment::Index(_) | UiValuePathSegment::Item { .. } => {
292                                "array"
293                            }
294                        },
295                        actual: value.kind_name(),
296                    });
297                }
298            };
299        }
300        Ok(current)
301    }
302
303    /// Replace one existing nested map/array path.
304    ///
305    /// An empty path replaces the complete value. This operation never grows
306    /// arrays or creates missing map keys.
307    ///
308    /// # Errors
309    ///
310    /// Returns a precise type, missing-key, or out-of-bounds path error.
311    pub fn set_path(&mut self, path: &UiValuePath, value: Self) -> Result<(), UiValuePathError> {
312        let Some((last, parents)) = path.segments().split_last() else {
313            *self = value;
314            return Ok(());
315        };
316        let parent = Self::get_path_mut(self, parents)?;
317        match (parent, last) {
318            (Self::Map(values), UiValuePathSegment::Key(key)) => {
319                let slot = values
320                    .get_mut(key)
321                    .ok_or_else(|| UiValuePathError::MissingKey {
322                        depth: parents.len(),
323                        key: key.clone(),
324                    })?;
325                *slot = value;
326            }
327            (Self::Array(values), UiValuePathSegment::Index(index)) => {
328                let len = values.len();
329                let slot = values
330                    .get_mut(*index)
331                    .ok_or(UiValuePathError::IndexOutOfBounds {
332                        depth: parents.len(),
333                        index: *index,
334                        len,
335                    })?;
336                *slot = value;
337            }
338            (Self::Array(values), UiValuePathSegment::Item { key_field, key }) => {
339                let index = Self::keyed_item_index(values, parents.len(), key_field, key)?;
340                values[index] = value;
341            }
342            (parent, segment) => {
343                return Err(UiValuePathError::TypeMismatch {
344                    depth: parents.len(),
345                    expected: match segment {
346                        UiValuePathSegment::Key(_) => "map",
347                        UiValuePathSegment::Index(_) | UiValuePathSegment::Item { .. } => "array",
348                    },
349                    actual: parent.kind_name(),
350                });
351            }
352        }
353        Ok(())
354    }
355
356    fn get_path_mut<'a>(
357        mut current: &'a mut Self,
358        segments: &[UiValuePathSegment],
359    ) -> Result<&'a mut Self, UiValuePathError> {
360        for (depth, segment) in segments.iter().enumerate() {
361            current = match (current, segment) {
362                (Self::Map(values), UiValuePathSegment::Key(key)) => values
363                    .get_mut(key)
364                    .ok_or_else(|| UiValuePathError::MissingKey {
365                        depth,
366                        key: key.clone(),
367                    })?,
368                (Self::Array(values), UiValuePathSegment::Index(index)) => {
369                    let len = values.len();
370                    values
371                        .get_mut(*index)
372                        .ok_or(UiValuePathError::IndexOutOfBounds {
373                            depth,
374                            index: *index,
375                            len,
376                        })?
377                }
378                (Self::Array(values), UiValuePathSegment::Item { key_field, key }) => {
379                    let index = Self::keyed_item_index(values, depth, key_field, key)?;
380                    &mut values[index]
381                }
382                (value, segment) => {
383                    return Err(UiValuePathError::TypeMismatch {
384                        depth,
385                        expected: match segment {
386                            UiValuePathSegment::Key(_) => "map",
387                            UiValuePathSegment::Index(_) | UiValuePathSegment::Item { .. } => {
388                                "array"
389                            }
390                        },
391                        actual: value.kind_name(),
392                    });
393                }
394            };
395        }
396        Ok(current)
397    }
398
399    fn keyed_item_index(
400        values: &[Self],
401        depth: usize,
402        key_field: &str,
403        key: &str,
404    ) -> Result<usize, UiValuePathError> {
405        let mut found = None;
406        for (index, item) in values.iter().enumerate() {
407            let Self::Map(fields) = item else {
408                return Err(UiValuePathError::InvalidKeyedItem {
409                    depth,
410                    index,
411                    reason: "item is not a map",
412                });
413            };
414            let Some(item_key) = fields.get(key_field) else {
415                return Err(UiValuePathError::InvalidKeyedItem {
416                    depth,
417                    index,
418                    reason: "key field is missing",
419                });
420            };
421            let Self::String(item_key) = item_key else {
422                return Err(UiValuePathError::InvalidKeyedItem {
423                    depth,
424                    index,
425                    reason: "key field is not a string",
426                });
427            };
428            if item_key == key && found.replace(index).is_some() {
429                return Err(UiValuePathError::DuplicateItemKey {
430                    depth,
431                    key_field: key_field.to_owned(),
432                    key: key.to_owned(),
433                });
434            }
435        }
436        found.ok_or_else(|| UiValuePathError::MissingItem {
437            depth,
438            key_field: key_field.to_owned(),
439            key: key.to_owned(),
440        })
441    }
442
443    const fn kind_name(&self) -> &'static str {
444        match self {
445            Self::Null => "null",
446            Self::Bool(_) => "bool",
447            Self::Integer(_) => "integer",
448            Self::Float(_) => "float",
449            Self::String(_) => "string",
450            Self::Array(_) => "array",
451            Self::Map(_) => "map",
452            Self::Handle(_) => "handle",
453        }
454    }
455
456    /// Convert a Rhai value into the restricted capability data model.
457    ///
458    /// # Errors
459    ///
460    /// Returns [`UiValueError`] when the value contains a custom type such as a
461    /// `UiNode` or callback, or when one of its descendants is unsupported.
462    pub fn from_dynamic(value: Dynamic) -> Result<Self, UiValueError> {
463        let value = Self::from_dynamic_at(value, "$".to_owned())?;
464        value.validate()?;
465        Ok(value)
466    }
467
468    fn from_dynamic_at(value: Dynamic, path: String) -> Result<Self, UiValueError> {
469        if value.is_unit() {
470            return Ok(Self::Null);
471        }
472        if value.is::<bool>() {
473            return Ok(Self::Bool(value.cast::<bool>()));
474        }
475        if value.is::<INT>() {
476            return Ok(Self::Integer(value.cast::<INT>()));
477        }
478        if value.is::<FLOAT>() {
479            return Ok(Self::Float(value.cast::<FLOAT>()));
480        }
481        if value.is::<ImmutableString>() {
482            return Ok(Self::String(value.cast::<ImmutableString>().to_string()));
483        }
484        if value.is::<Array>() {
485            let values = value.cast::<Array>();
486            return values
487                .into_iter()
488                .enumerate()
489                .map(|(index, value)| Self::from_dynamic_at(value, format!("{path}[{index}]")))
490                .collect::<Result<Vec<_>, _>>()
491                .map(Self::Array);
492        }
493        if value.is::<Map>() {
494            let values = value.cast::<Map>();
495            return values
496                .into_iter()
497                .map(|(key, value)| {
498                    let key = key.to_string();
499                    let value = Self::from_dynamic_at(value, format!("{path}.{key}"))?;
500                    Ok((key, value))
501                })
502                .collect::<Result<BTreeMap<_, _>, _>>()
503                .map(Self::Map);
504        }
505        if value.is::<OpaqueHandle>() {
506            return Ok(Self::Handle(value.cast::<OpaqueHandle>()));
507        }
508
509        Err(UiValueError::UnsupportedType {
510            path,
511            type_name: value.type_name().to_owned(),
512        })
513    }
514
515    pub fn into_dynamic(self) -> Dynamic {
516        match self {
517            Self::Null => Dynamic::UNIT,
518            Self::Bool(value) => Dynamic::from(value),
519            Self::Integer(value) => Dynamic::from(value),
520            Self::Float(value) => Dynamic::from(value),
521            Self::String(value) => Dynamic::from(value),
522            Self::Array(values) => {
523                Dynamic::from_array(values.into_iter().map(Self::into_dynamic).collect())
524            }
525            Self::Map(values) => {
526                let map = values
527                    .into_iter()
528                    .map(|(key, value)| (key.into(), value.into_dynamic()))
529                    .collect::<Map>();
530                Dynamic::from_map(map)
531            }
532            Self::Handle(handle) => Dynamic::from(handle),
533        }
534    }
535}
536
537#[derive(Clone, Debug, Error, Eq, PartialEq)]
538pub enum UiValueError {
539    #[error("unsupported Rhai type `{type_name}` at {path}")]
540    UnsupportedType { path: String, type_name: String },
541    #[error("non-finite float at {path}")]
542    NonFiniteFloat { path: String },
543    #[error("UiValue {resource} limit {limit} exceeded at {path}")]
544    Limit {
545        path: String,
546        resource: &'static str,
547        limit: usize,
548    },
549}
550
551#[derive(Clone, Debug, Error, Eq, PartialEq)]
552pub enum UiValuePathError {
553    #[error("UiValue path must have at most 64 segments and 1-256 byte keys")]
554    InvalidPath,
555    #[error("UiValue path segment {depth} is missing map key `{key}`")]
556    MissingKey { depth: usize, key: String },
557    #[error("UiValue path segment {depth} index {index} is outside array length {len}")]
558    IndexOutOfBounds {
559        depth: usize,
560        index: usize,
561        len: usize,
562    },
563    #[error("UiValue path segment {depth} requires {expected}, got {actual}")]
564    TypeMismatch {
565        depth: usize,
566        expected: &'static str,
567        actual: &'static str,
568    },
569    #[error("UiValue path segment {depth} keyed item {index} is invalid: {reason}")]
570    InvalidKeyedItem {
571        depth: usize,
572        index: usize,
573        reason: &'static str,
574    },
575    #[error("UiValue path segment {depth} has no item where `{key_field}` is `{key}`")]
576    MissingItem {
577        depth: usize,
578        key_field: String,
579        key: String,
580    },
581    #[error("UiValue path segment {depth} has duplicate items where `{key_field}` is `{key}`")]
582    DuplicateItemKey {
583        depth: usize,
584        key_field: String,
585        key: String,
586    },
587}
588
589/// An integer or float script argument as a float; `None` for other values.
590pub(crate) fn script_number(value: &Dynamic) -> Option<f64> {
591    if let Ok(value) = value.as_float() {
592        return Some(value);
593    }
594    value
595        .as_int()
596        .ok()
597        .and_then(|value| value.to_string().parse().ok())
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603    use crate::UiNode;
604
605    #[test]
606    fn nested_values_round_trip() {
607        let value = UiValue::Map(BTreeMap::from([
608            ("enabled".to_owned(), UiValue::Bool(true)),
609            (
610                "items".to_owned(),
611                UiValue::Array(vec![UiValue::Integer(1), UiValue::String("two".to_owned())]),
612            ),
613            (
614                "image".to_owned(),
615                UiValue::Handle(OpaqueHandle::new("image", 7)),
616            ),
617        ]));
618
619        assert_eq!(
620            UiValue::from_dynamic(value.clone().into_dynamic()).unwrap(),
621            value
622        );
623    }
624
625    #[test]
626    fn ui_nodes_cannot_cross_capability_boundary() {
627        let error = UiValue::from_dynamic(Dynamic::from(UiNode::text("no"))).unwrap_err();
628        assert!(matches!(
629            error,
630            UiValueError::UnsupportedType { ref path, .. } if path == "$"
631        ));
632    }
633
634    #[test]
635    fn non_finite_numbers_are_rejected_from_rhai_and_serialization() {
636        assert!(matches!(
637            UiValue::from_dynamic(Dynamic::from(f64::NAN)),
638            Err(UiValueError::NonFiniteFloat { .. })
639        ));
640        assert!(serde_json::to_string(&UiValue::Float(f64::INFINITY)).is_err());
641        assert!(serde_json::from_str::<UiValue>(r#"{"type":"float","value":1e999}"#).is_err());
642    }
643
644    #[test]
645    fn bounded_paths_read_and_replace_existing_nested_values() {
646        let path = UiValuePath::new(vec![
647            UiValuePathSegment::Key("items".to_owned()),
648            UiValuePathSegment::Index(1),
649            UiValuePathSegment::Key("name".to_owned()),
650        ])
651        .unwrap();
652        let mut value = UiValue::Map(BTreeMap::from([(
653            "items".to_owned(),
654            UiValue::Array(vec![
655                UiValue::Map(BTreeMap::from([(
656                    "name".to_owned(),
657                    UiValue::String("first".to_owned()),
658                )])),
659                UiValue::Map(BTreeMap::from([(
660                    "name".to_owned(),
661                    UiValue::String("second".to_owned()),
662                )])),
663            ]),
664        )]));
665        assert_eq!(
666            value.get_path(&path).unwrap(),
667            &UiValue::String("second".to_owned())
668        );
669        value
670            .set_path(&path, UiValue::String("updated".to_owned()))
671            .unwrap();
672        assert_eq!(
673            value.get_path(&path).unwrap(),
674            &UiValue::String("updated".to_owned())
675        );
676        assert!(
677            value
678                .get_path(&UiValuePath::new(vec![UiValuePathSegment::Index(0)]).unwrap())
679                .is_err()
680        );
681    }
682
683    #[test]
684    fn keyed_item_paths_survive_array_reorder() {
685        let path = UiValuePath::new(vec![
686            UiValuePathSegment::Key("items".to_owned()),
687            UiValuePathSegment::Item {
688                key_field: "id".to_owned(),
689                key: "second".to_owned(),
690            },
691            UiValuePathSegment::Key("name".to_owned()),
692        ])
693        .unwrap();
694        let item = |id: &str, name: &str| {
695            UiValue::Map(BTreeMap::from([
696                ("id".to_owned(), UiValue::String(id.to_owned())),
697                ("name".to_owned(), UiValue::String(name.to_owned())),
698            ]))
699        };
700        let mut value = UiValue::Map(BTreeMap::from([(
701            "items".to_owned(),
702            UiValue::Array(vec![item("first", "Alpha"), item("second", "Beta")]),
703        )]));
704        assert_eq!(
705            value.get_path(&path).unwrap(),
706            &UiValue::String("Beta".to_owned())
707        );
708        if let UiValue::Map(root) = &mut value
709            && let Some(UiValue::Array(items)) = root.get_mut("items")
710        {
711            items.reverse();
712        }
713        assert_eq!(
714            value.get_path(&path).unwrap(),
715            &UiValue::String("Beta".to_owned())
716        );
717    }
718}