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#[cfg(test)]
590mod tests {
591    use super::*;
592    use crate::UiNode;
593
594    #[test]
595    fn nested_values_round_trip() {
596        let value = UiValue::Map(BTreeMap::from([
597            ("enabled".to_owned(), UiValue::Bool(true)),
598            (
599                "items".to_owned(),
600                UiValue::Array(vec![UiValue::Integer(1), UiValue::String("two".to_owned())]),
601            ),
602            (
603                "image".to_owned(),
604                UiValue::Handle(OpaqueHandle::new("image", 7)),
605            ),
606        ]));
607
608        assert_eq!(
609            UiValue::from_dynamic(value.clone().into_dynamic()).unwrap(),
610            value
611        );
612    }
613
614    #[test]
615    fn ui_nodes_cannot_cross_capability_boundary() {
616        let error = UiValue::from_dynamic(Dynamic::from(UiNode::text("no"))).unwrap_err();
617        assert!(matches!(
618            error,
619            UiValueError::UnsupportedType { ref path, .. } if path == "$"
620        ));
621    }
622
623    #[test]
624    fn non_finite_numbers_are_rejected_from_rhai_and_serialization() {
625        assert!(matches!(
626            UiValue::from_dynamic(Dynamic::from(f64::NAN)),
627            Err(UiValueError::NonFiniteFloat { .. })
628        ));
629        assert!(serde_json::to_string(&UiValue::Float(f64::INFINITY)).is_err());
630        assert!(serde_json::from_str::<UiValue>(r#"{"type":"float","value":1e999}"#).is_err());
631    }
632
633    #[test]
634    fn bounded_paths_read_and_replace_existing_nested_values() {
635        let path = UiValuePath::new(vec![
636            UiValuePathSegment::Key("items".to_owned()),
637            UiValuePathSegment::Index(1),
638            UiValuePathSegment::Key("name".to_owned()),
639        ])
640        .unwrap();
641        let mut value = UiValue::Map(BTreeMap::from([(
642            "items".to_owned(),
643            UiValue::Array(vec![
644                UiValue::Map(BTreeMap::from([(
645                    "name".to_owned(),
646                    UiValue::String("first".to_owned()),
647                )])),
648                UiValue::Map(BTreeMap::from([(
649                    "name".to_owned(),
650                    UiValue::String("second".to_owned()),
651                )])),
652            ]),
653        )]));
654        assert_eq!(
655            value.get_path(&path).unwrap(),
656            &UiValue::String("second".to_owned())
657        );
658        value
659            .set_path(&path, UiValue::String("updated".to_owned()))
660            .unwrap();
661        assert_eq!(
662            value.get_path(&path).unwrap(),
663            &UiValue::String("updated".to_owned())
664        );
665        assert!(
666            value
667                .get_path(&UiValuePath::new(vec![UiValuePathSegment::Index(0)]).unwrap())
668                .is_err()
669        );
670    }
671
672    #[test]
673    fn keyed_item_paths_survive_array_reorder() {
674        let path = UiValuePath::new(vec![
675            UiValuePathSegment::Key("items".to_owned()),
676            UiValuePathSegment::Item {
677                key_field: "id".to_owned(),
678                key: "second".to_owned(),
679            },
680            UiValuePathSegment::Key("name".to_owned()),
681        ])
682        .unwrap();
683        let item = |id: &str, name: &str| {
684            UiValue::Map(BTreeMap::from([
685                ("id".to_owned(), UiValue::String(id.to_owned())),
686                ("name".to_owned(), UiValue::String(name.to_owned())),
687            ]))
688        };
689        let mut value = UiValue::Map(BTreeMap::from([(
690            "items".to_owned(),
691            UiValue::Array(vec![item("first", "Alpha"), item("second", "Beta")]),
692        )]));
693        assert_eq!(
694            value.get_path(&path).unwrap(),
695            &UiValue::String("Beta".to_owned())
696        );
697        if let UiValue::Map(root) = &mut value
698            && let Some(UiValue::Array(items)) = root.get_mut("items")
699        {
700            items.reverse();
701        }
702        assert_eq!(
703            value.get_path(&path).unwrap(),
704            &UiValue::String("Beta".to_owned())
705        );
706    }
707}