use serde_json::Value;
use crate::widget::capability::{
widget_property_names, widget_property_set, CapabilityValue, PropertyValueKind,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ApplyOutcome {
Applied,
NotAProperty,
Rejected,
}
fn normalize(name: &str) -> String {
name.chars().filter(|c| !matches!(c, '-' | '_' | ' ')).flat_map(char::to_lowercase).collect()
}
pub fn is_widget_property(widget: &dyn crate::widget::Widget, name: &str) -> bool {
let Some(published) = widget_property_names(widget) else {
return false;
};
let wanted = normalize(name);
published.iter().any(|candidate| normalize(candidate) == wanted)
}
pub(crate) fn resolve_property_name(
widget: &dyn crate::widget::Widget,
name: &str,
) -> Option<&'static str> {
let published = widget_property_names(widget)?;
let wanted = normalize(name);
published.iter().copied().find(|candidate| normalize(candidate) == wanted)
}
fn to_capability_value(
value: &Value,
declared: Option<PropertyValueKind>,
) -> Option<CapabilityValue> {
match declared {
Some(PropertyValueKind::Bool) => value.as_bool().map(CapabilityValue::Bool),
Some(PropertyValueKind::Int) => value.as_i64().map(CapabilityValue::Int),
Some(PropertyValueKind::UInt) => value.as_u64().map(CapabilityValue::UInt),
Some(PropertyValueKind::Float) => value.as_f64().map(CapabilityValue::Float),
Some(PropertyValueKind::String) | Some(PropertyValueKind::Enum) => {
value.as_str().map(|s| CapabilityValue::String(s.to_string()))
}
Some(PropertyValueKind::Color) => value
.as_str()
.and_then(|s| crate::style::CssParser::parse_color(s).ok())
.map(CapabilityValue::Color),
Some(PropertyValueKind::Rect) => rect_from_json(value).map(CapabilityValue::Rect),
None => match value {
Value::Null => Some(CapabilityValue::Null),
Value::Bool(b) => Some(CapabilityValue::Bool(*b)),
Value::Number(n) => {
if let Some(u) = n.as_u64() {
Some(CapabilityValue::UInt(u))
} else if let Some(i) = n.as_i64() {
Some(CapabilityValue::Int(i))
} else {
n.as_f64().map(CapabilityValue::Float)
}
}
Value::String(s) => Some(CapabilityValue::String(s.clone())),
Value::Array(_) | Value::Object(_) => None,
},
}
}
fn rect_from_json(value: &Value) -> Option<crate::core::Rect> {
let components: [i64; 4] = match value {
Value::String(text) => {
let mut parts = text.split(',');
let mut parsed = [0i64; 4];
for slot in parsed.iter_mut() {
*slot = parts.next()?.trim().parse().ok()?;
}
if parts.next().is_some() {
return None;
}
parsed
}
Value::Array(items) => {
if items.len() != 4 {
return None;
}
let mut parsed = [0i64; 4];
for (slot, item) in parsed.iter_mut().zip(items) {
*slot = item.as_i64()?;
}
parsed
}
_ => return None,
};
let width = u32::try_from(components[2]).ok()?;
let height = u32::try_from(components[3]).ok()?;
Some(crate::core::Rect::new(components[0] as i32, components[1] as i32, width, height))
}
fn declared_kind(widget: &dyn crate::widget::Widget, name: &str) -> Option<PropertyValueKind> {
let factory = crate::json::schema_factory();
let capability = factory.capability_for_kind_instance(widget)?;
let wanted = normalize(name);
capability
.properties
.iter()
.find(|schema| normalize(schema.name) == wanted)
.map(|schema| schema.value_kind)
}
pub(crate) fn apply_widget_property(
widget: &mut dyn crate::widget::Widget,
key: &str,
value: &Value,
) -> ApplyOutcome {
let Some(published) = resolve_property_name(widget, key) else {
return ApplyOutcome::NotAProperty;
};
let declared = declared_kind(widget, published);
let Some(capability_value) = to_capability_value(value, declared) else {
return ApplyOutcome::Rejected;
};
match widget_property_set(widget, published, capability_value) {
Ok(()) => ApplyOutcome::Applied,
Err(_) => ApplyOutcome::Rejected,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Rect;
use crate::widget::Button;
#[test]
fn normalize_folds_case_and_separators() {
assert_eq!(normalize("Font-Size"), "fontsize");
assert_eq!(normalize("font_size"), "fontsize");
assert_eq!(normalize("font size"), "fontsize");
assert_eq!(normalize("fontsize"), "fontsize");
}
#[test]
fn to_capability_value_honours_declared_kind() {
let json = serde_json::json!(50);
assert_eq!(
to_capability_value(&json, Some(PropertyValueKind::Int)),
Some(CapabilityValue::Int(50))
);
assert_eq!(
to_capability_value(&json, Some(PropertyValueKind::UInt)),
Some(CapabilityValue::UInt(50))
);
assert_eq!(to_capability_value(&json, Some(PropertyValueKind::String)), None);
let quoted = serde_json::json!("50");
assert_eq!(to_capability_value("ed, Some(PropertyValueKind::Int)), None);
}
#[test]
fn to_capability_value_without_schema_prefers_unsigned_for_counts() {
assert_eq!(
to_capability_value(&serde_json::json!(7), None),
Some(CapabilityValue::UInt(7))
);
assert_eq!(
to_capability_value(&serde_json::json!(-7), None),
Some(CapabilityValue::Int(-7))
);
assert_eq!(
to_capability_value(&serde_json::json!(7.5), None),
Some(CapabilityValue::Float(7.5))
);
assert_eq!(to_capability_value(&serde_json::json!([1, 2]), None), None);
assert_eq!(to_capability_value(&serde_json::json!({"a": 1}), None), None);
}
#[test]
fn button_publishes_text_and_accepts_a_write_by_name() {
let mut button = Button::new("before".to_string(), Rect::new(0, 0, 10, 10));
assert!(is_widget_property(&button, "text"));
assert!(is_widget_property(&button, "Text"), "lookup is case-insensitive");
assert!(!is_widget_property(&button, "no_such_property"));
let outcome = apply_widget_property(&mut button, "text", &serde_json::json!("after"));
assert_eq!(outcome, ApplyOutcome::Applied);
assert_eq!(button.text(), "after");
}
#[test]
fn unknown_name_is_reported_as_not_a_property() {
let mut button = Button::new("x".to_string(), Rect::new(0, 0, 10, 10));
assert_eq!(
apply_widget_property(&mut button, "colour", &serde_json::json!("#ff0000")),
ApplyOutcome::NotAProperty
);
}
#[test]
fn wrong_json_type_is_rejected_not_ignored() {
let mut button = Button::new("x".to_string(), Rect::new(0, 0, 10, 10));
assert_eq!(
apply_widget_property(&mut button, "enabled", &serde_json::json!("yes")),
ApplyOutcome::Rejected
);
assert_eq!(
apply_widget_property(&mut button, "enabled", &serde_json::json!(false)),
ApplyOutcome::Applied
);
}
#[test]
fn resolve_property_name_returns_the_published_spelling() {
let button = Button::new("x".to_string(), Rect::new(0, 0, 10, 10));
assert_eq!(resolve_property_name(&button, "Text"), Some("text"));
assert_eq!(resolve_property_name(&button, "not_there"), None);
}
}