1use serde_json::Value;
27
28#[derive(Debug, Clone)]
47pub enum PropValue {
48 Null,
50 Bool(bool),
52 F64(f64),
54 I64(i64),
56 U64(u64),
58 Str(String),
60 Array(Vec<PropValue>),
62 Object(PropMap),
64}
65
66const I64_MIN_AS_F64: f64 = -9_223_372_036_854_775_808.0;
67const I64_MAX_PLUS_ONE_AS_F64: f64 = 9_223_372_036_854_775_808.0;
68const U64_MAX_PLUS_ONE_AS_F64: f64 = 18_446_744_073_709_551_616.0;
69
70fn exact_i64_to_f64(value: i64) -> Option<f64> {
71 let float = value as f64;
72
73 ((I64_MIN_AS_F64..I64_MAX_PLUS_ONE_AS_F64).contains(&float) && float as i64 == value)
74 .then_some(float)
75}
76
77fn exact_u64_to_f64(value: u64) -> Option<f64> {
78 let float = value as f64;
79
80 ((0.0..U64_MAX_PLUS_ONE_AS_F64).contains(&float) && float as u64 == value).then_some(float)
81}
82
83fn exact_f64_to_i64(value: f64) -> Option<i64> {
84 (value.is_finite()
85 && value.fract() == 0.0
86 && (I64_MIN_AS_F64..I64_MAX_PLUS_ONE_AS_F64).contains(&value))
87 .then_some(value as i64)
88}
89
90fn exact_f64_to_u64(value: f64) -> Option<u64> {
91 (value.is_finite() && value.fract() == 0.0 && (0.0..U64_MAX_PLUS_ONE_AS_F64).contains(&value))
92 .then_some(value as u64)
93}
94
95fn finite_f64_to_f32(value: f64) -> Option<f32> {
96 let narrowed = value as f32;
97
98 (value.is_finite() && narrowed.is_finite()).then_some(narrowed)
99}
100
101impl PartialEq for PropValue {
102 fn eq(&self, other: &Self) -> bool {
103 match (self, other) {
104 (Self::Null, Self::Null) => true,
105 (Self::Bool(a), Self::Bool(b)) => a == b,
106 (Self::Str(a), Self::Str(b)) => a == b,
107 (Self::Array(a), Self::Array(b)) => a == b,
108 (Self::Object(a), Self::Object(b)) => a == b,
109 (Self::I64(a), Self::I64(b)) => a == b,
112 (Self::U64(a), Self::U64(b)) => a == b,
113 (Self::F64(a), Self::F64(b)) => a == b,
114 (Self::I64(i), Self::U64(u)) | (Self::U64(u), Self::I64(i)) => {
115 u64::try_from(*i).is_ok_and(|iu| iu == *u)
116 }
117 (Self::I64(i), Self::F64(f)) | (Self::F64(f), Self::I64(i)) => {
118 exact_f64_to_i64(*f).is_some_and(|fi| fi == *i)
119 }
120 (Self::U64(u), Self::F64(f)) | (Self::F64(f), Self::U64(u)) => {
121 exact_f64_to_u64(*f).is_some_and(|fu| fu == *u)
122 }
123 _ => false,
124 }
125 }
126}
127
128impl PropValue {
129 pub fn as_str(&self) -> Option<&str> {
131 match self {
132 Self::Str(s) => Some(s),
133 _ => None,
134 }
135 }
136
137 pub fn as_f64(&self) -> Option<f64> {
139 match self {
140 Self::F64(v) => v.is_finite().then_some(*v),
141 Self::I64(v) => exact_i64_to_f64(*v),
142 Self::U64(v) => exact_u64_to_f64(*v),
143 _ => None,
144 }
145 }
146
147 pub fn as_bool(&self) -> Option<bool> {
149 match self {
150 Self::Bool(v) => Some(*v),
151 _ => None,
152 }
153 }
154
155 pub fn as_i64(&self) -> Option<i64> {
157 match self {
158 Self::I64(v) => Some(*v),
159 Self::U64(v) => i64::try_from(*v).ok(),
160 Self::F64(v) => exact_f64_to_i64(*v),
161 _ => None,
162 }
163 }
164
165 pub fn as_u64(&self) -> Option<u64> {
167 match self {
168 Self::U64(v) => Some(*v),
169 Self::I64(v) => u64::try_from(*v).ok(),
170 Self::F64(v) => exact_f64_to_u64(*v),
171 _ => None,
172 }
173 }
174
175 pub fn as_array(&self) -> Option<&[PropValue]> {
177 match self {
178 Self::Array(a) => Some(a),
179 _ => None,
180 }
181 }
182
183 pub fn as_object(&self) -> Option<&PropMap> {
185 match self {
186 Self::Object(m) => Some(m),
187 _ => None,
188 }
189 }
190
191 pub fn is_null(&self) -> bool {
193 matches!(self, Self::Null)
194 }
195}
196
197impl From<bool> for PropValue {
199 fn from(v: bool) -> Self {
200 Self::Bool(v)
201 }
202}
203impl From<f32> for PropValue {
204 fn from(v: f32) -> Self {
205 Self::F64(v as f64)
206 }
207}
208impl From<f64> for PropValue {
209 fn from(v: f64) -> Self {
210 Self::F64(v)
211 }
212}
213impl From<i32> for PropValue {
214 fn from(v: i32) -> Self {
215 Self::I64(v as i64)
216 }
217}
218impl From<i64> for PropValue {
219 fn from(v: i64) -> Self {
220 Self::I64(v)
221 }
222}
223impl From<u32> for PropValue {
224 fn from(v: u32) -> Self {
225 Self::U64(v as u64)
226 }
227}
228impl From<u64> for PropValue {
229 fn from(v: u64) -> Self {
230 Self::U64(v)
231 }
232}
233impl From<&str> for PropValue {
234 fn from(v: &str) -> Self {
235 Self::Str(v.to_string())
236 }
237}
238impl From<String> for PropValue {
239 fn from(v: String) -> Self {
240 Self::Str(v)
241 }
242}
243
244impl From<Value> for PropValue {
249 fn from(v: Value) -> Self {
250 match v {
251 Value::Null => Self::Null,
252 Value::Bool(b) => Self::Bool(b),
253 Value::Number(n) => {
254 if let Some(i) = n.as_i64() {
255 Self::I64(i)
256 } else if let Some(u) = n.as_u64() {
257 Self::U64(u)
258 } else if let Some(f) = n.as_f64() {
259 Self::F64(f)
260 } else {
261 Self::Null
262 }
263 }
264 Value::String(s) => Self::Str(s),
265 Value::Array(arr) => Self::Array(arr.into_iter().map(PropValue::from).collect()),
266 Value::Object(map) => Self::Object(PropMap::from_json_map(map)),
267 }
268 }
269}
270
271impl From<PropValue> for Value {
272 fn from(v: PropValue) -> Self {
273 match v {
274 PropValue::Null => Value::Null,
275 PropValue::Bool(b) => Value::Bool(b),
276 PropValue::F64(f) => {
277 if !f.is_finite() {
278 log::warn!(
279 "non-finite f64 ({f}) in PropValue silently encoded as JSON null; \
280 caller passed an invalid value through `From<f32>`/`From<f64>`"
281 );
282 }
283 serde_json::json!(f)
284 }
285 PropValue::I64(i) => Value::Number(i.into()),
286 PropValue::U64(u) => Value::Number(u.into()),
287 PropValue::Str(s) => Value::String(s),
288 PropValue::Array(arr) => Value::Array(arr.into_iter().map(Value::from).collect()),
289 PropValue::Object(map) => Value::Object(map.into_json_map()),
290 }
291 }
292}
293
294#[derive(Debug, Clone, Default)]
314pub struct PropMap(Vec<(String, PropValue)>);
315
316impl PartialEq for PropMap {
317 fn eq(&self, other: &Self) -> bool {
325 let non_null =
326 |pairs: &[(String, PropValue)]| pairs.iter().filter(|(_, v)| !v.is_null()).count();
327 if non_null(&self.0) != non_null(&other.0) {
328 return false;
329 }
330 self.0
331 .iter()
332 .filter(|(_, v)| !v.is_null())
333 .all(|(k, v)| match other.get(k) {
334 Some(ov) if !ov.is_null() => ov == v,
335 _ => false,
336 })
337 }
338}
339
340impl Eq for PropMap {}
341
342impl PropMap {
343 pub fn new() -> Self {
345 Self(Vec::new())
346 }
347
348 pub fn with_capacity(cap: usize) -> Self {
350 Self(Vec::with_capacity(cap))
351 }
352
353 pub fn get(&self, key: &str) -> Option<&PropValue> {
355 self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v)
356 }
357
358 pub fn get_mut(&mut self, key: &str) -> Option<&mut PropValue> {
360 self.0.iter_mut().find(|(k, _)| k == key).map(|(_, v)| v)
361 }
362
363 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<PropValue>) {
365 let key = key.into();
366 let value = value.into();
367 if let Some(entry) = self.0.iter_mut().find(|(k, _)| *k == key) {
368 entry.1 = value;
369 } else {
370 self.0.push((key, value));
371 }
372 }
373
374 pub fn remove(&mut self, key: &str) -> Option<PropValue> {
376 let idx = self.0.iter().position(|(k, _)| k == key)?;
377 Some(self.0.remove(idx).1)
378 }
379
380 pub fn contains_key(&self, key: &str) -> bool {
382 self.0.iter().any(|(k, _)| k == key)
383 }
384
385 pub fn is_empty(&self) -> bool {
387 self.0.is_empty()
388 }
389 pub fn len(&self) -> usize {
391 self.0.len()
392 }
393
394 pub fn iter(&self) -> impl Iterator<Item = (&str, &PropValue)> {
396 self.0.iter().map(|(k, v)| (k.as_str(), v))
397 }
398
399 pub fn keys(&self) -> impl Iterator<Item = &str> {
401 self.0.iter().map(|(k, _)| k.as_str())
402 }
403
404 pub fn from_json_map(map: serde_json::Map<String, Value>) -> Self {
406 Self(
407 map.into_iter()
408 .map(|(k, v)| (k, PropValue::from(v)))
409 .collect(),
410 )
411 }
412
413 pub fn into_json_map(self) -> serde_json::Map<String, Value> {
415 self.0
416 .into_iter()
417 .map(|(k, v)| (k, Value::from(v)))
418 .collect()
419 }
420}
421
422#[derive(Debug, Clone, Default, PartialEq, Eq)]
432pub struct Props(PropMap);
433
434impl Props {
435 pub fn from_json(value: Value) -> Self {
439 match value {
440 Value::Object(map) => Self(PropMap::from_json_map(map)),
441 _ => Self(PropMap::new()),
442 }
443 }
444
445 pub fn get_str(&self, key: &str) -> Option<&str> {
447 self.0.get(key)?.as_str()
448 }
449
450 pub fn get_f64(&self, key: &str) -> Option<f64> {
452 self.0.get(key)?.as_f64()
453 }
454
455 pub fn get_f32(&self, key: &str) -> Option<f32> {
457 finite_f64_to_f32(self.get_f64(key)?)
458 }
459
460 pub fn get_bool(&self, key: &str) -> Option<bool> {
462 self.0.get(key)?.as_bool()
463 }
464
465 pub fn get_i64(&self, key: &str) -> Option<i64> {
467 self.0.get(key)?.as_i64()
468 }
469
470 pub fn get_u64(&self, key: &str) -> Option<u64> {
472 self.0.get(key)?.as_u64()
473 }
474
475 pub fn contains_key(&self, key: &str) -> bool {
477 self.0.contains_key(key)
478 }
479
480 pub fn as_value_cow(&self) -> std::borrow::Cow<'_, Value> {
486 std::borrow::Cow::Owned(Value::Object(self.0.clone().into_json_map()))
487 }
488
489 pub fn as_prop_map(&self) -> &PropMap {
491 &self.0
492 }
493
494 pub fn as_prop_map_mut(&mut self) -> &mut PropMap {
496 &mut self.0
497 }
498
499 pub fn get(&self, key: &str) -> Option<&PropValue> {
501 self.0.get(key)
502 }
503
504 pub fn get_value(&self, key: &str) -> Option<Value> {
508 self.0.get(key).map(|pv| Value::from(pv.clone()))
509 }
510
511 pub fn to_value(&self) -> Value {
513 Value::Object(self.0.clone().into_json_map())
514 }
515
516 pub fn is_object(&self) -> bool {
519 true
520 }
521}
522
523impl From<PropMap> for Props {
524 fn from(map: PropMap) -> Self {
525 Self(map)
526 }
527}
528
529impl serde::Serialize for Props {
535 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
536 self.to_value().serialize(serializer)
537 }
538}
539
540impl<'de> serde::Deserialize<'de> for Props {
541 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
542 let value = Value::deserialize(deserializer)?;
543 Ok(Self::from_json(value))
544 }
545}
546
547#[cfg(test)]
552mod tests {
553 use super::*;
554 use serde_json::json;
555
556 #[test]
557 fn prop_map_insert_and_get() {
558 let mut map = PropMap::new();
559 map.insert("label", "Save");
560 map.insert("size", 18.0f64);
561 map.insert("disabled", false);
562
563 assert_eq!(map.get("label").unwrap().as_str(), Some("Save"));
564 assert_eq!(map.get("size").unwrap().as_f64(), Some(18.0));
565 assert_eq!(map.get("disabled").unwrap().as_bool(), Some(false));
566 assert!(map.get("missing").is_none());
567 }
568
569 #[test]
570 fn prop_map_insert_replaces() {
571 let mut map = PropMap::new();
572 map.insert("value", 1.0f64);
573 map.insert("value", 2.0f64);
574 assert_eq!(map.len(), 1);
575 assert_eq!(map.get("value").unwrap().as_f64(), Some(2.0));
576 }
577
578 #[test]
579 fn prop_map_remove() {
580 let mut map = PropMap::new();
581 map.insert("a", "hello");
582 map.insert("b", "world");
583 assert_eq!(map.remove("a").unwrap().as_str(), Some("hello"));
584 assert_eq!(map.len(), 1);
585 assert!(map.get("a").is_none());
586 }
587
588 #[test]
589 fn props_typed_accessors() {
590 let mut map = PropMap::new();
591 map.insert("title", "Hello");
592 map.insert("size", 24.0f64);
593 map.insert("visible", true);
594 let props = Props::from(map);
595
596 assert_eq!(props.get_str("title"), Some("Hello"));
597 assert_eq!(props.get_f64("size"), Some(24.0));
598 assert_eq!(props.get_f32("size"), Some(24.0));
599 assert_eq!(props.get_bool("visible"), Some(true));
600 assert!(props.get_str("missing").is_none());
601 }
602
603 #[test]
604 fn props_wire_accessors() {
605 let props = Props::from_json(json!({"title": "Hello", "size": 24.0, "visible": true}));
606
607 assert_eq!(props.get_str("title"), Some("Hello"));
608 assert_eq!(props.get_f64("size"), Some(24.0));
609 assert_eq!(props.get_bool("visible"), Some(true));
610 }
611
612 #[test]
613 fn props_deserialize_round_trip_accessors() {
614 let json_str = r#"{"a": 1, "b": "x", "c": true}"#;
615 let props: Props = serde_json::from_str(json_str).unwrap();
616 assert_eq!(props.get_i64("a"), Some(1));
617 assert_eq!(props.get_str("b"), Some("x"));
618 assert_eq!(props.get_bool("c"), Some(true));
619 }
620
621 #[test]
622 fn props_from_non_object_json_is_empty() {
623 let props = Props::from_json(json!("stray string"));
624 assert!(props.as_prop_map().is_empty());
625 assert!(props.is_object());
626 assert_eq!(props.get_str("anything"), None);
627 }
628
629 #[test]
630 fn props_null_entries_are_absent_for_eq() {
631 let mut with_null = PropMap::new();
632 with_null.insert("content", "hello");
633 with_null.insert("size", PropValue::Null);
634 let empty_size = PropMap::new();
635 let mut plain = empty_size.clone();
636 plain.insert("content", "hello");
637
638 assert_eq!(Props::from(with_null), Props::from(plain));
639 }
640
641 #[test]
642 fn props_typed_eq_wire() {
643 let mut map = PropMap::new();
644 map.insert("content", "hello");
645 map.insert("size", 18.0f64);
646 let typed = Props::from(map);
647
648 let wire = Props::from_json(json!({"content": "hello", "size": 18.0}));
649
650 assert_eq!(typed, wire);
651 }
652
653 #[test]
654 fn prop_value_round_trip_through_json() {
655 let mut map = PropMap::new();
656 map.insert("text", "hello");
657 map.insert("num", 42.0f64);
658 map.insert("flag", true);
659 map.insert(
660 "items",
661 PropValue::Array(vec![PropValue::from(1.0f64), PropValue::from(2.0f64)]),
662 );
663
664 let json_map = map.clone().into_json_map();
665 let round_tripped = PropMap::from_json_map(json_map);
666 assert_eq!(map, round_tripped);
667 }
668
669 #[test]
670 fn props_serializes_as_json_object() {
671 let mut map = PropMap::new();
672 map.insert("label", "Save");
673 let props = Props::from(map);
674
675 let json_str = serde_json::to_string(&props).unwrap();
676 assert!(json_str.contains("\"label\":\"Save\""));
677 }
678
679 #[test]
680 fn props_deserializes_to_prop_map() {
681 let json_str = r#"{"label":"Save","size":18}"#;
682 let props: Props = serde_json::from_str(json_str).unwrap();
683 assert_eq!(props.get_str("label"), Some("Save"));
684 assert_eq!(props.get_i64("size"), Some(18));
685 }
686
687 #[test]
688 fn props_default_is_empty() {
689 let props = Props::default();
690 assert!(props.as_prop_map().is_empty());
691 }
692
693 #[test]
694 fn prop_value_numeric_coercion() {
695 assert_eq!(PropValue::I64(42).as_f64(), Some(42.0));
696 assert_eq!(PropValue::U64(42).as_f64(), Some(42.0));
697 assert_eq!(
698 PropValue::I64(9_007_199_254_740_994).as_f64(),
699 Some(9_007_199_254_740_994.0)
700 );
701 assert_eq!(
702 PropValue::U64(9_007_199_254_740_994).as_f64(),
703 Some(9_007_199_254_740_994.0)
704 );
705 assert_eq!(PropValue::F64(42.0).as_i64(), Some(42));
706 assert_eq!(PropValue::F64(42.0).as_u64(), Some(42));
707 assert_eq!(PropValue::I64(42).as_u64(), Some(42));
708 }
709
710 #[test]
711 fn prop_value_rejects_fractional_float_integer_access() {
712 let value = PropValue::F64(42.9);
713
714 assert_eq!(value.as_i64(), None);
715 assert_eq!(value.as_u64(), None);
716
717 let mut map = PropMap::new();
718 map.insert("value", value);
719 let props = Props::from(map);
720
721 assert_eq!(props.get_i64("value"), None);
722 assert_eq!(props.get_u64("value"), None);
723 }
724
725 #[test]
726 fn prop_value_rejects_non_finite_float_access() {
727 for value in [
728 PropValue::F64(f64::NAN),
729 PropValue::F64(f64::INFINITY),
730 PropValue::F64(f64::NEG_INFINITY),
731 ] {
732 assert_eq!(value.as_f64(), None);
733 assert_eq!(value.as_i64(), None);
734 assert_eq!(value.as_u64(), None);
735 }
736
737 let mut map = PropMap::new();
738 map.insert("value", PropValue::F64(f64::INFINITY));
739 let props = Props::from(map);
740
741 assert_eq!(props.get_f64("value"), None);
742 assert_eq!(props.get_f32("value"), None);
743 }
744
745 #[test]
746 fn prop_value_rejects_lossy_integer_float_access() {
747 assert_eq!(PropValue::I64(9_007_199_254_740_993).as_f64(), None);
748 assert_eq!(PropValue::I64(i64::MAX).as_f64(), None);
749 assert_eq!(PropValue::U64(9_007_199_254_740_993).as_f64(), None);
750 assert_eq!(PropValue::U64(u64::MAX).as_f64(), None);
751 }
752
753 #[test]
754 fn props_get_f32_accepts_finite_narrowing_and_rejects_overflow() {
755 let mut map = PropMap::new();
756 map.insert("exact_float", 1.5f64);
757 map.insert("from_f32", 1.1f32);
758 map.insert("lossy_float", 1.1f64);
759 map.insert("lossy_integer", 16_777_217_u64);
760 map.insert("too_large", f64::from(f32::MAX) * 2.0);
761 let props = Props::from(map);
762
763 assert_eq!(props.get_f32("exact_float"), Some(1.5));
764 assert_eq!(props.get_f32("from_f32"), Some(1.1f32));
765 assert_eq!(props.get_f32("lossy_float"), Some(1.1f32));
766 assert_eq!(props.get_f32("lossy_integer"), Some(16_777_216.0));
767 assert_eq!(props.get_f32("too_large"), None);
768 }
769
770 #[test]
784 fn props_serialise_keys_alphabetically() {
785 let mut map = PropMap::new();
786 map.insert("zebra", "z");
788 map.insert("mango", "m");
789 map.insert("apple", "a");
790 let props = Props::from(map);
791
792 let json_str = serde_json::to_string(&props).unwrap();
793 let expected = r#"{"apple":"a","mango":"m","zebra":"z"}"#;
795 assert_eq!(
796 json_str, expected,
797 "serde_json Props serialisation must be alphabetical; \
798 if this fails, serde_json's preserve_order feature may have \
799 leaked in via a transitive dependency"
800 );
801 }
802
803 #[test]
804 fn nested_props_serialise_keys_alphabetically() {
805 let mut inner = PropMap::new();
807 inner.insert("width", 100.0f64);
808 inner.insert("height", 50.0f64);
809 let mut outer = PropMap::new();
810 outer.insert("z_field", PropValue::Object(inner));
811 outer.insert("a_field", "a");
812 let props = Props::from(outer);
813
814 let json_str = serde_json::to_string(&props).unwrap();
815 assert_eq!(
816 json_str,
817 r#"{"a_field":"a","z_field":{"height":50.0,"width":100.0}}"#
818 );
819 }
820}