use iced::Color;
use plushie_core::protocol::Props;
use plushie_core::types::PlushieType;
use serde_json::Value;
use crate::iced_convert::hex_to_iced_color;
#[deprecated(note = "use &Props directly")]
pub type JsonProps<'a> = Option<&'a serde_json::Map<String, Value>>;
#[inline]
pub fn f64_to_f32(v: f64) -> f32 {
v.clamp(f32::MIN as f64, f32::MAX as f64) as f32
}
pub fn prop_str(props: &Props, key: &str) -> Option<String> {
props.get_str(key).map(|s| s.to_string())
}
pub fn prop_f32(props: &Props, key: &str) -> Option<f32> {
if let Some(f) = props.get_f64(key) {
return Some(f64_to_f32(f));
}
let s = props.get_str(key)?;
match s.parse::<f32>().ok().filter(|f| f.is_finite()) {
Some(f) => Some(f),
None => {
log::trace!("prop '{}': string not parseable as f32: {:?}", key, s);
None
}
}
}
pub fn prop_f64(props: &Props, key: &str) -> Option<f64> {
if let Some(f) = props.get_f64(key) {
return Some(f);
}
let s = props.get_str(key)?;
match s.parse::<f64>().ok().filter(|f| f.is_finite()) {
Some(f) => Some(f),
None => {
log::trace!("prop '{}': string not parseable as f64: {:?}", key, s);
None
}
}
}
pub fn prop_u32(props: &Props, key: &str) -> Option<u32> {
if let Some(u) = props.get_u64(key).and_then(|v| u32::try_from(v).ok()) {
return Some(u);
}
let s = props.get_str(key)?;
match s.parse::<u32>() {
Ok(u) => Some(u),
Err(_) => {
log::trace!("prop '{}': string not parseable as u32: {:?}", key, s);
None
}
}
}
pub fn prop_u64(props: &Props, key: &str) -> Option<u64> {
if let Some(u) = props.get_u64(key) {
return Some(u);
}
let s = props.get_str(key)?;
match s.parse::<u64>() {
Ok(u) => Some(u),
Err(_) => {
log::trace!("prop '{}': string not parseable as u64: {:?}", key, s);
None
}
}
}
pub fn prop_usize(props: &Props, key: &str) -> Option<usize> {
prop_u64(props, key).and_then(|v| usize::try_from(v).ok())
}
pub fn prop_i32(props: &Props, key: &str) -> Option<i32> {
if let Some(i) = props.get_i64(key).and_then(|v| i32::try_from(v).ok()) {
return Some(i);
}
let s = props.get_str(key)?;
match s.parse::<i32>() {
Ok(i) => Some(i),
Err(_) => {
log::trace!("prop '{}': string not parseable as i32: {:?}", key, s);
None
}
}
}
pub fn prop_i64(props: &Props, key: &str) -> Option<i64> {
if let Some(i) = props.get_i64(key) {
return Some(i);
}
let s = props.get_str(key)?;
match s.parse::<i64>() {
Ok(i) => Some(i),
Err(_) => {
log::trace!("prop '{}': string not parseable as i64: {:?}", key, s);
None
}
}
}
pub fn prop_bool(props: &Props, key: &str) -> Option<bool> {
props.get_bool(key)
}
pub fn prop_bool_default(props: &Props, key: &str, default: bool) -> bool {
prop_bool(props, key).unwrap_or(default)
}
pub fn prop_range_f32(props: &Props) -> std::ops::RangeInclusive<f32> {
props
.get_value("range")
.as_ref()
.and_then(|v| v.as_array())
.and_then(|arr| {
let mut min = f64_to_f32(arr.first()?.as_f64()?);
let mut max = f64_to_f32(arr.get(1)?.as_f64()?);
if min > max {
log::warn!("prop 'range': min ({min}) > max ({max}), swapping");
std::mem::swap(&mut min, &mut max);
}
Some(min..=max)
})
.unwrap_or(0.0..=100.0)
}
pub fn prop_range_f64(props: &Props) -> std::ops::RangeInclusive<f64> {
props
.get_value("range")
.as_ref()
.and_then(|v| v.as_array())
.and_then(|arr| {
let mut min = arr.first()?.as_f64()?;
let mut max = arr.get(1)?.as_f64()?;
if min > max {
log::warn!("prop 'range': min ({min}) > max ({max}), swapping");
std::mem::swap(&mut min, &mut max);
}
Some(min..=max)
})
.unwrap_or(0.0..=100.0)
}
pub fn prop_f32_array(props: &Props, key: &str) -> Option<Vec<f32>> {
let val = props.get_value(key)?;
match val.as_array() {
Some(arr) => {
let mut result = Vec::with_capacity(arr.len());
for (i, v) in arr.iter().enumerate() {
match v.as_f64() {
Some(f) => result.push(f64_to_f32(f)),
None => {
log::warn!(
"prop '{}': dropping non-numeric element at index {}: {:?}",
key,
i,
v
);
}
}
}
Some(result)
}
None => {
log::trace!("prop '{}': expected array, got {:?}", key, val);
None
}
}
}
pub fn prop_f64_array(props: &Props, key: &str) -> Option<Vec<f64>> {
let val = props.get_value(key)?;
match val.as_array() {
Some(arr) => {
let mut result = Vec::with_capacity(arr.len());
for (i, v) in arr.iter().enumerate() {
match v.as_f64() {
Some(f) => result.push(f),
None => {
log::warn!(
"prop '{}': dropping non-numeric element at index {}: {:?}",
key,
i,
v
);
}
}
}
Some(result)
}
None => {
log::trace!("prop '{}': expected array, got {:?}", key, val);
None
}
}
}
pub fn prop_str_array(props: &Props, key: &str) -> Option<Vec<String>> {
let val = props.get_value(key)?;
match val.as_array() {
Some(arr) => Some(
arr.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect(),
),
None => {
log::trace!("prop '{}': expected array, got {:?}", key, val);
None
}
}
}
pub fn prop_animated_f32(
interpolated: &std::collections::HashMap<String, serde_json::Map<String, Value>>,
node_id: &str,
props: &Props,
key: &str,
) -> Option<f32> {
if let Some(overrides) = interpolated.get(node_id)
&& let Some(val) = overrides.get(key)
{
return val.as_f64().map(f64_to_f32);
}
if let Some(plushie_core::protocol::PropValue::Object(_)) = props.get(key) {
return None;
}
prop_f32(props, key)
}
pub fn prop_animated_color(
interpolated: &std::collections::HashMap<String, serde_json::Map<String, Value>>,
node_id: &str,
props: &Props,
key: &str,
) -> Option<Color> {
if let Some(overrides) = interpolated.get(node_id)
&& let Some(val) = overrides.get(key)
{
return val.as_str().and_then(hex_to_iced_color);
}
if let Some(plushie_core::protocol::PropValue::Object(_)) = props.get(key) {
return None;
}
plushie_core::types::Color::extract(props, key).map(|c| crate::iced_convert::color(&c))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn make_props(val: Value) -> Props {
Props::from_json(val)
}
#[test]
fn test_prop_str() {
let p = make_props(json!({"label": "hello"}));
assert_eq!(prop_str(&p, "label"), Some("hello".to_string()));
assert_eq!(prop_str(&p, "missing"), None);
}
#[test]
fn test_prop_str_non_string() {
let p = make_props(json!({"num": 42}));
assert_eq!(prop_str(&p, "num"), None);
}
#[test]
fn test_prop_f32_number() {
let p = make_props(json!({"size": 14.5}));
let v = prop_f32(&p, "size").unwrap();
assert!((v - 14.5).abs() < 0.001);
}
#[test]
fn test_prop_f32_string() {
let p = make_props(json!({"size": "14.5"}));
let v = prop_f32(&p, "size").unwrap();
assert!((v - 14.5).abs() < 0.001);
}
#[test]
fn test_prop_f32_missing() {
let p = make_props(json!({}));
assert!(prop_f32(&p, "size").is_none());
}
#[test]
fn test_prop_f32_wrong_type() {
let p = make_props(json!({"size": true}));
assert!(prop_f32(&p, "size").is_none());
}
#[test]
fn test_prop_f64_number() {
let p = make_props(json!({"value": 99.9}));
let v = prop_f64(&p, "value").unwrap();
assert!((v - 99.9).abs() < 0.0001);
}
#[test]
fn test_prop_f64_string() {
let p = make_props(json!({"value": "99.9"}));
let v = prop_f64(&p, "value").unwrap();
assert!((v - 99.9).abs() < 0.0001);
}
#[test]
fn test_prop_u32_number() {
let p = make_props(json!({"count": 42}));
assert_eq!(prop_u32(&p, "count"), Some(42));
}
#[test]
fn test_prop_u32_string() {
let p = make_props(json!({"count": "123"}));
assert_eq!(prop_u32(&p, "count"), Some(123));
}
#[test]
fn test_prop_u32_missing() {
let p = make_props(json!({}));
assert_eq!(prop_u32(&p, "count"), None);
}
#[test]
fn test_prop_u32_negative() {
let p = make_props(json!({"count": -1}));
assert_eq!(prop_u32(&p, "count"), None);
}
#[test]
fn test_prop_u32_overflow() {
let p = make_props(json!({"count": 5_000_000_000u64}));
assert_eq!(prop_u32(&p, "count"), None);
}
#[test]
fn test_prop_u64_number() {
let p = make_props(json!({"big": 9_000_000_000u64}));
assert_eq!(prop_u64(&p, "big"), Some(9_000_000_000));
}
#[test]
fn test_prop_u64_string() {
let p = make_props(json!({"big": "999"}));
assert_eq!(prop_u64(&p, "big"), Some(999));
}
#[test]
fn test_prop_u64_missing() {
let p = make_props(json!({}));
assert_eq!(prop_u64(&p, "big"), None);
}
#[test]
fn test_prop_u64_negative() {
let p = make_props(json!({"big": -1}));
assert_eq!(prop_u64(&p, "big"), None);
}
#[test]
fn test_prop_usize_number() {
let p = make_props(json!({"idx": 7}));
assert_eq!(prop_usize(&p, "idx"), Some(7));
}
#[test]
fn test_prop_usize_string() {
let p = make_props(json!({"idx": "42"}));
assert_eq!(prop_usize(&p, "idx"), Some(42));
}
#[test]
fn test_prop_usize_missing() {
let p = make_props(json!({}));
assert_eq!(prop_usize(&p, "idx"), None);
}
#[test]
fn test_prop_i32_positive() {
let p = make_props(json!({"x": 42}));
assert_eq!(prop_i32(&p, "x"), Some(42));
}
#[test]
fn test_prop_i32_negative() {
let p = make_props(json!({"x": -100}));
assert_eq!(prop_i32(&p, "x"), Some(-100));
}
#[test]
fn test_prop_i32_string() {
let p = make_props(json!({"x": "-7"}));
assert_eq!(prop_i32(&p, "x"), Some(-7));
}
#[test]
fn test_prop_i32_missing() {
let p = make_props(json!({}));
assert_eq!(prop_i32(&p, "x"), None);
}
#[test]
fn test_prop_i32_overflow() {
let p = make_props(json!({"x": 5_000_000_000i64}));
assert_eq!(prop_i32(&p, "x"), None);
}
#[test]
fn test_prop_i64_positive() {
let p = make_props(json!({"offset": 100}));
assert_eq!(prop_i64(&p, "offset"), Some(100));
}
#[test]
fn test_prop_i64_negative() {
let p = make_props(json!({"offset": -50}));
assert_eq!(prop_i64(&p, "offset"), Some(-50));
}
#[test]
fn test_prop_i64_string() {
let p = make_props(json!({"offset": "-99"}));
assert_eq!(prop_i64(&p, "offset"), Some(-99));
}
#[test]
fn test_prop_i64_missing() {
let p = make_props(json!({}));
assert_eq!(prop_i64(&p, "offset"), None);
}
#[test]
fn test_prop_bool() {
let p = make_props(json!({"disabled": true}));
assert_eq!(prop_bool(&p, "disabled"), Some(true));
assert_eq!(prop_bool(&p, "missing"), None);
}
#[test]
fn test_prop_bool_default() {
let p = make_props(json!({"disabled": true}));
assert!(prop_bool_default(&p, "disabled", false));
assert!(!prop_bool_default(&p, "missing", false));
assert!(prop_bool_default(&p, "missing", true));
}
#[test]
fn test_prop_bool_wrong_type() {
let p = make_props(json!({"disabled": "yes"}));
assert_eq!(prop_bool(&p, "disabled"), None);
}
#[test]
fn test_prop_range_f32_present() {
let p = make_props(json!({"range": [10.0, 50.0]}));
let r = prop_range_f32(&p);
assert_eq!(*r.start(), 10.0);
assert_eq!(*r.end(), 50.0);
}
#[test]
fn test_prop_range_f32_default() {
let p = make_props(json!({}));
let r = prop_range_f32(&p);
assert_eq!(*r.start(), 0.0);
assert_eq!(*r.end(), 100.0);
}
#[test]
fn test_prop_range_f64_present() {
let p = make_props(json!({"range": [1.0, 2.0]}));
let r = prop_range_f64(&p);
assert_eq!(*r.start(), 1.0);
assert_eq!(*r.end(), 2.0);
}
#[test]
fn test_prop_f32_array() {
let p = make_props(json!({"data": [1.0, 2.5, 3.0]}));
let arr = prop_f32_array(&p, "data").unwrap();
assert_eq!(arr.len(), 3);
assert!((arr[0] - 1.0).abs() < 0.001);
assert!((arr[1] - 2.5).abs() < 0.001);
assert!((arr[2] - 3.0).abs() < 0.001);
}
#[test]
fn test_prop_f32_array_empty() {
let p = make_props(json!({"data": []}));
let arr = prop_f32_array(&p, "data").unwrap();
assert!(arr.is_empty());
}
#[test]
fn test_prop_f32_array_missing() {
let p = make_props(json!({}));
assert!(prop_f32_array(&p, "data").is_none());
}
#[test]
fn test_prop_f32_array_not_array() {
let p = make_props(json!({"data": "nope"}));
assert!(prop_f32_array(&p, "data").is_none());
}
#[test]
fn test_prop_f32_string_nan() {
let p = make_props(json!({"size": "NaN"}));
assert!(prop_f32(&p, "size").is_none());
}
#[test]
fn test_prop_f32_string_infinity() {
let p = make_props(json!({"size": "Infinity"}));
assert!(prop_f32(&p, "size").is_none());
}
#[test]
fn test_prop_f32_empty_string() {
let p = make_props(json!({"size": ""}));
assert!(prop_f32(&p, "size").is_none());
}
#[test]
fn test_prop_u32_non_numeric_string() {
let p = make_props(json!({"count": "not_a_number"}));
assert_eq!(prop_u32(&p, "count"), None);
}
#[test]
fn test_empty_props() {
let p = Props::default();
assert!(prop_str(&p, "anything").is_none());
assert!(prop_f32(&p, "anything").is_none());
assert!(prop_bool(&p, "anything").is_none());
}
#[test]
fn test_prop_f64_array() {
let p = make_props(json!({"data": [1.0, 2.5, 3.0]}));
let arr = prop_f64_array(&p, "data").unwrap();
assert_eq!(arr.len(), 3);
assert!((arr[0] - 1.0).abs() < 0.0001);
assert!((arr[1] - 2.5).abs() < 0.0001);
assert!((arr[2] - 3.0).abs() < 0.0001);
}
#[test]
fn test_prop_f64_array_skips_non_numeric() {
let p = make_props(json!({"data": [1.0, "nope", 3.0]}));
let arr = prop_f64_array(&p, "data").unwrap();
assert_eq!(arr.len(), 2);
assert!((arr[0] - 1.0).abs() < 0.0001);
assert!((arr[1] - 3.0).abs() < 0.0001);
}
#[test]
fn test_prop_f64_array_missing() {
let p = make_props(json!({}));
assert!(prop_f64_array(&p, "data").is_none());
}
#[test]
fn test_prop_str_array() {
let p = make_props(json!({"tags": ["a", "b", "c"]}));
let arr = prop_str_array(&p, "tags").unwrap();
assert_eq!(arr, vec!["a", "b", "c"]);
}
#[test]
fn test_prop_str_array_skips_non_string() {
let p = make_props(json!({"tags": ["a", 42, "c"]}));
let arr = prop_str_array(&p, "tags").unwrap();
assert_eq!(arr, vec!["a", "c"]);
}
#[test]
fn test_prop_str_array_missing() {
let p = make_props(json!({}));
assert!(prop_str_array(&p, "tags").is_none());
}
mod proptest_prop_helpers {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_f32_never_panics(val: f64) {
let p = make_props(json!({"v": val}));
let result = prop_f32(&p, "v");
if val.is_finite() {
prop_assert!(result.is_some(), "expected Some for finite {val}");
let f = result.unwrap();
prop_assert!(f.is_finite(), "expected finite f32 for finite input {val}");
} else {
}
}
}
}
fn make_typed_props() -> Props {
let mut map = plushie_core::protocol::PropMap::new();
map.insert("label", "hello");
map.insert("size", 14.5f64);
map.insert("count", 42i64);
map.insert("visible", true);
map.insert("color", "#ff0000");
Props::from(map)
}
#[test]
fn typed_prop_str() {
let p = make_typed_props();
assert_eq!(prop_str(&p, "label"), Some("hello".to_string()));
assert_eq!(prop_str(&p, "missing"), None);
}
#[test]
fn typed_prop_f32() {
let p = make_typed_props();
let v = prop_f32(&p, "size").unwrap();
assert!((v - 14.5).abs() < 0.001);
}
#[test]
fn typed_prop_f64() {
let p = make_typed_props();
assert_eq!(prop_f64(&p, "size"), Some(14.5));
}
#[test]
fn typed_prop_bool() {
let p = make_typed_props();
assert_eq!(prop_bool(&p, "visible"), Some(true));
assert!(prop_bool_default(&p, "visible", false));
assert!(!prop_bool_default(&p, "missing", false));
}
#[test]
fn typed_prop_i64() {
let p = make_typed_props();
assert_eq!(prop_i64(&p, "count"), Some(42));
}
#[test]
fn typed_prop_u32() {
let p = make_typed_props();
assert_eq!(prop_u32(&p, "count"), Some(42));
}
#[test]
fn typed_and_wire_produce_same_results() {
let typed = make_typed_props();
let wire = make_props(json!({
"label": "hello",
"size": 14.5,
"count": 42,
"visible": true,
"color": "#ff0000",
}));
assert_eq!(prop_str(&typed, "label"), prop_str(&wire, "label"));
assert_eq!(prop_f32(&typed, "size"), prop_f32(&wire, "size"));
assert_eq!(prop_f64(&typed, "size"), prop_f64(&wire, "size"));
assert_eq!(prop_bool(&typed, "visible"), prop_bool(&wire, "visible"));
assert_eq!(prop_i64(&typed, "count"), prop_i64(&wire, "count"));
assert_eq!(prop_u64(&typed, "count"), prop_u64(&wire, "count"));
}
}