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