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 json!({
92 "type": "object",
93 "oneOf": [
94 {
95 "required": ["createSurface"],
96 "properties": {
97 "createSurface": {
98 "type": "object",
99 "required": ["surfaceId", "catalogId"],
100 "properties": {
101 "surfaceId": { "type": "string" },
102 "catalogId": { "type": "string" },
103 "theme": { "type": "object" },
104 "sendDataModel": { "type": "boolean" }
105 }
106 }
107 }
108 },
109 {
110 "required": ["updateComponents"],
111 "properties": {
112 "updateComponents": {
113 "type": "object",
114 "required": ["surfaceId", "components"],
115 "properties": {
116 "surfaceId": { "type": "string" },
117 "components": {
118 "type": "array",
119 "minItems": 1,
120 "items": {
121 "type": "object",
122 "required": ["id", "component"],
123 "properties": {
124 "id": { "type": "string" },
125 "component": {
126 "oneOf": [
127 { "type": "string" },
128 { "type": "object" }
129 ],
130 "description": "Component discriminator in flat form (\"Text\") or legacy nested object form."
131 }
132 }
133 }
134 }
135 }
136 }
137 }
138 },
139 {
140 "required": ["updateDataModel"],
141 "properties": {
142 "updateDataModel": {
143 "type": "object",
144 "required": ["surfaceId"],
145 "properties": {
146 "surfaceId": { "type": "string" },
147 "path": { "type": "string" },
148 "value": {}
149 }
150 }
151 }
152 },
153 {
154 "required": ["deleteSurface"],
155 "properties": {
156 "deleteSurface": {
157 "type": "object",
158 "required": ["surfaceId"],
159 "properties": {
160 "surfaceId": { "type": "string" }
161 }
162 }
163 }
164 }
165 ]
166 })
167}
168
169fn schema_v0_8() -> Value {
170 json!({
171 "type": "object",
172 "oneOf": [
173 {
174 "required": ["beginRendering"],
175 "properties": {
176 "beginRendering": {
177 "type": "object",
178 "required": ["surfaceId", "root"],
179 "properties": {
180 "surfaceId": { "type": "string" },
181 "root": { "type": "string" },
182 "catalogId": { "type": "string" },
183 "styles": { "type": "object" }
184 }
185 }
186 }
187 },
188 {
189 "required": ["surfaceUpdate"],
190 "properties": {
191 "surfaceUpdate": {
192 "type": "object",
193 "required": ["surfaceId", "components"],
194 "properties": {
195 "surfaceId": { "type": "string" },
196 "components": {
197 "type": "array",
198 "minItems": 1,
199 "items": {
200 "type": "object",
201 "required": ["id", "component"],
202 "properties": {
203 "id": { "type": "string" },
204 "component": { "type": "object" }
205 }
206 }
207 }
208 }
209 }
210 }
211 },
212 {
213 "required": ["dataModelUpdate"],
214 "properties": {
215 "dataModelUpdate": {
216 "type": "object",
217 "required": ["surfaceId", "contents"],
218 "properties": {
219 "surfaceId": { "type": "string" },
220 "path": { "type": "string" },
221 "contents": { "type": "array" }
222 }
223 }
224 }
225 },
226 {
227 "required": ["deleteSurface"],
228 "properties": {
229 "deleteSurface": {
230 "type": "object",
231 "required": ["surfaceId"],
232 "properties": {
233 "surfaceId": { "type": "string" }
234 }
235 }
236 }
237 }
238 ]
239 })
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::a2ui::messages::{
246 A2uiMessage, CreateSurface, CreateSurfaceMessage, UpdateComponents, UpdateComponentsMessage,
247 };
248 use serde_json::json;
249
250 #[test]
251 fn validates_v0_9_create_surface() {
252 let validator = A2uiValidator::new().unwrap();
253 let value = json!({
254 "createSurface": {
255 "surfaceId": "main",
256 "catalogId": "catalog"
257 }
258 });
259 assert!(validator.validate_value(&value, A2uiSchemaVersion::V0_9).is_ok());
260 }
261
262 #[test]
263 fn rejects_invalid_v0_9_message() {
264 let validator = A2uiValidator::new().unwrap();
265 let value = json!({ "createSurface": { "catalogId": "missing_surface" } });
266 assert!(validator.validate_value(&value, A2uiSchemaVersion::V0_9).is_err());
267 }
268
269 #[test]
270 fn validates_struct_message() {
271 let validator = A2uiValidator::new().unwrap();
272 let message = A2uiMessage::CreateSurface(CreateSurfaceMessage {
273 create_surface: CreateSurface {
274 surface_id: "main".to_string(),
275 catalog_id: "catalog".to_string(),
276 theme: None,
277 send_data_model: None,
278 },
279 });
280 assert!(validator.validate_message(&message, A2uiSchemaVersion::V0_9).is_ok());
281 }
282
283 #[test]
284 fn validates_update_components_minimal() {
285 let validator = A2uiValidator::new().unwrap();
286 let message = A2uiMessage::UpdateComponents(UpdateComponentsMessage {
287 update_components: UpdateComponents {
288 surface_id: "main".to_string(),
289 components: vec![json!({
290 "id": "root",
291 "component": {
292 "Text": {
293 "text": { "literalString": "Hello" }
294 }
295 }
296 })],
297 },
298 });
299 assert!(validator.validate_message(&message, A2uiSchemaVersion::V0_9).is_ok());
300 }
301
302 #[test]
303 fn validates_update_components_flat_shape() {
304 let validator = A2uiValidator::new().unwrap();
305 let message = A2uiMessage::UpdateComponents(UpdateComponentsMessage {
306 update_components: UpdateComponents {
307 surface_id: "main".to_string(),
308 components: vec![json!({
309 "id": "root",
310 "component": "Text",
311 "text": "Hello"
312 })],
313 },
314 });
315 assert!(validator.validate_message(&message, A2uiSchemaVersion::V0_9).is_ok());
316 }
317}