Skip to main content

adk_ui/interop/
surface.rs

1use crate::a2ui::{
2    A2uiMessage, CreateSurface, CreateSurfaceMessage, UpdateComponents, UpdateComponentsMessage,
3    UpdateDataModel, UpdateDataModelMessage, encode_jsonl,
4};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// Supported UI interoperability protocols.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
11#[serde(rename_all = "snake_case")]
12pub enum UiProtocol {
13    #[default]
14    A2ui,
15    AgUi,
16    McpApps,
17}
18
19/// Protocol-neutral UI surface representation.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct UiSurface {
23    pub surface_id: String,
24    pub catalog_id: String,
25    pub components: Vec<Value>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub data_model: Option<Value>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub theme: Option<Value>,
30    pub send_data_model: bool,
31}
32
33impl UiSurface {
34    pub fn new(
35        surface_id: impl Into<String>,
36        catalog_id: impl Into<String>,
37        components: Vec<Value>,
38    ) -> Self {
39        Self {
40            surface_id: surface_id.into(),
41            catalog_id: catalog_id.into(),
42            components,
43            data_model: None,
44            theme: None,
45            send_data_model: true,
46        }
47    }
48
49    pub fn with_data_model(mut self, data_model: Option<Value>) -> Self {
50        self.data_model = data_model;
51        self
52    }
53
54    pub fn with_theme(mut self, theme: Option<Value>) -> Self {
55        self.theme = theme;
56        self
57    }
58
59    pub fn with_send_data_model(mut self, send_data_model: bool) -> Self {
60        self.send_data_model = send_data_model;
61        self
62    }
63
64    pub fn to_a2ui_messages(&self) -> Vec<A2uiMessage> {
65        let mut messages = vec![A2uiMessage::CreateSurface(CreateSurfaceMessage {
66            create_surface: CreateSurface {
67                surface_id: self.surface_id.clone(),
68                catalog_id: self.catalog_id.clone(),
69                theme: self.theme.clone(),
70                send_data_model: Some(self.send_data_model),
71            },
72        })];
73
74        if let Some(data_model) = self.data_model.clone() {
75            messages.push(A2uiMessage::UpdateDataModel(UpdateDataModelMessage {
76                update_data_model: UpdateDataModel {
77                    surface_id: self.surface_id.clone(),
78                    path: Some("/".to_string()),
79                    value: Some(data_model),
80                },
81            }));
82        }
83
84        messages.push(A2uiMessage::UpdateComponents(UpdateComponentsMessage {
85            update_components: UpdateComponents {
86                surface_id: self.surface_id.clone(),
87                components: self.components.clone(),
88            },
89        }));
90
91        messages
92    }
93
94    pub fn to_a2ui_jsonl(&self) -> Result<String, serde_json::Error> {
95        encode_jsonl(self.to_a2ui_messages())
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use serde_json::json;
103
104    #[test]
105    fn surface_to_a2ui_messages_emits_expected_order() {
106        let surface = UiSurface::new(
107            "main",
108            "catalog",
109            vec![json!({"id":"root","component":{"Column":{"children":[]}}})],
110        )
111        .with_data_model(Some(json!({"ok": true})));
112
113        let messages = surface.to_a2ui_messages();
114        assert_eq!(messages.len(), 3);
115
116        let first = serde_json::to_value(&messages[0]).unwrap();
117        let second = serde_json::to_value(&messages[1]).unwrap();
118        let third = serde_json::to_value(&messages[2]).unwrap();
119
120        assert!(first.get("createSurface").is_some());
121        assert!(second.get("updateDataModel").is_some());
122        assert!(third.get("updateComponents").is_some());
123    }
124
125    #[test]
126    fn surface_to_a2ui_jsonl_serializes() {
127        let surface = UiSurface::new(
128            "main",
129            "catalog",
130            vec![json!({"id":"root","component":{"Column":{"children":[]}}})],
131        );
132        let jsonl = surface.to_a2ui_jsonl().unwrap();
133        assert!(jsonl.contains("createSurface"));
134        assert!(jsonl.contains("updateComponents"));
135    }
136}