1use jsonschema::Validator;
2use serde_json::{Value, json};
3
4use super::messages::A2uiMessage;
5
6#[derive(Debug, Clone, Copy)]
7pub enum A2uiSchemaVersion {
8 V0_9,
9 V0_8,
10}
11
12#[derive(Debug, Clone)]
13pub struct A2uiValidationError {
14 pub message: String,
15 pub instance_path: String,
16}
17
18impl std::fmt::Display for A2uiValidationError {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 write!(f, "{} at {}", self.message, self.instance_path)
21 }
22}
23
24impl std::error::Error for A2uiValidationError {}
25
26pub struct A2uiValidator {
32 v0_9: Validator,
33 v0_8: Validator,
34}
35
36impl A2uiValidator {
37 pub fn new() -> Result<Self, A2uiValidationError> {
38 let v0_9 = Validator::new(&schema_v0_9()).map_err(|e| A2uiValidationError {
39 message: format!("Invalid v0.9 schema: {}", e),
40 instance_path: "/".to_string(),
41 })?;
42 let v0_8 = Validator::new(&schema_v0_8()).map_err(|e| A2uiValidationError {
43 message: format!("Invalid v0.8 schema: {}", e),
44 instance_path: "/".to_string(),
45 })?;
46
47 Ok(Self { v0_9, v0_8 })
48 }
49
50 pub fn validate_message(
51 &self,
52 message: &A2uiMessage,
53 version: A2uiSchemaVersion,
54 ) -> Result<(), Vec<A2uiValidationError>> {
55 let value = serde_json::to_value(message).map_err(|e| {
56 vec![A2uiValidationError {
57 message: format!("Serialization failed: {}", e),
58 instance_path: "/".to_string(),
59 }]
60 })?;
61 self.validate_value(&value, version)
62 }
63
64 pub fn validate_value(
65 &self,
66 value: &Value,
67 version: A2uiSchemaVersion,
68 ) -> Result<(), Vec<A2uiValidationError>> {
69 let validator = match version {
70 A2uiSchemaVersion::V0_9 => &self.v0_9,
71 A2uiSchemaVersion::V0_8 => &self.v0_8,
72 };
73
74 let mapped = validator
75 .iter_errors(value)
76 .map(|e| A2uiValidationError {
77 message: e.to_string(),
78 instance_path: e.instance_path.to_string(),
79 })
80 .collect::<Vec<_>>();
81
82 if !mapped.is_empty() {
83 return Err(mapped);
84 }
85
86 Ok(())
87 }
88}
89
90fn schema_v0_9() -> Value {
91 let version = json!({ "type": "string" });
93 json!({
94 "type": "object",
95 "oneOf": [
96 {
97 "required": ["createSurface"],
98 "properties": {
99 "version": version,
100 "createSurface": {
101 "type": "object",
102 "required": ["surfaceId", "catalogId"],
103 "properties": {
104 "surfaceId": { "type": "string" },
105 "catalogId": { "type": "string" },
106 "theme": { "type": "object" },
107 "sendDataModel": { "type": "boolean" },
108 "components": { "type": "array" },
109 "dataModel": {}
110 }
111 }
112 }
113 },
114 {
115 "required": ["updateComponents"],
116 "properties": {
117 "version": version,
118 "updateComponents": {
119 "type": "object",
120 "required": ["surfaceId", "components"],
121 "properties": {
122 "surfaceId": { "type": "string" },
123 "components": {
124 "type": "array",
125 "minItems": 1,
126 "items": {
127 "type": "object",
128 "required": ["id", "component"],
129 "properties": {
130 "id": { "type": "string" },
131 "component": {
132 "oneOf": [
133 { "type": "string" },
134 { "type": "object" }
135 ],
136 "description": "Component discriminator in flat form (\"Text\") or legacy nested object form."
137 }
138 }
139 }
140 }
141 }
142 }
143 }
144 },
145 {
146 "required": ["updateDataModel"],
147 "properties": {
148 "version": version,
149 "updateDataModel": {
150 "type": "object",
151 "required": ["surfaceId"],
152 "properties": {
153 "surfaceId": { "type": "string" },
154 "path": { "type": "string" },
155 "value": {}
156 }
157 }
158 }
159 },
160 {
161 "required": ["deleteSurface"],
162 "properties": {
163 "version": version,
164 "deleteSurface": {
165 "type": "object",
166 "required": ["surfaceId"],
167 "properties": {
168 "surfaceId": { "type": "string" }
169 }
170 }
171 }
172 },
173 {
174 "required": ["actionId", "actionResponse"],
175 "properties": {
176 "version": version,
177 "actionId": { "type": "string" },
178 "actionResponse": {
179 "type": "object",
180 "properties": {
181 "value": {},
182 "error": {
183 "type": "object",
184 "required": ["code", "message"],
185 "properties": {
186 "code": { "type": "string" },
187 "message": { "type": "string" }
188 }
189 }
190 }
191 }
192 }
193 },
194 {
195 "required": ["functionCallId", "callFunction"],
196 "properties": {
197 "version": version,
198 "functionCallId": { "type": "string" },
199 "wantResponse": { "type": "boolean" },
200 "callFunction": {
201 "type": "object",
202 "required": ["call"],
203 "properties": {
204 "call": { "type": "string" },
205 "args": {}
206 }
207 }
208 }
209 }
210 ]
211 })
212}
213
214fn schema_v0_8() -> Value {
215 json!({
216 "type": "object",
217 "oneOf": [
218 {
219 "required": ["beginRendering"],
220 "properties": {
221 "beginRendering": {
222 "type": "object",
223 "required": ["surfaceId", "root"],
224 "properties": {
225 "surfaceId": { "type": "string" },
226 "root": { "type": "string" },
227 "catalogId": { "type": "string" },
228 "styles": { "type": "object" }
229 }
230 }
231 }
232 },
233 {
234 "required": ["surfaceUpdate"],
235 "properties": {
236 "surfaceUpdate": {
237 "type": "object",
238 "required": ["surfaceId", "components"],
239 "properties": {
240 "surfaceId": { "type": "string" },
241 "components": {
242 "type": "array",
243 "minItems": 1,
244 "items": {
245 "type": "object",
246 "required": ["id", "component"],
247 "properties": {
248 "id": { "type": "string" },
249 "component": { "type": "object" }
250 }
251 }
252 }
253 }
254 }
255 }
256 },
257 {
258 "required": ["dataModelUpdate"],
259 "properties": {
260 "dataModelUpdate": {
261 "type": "object",
262 "required": ["surfaceId", "contents"],
263 "properties": {
264 "surfaceId": { "type": "string" },
265 "path": { "type": "string" },
266 "contents": { "type": "array" }
267 }
268 }
269 }
270 },
271 {
272 "required": ["deleteSurface"],
273 "properties": {
274 "deleteSurface": {
275 "type": "object",
276 "required": ["surfaceId"],
277 "properties": {
278 "surfaceId": { "type": "string" }
279 }
280 }
281 }
282 }
283 ]
284 })
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::a2ui::messages::{
291 A2uiMessage, CreateSurface, CreateSurfaceMessage, UpdateComponents, UpdateComponentsMessage,
292 };
293 use serde_json::json;
294
295 #[test]
296 fn validates_v0_9_create_surface() {
297 let validator = A2uiValidator::new().unwrap();
298 let value = json!({
299 "createSurface": {
300 "surfaceId": "main",
301 "catalogId": "catalog"
302 }
303 });
304 assert!(
305 validator
306 .validate_value(&value, A2uiSchemaVersion::V0_9)
307 .is_ok()
308 );
309 }
310
311 #[test]
312 fn rejects_invalid_v0_9_message() {
313 let validator = A2uiValidator::new().unwrap();
314 let value = json!({ "createSurface": { "catalogId": "missing_surface" } });
315 assert!(
316 validator
317 .validate_value(&value, A2uiSchemaVersion::V0_9)
318 .is_err()
319 );
320 }
321
322 #[test]
323 fn validates_struct_message() {
324 let validator = A2uiValidator::new().unwrap();
325 let message = A2uiMessage::CreateSurface(CreateSurfaceMessage::new(CreateSurface {
326 surface_id: "main".to_string(),
327 catalog_id: "catalog".to_string(),
328 theme: None,
329 send_data_model: None,
330 components: None,
331 data_model: None,
332 }));
333 assert!(
334 validator
335 .validate_message(&message, A2uiSchemaVersion::V0_9)
336 .is_ok()
337 );
338 }
339
340 #[test]
341 fn validates_update_components_minimal() {
342 let validator = A2uiValidator::new().unwrap();
343 let message =
344 A2uiMessage::UpdateComponents(UpdateComponentsMessage::new(UpdateComponents {
345 surface_id: "main".to_string(),
346 components: vec![json!({
347 "id": "root",
348 "component": {
349 "Text": {
350 "text": { "literalString": "Hello" }
351 }
352 }
353 })],
354 }));
355 assert!(
356 validator
357 .validate_message(&message, A2uiSchemaVersion::V0_9)
358 .is_ok()
359 );
360 }
361
362 #[test]
363 fn validates_update_components_flat_shape() {
364 let validator = A2uiValidator::new().unwrap();
365 let message =
366 A2uiMessage::UpdateComponents(UpdateComponentsMessage::new(UpdateComponents {
367 surface_id: "main".to_string(),
368 components: vec![json!({
369 "id": "root",
370 "component": "Text",
371 "text": "Hello"
372 })],
373 }));
374 assert!(
375 validator
376 .validate_message(&message, A2uiSchemaVersion::V0_9)
377 .is_ok()
378 );
379 }
380}