Skip to main content

a2ui_base/model/
component_model.rs

1//! A single A2UI component's configuration.
2
3use serde_json::Value;
4
5/// Represents one component in the flat component map.
6#[derive(Debug, Clone)]
7pub struct ComponentModel {
8    /// Unique component ID within the surface.
9    pub id: String,
10    /// Component type name (e.g. "Text", "Button", "Column").
11    pub component_type: String,
12    /// All component properties as raw JSON (type-specific).
13    pub properties: serde_json::Map<String, Value>,
14}
15
16impl ComponentModel {
17    /// Parse from a raw JSON value.
18    /// Extracts `id` and `component` fields, puts the rest into `properties`.
19    pub fn from_json(value: &Value) -> Result<Self, crate::error::A2uiError> {
20        let obj = value
21            .as_object()
22            .ok_or_else(|| crate::error::A2uiError::Validation("component must be an object".into()))?;
23
24        let id = obj
25            .get("id")
26            .and_then(|v| v.as_str())
27            .ok_or_else(|| crate::error::A2uiError::Validation("component missing 'id'".into()))?
28            .to_string();
29
30        let component_type = obj
31            .get("component")
32            .and_then(|v| v.as_str())
33            .ok_or_else(|| crate::error::A2uiError::Validation(format!("component '{}' missing 'component' type", id)))?
34            .to_string();
35
36        // Collect remaining fields as properties (excluding id, component)
37        let properties: serde_json::Map<String, Value> = obj
38            .iter()
39            .filter(|(k, _)| *k != "id" && *k != "component")
40            .map(|(k, v)| (k.clone(), v.clone()))
41            .collect();
42
43        Ok(Self {
44            id,
45            component_type,
46            properties,
47        })
48    }
49
50    /// Get a typed property value.
51    pub fn get_property<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
52        self.properties.get(key).and_then(|v| serde_json::from_value(v.clone()).ok())
53    }
54
55    /// Get a raw property value.
56    pub fn get_raw(&self, key: &str) -> Option<&Value> {
57        self.properties.get(key)
58    }
59
60    /// Get the `children` property as a ChildList.
61    pub fn children(&self) -> Option<crate::protocol::common_types::ChildList> {
62        self.properties
63            .get("children")
64            .and_then(|v| serde_json::from_value(v.clone()).ok())
65    }
66
67    /// Get the `child` property as a single ComponentId.
68    pub fn child(&self) -> Option<String> {
69        self.properties.get("child").and_then(|v| v.as_str()).map(|s| s.to_string())
70    }
71
72    /// Get the `action` property.
73    pub fn action(&self) -> Option<crate::protocol::common_types::Action> {
74        self.properties
75            .get("action")
76            .and_then(|v| serde_json::from_value(v.clone()).ok())
77    }
78
79    /// Get the `weight` property.
80    pub fn weight(&self) -> Option<f64> {
81        self.properties.get("weight").and_then(|v| v.as_f64())
82    }
83
84    /// Get the `minHeight` property — a total-footprint height floor (incl. margins/borders).
85    ///
86    /// Used by the measure pass to enforce a minimum vertical size regardless of the
87    /// component's natural content height. CamelCase key matches the existing protocol
88    /// convention (`activeTab`, `enableDate`, `displayStyle`).
89    pub fn min_height(&self) -> Option<u16> {
90        self.properties
91            .get("minHeight")
92            .and_then(|v| v.as_f64())
93            .map(|f| f.round().max(0.0) as u16)
94    }
95
96    /// Get the `checks` property.
97    pub fn checks(&self) -> Option<Vec<crate::protocol::common_types::CheckRule>> {
98        self.properties
99            .get("checks")
100            .and_then(|v| serde_json::from_value(v.clone()).ok())
101    }
102
103    /// Get the accessibility attributes for this component.
104    pub fn accessibility(&self) -> Option<crate::protocol::common_types::AccessibilityAttributes> {
105        self.properties
106            .get("accessibility")
107            .and_then(|v| serde_json::from_value(v.clone()).ok())
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use serde_json::json;
115
116    #[test]
117    fn test_from_json() {
118        let raw = json!({
119            "id": "my_button",
120            "component": "Button",
121            "variant": "primary",
122            "child": "button_label"
123        });
124        let model = ComponentModel::from_json(&raw).unwrap();
125        assert_eq!(model.id, "my_button");
126        assert_eq!(model.component_type, "Button");
127        assert_eq!(model.child(), Some("button_label".to_string()));
128    }
129
130    #[test]
131    fn test_children_static() {
132        let raw = json!({
133            "id": "root",
134            "component": "Column",
135            "children": ["a", "b", "c"]
136        });
137        let model = ComponentModel::from_json(&raw).unwrap();
138        let children = model.children().unwrap();
139        match children {
140            crate::protocol::common_types::ChildList::Static(ids) => {
141                assert_eq!(ids, vec!["a", "b", "c"]);
142            }
143            _ => panic!("expected static child list"),
144        }
145    }
146}