Skip to main content

agpu/ontology/
schema.rs

1//! Widget schema definitions for agent discoverability.
2
3use serde::{Deserialize, Serialize};
4
5/// Describes the schema of a widget type for agent discoverability.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct WidgetSchema {
8    /// Unique type name (e.g., "Button", "TextInput", "Panel").
9    pub name: String,
10    /// Human-readable description of the widget's purpose.
11    pub description: String,
12    /// The semantic role this widget type typically plays.
13    pub default_role: SemanticRole,
14    /// Properties that can be configured on this widget.
15    pub properties: Vec<PropertySchema>,
16    /// Actions that agents can invoke on this widget type.
17    #[serde(default, skip_serializing_if = "Vec::is_empty")]
18    pub actions: Vec<super::AgentAction>,
19    /// Brief usage example for agents.
20    pub usage_hint: Option<String>,
21    /// Tags for fuzzy search (e.g., ["button", "action", "click"]).
22    pub tags: Vec<String>,
23}
24
25/// Describes a single configurable property on a widget.
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27pub struct PropertySchema {
28    /// Property name (e.g., "text", "style", "enabled").
29    pub name: String,
30    /// Human-readable description.
31    pub description: String,
32    /// The type of this property.
33    pub property_type: PropertyType,
34    /// Whether this property is required.
35    pub required: bool,
36    /// Default value as JSON.
37    pub default_value: Option<serde_json::Value>,
38    /// Optional constraints on the property value.
39    pub constraints: Vec<PropertyConstraint>,
40}
41
42/// Type of a widget property.
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
44pub enum PropertyType {
45    String,
46    Integer,
47    Float,
48    Boolean,
49    Color,
50    Style,
51    Rect,
52    Size,
53    Position,
54    Enum(Vec<String>),
55    Array(Box<PropertyType>),
56    Object(Vec<PropertySchema>),
57    Widget,
58    Any,
59}
60
61/// A constraint on a property value.
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub enum PropertyConstraint {
64    Min(f64),
65    Max(f64),
66    MinLength(usize),
67    MaxLength(usize),
68    Pattern(String),
69    OneOf(Vec<serde_json::Value>),
70}
71
72/// The semantic role a widget plays in the UI.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
74pub enum SemanticRole {
75    /// Displays text or data to the user (read-only).
76    Display,
77    /// Accepts user input (text fields, checkboxes, sliders, etc.).
78    Input,
79    /// Provides navigation between views or sections.
80    Navigation,
81    /// Contains and arranges other widgets.
82    Container,
83    /// Indicates progress or loading state.
84    Progress,
85    /// Displays a selection from a set of options.
86    Selection,
87    /// Provides interactive data visualization.
88    DataVisualization,
89    /// A decorative or structural element (borders, spacers).
90    Decoration,
91    /// An interactive button or action trigger.
92    Action,
93    /// A scrollable region.
94    Scrollable,
95    /// A modal dialog or overlay.
96    Modal,
97    /// A menu or menu item.
98    Menu,
99    /// A toolbar or toolbar item.
100    Toolbar,
101    /// A tab or tab bar.
102    Tab,
103    /// A tree node in a hierarchical view.
104    TreeNode,
105    /// A canvas or drawing area.
106    Canvas,
107    /// A media element (image, video, audio).
108    Media,
109    /// A system-level service (i18n, plugins, window management, theme).
110    System,
111    /// A diagnostic or metrics component (profiling, performance).
112    Diagnostic,
113    /// A platform integration component (dialogs, tray, file system).
114    Configuration,
115    /// An unknown or custom role.
116    Custom,
117}
118
119impl WidgetSchema {
120    /// Create a minimal widget schema.
121    pub fn new(
122        name: impl Into<String>,
123        description: impl Into<String>,
124        role: SemanticRole,
125    ) -> Self {
126        Self {
127            name: name.into(),
128            description: description.into(),
129            default_role: role,
130            properties: vec![],
131            actions: vec![],
132            usage_hint: None,
133            tags: vec![],
134        }
135    }
136}
137
138impl PropertySchema {
139    /// Create a simple property schema.
140    pub fn new(
141        name: impl Into<String>,
142        description: impl Into<String>,
143        property_type: PropertyType,
144        required: bool,
145    ) -> Self {
146        Self {
147            name: name.into(),
148            description: description.into(),
149            property_type,
150            required,
151            default_value: None,
152            constraints: vec![],
153        }
154    }
155}
156
157impl std::fmt::Display for SemanticRole {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        let s = match self {
160            Self::Display => "display",
161            Self::Input => "input",
162            Self::Navigation => "navigation",
163            Self::Container => "container",
164            Self::Progress => "progress",
165            Self::Selection => "selection",
166            Self::DataVisualization => "data-visualization",
167            Self::Decoration => "decoration",
168            Self::Action => "action",
169            Self::Scrollable => "scrollable",
170            Self::Modal => "modal",
171            Self::Menu => "menu",
172            Self::Toolbar => "toolbar",
173            Self::Tab => "tab",
174            Self::TreeNode => "tree-node",
175            Self::Canvas => "canvas",
176            Self::Media => "media",
177            Self::System => "system",
178            Self::Diagnostic => "diagnostic",
179            Self::Configuration => "configuration",
180            Self::Custom => "custom",
181        };
182        f.write_str(s)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn widget_schema_new() {
192        let ws = WidgetSchema::new("Button", "A clickable button", SemanticRole::Action);
193        assert_eq!(ws.name, "Button");
194        assert_eq!(ws.default_role, SemanticRole::Action);
195        assert!(ws.properties.is_empty());
196        assert!(ws.actions.is_empty());
197        assert!(ws.tags.is_empty());
198    }
199
200    #[test]
201    fn property_schema_new() {
202        let ps = PropertySchema::new("text", "The text content", PropertyType::String, true);
203        assert_eq!(ps.name, "text");
204        assert!(ps.required);
205        assert!(ps.default_value.is_none());
206        assert!(ps.constraints.is_empty());
207    }
208
209    #[test]
210    fn semantic_role_display() {
211        assert_eq!(format!("{}", SemanticRole::Display), "display");
212        assert_eq!(format!("{}", SemanticRole::Input), "input");
213        assert_eq!(format!("{}", SemanticRole::Action), "action");
214        assert_eq!(format!("{}", SemanticRole::Container), "container");
215        assert_eq!(
216            format!("{}", SemanticRole::DataVisualization),
217            "data-visualization"
218        );
219        assert_eq!(format!("{}", SemanticRole::Custom), "custom");
220    }
221
222    #[test]
223    fn widget_schema_serialize_roundtrip() {
224        let mut ws = WidgetSchema::new("Slider", "A range slider", SemanticRole::Input);
225        ws.properties.push(PropertySchema::new(
226            "value",
227            "Current value",
228            PropertyType::Float,
229            true,
230        ));
231        ws.tags = vec!["slider".into(), "range".into()];
232
233        let json = serde_json::to_string(&ws).unwrap();
234        let ws2: WidgetSchema = serde_json::from_str(&json).unwrap();
235        assert_eq!(ws2.name, "Slider");
236        assert_eq!(ws2.properties.len(), 1);
237        assert_eq!(ws2.tags.len(), 2);
238    }
239
240    #[test]
241    fn property_type_nested() {
242        let arr = PropertyType::Array(Box::new(PropertyType::String));
243        let json = serde_json::to_string(&arr).unwrap();
244        let arr2: PropertyType = serde_json::from_str(&json).unwrap();
245        assert_eq!(arr, arr2);
246    }
247
248    #[test]
249    fn property_constraint_serialize() {
250        let constraints = vec![
251            PropertyConstraint::Min(0.0),
252            PropertyConstraint::Max(100.0),
253            PropertyConstraint::MinLength(1),
254            PropertyConstraint::MaxLength(255),
255            PropertyConstraint::Pattern(r"^\d+$".into()),
256        ];
257        for c in constraints {
258            let json = serde_json::to_string(&c).unwrap();
259            let c2: PropertyConstraint = serde_json::from_str(&json).unwrap();
260            assert_eq!(c, c2);
261        }
262    }
263}