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 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 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#[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#[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 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 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 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 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}