1use std::collections::{BTreeMap, BTreeSet};
62
63use serde_json::{Map, Value};
64
65use crate::catalog::Catalog;
66use crate::constants::{A2UI_CLOSE_TAG, A2UI_OPEN_TAG, PROTOCOL_VERSION, ROOT_ID};
67use crate::error::{Error, Result};
68use crate::toolkit::parser::ResponsePart;
69use crate::validate::{OPERATIONS, ValidateOptions, Validator};
70
71const MSG_CREATE_SURFACE: &str = "createSurface";
73const MSG_UPDATE_COMPONENTS: &str = "updateComponents";
74const MSG_UPDATE_DATA_MODEL: &str = "updateDataModel";
75const MSG_DELETE_SURFACE: &str = "deleteSurface";
76
77const MESSAGE_KEYS: [&str; 4] = [
79 MSG_CREATE_SURFACE,
80 MSG_UPDATE_COMPONENTS,
81 MSG_UPDATE_DATA_MODEL,
82 MSG_DELETE_SURFACE,
83];
84
85const CHILD_FIELDS: [&str; 6] = [
90 "children",
91 "explicitList",
92 "child",
93 "contentChild",
94 "entryPointChild",
95 "componentId",
96];
97
98const SNIFF_OVERLAP: usize = 16;
103
104pub const DEFAULT_CUTTABLE_KEYS: [&str; 7] = [
110 "literalString",
111 "valueString",
112 "label",
113 "hint",
114 "caption",
115 "altText",
116 "text",
117];
118
119#[derive(Clone, Debug)]
121pub struct StreamParser {
122 catalog: Catalog,
123 validate: bool,
124 cuttable_keys: BTreeSet<String>,
125
126 buffer: String,
128 found_delimiter: bool,
129 json_buffer: String,
130 brace_stack: Vec<(char, usize)>,
134 in_string: bool,
135 string_escaped: bool,
136 found_valid_json_in_block: bool,
137 sniff_cursor: usize,
139
140 seen_components: BTreeMap<String, Value>,
142 yielded_data_model: BTreeMap<String, Map<String, Value>>,
146 deleted_surfaces: BTreeSet<String>,
147 yielded_ids: BTreeMap<String, BTreeSet<String>>,
149 yielded_contents: BTreeMap<(String, String), String>,
151 root_ids: BTreeMap<String, String>,
152 unbound_root_id: Option<String>,
153 surface_id: Option<String>,
154 yielded_start_messages: BTreeSet<String>,
155 yielded_surfaces: BTreeSet<String>,
156 active_msg_type: Option<String>,
157 buffered_start_message: Option<Value>,
158 topology_dirty: bool,
159}
160
161impl StreamParser {
162 pub fn new(catalog: Catalog) -> Self {
164 Self {
165 catalog,
166 validate: true,
167 cuttable_keys: DEFAULT_CUTTABLE_KEYS
168 .iter()
169 .map(|k| (*k).to_string())
170 .collect(),
171 buffer: String::new(),
172 found_delimiter: false,
173 json_buffer: String::new(),
174 brace_stack: Vec::new(),
175 in_string: false,
176 string_escaped: false,
177 found_valid_json_in_block: false,
178 sniff_cursor: 0,
179 seen_components: BTreeMap::new(),
180 yielded_data_model: BTreeMap::new(),
181 deleted_surfaces: BTreeSet::new(),
182 yielded_ids: BTreeMap::new(),
183 yielded_contents: BTreeMap::new(),
184 root_ids: BTreeMap::new(),
185 unbound_root_id: None,
186 surface_id: None,
187 yielded_start_messages: BTreeSet::new(),
188 yielded_surfaces: BTreeSet::new(),
189 active_msg_type: None,
190 buffered_start_message: None,
191 topology_dirty: false,
192 }
193 }
194
195 #[must_use]
200 pub fn with_cuttable_keys<I, S>(mut self, keys: I) -> Self
201 where
202 I: IntoIterator<Item = S>,
203 S: Into<String>,
204 {
205 self.cuttable_keys = keys.into_iter().map(Into::into).collect();
206 self
207 }
208
209 #[must_use]
215 pub fn without_validation(mut self) -> Self {
216 self.validate = false;
217 self
218 }
219
220 pub fn surface_id(&self) -> Option<&str> {
222 self.surface_id.as_deref()
223 }
224
225 pub fn root_id(&self) -> &str {
227 match &self.surface_id {
228 Some(sid) => self
229 .root_ids
230 .get(sid)
231 .map(String::as_str)
232 .unwrap_or(ROOT_ID),
233 None => self.unbound_root_id.as_deref().unwrap_or(ROOT_ID),
234 }
235 }
236
237 fn set_surface_id(&mut self, value: Option<String>) {
238 if let (Some(sid), Some(root)) = (&value, self.unbound_root_id.take()) {
239 self.root_ids.insert(sid.clone(), root);
240 }
241 self.surface_id = value;
242 }
243
244 fn set_root_id(&mut self, value: String) {
245 match &self.surface_id {
246 Some(sid) => {
247 self.root_ids.insert(sid.clone(), value);
248 }
249 None => self.unbound_root_id = Some(value),
250 }
251 }
252
253 pub fn process_chunk(&mut self, chunk: &str) -> Result<Vec<ResponsePart>> {
266 let mut parts: Vec<ResponsePart> = Vec::new();
267 self.buffer.push_str(chunk);
268
269 loop {
270 if !self.found_delimiter {
271 match find_open_tag(&self.buffer) {
272 Some((start, Some(end))) => {
273 if start > 0 {
274 parts.push(text_part(&self.buffer[..start]));
275 }
276 self.buffer = self.buffer[end..].to_string();
277 self.found_delimiter = true;
278 continue;
279 }
280 Some((start, None)) => {
284 if start > 0 {
285 parts.push(text_part(&self.buffer[..start]));
286 self.buffer = self.buffer[start..].to_string();
287 }
288 }
289 None => {
290 if !self.buffer.is_empty() {
291 parts.push(text_part(&self.buffer));
292 self.buffer.clear();
293 }
294 }
295 }
296 break;
297 }
298
299 if let Some(index) = self.buffer.find(A2UI_CLOSE_TAG) {
300 let fragment = self.buffer[..index].to_string();
301 self.process_json_chunk(&fragment, &mut parts)?;
302 if !self.found_valid_json_in_block {
303 return Err(Error::parse(
304 "Failed to parse JSON: No valid JSON object found in A2UI block.",
305 ));
306 }
307 self.buffer = self.buffer[index + A2UI_CLOSE_TAG.len()..].to_string();
308 self.found_delimiter = false;
309 self.reset_json_state();
310 continue;
311 }
312
313 let keep = trailing_prefix_len(&self.buffer, A2UI_CLOSE_TAG);
314 if keep < self.buffer.len() {
315 let split = self.buffer.len() - keep;
316 let fragment = self.buffer[..split].to_string();
317 self.buffer = self.buffer[split..].to_string();
318 self.process_json_chunk(&fragment, &mut parts)?;
319 }
320 break;
321 }
322 Ok(parts)
323 }
324
325 fn reset_json_state(&mut self) {
326 self.json_buffer.clear();
327 self.brace_stack.clear();
328 self.in_string = false;
329 self.string_escaped = false;
330 self.found_valid_json_in_block = false;
331 self.sniff_cursor = 0;
332 }
335
336 fn in_top_level_list(&self) -> bool {
338 matches!(self.brace_stack.first(), Some(('[', _)))
339 }
340
341 fn process_json_chunk(&mut self, chunk: &str, parts: &mut Vec<ResponsePart>) -> Result<()> {
343 for ch in chunk.chars() {
344 if self.brace_stack.is_empty() && ch != '[' && ch != '{' {
346 continue;
347 }
348
349 if self.in_string {
350 self.scan_string_char(ch);
351 } else {
352 match ch {
353 '"' => {
354 self.in_string = true;
355 self.string_escaped = false;
356 self.push_json(ch);
357 }
358 '{' | '[' => {
359 self.brace_stack.push((ch, self.json_buffer.len()));
360 self.json_buffer.push(ch);
361 }
362 '}' => self.close_object(parts)?,
363 ']' => {
364 if self.brace_stack.last().map(|(k, _)| *k) == Some('[') {
365 self.brace_stack.pop();
366 self.json_buffer.push(']');
367 }
368 }
369 _ => self.push_json(ch),
370 }
371 }
372
373 if !self.brace_stack.is_empty() && matches!(ch, '"' | ':' | ',' | '}' | ']') {
376 self.sniff_metadata();
377 }
378 }
379
380 if !self.brace_stack.is_empty() && !self.json_buffer.is_empty() {
381 self.sniff_partial_component();
382 self.sniff_partial_data_model(parts);
383 }
384 if self.topology_dirty {
385 self.yield_reachable(parts)?;
386 self.topology_dirty = false;
387 }
388 Ok(())
389 }
390
391 fn push_json(&mut self, ch: char) {
392 if !self.brace_stack.is_empty() {
393 self.json_buffer.push(ch);
394 }
395 }
396
397 fn scan_string_char(&mut self, ch: char) {
398 if self.string_escaped {
399 self.string_escaped = false;
400 } else if ch == '\\' {
401 self.string_escaped = true;
402 } else if ch == '"' {
403 self.in_string = false;
404 }
405 self.push_json(ch);
406 }
407
408 fn close_object(&mut self, parts: &mut Vec<ResponsePart>) -> Result<()> {
410 let Some((_, start)) = self.brace_stack.pop() else {
411 return Ok(());
412 };
413 self.json_buffer.push('}');
414
415 let fragment = self.json_buffer[start..].to_string();
416 if !fragment.starts_with('{') || !fragment.ends_with('}') {
417 return Ok(());
418 }
419 let Ok(Value::Object(map)) = serde_json::from_str::<Value>(&fragment) else {
420 return Ok(());
421 };
422 self.found_valid_json_in_block = true;
423 let obj = Value::Object(map);
424
425 let is_protocol = self.in_top_level_list() && is_protocol_message(&obj);
426 let is_component = obj.get("id").is_some() && obj.get("component").is_some();
427 let is_top_level = self.brace_stack.is_empty()
428 || (self.in_top_level_list() && self.brace_stack.len() == 1);
429
430 if is_component {
431 self.handle_partial_component(&obj);
432 } else if (is_top_level || is_protocol) && !self.handle_complete_object(&obj, parts)? {
433 self.yield_message(obj.clone(), parts, false)?;
435 }
436
437 if is_top_level {
440 if self.in_top_level_list() && self.brace_stack.len() == 1 {
441 let mut kept = self.json_buffer[..start].to_string();
442 kept.push_str(&self.json_buffer[start + fragment.len()..]);
443 self.json_buffer = kept;
444 } else {
445 self.json_buffer = self.json_buffer[fragment.len()..].to_string();
446 let shift = fragment.len();
447 for entry in &mut self.brace_stack {
448 entry.1 = entry.1.saturating_sub(shift);
449 }
450 }
451 }
452 Ok(())
453 }
454
455 fn handle_complete_object(
457 &mut self,
458 obj: &Value,
459 parts: &mut Vec<ResponsePart>,
460 ) -> Result<bool> {
461 if !is_protocol_message(obj) {
462 return Ok(false);
463 }
464 if self.validate {
465 self.validate_message(obj)?;
466 }
467
468 let payload_surface = MESSAGE_KEYS
469 .iter()
470 .find_map(|key| obj.get(*key))
471 .and_then(|payload| payload.get("surfaceId"))
472 .and_then(Value::as_str)
473 .map(str::to_string);
474 if payload_surface.is_some() {
475 self.set_surface_id(payload_surface);
476 }
477 let sid = self
478 .surface_id
479 .clone()
480 .unwrap_or_else(|| "unknown".to_string());
481
482 if let Some(payload) = obj.get(MSG_CREATE_SURFACE) {
483 if let Some(root) = payload.get("root").and_then(Value::as_str) {
484 self.set_root_id(root.to_string());
485 }
486 self.active_msg_type = Some(MSG_CREATE_SURFACE.to_string());
487 self.buffered_start_message = Some(obj.clone());
488 if !self.yielded_start_messages.contains(&sid) {
489 self.yield_message(obj.clone(), parts, false)?;
490 self.yielded_start_messages.insert(sid.clone());
491 self.yielded_surfaces.insert(sid.clone());
492 self.buffered_start_message = None;
493 }
494 self.deleted_surfaces.remove(&sid);
498 self.yield_reachable(parts)?;
499 return Ok(true);
500 }
501
502 if let Some(payload) = obj.get(MSG_UPDATE_COMPONENTS) {
503 self.active_msg_type = Some(MSG_UPDATE_COMPONENTS.to_string());
504 if let Some(root) = payload.get("root").and_then(Value::as_str) {
505 self.set_root_id(root.to_string());
506 }
507 if let Some(components) = payload.get("components").and_then(Value::as_array) {
508 for component in components {
509 if let Some(id) = component.get("id").and_then(Value::as_str) {
510 self.seen_components
511 .insert(id.to_string(), component.clone());
512 }
513 }
514 }
515 self.yield_reachable(parts)?;
516 return Ok(true);
517 }
518
519 if obj.get(MSG_DELETE_SURFACE).is_some() {
520 if !self.yielded_start_messages.contains(&sid) {
521 return Ok(true);
525 }
526 self.delete_surface(&sid);
527 self.yield_message(obj.clone(), parts, false)?;
528 return Ok(true);
529 }
530
531 if obj.get(MSG_UPDATE_DATA_MODEL).is_some() {
532 self.yield_message(obj.clone(), parts, false)?;
533 return Ok(true);
534 }
535 Ok(false)
536 }
537
538 fn delete_surface(&mut self, sid: &str) {
539 self.yielded_ids.remove(sid);
540 self.yielded_contents
541 .retain(|(surface, _), _| surface != sid);
542 self.yielded_surfaces.remove(sid);
543 self.yielded_start_messages.remove(sid);
544 self.deleted_surfaces.insert(sid.to_string());
545 }
546
547 fn handle_partial_component(&mut self, comp: &Value) {
549 let Some(id) = comp.get("id").and_then(Value::as_str) else {
550 return;
551 };
552 if has_empty_object(comp) {
556 return;
557 }
558 self.seen_components.insert(id.to_string(), comp.clone());
559 self.topology_dirty = true;
560 }
561
562 fn yield_reachable(&mut self, parts: &mut Vec<ResponsePart>) -> Result<()> {
564 let Some(active_msg_type) = self.active_msg_type.clone() else {
565 return Ok(());
566 };
567 let Some(sid) = self.surface_id.clone() else {
568 return Ok(());
569 };
570 if !self.yielded_surfaces.contains(&sid) && self.buffered_start_message.is_none() {
572 return Ok(());
573 }
574 if self.deleted_surfaces.contains(&sid) {
575 return Ok(());
576 }
577
578 let root_id = self.root_id().to_string();
579 let reachable = self.analyze_topology(&root_id)?;
580
581 let seen: BTreeSet<&str> = self.seen_components.keys().map(String::as_str).collect();
585 let mut processed: Vec<Value> = Vec::new();
586 let mut extras: Vec<Value> = Vec::new();
587 for id in &reachable {
588 let Some(component) = self.seen_components.get(id) else {
589 continue;
590 };
591 let mut component = component.clone();
592 let comp_id = component
593 .get("id")
594 .and_then(Value::as_str)
595 .unwrap_or("unknown")
596 .to_string();
597 rewrite_children(
598 &mut component,
599 &comp_id,
600 &seen,
601 &mut extras,
602 &self.json_buffer,
603 );
604 processed.push(component);
605 }
606 processed.extend(extras);
607
608 let yielded = self.yielded_ids.entry(sid.clone()).or_default().clone();
609 let mut should_yield = reachable.difference(&yielded).next().is_some();
610 if !should_yield {
611 should_yield = processed.iter().any(|component| {
613 let Some(id) = component.get("id").and_then(Value::as_str) else {
614 return false;
615 };
616 let key = (sid.clone(), id.to_string());
617 self.yielded_contents.get(&key) != Some(&canonical_json(component))
618 });
619 }
620 if !should_yield {
621 return Ok(());
622 }
623
624 if let Some(start) = self.buffered_start_message.clone() {
625 if !self.yielded_start_messages.contains(&sid) {
626 self.yield_message(start, parts, false)?;
627 self.yielded_start_messages.insert(sid.clone());
628 self.yielded_surfaces.insert(sid.clone());
629 }
630 }
631
632 let mut payload = Map::new();
633 payload.insert("surfaceId".to_string(), Value::String(sid.clone()));
634 payload.insert("components".to_string(), Value::Array(processed.clone()));
635 let key = if active_msg_type == MSG_CREATE_SURFACE {
636 MSG_UPDATE_COMPONENTS
637 } else {
638 active_msg_type.as_str()
639 };
640 let mut message = Map::new();
641 message.insert(
642 "version".to_string(),
643 Value::String(PROTOCOL_VERSION.into()),
644 );
645 message.insert(key.to_string(), Value::Object(payload));
646
647 if !self.yield_message(Value::Object(message), parts, true)? {
650 return Ok(());
651 }
652
653 self.yielded_ids
654 .entry(sid.clone())
655 .or_default()
656 .extend(reachable.iter().cloned());
657 for component in &processed {
658 if let Some(id) = component.get("id").and_then(Value::as_str) {
659 self.yielded_contents
660 .insert((sid.clone(), id.to_string()), canonical_json(component));
661 }
662 }
663 Ok(())
664 }
665
666 fn analyze_topology(&self, root_id: &str) -> Result<BTreeSet<String>> {
671 let mut adjacency: BTreeMap<&str, Vec<(&str, String)>> = BTreeMap::new();
672 for (id, component) in &self.seen_components {
673 let mut edges = Vec::new();
674 let mut references = Vec::new();
675 collect_child_refs(component, &mut references);
676 for reference in references {
677 if reference.0 == *id {
678 return Err(Error::Parse(format!(
679 "Self-reference detected: Component '{id}' references itself in field \
680 '{}'",
681 reference.1
682 )));
683 }
684 edges.push((
685 self.seen_components
686 .get_key_value(&reference.0)
687 .map(|(k, _)| k.as_str())
688 .unwrap_or_default(),
689 reference.0,
690 ));
691 }
692 adjacency.insert(id.as_str(), edges);
693 }
694
695 let mut visited: BTreeSet<String> = BTreeSet::new();
696 let mut on_path: BTreeSet<String> = BTreeSet::new();
697 if self.seen_components.contains_key(root_id) {
698 walk(root_id, &adjacency, &mut visited, &mut on_path)?;
699 }
700 Ok(visited
701 .into_iter()
702 .filter(|id| self.seen_components.contains_key(id))
703 .collect())
704 }
705
706 fn yield_message(
711 &mut self,
712 message: Value,
713 parts: &mut Vec<ResponsePart>,
714 partial: bool,
715 ) -> Result<bool> {
716 if self.validate {
717 if let Err(error) = self.validate_message(&message) {
718 if partial {
719 return Ok(false);
720 }
721 return Err(error);
722 }
723 }
724 if !self.deduplicate_data_model(&message) {
725 return Ok(false);
726 }
727
728 match parts.last_mut() {
729 Some(last) if last.a2ui.is_none() => last.a2ui = Some(vec![message]),
730 Some(last) => {
731 if let Some(existing) = last.a2ui.as_mut() {
732 existing.push(message);
733 }
734 }
735 None => parts.push(ResponsePart {
736 text: String::new(),
737 raw: None,
738 a2ui: Some(vec![message]),
739 is_final: true,
740 }),
741 }
742 Ok(true)
743 }
744
745 fn deduplicate_data_model(&mut self, message: &Value) -> bool {
748 let Some(Value::Object(update)) = message.get(MSG_UPDATE_DATA_MODEL) else {
749 return true;
750 };
751 let sid = self.data_model_surface(update.get("surfaceId"));
752 let yielded = self.yielded_data_model.entry(sid).or_default();
753 let is_new = update
754 .iter()
755 .any(|(k, v)| k != "surfaceId" && k != "root" && yielded.get(k) != Some(v));
756 if !is_new {
757 return false;
758 }
759 for (k, v) in update {
760 if k != "surfaceId" && k != "root" {
761 yielded.insert(k.clone(), v.clone());
762 }
763 }
764 true
765 }
766
767 fn data_model_surface(&self, named: Option<&Value>) -> String {
770 named
771 .and_then(Value::as_str)
772 .map(str::to_string)
773 .or_else(|| self.surface_id.clone())
774 .unwrap_or_else(|| "default".to_string())
775 }
776
777 fn validate_message(&self, message: &Value) -> Result<()> {
779 validate_envelope(message)?;
780
781 let Some(components) = message
782 .get(MSG_UPDATE_COMPONENTS)
783 .and_then(|payload| payload.get("components"))
784 .and_then(Value::as_array)
785 else {
786 return Ok(());
787 };
788 let options = ValidateOptions {
792 require_root: false,
793 allow_dangling_children: true,
794 check_bindings: false,
795 check_binding_syntax: false,
796 ..ValidateOptions::full_surface()
797 };
798 Validator::with_options(&self.catalog, options)
799 .validate_json(components, None)
800 .into_result()
801 }
802
803 fn sniff_metadata(&mut self) {
807 if self.json_buffer.len() < self.sniff_cursor {
812 self.sniff_cursor = 0;
814 }
815 let start = floor_char_boundary(&self.json_buffer, self.sniff_cursor);
816 let region = &self.json_buffer[start..];
817 let mut next_cursor = self.json_buffer.len().saturating_sub(SNIFF_OVERLAP);
821
822 let mut found: Vec<(&str, String)> = Vec::new();
823 for key in ["surfaceId", "root"] {
824 let (value, incomplete) = scan_string_values(region, key);
825 if let Some(value) = value {
826 found.push((key, value));
827 }
828 if let Some(offset) = incomplete {
829 next_cursor = next_cursor.min(start + offset);
832 }
833 }
834 for (key, value) in found {
835 match key {
836 "surfaceId" => self.set_surface_id(Some(value)),
837 _ => self.set_root_id(value),
838 }
839 }
840 self.sniff_cursor = next_cursor;
841
842 if self.active_msg_type.is_none() {
843 for key in [MSG_CREATE_SURFACE, MSG_UPDATE_COMPONENTS] {
844 if self.json_buffer.contains(&format!("\"{key}\":")) {
845 self.active_msg_type = Some(key.to_string());
846 break;
847 }
848 }
849 }
850 }
851
852 fn sniff_partial_component(&mut self) {
854 if !self.json_buffer.contains("\"components\"") {
855 return;
856 }
857 let frames: Vec<usize> = self
858 .brace_stack
859 .iter()
860 .rev()
861 .filter(|(kind, _)| *kind == '{')
862 .map(|(_, start)| *start)
863 .collect();
864 for start in frames {
865 let Some(fragment) = self.json_buffer.get(start..) else {
866 continue;
867 };
868 let healed = self.heal_json(fragment);
869 if healed.is_empty() {
870 continue;
871 }
872 let Ok(obj) = serde_json::from_str::<Value>(&healed) else {
873 continue;
874 };
875 let has_identity =
876 obj.get("id").is_some() && obj.get("component").and_then(Value::as_str).is_some();
877 if has_identity {
878 self.handle_partial_component(&obj);
879 }
880 }
881 }
882
883 fn sniff_partial_data_model(&mut self, parts: &mut Vec<ResponsePart>) {
886 if !self
887 .json_buffer
888 .contains(&format!("\"{MSG_UPDATE_DATA_MODEL}\""))
889 {
890 return;
891 }
892 let frames: Vec<usize> = self
893 .brace_stack
894 .iter()
895 .rev()
896 .filter(|(kind, _)| *kind == '{')
897 .map(|(_, start)| *start)
898 .collect();
899
900 for start in frames {
901 let Some(fragment) = self.json_buffer.get(start..) else {
902 continue;
903 };
904 let Some(obj) = self.parse_healed_or_trimmed(fragment) else {
905 continue;
906 };
907 let Some(update) = obj.get(MSG_UPDATE_DATA_MODEL).and_then(Value::as_object) else {
908 continue;
909 };
910 let Some(Value::Object(value)) = update.get("value") else {
911 continue;
912 };
913
914 let sid = self.data_model_surface(update.get("surfaceId"));
915 let known = self.yielded_data_model.get(&sid);
916 let mut delta = Map::new();
917 for (key, item) in value {
918 if known.and_then(|entries| entries.get(key)) != Some(item) {
919 delta.insert(key.clone(), item.clone());
920 }
921 }
922 if delta.is_empty() {
923 continue;
924 }
925
926 let mut payload = Map::new();
927 payload.insert("surfaceId".to_string(), Value::String(sid.clone()));
928 payload.insert("value".to_string(), Value::Object(delta.clone()));
929 let mut message = Map::new();
930 message.insert(
931 "version".to_string(),
932 Value::String(PROTOCOL_VERSION.into()),
933 );
934 message.insert(MSG_UPDATE_DATA_MODEL.to_string(), Value::Object(payload));
935
936 let _ = self.yield_message(Value::Object(message), parts, true);
939 let yielded = self.yielded_data_model.entry(sid).or_default();
940 for (key, item) in delta {
941 yielded.insert(key, item);
942 }
943 }
944 }
945
946 fn parse_healed_or_trimmed(&self, fragment: &str) -> Option<Value> {
948 let healed = self.heal_json(fragment);
949 if let Ok(value) = serde_json::from_str::<Value>(&healed) {
950 return Some(value);
951 }
952 let mut trimmed = fragment.to_string();
955 while let Some(index) = trimmed.rfind(',') {
956 trimmed.truncate(index);
957 let healed = self.heal_json(&trimmed);
958 if healed.is_empty() {
959 continue;
960 }
961 if let Ok(value) = serde_json::from_str::<Value>(&healed) {
962 return Some(value);
963 }
964 }
965 None
966 }
967
968 fn heal_json(&self, fragment: &str) -> String {
973 let mut fixed = fragment.trim_end().to_string();
974 if fixed.is_empty() {
975 return String::new();
976 }
977
978 let mut stack: Vec<char> = Vec::new();
979 let mut in_string = false;
980 let mut escaped = false;
981 let mut last_quote = None;
982 for (index, ch) in fixed.char_indices() {
983 if escaped {
984 escaped = false;
985 continue;
986 }
987 match ch {
988 '\\' => escaped = true,
989 '"' => {
990 in_string = !in_string;
991 if in_string {
992 last_quote = Some(index);
993 }
994 }
995 '{' | '[' if !in_string => stack.push(ch),
996 '}' | ']' if !in_string => {
997 stack.pop();
998 }
999 _ => {}
1000 }
1001 }
1002
1003 if in_string {
1004 if let Some(quote) = last_quote {
1005 let prefix = fixed[..quote].trim_end();
1006 if prefix.ends_with(':') {
1007 match key_before_colon(prefix) {
1008 Some(key) if self.cuttable_keys.contains(&key) => {}
1009 _ => return String::new(),
1011 }
1012 }
1013 }
1014 fixed.push('"');
1015 }
1016
1017 let trimmed = fixed.trim_end();
1018 let mut fixed = trimmed
1019 .strip_suffix(',')
1020 .unwrap_or(trimmed)
1021 .trim_end()
1022 .to_string();
1023 while let Some(open) = stack.pop() {
1024 fixed.push(if open == '{' { '}' } else { ']' });
1025 }
1026 fixed
1027 }
1028}
1029
1030fn walk<'a>(
1032 root: &'a str,
1033 adjacency: &BTreeMap<&'a str, Vec<(&'a str, String)>>,
1034 visited: &mut BTreeSet<String>,
1035 on_path: &mut BTreeSet<String>,
1036) -> Result<()> {
1037 let mut stack: Vec<(&str, usize)> = vec![(root, 0)];
1042 visited.insert(root.to_string());
1043 on_path.insert(root.to_string());
1044
1045 while let Some(&(node, edge_index)) = stack.last() {
1046 let edges = adjacency.get(node).map(Vec::as_slice).unwrap_or_default();
1047 let Some((_, target)) = edges.get(edge_index) else {
1048 on_path.remove(node);
1049 stack.pop();
1050 continue;
1051 };
1052 if let Some(top) = stack.last_mut() {
1053 top.1 += 1;
1054 }
1055 if on_path.contains(target.as_str()) {
1056 return Err(Error::Parse(format!(
1057 "Circular reference detected involving component '{target}'"
1058 )));
1059 }
1060 if visited.insert(target.to_string()) {
1061 on_path.insert(target.to_string());
1062 let next = adjacency
1065 .get_key_value(target.as_str())
1066 .map(|(key, _)| *key)
1067 .unwrap_or(target.as_str());
1068 stack.push((next, 0));
1069 }
1070 }
1071 Ok(())
1072}
1073
1074fn is_protocol_message(obj: &Value) -> bool {
1075 MESSAGE_KEYS.iter().any(|key| obj.get(*key).is_some())
1076}
1077
1078fn validate_envelope(message: &Value) -> Result<()> {
1087 let Some(map) = message.as_object() else {
1088 return Err(validation_error(
1089 "Validation failed: message must be an object",
1090 ));
1091 };
1092
1093 let Some(key) = MESSAGE_KEYS.into_iter().find(|key| map.contains_key(*key)) else {
1094 return Err(validation_error(format!(
1095 "Validation failed: {:?} is not a valid A2UI message; it must contain exactly one of \
1096 {}",
1097 map.keys().collect::<Vec<_>>(),
1098 MESSAGE_KEYS.join(", ")
1099 )));
1100 };
1101 if !map.contains_key("version") {
1102 return Err(validation_error(
1103 "Validation failed: 'version' is a required property",
1104 ));
1105 }
1106
1107 let Some(payload) = map.get(key).and_then(Value::as_object) else {
1108 return Err(validation_error(format!(
1109 "Validation failed: '{key}' must be an object"
1110 )));
1111 };
1112 let fields = OPERATIONS
1113 .iter()
1114 .find(|(name, _)| *name == key)
1115 .map_or(&[][..], |(_, fields)| *fields);
1116 for (field, _, required) in fields {
1117 if *required && !payload.contains_key(*field) {
1118 return Err(validation_error(format!(
1119 "Validation failed: '{field}' is a required property of {key}"
1120 )));
1121 }
1122 }
1123 Ok(())
1124}
1125
1126fn validation_error(message: impl Into<String>) -> Error {
1127 Error::Validation {
1128 errors: crate::ValidationErrors(vec![crate::validate::ValidationError::new(
1129 crate::validate::ErrorCode::MissingField,
1130 "message",
1131 message,
1132 )]),
1133 }
1134}
1135
1136fn text_part(text: &str) -> ResponsePart {
1137 ResponsePart {
1138 text: text.to_string(),
1139 raw: None,
1140 a2ui: None,
1141 is_final: true,
1142 }
1143}
1144
1145fn find_open_tag(text: &str) -> Option<(usize, Option<usize>)> {
1157 let name = &A2UI_OPEN_TAG[1..A2UI_OPEN_TAG.len() - 1];
1159
1160 for (start, _) in text.match_indices('<') {
1161 let rest = &text[start + 1..];
1162 if rest.len() < name.len() {
1163 if name.as_bytes()[..rest.len()].eq_ignore_ascii_case(rest.as_bytes()) {
1164 return Some((start, None));
1165 }
1166 continue;
1167 }
1168 if !rest.as_bytes()[..name.len()].eq_ignore_ascii_case(name.as_bytes()) {
1169 continue;
1170 }
1171 let after = &rest[name.len()..];
1172 match after.chars().next() {
1173 None => return Some((start, None)),
1174 Some(c) if c.is_alphanumeric() || c == '_' => continue,
1175 Some(_) => {}
1176 }
1177 return Some((
1178 start,
1179 after
1180 .find('>')
1181 .map(|close| start + 1 + name.len() + close + 1),
1182 ));
1183 }
1184 None
1185}
1186
1187fn trailing_prefix_len(text: &str, tag: &str) -> usize {
1189 let max = tag.len().saturating_sub(1).min(text.len());
1190 (1..=max)
1191 .rev()
1192 .find(|len| text.is_char_boundary(text.len() - len) && text.ends_with(&tag[..*len]))
1193 .unwrap_or(0)
1194}
1195
1196fn key_before_colon(prefix: &str) -> Option<String> {
1198 let without_colon = prefix.strip_suffix(':')?.trim_end();
1199 let inner = without_colon.strip_suffix('"')?;
1200 let start = inner.rfind('"')?;
1201 Some(inner[start + 1..].to_string())
1202}
1203
1204fn scan_string_values(region: &str, key: &str) -> (Option<String>, Option<usize>) {
1210 let needle = format!("\"{key}\"");
1211 let mut latest = None;
1212 let mut incomplete = None;
1213 let mut cursor = 0;
1214
1215 while let Some(offset) = region[cursor..].find(&needle) {
1216 let at = cursor + offset;
1217 let rest = region[at + needle.len()..].trim_start();
1218 match rest
1219 .strip_prefix(':')
1220 .map(str::trim_start)
1221 .and_then(|rest| rest.strip_prefix('"'))
1222 {
1223 Some(value) => match value.find('"') {
1224 Some(end) => latest = Some(value[..end].to_string()),
1225 None => {
1226 incomplete = Some(at);
1227 break;
1228 }
1229 },
1230 None => {
1233 if rest.is_empty() || rest == ":" {
1234 incomplete = Some(at);
1235 break;
1236 }
1237 }
1238 }
1239 cursor = at + needle.len();
1240 }
1241 (latest, incomplete)
1242}
1243
1244fn floor_char_boundary(text: &str, index: usize) -> usize {
1246 let mut index = index.min(text.len());
1247 while index > 0 && !text.is_char_boundary(index) {
1248 index -= 1;
1249 }
1250 index
1251}
1252
1253fn has_empty_object(value: &Value) -> bool {
1255 match value {
1256 Value::Object(map) => map.is_empty() || map.values().any(has_empty_object),
1257 Value::Array(items) => items.iter().any(has_empty_object),
1258 _ => false,
1259 }
1260}
1261
1262fn collect_child_refs(value: &Value, refs: &mut Vec<(String, String)>) {
1264 match value {
1265 Value::Object(map) => {
1266 for field in CHILD_FIELDS {
1267 match map.get(field) {
1268 Some(Value::String(id)) => refs.push((id.clone(), field.to_string())),
1269 Some(Value::Array(items)) => {
1270 for item in items {
1271 if let Value::String(id) = item {
1272 refs.push((id.clone(), field.to_string()));
1273 }
1274 }
1275 }
1276 _ => {}
1277 }
1278 }
1279 for (key, child) in map {
1280 if key == "id" || key == "component" {
1281 continue;
1282 }
1283 collect_child_refs(child, refs);
1284 }
1285 }
1286 Value::Array(items) => {
1287 for item in items {
1288 collect_child_refs(item, refs);
1289 }
1290 }
1291 _ => {}
1292 }
1293}
1294
1295fn rewrite_children(
1297 value: &mut Value,
1298 comp_id: &str,
1299 seen: &BTreeSet<&str>,
1300 extras: &mut Vec<Value>,
1301 buffer: &str,
1302) {
1303 match value {
1304 Value::Object(map) => {
1305 for field in CHILD_FIELDS {
1306 match map.get_mut(field) {
1307 Some(Value::Array(items)) => {
1308 let mut resolved: Vec<Value> = Vec::with_capacity(items.len());
1309 for item in items.iter() {
1310 let Some(id) = item.as_str() else { continue };
1311 if seen.contains(id) {
1312 resolved.push(Value::String(id.to_string()));
1313 } else {
1314 let placeholder = format!("loading_{id}");
1315 push_placeholder(&placeholder, extras);
1316 resolved.push(Value::String(placeholder));
1317 }
1318 }
1319 if resolved.is_empty()
1320 && matches!(field, "children" | "explicitList")
1321 && list_is_still_open(buffer, field)
1322 {
1323 let placeholder = format!("loading_children_{comp_id}");
1326 push_placeholder(&placeholder, extras);
1327 resolved.push(Value::String(placeholder));
1328 }
1329 *map.get_mut(field).expect("field present") = Value::Array(resolved);
1330 }
1331 Some(Value::String(id)) if !seen.contains(id.as_str()) => {
1332 let placeholder = format!("loading_{id}");
1333 push_placeholder(&placeholder, extras);
1334 *id = placeholder;
1335 }
1336 _ => {}
1337 }
1338 }
1339 for (key, child) in map.iter_mut() {
1340 if key == "id" || key == "component" {
1341 continue;
1342 }
1343 rewrite_children(child, comp_id, seen, extras, buffer);
1344 }
1345 }
1346 Value::Array(items) => {
1347 for item in items {
1348 rewrite_children(item, comp_id, seen, extras, buffer);
1349 }
1350 }
1351 _ => {}
1352 }
1353}
1354
1355fn push_placeholder(id: &str, extras: &mut Vec<Value>) {
1356 let already = extras
1357 .iter()
1358 .any(|extra| extra.get("id").and_then(Value::as_str) == Some(id));
1359 if already {
1360 return;
1361 }
1362 let mut placeholder = Map::new();
1363 placeholder.insert("id".to_string(), Value::String(id.to_string()));
1364 placeholder.insert("component".to_string(), Value::String("Row".to_string()));
1365 placeholder.insert("children".to_string(), Value::Array(Vec::new()));
1366 extras.push(Value::Object(placeholder));
1367}
1368
1369fn list_is_still_open(buffer: &str, field: &str) -> bool {
1371 let needle = format!("\"{field}\"");
1372 let Some(index) = buffer.rfind(&needle) else {
1373 return false;
1374 };
1375 let after = &buffer[index + needle.len()..];
1376 match after.find('[') {
1377 Some(open) => !after[..open].contains(']'),
1378 None => false,
1379 }
1380}
1381
1382fn canonical_json(value: &Value) -> String {
1384 match value {
1385 Value::Object(map) => {
1386 let mut keys: Vec<&String> = map.keys().collect();
1387 keys.sort();
1388 let body: Vec<String> = keys
1389 .into_iter()
1390 .map(|key| {
1391 format!(
1392 "{}:{}",
1393 serde_json::to_string(key).unwrap_or_default(),
1394 canonical_json(&map[key])
1395 )
1396 })
1397 .collect();
1398 format!("{{{}}}", body.join(","))
1399 }
1400 Value::Array(items) => {
1401 let body: Vec<String> = items.iter().map(canonical_json).collect();
1402 format!("[{}]", body.join(","))
1403 }
1404 other => serde_json::to_string(other).unwrap_or_default(),
1405 }
1406}
1407
1408#[cfg(test)]
1409mod tests {
1410 use super::*;
1411 use crate::catalog::Catalog;
1412 use serde_json::json;
1413
1414 fn catalog() -> Catalog {
1417 Catalog::from_schema(&json!({
1418 "catalogId": "test",
1419 "components": {
1420 "Text": {
1421 "type": "object",
1422 "properties": {"component": {"const": "Text"}, "text": {}},
1423 "required": ["component"]
1424 },
1425 "Card": {
1426 "type": "object",
1427 "properties": {
1428 "component": {"const": "Card"},
1429 "child": {"$ref": "common_types.json#/$defs/ComponentId"}
1430 },
1431 "required": ["component"]
1432 },
1433 "Row": {
1434 "type": "object",
1435 "properties": {
1436 "component": {"const": "Row"},
1437 "children": {"$ref": "common_types.json#/$defs/ChildList"}
1438 },
1439 "required": ["component"]
1440 },
1441 "Audio": {
1442 "type": "object",
1443 "properties": {"component": {"const": "Audio"}, "url": {}, "label": {}},
1444 "required": ["component", "url"]
1445 }
1446 }
1447 }))
1448 .expect("test catalog")
1449 }
1450
1451 fn parser() -> StreamParser {
1452 StreamParser::new(catalog())
1453 }
1454
1455 fn feed(parser: &mut StreamParser, chunks: &[&str]) -> Vec<Value> {
1457 let mut last = Vec::new();
1458 for chunk in chunks {
1459 last = parser
1460 .process_chunk(chunk)
1461 .expect("chunk should parse")
1462 .into_iter()
1463 .filter_map(|part| part.a2ui)
1464 .flatten()
1465 .collect();
1466 }
1467 last
1468 }
1469
1470 const CREATE: &str =
1471 r#"{"version":"v0.9","createSurface":{"surfaceId":"s1","catalogId":"test"}},"#;
1472
1473 const UPDATE_OPEN: &str =
1474 r#"{"version":"v0.9","updateComponents":{"surfaceId":"s1","components":"#;
1475
1476 #[test]
1477 fn conversational_text_streams_before_and_after_the_block() {
1478 let mut parser = parser();
1479 let parts = parser.process_chunk("Hello! ").unwrap();
1480 assert_eq!(parts[0].text, "Hello! ");
1481 assert!(parts[0].a2ui.is_none());
1482
1483 let parts = parser.process_chunk("Here you go: <a2ui-json>[").unwrap();
1484 assert_eq!(parts[0].text, "Here you go: ");
1485
1486 parser.process_chunk(CREATE).unwrap();
1487 let parts = parser
1488 .process_chunk("]</a2ui-json> Anything else?")
1489 .unwrap();
1490 assert_eq!(parts.last().unwrap().text, " Anything else?");
1491 }
1492
1493 #[test]
1494 fn a_tag_split_across_chunks_is_not_leaked_as_text() {
1495 let mut parser = parser();
1496 let parts = parser.process_chunk("Talking <a2u").unwrap();
1497 assert_eq!(parts.len(), 1);
1498 assert_eq!(
1499 parts[0].text, "Talking ",
1500 "the partial tag must be held back"
1501 );
1502
1503 let parts = parser.process_chunk("i-json>").unwrap();
1504 assert!(parts.is_empty());
1505 }
1506
1507 #[test]
1508 fn an_open_tag_carrying_attributes_still_opens_a_block() {
1509 for open in [
1514 r#"<a2ui-json version="v0.9">"#,
1515 "<a2ui-json >",
1516 "<A2UI-JSON>",
1517 ] {
1518 let mut parser = parser();
1519 let mut messages = Vec::new();
1520 for chunk in [open, "[", CREATE, "]</a2ui-json>"] {
1521 messages.extend(feed(&mut parser, &[chunk]));
1522 }
1523 assert!(
1524 messages.iter().any(|m| m.get("createSurface").is_some()),
1525 "{open} did not open a block"
1526 );
1527 }
1528
1529 let mut parser = parser();
1531 let parts = parser
1532 .process_chunk(r#"<a2ui-jsonx>[{"id":"t"}]</a2ui-jsonx>"#)
1533 .unwrap();
1534 assert_eq!(parts[0].text, r#"<a2ui-jsonx>[{"id":"t"}]</a2ui-jsonx>"#);
1535 }
1536
1537 #[test]
1538 fn a_message_is_emitted_the_moment_it_closes() {
1539 let mut parser = parser();
1540 assert!(
1541 feed(
1542 &mut parser,
1543 &["<a2ui-json>[", r#"{"version":"v0.9","createSur"#]
1544 )
1545 .is_empty()
1546 );
1547
1548 let messages = feed(
1549 &mut parser,
1550 &[r#"face":{"surfaceId":"s1","catalogId":"test"}},"#],
1551 );
1552 assert_eq!(messages.len(), 1);
1553 assert_eq!(messages[0]["createSurface"]["surfaceId"], "s1");
1554 }
1555
1556 #[test]
1557 fn cut_text_is_healed_and_extended_as_more_arrives() {
1558 let mut parser = parser();
1559 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1560
1561 let chunk = format!(r#"{UPDATE_OPEN}[{{"id":"root","component":"Text","text":"Em"#);
1562 let messages = feed(&mut parser, &[&chunk]);
1563 assert_eq!(
1564 messages[0]["updateComponents"]["components"][0]["text"],
1565 "Em"
1566 );
1567
1568 let messages = feed(&mut parser, &[r#"ail"}]}}"#]);
1569 assert_eq!(
1570 messages[0]["updateComponents"]["components"][0]["text"],
1571 "Email"
1572 );
1573 }
1574
1575 #[test]
1576 fn a_cut_identifier_is_held_back_rather_than_invented() {
1577 let mut parser = parser();
1578 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1579 let chunk = format!(r#"{UPDATE_OPEN}[{{"id":"but"#);
1581 assert!(feed(&mut parser, &[&chunk]).is_empty());
1582 }
1583
1584 #[test]
1585 fn a_missing_child_becomes_a_placeholder_and_is_swapped_in_later() {
1586 let mut parser = parser();
1587 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1588
1589 let chunk = format!(r#"{UPDATE_OPEN}[{{"id":"root","component":"Card","child":"c1"}}, "#);
1590 let messages = feed(&mut parser, &[&chunk]);
1591 let components = messages[0]["updateComponents"]["components"]
1592 .as_array()
1593 .unwrap();
1594 assert_eq!(components[0]["child"], "loading_c1");
1595 assert_eq!(components[1]["id"], "loading_c1");
1596 assert_eq!(components[1]["component"], "Row");
1597
1598 let messages = feed(
1599 &mut parser,
1600 &[r#"{"id":"c1","component":"Text","text":"hi"}]}}"#],
1601 );
1602 let components = messages[0]["updateComponents"]["components"]
1603 .as_array()
1604 .unwrap();
1605 assert_eq!(components[0]["id"], "c1");
1607 assert_eq!(components[1]["child"], "c1");
1608 assert_eq!(components.len(), 2);
1609 }
1610
1611 #[test]
1612 fn components_wait_until_they_are_reachable_from_the_root() {
1613 let mut parser = parser();
1614 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1615
1616 let chunk = format!(r#"{UPDATE_OPEN}[{{"id":"c1","component":"Text","text":"hi"}}"#);
1618 assert!(feed(&mut parser, &[&chunk]).is_empty());
1619
1620 let messages = feed(
1621 &mut parser,
1622 &[r#", {"id":"root","component":"Card","child":"c1"}]}}"#],
1623 );
1624 let components = messages[0]["updateComponents"]["components"]
1625 .as_array()
1626 .unwrap();
1627 assert_eq!(components.len(), 2);
1628 }
1629
1630 #[test]
1631 fn unreachable_components_are_never_emitted() {
1632 let mut parser = parser();
1633 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1634 let chunk = format!(
1635 r#"{UPDATE_OPEN}[{{"id":"root","component":"Text","text":"root"}},{{"id":"orphan","component":"Text","text":"orphan"}}]}}}}"#
1636 );
1637 let messages = feed(&mut parser, &[&chunk]);
1638 let components = messages[0]["updateComponents"]["components"]
1639 .as_array()
1640 .unwrap();
1641 assert_eq!(components.len(), 1);
1642 assert_eq!(components[0]["id"], "root");
1643 }
1644
1645 #[test]
1646 fn components_are_buffered_until_the_surface_exists() {
1647 let mut parser = parser();
1648 let chunk =
1649 format!(r#"{UPDATE_OPEN}[{{"id":"root","component":"Text","text":"hi"}}]}}}}, "#);
1650 let messages = feed(&mut parser, &["<a2ui-json>[", &chunk]);
1651 assert!(messages.is_empty(), "no surface to attach to yet");
1652
1653 let messages = feed(&mut parser, &[CREATE]);
1654 assert_eq!(
1655 messages.len(),
1656 2,
1657 "the surface and its tree arrive together"
1658 );
1659 assert!(messages[0].get("createSurface").is_some());
1660 assert!(messages[1].get("updateComponents").is_some());
1661 }
1662
1663 #[test]
1664 fn deleting_a_surface_that_was_never_created_is_dropped() {
1665 let mut parser = parser();
1666 let messages = feed(
1667 &mut parser,
1668 &[
1669 "<a2ui-json>[",
1670 r#"{"version":"v0.9","deleteSurface":{"surfaceId":"s1"}}, "#,
1671 CREATE,
1672 ],
1673 );
1674 assert_eq!(messages.len(), 1);
1675 assert!(messages[0].get("createSurface").is_some());
1676 }
1677
1678 #[test]
1679 fn a_surface_recreated_after_a_delete_renders_again() {
1680 let mut parser = parser();
1681 let messages = feed(
1682 &mut parser,
1683 &[
1684 "<a2ui-json>[",
1685 CREATE,
1686 r#"{"version":"v0.9","deleteSurface":{"surfaceId":"s1"}}, "#,
1687 CREATE,
1688 &format!(r#"{UPDATE_OPEN}[{{"id":"root","component":"Text","text":"back"}}]}}}}"#),
1689 ],
1690 );
1691 let components = &messages
1695 .iter()
1696 .rev()
1697 .find(|m| m.get("updateComponents").is_some())
1698 .expect("the recreated surface must render")["updateComponents"]["components"];
1699 assert_eq!(components[0]["text"], "back");
1700 }
1701
1702 #[test]
1703 fn a_self_reference_is_an_error_no_further_input_can_fix() {
1704 let mut parser = parser();
1705 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1706 let chunk =
1707 format!(r#"{UPDATE_OPEN}[{{"id":"root","component":"Card","child":"root"}}]}}}}"#);
1708 let error = parser.process_chunk(&chunk).unwrap_err();
1709 assert!(
1710 error.to_string().contains("Self-reference detected"),
1711 "{error}"
1712 );
1713 }
1714
1715 #[test]
1716 fn a_reference_loop_across_messages_is_an_error() {
1717 let mut parser = parser();
1718 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1719 let chunk = format!(
1720 r#"{UPDATE_OPEN}[{{"id":"root","component":"Card","child":"c1"}}]}}}},{UPDATE_OPEN}[{{"id":"c1","component":"Card","child":"root"}}]}}}}"#
1721 );
1722 let error = parser.process_chunk(&chunk).unwrap_err();
1723 assert!(
1724 error.to_string().contains("Circular reference detected"),
1725 "{error}"
1726 );
1727 }
1728
1729 #[test]
1730 fn a_message_matching_no_envelope_is_an_error() {
1731 let mut parser = parser();
1732 let error = parser
1733 .process_chunk(r#"<a2ui-json>[{"unknownMessage":"invalid"}]"#)
1734 .unwrap_err();
1735 assert!(error.to_string().contains("Validation failed"), "{error}");
1736 }
1737
1738 #[test]
1739 fn a_missing_required_envelope_field_is_an_error() {
1740 let mut first = parser();
1741 let error = first
1742 .process_chunk(r#"<a2ui-json>[{"version":"v0.9","createSurface":{"surfaceId":"s1"}}]"#)
1743 .unwrap_err();
1744 assert!(error.to_string().contains("required property"), "{error}");
1745
1746 let mut parser = parser();
1747 let error = parser
1748 .process_chunk(r#"<a2ui-json>[{"updateComponents":{"components":[]}}]"#)
1749 .unwrap_err();
1750 assert!(error.to_string().contains("Validation failed"), "{error}");
1751 }
1752
1753 #[test]
1758 fn the_envelope_contract_is_the_validators_own_table() {
1759 for (key, fields) in OPERATIONS {
1760 let required: Vec<&str> = fields
1761 .iter()
1762 .filter(|(_, _, required)| *required)
1763 .map(|(field, _, _)| *field)
1764 .collect();
1765 if !MESSAGE_KEYS.contains(&key) {
1766 continue;
1767 }
1768 for omitted in &required {
1769 let payload: Map<String, Value> = required
1770 .iter()
1771 .filter(|field| *field != omitted)
1772 .map(|field| ((*field).to_string(), json!("x")))
1773 .collect();
1774 let message = json!({"version": "v0.9", key: payload});
1775 let error = validate_envelope(&message)
1776 .expect_err("a message missing a required field must not validate");
1777 assert!(
1778 error.to_string().contains(&format!("'{omitted}'")),
1779 "{key} without {omitted}: {error}"
1780 );
1781 }
1782 }
1783 }
1784
1785 #[test]
1786 fn a_partial_component_missing_a_required_property_is_not_emitted_yet() {
1787 let mut parser = parser();
1788 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1789 let chunk =
1791 format!(r#"{UPDATE_OPEN}[{{"id":"root","component":"Audio","label":"almost ready""#);
1792 assert!(feed(&mut parser, &[&chunk]).is_empty());
1793
1794 let messages = feed(&mut parser, &[r#", "url":"http://a.mp3"}]}}"#]);
1795 assert_eq!(
1796 messages[0]["updateComponents"]["components"][0]["url"],
1797 "http://a.mp3"
1798 );
1799 }
1800
1801 #[test]
1802 fn a_placeholder_the_catalog_cannot_render_suppresses_the_partial_update() {
1803 let catalog = Catalog::from_schema(&json!({
1806 "catalogId": "no-row",
1807 "components": {
1808 "Card": {
1809 "type": "object",
1810 "properties": {
1811 "component": {"const": "Card"},
1812 "child": {"$ref": "common_types.json#/$defs/ComponentId"}
1813 },
1814 "required": ["component"]
1815 },
1816 "Text": {"type": "object", "properties": {"component": {"const": "Text"}}}
1817 }
1818 }))
1819 .unwrap();
1820 let mut parser = StreamParser::new(catalog);
1821 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1822
1823 let chunk = format!(r#"{UPDATE_OPEN}[{{"id":"root","component":"Card","child":"c1"}}"#);
1824 assert!(feed(&mut parser, &[&chunk]).is_empty());
1825 }
1826
1827 #[test]
1828 fn the_data_model_streams_as_deltas_then_settles() {
1829 let mut parser = parser();
1830 let messages = feed(
1831 &mut parser,
1832 &[
1833 "<a2ui-json>[",
1834 CREATE,
1835 r#"{"version":"v0.9","updateDataModel":{"surfaceId":"s1","value":{"a":1,"b":"#,
1836 ],
1837 );
1838 assert_eq!(messages[0]["updateDataModel"]["value"], json!({"a": 1}));
1840
1841 let messages = feed(&mut parser, &["2}}}"]);
1844 assert_eq!(
1845 messages[0]["updateDataModel"]["value"],
1846 json!({"a": 1, "b": 2})
1847 );
1848 }
1849
1850 #[test]
1851 fn an_unchanged_data_model_key_is_not_offered_twice() {
1852 let mut parser = parser();
1853 feed(
1854 &mut parser,
1855 &[
1856 "<a2ui-json>[",
1857 CREATE,
1858 r#"{"version":"v0.9","updateDataModel":{"surfaceId":"s1","value":{"a":1"#,
1859 ],
1860 );
1861 let messages = feed(&mut parser, &[", "]);
1862 assert!(messages.is_empty(), "nothing changed, so nothing to send");
1863 }
1864
1865 #[test]
1866 fn two_surfaces_may_carry_the_same_data_model() {
1867 let mut parser = parser();
1868 let messages = feed(
1869 &mut parser,
1870 &[
1871 "<a2ui-json>[",
1872 CREATE,
1873 r#"{"version":"v0.9","updateDataModel":{"surfaceId":"s1","path":"/","value":{"a":1}}}, "#,
1874 r#"{"version":"v0.9","createSurface":{"surfaceId":"s2","catalogId":"test"}}, "#,
1875 r#"{"version":"v0.9","updateDataModel":{"surfaceId":"s2","path":"/","value":{"a":1}}}"#,
1876 ],
1877 );
1878 let updated: Vec<&Value> = messages
1882 .iter()
1883 .filter(|m| m.get(MSG_UPDATE_DATA_MODEL).is_some())
1884 .collect();
1885 assert_eq!(updated.len(), 1, "{messages:?}");
1886 assert_eq!(updated[0][MSG_UPDATE_DATA_MODEL]["surfaceId"], "s2");
1887 }
1888
1889 #[test]
1890 fn custom_cuttable_keys_replace_the_defaults() {
1891 let mut parser = StreamParser::new(catalog())
1892 .with_cuttable_keys(["label"])
1893 .without_validation();
1894 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1895
1896 let chunk = format!(r#"{UPDATE_OPEN}[{{"id":"root","component":"Audio","label":"partial"#);
1898 let messages = feed(&mut parser, &[&chunk]);
1899 assert_eq!(
1900 messages[0]["updateComponents"]["components"][0]["label"],
1901 "partial"
1902 );
1903 }
1904
1905 #[test]
1906 fn a_block_with_no_json_at_all_is_a_parse_error() {
1907 let mut parser = parser();
1908 let error = parser
1909 .process_chunk("<a2ui-json>not json at all</a2ui-json>")
1910 .unwrap_err();
1911 assert!(matches!(error, Error::Parse(_)), "{error}");
1912 }
1913
1914 #[test]
1915 fn a_second_block_keeps_updating_the_first_surface() {
1916 let mut parser = parser();
1917 let first = format!(
1918 r#"{UPDATE_OPEN}[{{"id":"root","component":"Text","text":"first"}}]}}}}]</a2ui-json>"#
1919 );
1920 feed(&mut parser, &["<a2ui-json>[", CREATE, &first]);
1921
1922 let second = format!(
1923 r#"<a2ui-json>[{UPDATE_OPEN}[{{"id":"root","component":"Text","text":"second"}}]}}}}]</a2ui-json>"#
1924 );
1925 let messages = feed(&mut parser, &[&second]);
1926 assert_eq!(
1927 messages[0]["updateComponents"]["components"][0]["text"],
1928 "second"
1929 );
1930 }
1931
1932 #[test]
1933 fn the_root_id_and_surface_id_are_readable_while_streaming() {
1934 let mut parser = parser();
1935 assert_eq!(parser.surface_id(), None);
1936 assert_eq!(parser.root_id(), "root");
1937
1938 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1939 assert_eq!(parser.surface_id(), Some("s1"));
1940 }
1941
1942 #[test]
1943 fn healing_refuses_to_close_a_non_cuttable_string() {
1944 let parser = parser();
1945 assert_eq!(parser.heal_json(r#"{"id": "part"#), "");
1946 assert_eq!(parser.heal_json(r#"{"path": "/us"#), "");
1947 assert_eq!(parser.heal_json(r#"{"text": "par"#), r#"{"text": "par"}"#);
1948 assert_eq!(parser.heal_json(r#"{"a": 1,"#), r#"{"a": 1}"#);
1949 }
1950
1951 #[test]
1952 fn canonical_json_sorts_keys_at_every_level() {
1953 let a = json!({"b": 1, "a": {"d": 2, "c": [3, {"f": 4, "e": 5}]}});
1954 let b = json!({"a": {"c": [3, {"e": 5, "f": 4}], "d": 2}, "b": 1});
1955 assert_eq!(canonical_json(&a), canonical_json(&b));
1956 assert_ne!(canonical_json(&a), canonical_json(&json!({"b": 2})));
1957 }
1958
1959 #[test]
1960 fn a_very_deep_component_chain_does_not_blow_the_stack() {
1961 let mut parser = StreamParser::new(catalog()).without_validation();
1965 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1966
1967 let depth = 20_000;
1968 let mut components = String::from(r#"[{"id":"root","component":"Card","child":"n0"}"#);
1969 for i in 0..depth {
1970 let child = if i + 1 == depth {
1971 "leaf".to_string()
1972 } else {
1973 format!("n{}", i + 1)
1974 };
1975 components.push_str(&format!(
1976 r#",{{"id":"n{i}","component":"Card","child":"{child}"}}"#
1977 ));
1978 }
1979 components.push_str(r#",{"id":"leaf","component":"Text","text":"end"}]"#);
1980
1981 let chunk = format!(r#"{UPDATE_OPEN}{components}}}}}"#);
1982 let messages = feed(&mut parser, &[&chunk]);
1983 let emitted = messages[0]["updateComponents"]["components"]
1984 .as_array()
1985 .expect("components");
1986 assert_eq!(emitted.len(), depth + 2);
1987 }
1988
1989 #[test]
1990 fn a_deep_chain_with_a_loop_at_the_bottom_is_still_caught() {
1991 let mut parser = StreamParser::new(catalog()).without_validation();
1993 feed(&mut parser, &["<a2ui-json>[", CREATE]);
1994
1995 let depth = 5_000;
1996 let mut components = String::from(r#"[{"id":"root","component":"Card","child":"n0"}"#);
1997 for i in 0..depth {
1998 let child = if i + 1 == depth {
1999 "root".to_string() } else {
2001 format!("n{}", i + 1)
2002 };
2003 components.push_str(&format!(
2004 r#",{{"id":"n{i}","component":"Card","child":"{child}"}}"#
2005 ));
2006 }
2007 components.push(']');
2008
2009 let chunk = format!(r#"{UPDATE_OPEN}{components}}}}}"#);
2010 let error = parser.process_chunk(&chunk).unwrap_err();
2011 assert!(
2012 error.to_string().contains("Circular reference detected"),
2013 "{error}"
2014 );
2015 }
2016
2017 #[test]
2018 fn a_surface_id_split_across_chunks_is_still_picked_up() {
2019 let mut parser = parser();
2022 feed(&mut parser, &["<a2ui-json>[", CREATE]);
2023
2024 let messages = feed(
2026 &mut parser,
2027 &[
2028 r#"{"version":"v0.9","updateComponents":{"surf"#,
2029 r#"aceId":"s1","components":[{"id":"root","component":"Text","text":"hi"}"#,
2030 ],
2031 );
2032 assert_eq!(
2033 messages[0]["updateComponents"]["surfaceId"], "s1",
2034 "the split key must still be found"
2035 );
2036 }
2037
2038 #[test]
2039 fn a_later_surface_does_not_leak_into_an_earlier_one() {
2040 let mut parser = parser();
2041 feed(
2042 &mut parser,
2043 &[
2044 "<a2ui-json>[",
2045 r#"{"version":"v0.9","createSurface":{"surfaceId":"one","catalogId":"test"}},"#,
2046 r#"{"version":"v0.9","createSurface":{"surfaceId":"two","catalogId":"test"}},"#,
2047 ],
2048 );
2049 let messages = feed(
2051 &mut parser,
2052 &[concat!(
2053 r#"{"version":"v0.9","updateComponents":{"surfaceId":"one","components":"#,
2054 r#"[{"id":"root","component":"Text","text":"hi"}"#
2055 )],
2056 );
2057 assert_eq!(messages[0]["updateComponents"]["surfaceId"], "one");
2058 }
2059
2060 #[test]
2061 fn scanning_reads_the_last_pair_and_flags_an_unfinished_one() {
2062 let buffer = r#"{"surfaceId": "first"} {"surfaceId" : "second"}"#;
2063 let (value, incomplete) = scan_string_values(buffer, "surfaceId");
2064 assert_eq!(value, Some("second".to_string()));
2065 assert_eq!(incomplete, None);
2066
2067 assert_eq!(scan_string_values(buffer, "missing"), (None, None));
2068
2069 assert_eq!(scan_string_values(r#"{"root": 3}"#, "root"), (None, None));
2071
2072 let cut = r#"{"surfaceId": "done"}, {"surfaceId": "partia"#;
2075 let (value, incomplete) = scan_string_values(cut, "surfaceId");
2076 assert_eq!(value, Some("done".to_string()));
2077 assert_eq!(incomplete, Some(cut.rfind("\"surfaceId\"").unwrap()));
2078 }
2079}