Skip to main content

a2ui_base/protocol/
common_types.rs

1//! A2UI v1.0 Common Types
2//!
3//! Mirrors the JSON Schema `common_types.json` — the core data binding types
4//! used throughout the A2UI protocol.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10// ---------------------------------------------------------------------------
11// Component references
12// ---------------------------------------------------------------------------
13
14/// Unique identifier for a component instance within a surface.
15pub type ComponentId = String;
16
17// ---------------------------------------------------------------------------
18// Data binding
19// ---------------------------------------------------------------------------
20
21/// A JSON Pointer path into the data model.
22/// Serialized as `{ "path": "/some/pointer" }`.
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24pub struct DataBinding {
25    pub path: String,
26}
27
28/// A named function call with arguments.
29/// Each argument value can itself be any JSON value (including nested Dynamic values).
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31pub struct FunctionCall {
32    pub call: String,
33    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
34    pub args: HashMap<String, serde_json::Value>,
35}
36
37// ---------------------------------------------------------------------------
38// Dynamic value types — can be a literal, a data-binding, or a function call
39// ---------------------------------------------------------------------------
40
41/// A value that is either a literal string, a data-binding, or a function call.
42///
43/// JSON representations:
44/// - Literal: `"Hello"`
45/// - Binding: `{ "path": "/user/name" }`
46/// - Function: `{ "call": "capitalize", "args": { ... } }`
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48#[serde(untagged)]
49pub enum DynamicString {
50    /// A literal string value.
51    Literal(String),
52    /// A binding to a data model path.
53    Binding(DataBinding),
54    /// A function call that returns a string.
55    Function(FunctionCall),
56}
57
58impl DynamicString {
59    /// Returns `true` if this is a literal string value.
60    pub fn is_literal(&self) -> bool {
61        matches!(self, Self::Literal(_))
62    }
63
64    /// Returns the literal value if this is a literal, otherwise `None`.
65    pub fn as_literal(&self) -> Option<&str> {
66        match self {
67            Self::Literal(s) => Some(s),
68            _ => None,
69        }
70    }
71}
72
73impl Default for DynamicString {
74    fn default() -> Self {
75        Self::Literal(String::new())
76    }
77}
78
79impl From<String> for DynamicString {
80    fn from(s: String) -> Self {
81        Self::Literal(s)
82    }
83}
84
85impl From<&str> for DynamicString {
86    fn from(s: &str) -> Self {
87        Self::Literal(s.to_string())
88    }
89}
90
91/// A value that is either a literal number, a data-binding, or a function call.
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93#[serde(untagged)]
94pub enum DynamicNumber {
95    Literal(f64),
96    Binding(DataBinding),
97    Function(FunctionCall),
98}
99
100/// A value that is either a literal boolean, a data-binding, or a function call.
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102#[serde(untagged)]
103pub enum DynamicBoolean {
104    Literal(bool),
105    Binding(DataBinding),
106    Function(FunctionCall),
107}
108
109/// A value that is either a literal boolean (via `condition` key),
110/// a data-binding, or a function call — used in `CheckRule` conditions.
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
112#[serde(untagged)]
113pub enum DynamicBooleanCondition {
114    Literal(bool),
115    Binding(DataBinding),
116    Function(FunctionCall),
117}
118
119/// A general-purpose dynamic value — can be any JSON primitive, binding, or function call.
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121#[serde(untagged)]
122pub enum DynamicValue {
123    String(String),
124    Number(f64),
125    Boolean(bool),
126    Array(Vec<serde_json::Value>),
127    Binding(DataBinding),
128    Function(FunctionCall),
129}
130
131/// A dynamic value that resolves to a list of strings.
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
133#[serde(untagged)]
134pub enum DynamicStringList {
135    Literal(Vec<String>),
136    Binding(DataBinding),
137    Function(FunctionCall),
138}
139
140// ---------------------------------------------------------------------------
141// Child list — how containers reference their children
142// ---------------------------------------------------------------------------
143
144/// Describes the children of a container component.
145///
146/// Either a static array of component IDs, or a dynamic template that
147/// generates children from a data-bound array.
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149#[serde(untagged)]
150pub enum ChildList {
151    /// A fixed list of child component IDs.
152    Static(Vec<ComponentId>),
153    /// A template that iterates over a data-bound array,
154    /// instantiating `component_id` for each item.
155    #[serde(rename_all = "camelCase")]
156    Template {
157        component_id: ComponentId,
158        path: String,
159    },
160}
161
162impl Default for ChildList {
163    fn default() -> Self {
164        Self::Static(Vec::new())
165    }
166}
167
168// ---------------------------------------------------------------------------
169// Actions
170// ---------------------------------------------------------------------------
171
172/// An action triggered by user interaction (e.g. button click).
173///
174/// Either dispatches an event to the server, or calls a local function.
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
176#[serde(untagged)]
177pub enum Action {
178    /// Send an event to the server.
179    Event { event: ActionEvent },
180    /// Execute a local client-side function.
181    FunctionCall { function_call: FunctionCall },
182}
183
184/// A server-bound event with optional context.
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
186pub struct ActionEvent {
187    pub name: String,
188    #[serde(default)]
189    pub context: HashMap<String, DynamicValue>,
190    #[serde(default, skip_serializing_if = "is_false")]
191    pub want_response: bool,
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub response_path: Option<String>,
194}
195
196fn is_false(v: &bool) -> bool {
197    !v
198}
199
200// ---------------------------------------------------------------------------
201// Validation
202// ---------------------------------------------------------------------------
203
204/// A validation check with a boolean condition and an error message.
205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
206pub struct CheckRule {
207    pub condition: DynamicBooleanCondition,
208    pub message: String,
209}
210
211// ---------------------------------------------------------------------------
212// Accessibility
213// ---------------------------------------------------------------------------
214
215/// Accessibility attributes for a component.
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
217pub struct AccessibilityAttributes {
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub label: Option<DynamicString>,
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub description: Option<DynamicString>,
222}
223
224// ---------------------------------------------------------------------------
225// Alignment / Justify enums
226// ---------------------------------------------------------------------------
227
228/// Main-axis alignment (maps to flexbox justify-content).
229#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
230#[serde(rename_all = "camelCase")]
231pub enum Justify {
232    Start,
233    Center,
234    End,
235    SpaceBetween,
236    SpaceAround,
237    SpaceEvenly,
238    Stretch,
239}
240
241/// Cross-axis alignment (maps to flexbox align-items).
242#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
243#[serde(rename_all = "camelCase")]
244pub enum Align {
245    Start,
246    Center,
247    End,
248    Stretch,
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn childlist_static_array_deserializes() {
257        let json = serde_json::json!(["a", "b", "c"]);
258        let cl: ChildList = serde_json::from_value(json).unwrap();
259        assert_eq!(cl, ChildList::Static(vec!["a".to_string(), "b".to_string(), "c".to_string()]));
260    }
261
262    #[test]
263    fn childlist_template_deserializes_camel_case_component_id() {
264        // The spec schema (common_types.json) requires the camelCase key
265        // `componentId`. This is the form every sample uses, e.g. the
266        // "Incremental List" sample's root Column.
267        let json = serde_json::json!({ "path": "/restaurants", "componentId": "restaurant_card" });
268        let cl: ChildList = serde_json::from_value(json).unwrap();
269        match cl {
270            ChildList::Template { component_id, path } => {
271                assert_eq!(component_id, "restaurant_card");
272                assert_eq!(path, "/restaurants");
273            }
274            other => panic!("expected Template, got {other:?}"),
275        }
276    }
277}