adk_ui/interop/
surface.rs1use 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#[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#[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}