1use indexmap::IndexMap;
6use serde::{Deserialize, Serialize};
7use serde_json::{json, Map, Value};
8
9use super::contract::{error_message, INVALID_MESSAGE, MESSAGE_KINDS, SOURCE_DENIED, SURFACE_NOT_FOUND, TEMPLATE_NOT_FOUND, UNSUPPORTED_CATALOG};
10use super::package::{package_sources, source_allowed, supported_catalog_ids, template_messages, validate_package_with, AvailableExtension, PackageIssue};
11use super::sources::parse_source;
12use crate::catalog::A2UI_VERSION;
13use crate::limits;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct SurfaceInfo {
18 pub surface_id: String,
19 pub catalog_id: String,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub package_id: Option<String>,
23}
24
25#[derive(Debug, Clone, Default)]
28pub struct HostRouter {
29 extension_ids: Vec<String>,
30 extensions: Option<Vec<AvailableExtension>>,
33 surfaces: IndexMap<String, SurfaceInfo>,
34 packages: IndexMap<String, Value>,
35}
36
37fn send(message: Value) -> Value {
38 json!({"op": "send", "message": message})
39}
40
41fn invalid(surface_id: &str, message: &str) -> Value {
42 send(error_message(INVALID_MESSAGE, surface_id, message, None))
43}
44
45fn missing(surface_id: &str) -> Value {
46 send(error_message(SURFACE_NOT_FOUND, surface_id, &format!("no surface {surface_id}; send createSurface first"), None))
47}
48
49fn oversized(message: &Value) -> bool {
52 struct Count(usize);
53 impl std::io::Write for Count {
54 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
55 self.0 += buf.len();
56 if self.0 > limits::MAX_MESSAGE_BYTES {
57 return Err(std::io::Error::other("over the limit"));
58 }
59 Ok(buf.len())
60 }
61 fn flush(&mut self) -> std::io::Result<()> {
62 Ok(())
63 }
64 }
65 let mut count = Count(0);
66 serde_json::to_writer(&mut count, message).is_err()
67}
68
69fn surface_id_of(message: &Map<String, Value>) -> String {
70 message.values().find_map(|v| v.as_object().and_then(|o| o.get("surfaceId")).and_then(Value::as_str)).unwrap_or("").to_string()
71}
72
73pub fn normalize_path(path: Option<&str>) -> String {
76 match path {
77 None | Some("/") => String::new(),
78 Some(p) => p.to_string(),
79 }
80}
81
82impl HostRouter {
83 pub fn new<S: AsRef<str>>(extension_ids: &[S]) -> Self {
85 HostRouter { extension_ids: extension_ids.iter().map(|s| s.as_ref().to_string()).collect(), ..Default::default() }
86 }
87
88 pub fn with_extensions<S: AsRef<str>>(extension_ids: &[S], extensions: Option<Vec<AvailableExtension>>) -> Self {
91 HostRouter { extensions, ..Self::new(extension_ids) }
92 }
93
94 pub fn supported_catalog_ids(&self) -> Vec<String> {
95 supported_catalog_ids(&self.extension_ids)
96 }
97
98 pub fn register_extension(&mut self, id: &str) {
99 self.register_extension_with(id, None);
100 }
101
102 pub fn register_extension_with(&mut self, id: &str, available: Option<AvailableExtension>) {
105 if !self.extension_ids.iter().any(|x| x == id) {
106 self.extension_ids.push(id.to_string());
107 }
108 if let Some(extensions) = &mut self.extensions {
109 if !extensions.iter().any(|e| e.id == id) {
110 extensions.push(available.unwrap_or_else(|| AvailableExtension { id: id.to_string(), ..Default::default() }));
111 }
112 }
113 }
114
115 pub fn install_package(&mut self, pkg: &Value) -> Vec<PackageIssue> {
118 let issues = validate_package_with(pkg, &self.supported_catalog_ids(), self.extensions.as_deref());
119 if issues.is_empty() {
120 let id = pkg["id"].as_str().unwrap_or("").to_string();
121 self.packages.insert(id, pkg.clone());
122 }
123 issues
124 }
125
126 pub fn package(&self, id: &str) -> Option<&Value> {
127 self.packages.get(id)
128 }
129
130 pub fn surface(&self, id: &str) -> Option<&SurfaceInfo> {
131 self.surfaces.get(id)
132 }
133
134 pub fn surface_ids(&self) -> Vec<String> {
136 self.surfaces.keys().cloned().collect()
137 }
138
139 pub fn package_id_of(&self, surface_id: &str) -> Option<String> {
141 self.surfaces.get(surface_id).and_then(|s| s.package_id.clone())
142 }
143
144 pub fn route(&mut self, message: &Value) -> Vec<Value> {
148 if oversized(message) {
149 let sid = message.as_object().map(surface_id_of).unwrap_or_default();
150 return vec![invalid(&sid, &limits::message_bytes_issue())];
151 }
152 let Some(message) = message.as_object() else { return vec![invalid("", "a message is a JSON object")] };
153 if let Some(version) = message.get("version") {
154 if version.as_str() != Some(A2UI_VERSION) {
155 return vec![invalid(&surface_id_of(message), &format!("unsupported version {}", crate::json::to_js_string(version)))];
156 }
157 }
158 let kinds: Vec<&String> = message.keys().filter(|k| *k != "version").collect();
159 if kinds.len() != 1 || !MESSAGE_KINDS.contains(&kinds[0].as_str()) {
160 return vec![invalid(&surface_id_of(message), &format!("expected exactly one of {}", MESSAGE_KINDS.join(", ")))];
161 }
162 let kind = kinds[0].as_str();
163 let Some(body) = message[kind].as_object().filter(|b| b.get("surfaceId").and_then(Value::as_str).is_some_and(|s| !s.is_empty())) else {
164 return vec![invalid("", &format!("{kind}.surfaceId is required"))];
165 };
166 let surface_id = body["surfaceId"].as_str().unwrap_or("").to_string();
167 let sid = surface_id.as_str();
168 match kind {
169 "createSurface" => {
170 let Some(catalog_id) = body.get("catalogId").and_then(Value::as_str) else {
171 return vec![invalid(sid, "createSurface.catalogId is required")];
172 };
173 if !self.supported_catalog_ids().iter().any(|c| c == catalog_id) {
174 return vec![send(error_message(UNSUPPORTED_CATALOG, sid, &format!("catalog {catalog_id} is not supported"), None))];
175 }
176 self.surfaces.insert(surface_id.clone(), SurfaceInfo { surface_id: surface_id.clone(), catalog_id: catalog_id.to_string(), package_id: None });
177 let mut op = Map::new();
178 op.insert("op".into(), json!("create"));
179 op.insert("surfaceId".into(), json!(sid));
180 op.insert("catalogId".into(), json!(catalog_id));
181 if let Some(theme) = body.get("theme") {
182 op.insert("theme".into(), theme.clone());
183 }
184 if body.get("sendDataModel") == Some(&Value::Bool(true)) {
185 op.insert("sendDataModel".into(), Value::Bool(true));
186 }
187 vec![Value::Object(op)]
188 }
189 "updateComponents" => {
190 let Some(components) = body.get("components").filter(|c| c.is_array()) else {
191 return vec![invalid(sid, "updateComponents.components is required")];
192 };
193 if !self.surfaces.contains_key(sid) {
194 return vec![missing(sid)];
195 }
196 vec![json!({"op": "components", "surfaceId": sid, "components": components})]
197 }
198 "updateDataModel" => {
199 if !self.surfaces.contains_key(sid) {
200 return vec![missing(sid)];
201 }
202 let path = match body.get("path") {
203 None => None,
204 Some(Value::String(p)) => Some(p.as_str()),
205 Some(_) => return vec![invalid(sid, "updateDataModel.path is a string")],
206 };
207 let path = normalize_path(path);
208 match body.get("value") {
209 Some(value) => vec![json!({"op": "data", "surfaceId": sid, "path": path, "value": value})],
210 None => vec![json!({"op": "data", "surfaceId": sid, "path": path})],
211 }
212 }
213 "deleteSurface" => {
214 if self.surfaces.shift_remove(sid).is_none() {
215 return vec![missing(sid)];
216 }
217 vec![json!({"op": "delete", "surfaceId": sid})]
218 }
219 "bindDataModel" => {
220 let Some(info) = self.surfaces.get(sid) else {
221 return vec![missing(sid)];
222 };
223 match (body.get("path").and_then(Value::as_str), body.get("source").and_then(Value::as_str)) {
224 (Some(path), Some(source)) if parse_source(source).is_some() => {
225 if let Some(pkg) = info.package_id.as_deref().and_then(|id| self.packages.get(id)) {
227 if !source_allowed(package_sources(pkg).as_deref(), source) {
228 let pkg_id = pkg.get("id").and_then(Value::as_str).unwrap_or("");
229 let at = normalize_path(Some(path));
230 let at = if at.is_empty() { "/".to_string() } else { at };
231 return vec![send(error_message(SOURCE_DENIED, sid, &format!("source {source} is not in package {pkg_id}'s sources"), Some(&at)))];
232 }
233 }
234 vec![json!({"op": "bind", "surfaceId": sid, "path": normalize_path(Some(path)), "source": source})]
235 }
236 _ => vec![invalid(sid, "bindDataModel needs a path and a scheme:name source")],
237 }
238 }
239 "applyTemplate" => {
240 let Some(template_id) = body.get("templateId").and_then(Value::as_str) else {
241 return vec![invalid(sid, "applyTemplate.templateId is required")];
242 };
243 let Some(pkg) = self.find_package(template_id, body.get("packageId").and_then(Value::as_str)).cloned() else {
244 return vec![send(error_message(TEMPLATE_NOT_FOUND, sid, &format!("no installed package has the template {template_id}"), None))];
245 };
246 let mut ops = Vec::new();
247 let pkg_id = pkg.get("id").and_then(Value::as_str).map(str::to_string);
248 for m in template_messages(&pkg, template_id, sid, body.get("data")).unwrap_or_default() {
249 ops.extend(self.route(&m));
250 if let Some(info) = self.surfaces.get_mut(sid) {
253 info.package_id = pkg_id.clone();
254 }
255 }
256 ops
257 }
258 _ => vec![invalid(sid, "unknown message")],
259 }
260 }
261
262 fn find_package(&self, template_id: &str, package_id: Option<&str>) -> Option<&Value> {
263 let has = |pkg: &Value| pkg.get("templates").and_then(|t| t.get(template_id)).is_some_and(super::js_truthy);
264 match package_id {
265 Some(id) => self.packages.get(id).filter(|p| has(p)),
266 None => self.packages.values().find(|p| has(p)),
267 }
268 }
269}