1pub use vox_schema::*;
8
9use facet::Facet;
10use facet_core::{DeclId, Def, ScalarType, Shape, StructKind, Type, UserType};
11use indexmap::IndexMap;
12use std::collections::{HashMap, HashSet};
13use std::sync::Mutex;
14
15use crate::{MethodId, RequestCall, RequestResponse, is_rx, is_tx};
16
17#[derive(Debug)]
23pub enum SchemaExtractError {
24 UnhandledType { type_desc: String },
26
27 PointerWithoutTypeParams { shape_desc: String },
29
30 UnresolvedTempId { temp_id: CycleSchemaIndex },
32
33 MissingAssignment { context: String },
35}
36
37impl std::fmt::Display for SchemaExtractError {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 Self::UnhandledType { type_desc } => {
41 write!(f, "schema extraction: unhandled type: {type_desc}")
42 }
43 Self::PointerWithoutTypeParams { shape_desc } => {
44 write!(
45 f,
46 "schema extraction: Pointer type without type_params: {shape_desc}"
47 )
48 }
49 Self::UnresolvedTempId { temp_id } => {
50 write!(
51 f,
52 "schema extraction: unresolved temp ID {temp_id:?} during finalization"
53 )
54 }
55 Self::MissingAssignment { context } => {
56 write!(f, "schema extraction: missing DeclId assignment: {context}")
57 }
58 }
59 }
60}
61
62pub trait Schematic {
64 fn direction(&self) -> BindingDirection;
65 fn attach_schemas(&mut self, schemas: CborPayload);
66}
67
68impl<'payload> Schematic for RequestCall<'payload> {
69 fn direction(&self) -> BindingDirection {
70 BindingDirection::Args
71 }
72
73 fn attach_schemas(&mut self, schemas: CborPayload) {
74 self.schemas = schemas;
75 }
76}
77
78impl<'payload> Schematic for RequestResponse<'payload> {
79 fn direction(&self) -> BindingDirection {
80 BindingDirection::Response
81 }
82
83 fn attach_schemas(&mut self, schemas: CborPayload) {
84 self.schemas = schemas;
85 }
86}
87
88impl std::error::Error for SchemaExtractError {}
89
90pub struct SchemaSendTracker {
101 sent_bindings: HashSet<(MethodId, BindingDirection)>,
104
105 sent_schemas: HashSet<SchemaHash>,
107
108 registry: SchemaRegistry,
110}
111
112impl SchemaSendTracker {
113 pub fn new() -> Self {
114 SchemaSendTracker {
115 registry: HashMap::new(),
116 sent_bindings: HashSet::new(),
117 sent_schemas: HashSet::new(),
118 }
119 }
120
121 pub fn reset(&mut self) {
124 self.sent_bindings.clear();
125 self.sent_schemas.clear();
126 }
127
128 pub fn registry(&self) -> &SchemaRegistry {
131 &self.registry
132 }
133
134 pub fn attach_schemas_for_shape_if_needed(
144 &mut self,
145 method_id: MethodId,
146 shape: &'static Shape,
147 schematic: &mut impl Schematic,
148 ) -> Result<CborPayload, SchemaExtractError> {
149 let key = (method_id, schematic.direction());
150
151 if self.sent_bindings.contains(&key) {
153 let empty = CborPayload::default();
154 schematic.attach_schemas(empty.clone());
155 return Ok(empty);
156 }
157
158 let already_sent = self.sent_schemas.clone();
160
161 let extracted = extract_schemas(shape)?;
165
166 for schema in &extracted.schemas {
168 self.registry
169 .entry(schema.id)
170 .or_insert_with(|| schema.clone());
171 }
172
173 let unsent: Vec<Schema> = extracted
175 .schemas
176 .into_iter()
177 .filter(|s| !already_sent.contains(&s.id))
178 .collect();
179
180 for s in &unsent {
182 self.sent_schemas.insert(s.id);
183 }
184
185 let schema_payload = SchemaPayload {
186 schemas: unsent,
187 root: extracted.root,
188 };
189 dlog!(
190 "[schema] send binding: method={:?} direction={:?} root={:?} schema_count={}",
191 method_id,
192 schematic.direction(),
193 schema_payload.root,
194 schema_payload.schemas.len()
195 );
196 let cbor = schema_payload.to_cbor();
197 schematic.attach_schemas(cbor.clone());
198 self.sent_bindings.insert(key);
199 Ok(cbor)
200 }
201
202 pub fn prepare_send(
207 &mut self,
208 method_id: MethodId,
209 direction: BindingDirection,
210 root_type: &TypeRef,
211 source: &dyn SchemaSource,
212 ) -> CborPayload {
213 let key = (method_id, direction);
214 if self.sent_bindings.contains(&key) {
215 return CborPayload::default();
216 }
217
218 let already_sent = self.sent_schemas.clone();
219 let mut all_schemas = Vec::new();
220 let mut visited = HashSet::new();
221 let mut queue = Vec::new();
222 root_type.collect_ids(&mut queue);
223
224 while let Some(id) = queue.pop() {
225 if !visited.insert(id) {
226 continue;
227 }
228 if let Some(schema) = source.get_schema(id) {
229 for child_id in schema_child_ids(&schema.kind) {
230 queue.push(child_id);
231 }
232 all_schemas.push(schema);
233 }
234 }
235
236 for schema in &all_schemas {
237 self.registry
238 .entry(schema.id)
239 .or_insert_with(|| schema.clone());
240 }
241
242 let unsent: Vec<Schema> = all_schemas
243 .into_iter()
244 .filter(|schema| !already_sent.contains(&schema.id))
245 .collect();
246
247 for schema in &unsent {
248 self.sent_schemas.insert(schema.id);
249 }
250
251 let schema_payload = SchemaPayload {
252 schemas: unsent,
253 root: root_type.clone(),
254 };
255 dlog!(
256 "[schema] resend binding: method={:?} direction={:?} root={:?} schema_count={}",
257 method_id,
258 direction,
259 schema_payload.root,
260 schema_payload.schemas.len()
261 );
262 let cbor = schema_payload.to_cbor();
263 self.sent_bindings.insert(key);
264 cbor
265 }
266
267 pub fn extract_schemas(
270 &mut self,
271 shape: &'static Shape,
272 ) -> Result<ExtractedSchemas, SchemaExtractError> {
273 self::extract_schemas(shape)
274 }
275}
276
277impl Default for SchemaSendTracker {
278 fn default() -> Self {
279 Self::new()
280 }
281}
282
283impl std::fmt::Debug for SchemaSendTracker {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 f.debug_struct("SchemaSendTracker").finish_non_exhaustive()
286 }
287}
288
289pub struct SchemaRecvTracker {
301 received: Mutex<HashMap<SchemaHash, Schema>>,
303 received_args_bindings: Mutex<HashMap<MethodId, TypeRef>>,
305 received_response_bindings: Mutex<HashMap<MethodId, TypeRef>>,
307}
308
309#[derive(Debug)]
311pub struct DuplicateSchemaError {
312 pub type_id: SchemaHash,
313}
314
315impl std::fmt::Display for DuplicateSchemaError {
316 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317 write!(
318 f,
319 "duplicate TypeSchemaId {:?} received on same connection — protocol error",
320 self.type_id
321 )
322 }
323}
324
325impl std::error::Error for DuplicateSchemaError {}
326
327impl SchemaRecvTracker {
328 pub fn new() -> Self {
329 SchemaRecvTracker {
330 received: Mutex::new(HashMap::new()),
331 received_args_bindings: Mutex::new(HashMap::new()),
332 received_response_bindings: Mutex::new(HashMap::new()),
333 }
334 }
335
336 pub fn record_received(
341 &self,
342 method_id: MethodId,
343 direction: BindingDirection,
344 payload: SchemaPayload,
345 ) -> Result<(), DuplicateSchemaError> {
346 {
347 let mut received = self.received.lock().unwrap();
348 for schema in &payload.schemas {
349 dlog!("[schema] record_received: id={:?}", schema.id);
350 }
351 for schema in payload.schemas {
352 if let Some(existing) = received.get(&schema.id) {
353 dlog!(
354 "[schema] DUPLICATE: id={:?} existing={:?} new={:?}",
355 schema.id,
356 existing,
357 schema
358 );
359 return Err(DuplicateSchemaError { type_id: schema.id });
360 }
361 received.insert(schema.id, schema);
362 }
363 }
364 let map = match direction {
365 BindingDirection::Args => &self.received_args_bindings,
366 BindingDirection::Response => &self.received_response_bindings,
367 };
368 dlog!(
369 "[schema] record binding: method={:?} direction={:?} root={:?}",
370 method_id,
371 direction,
372 payload.root
373 );
374 map.lock().unwrap().insert(method_id, payload.root);
375 Ok(())
376 }
377
378 pub fn get_remote_args_root(&self, method_id: MethodId) -> Option<TypeRef> {
380 self.received_args_bindings
381 .lock()
382 .unwrap()
383 .get(&method_id)
384 .cloned()
385 }
386
387 pub fn get_remote_response_root(&self, method_id: MethodId) -> Option<TypeRef> {
389 self.received_response_bindings
390 .lock()
391 .unwrap()
392 .get(&method_id)
393 .cloned()
394 }
395
396 pub fn get_received(&self, type_id: &SchemaHash) -> Option<Schema> {
398 self.received.lock().unwrap().get(type_id).cloned()
399 }
400
401 pub fn received_registry(&self) -> SchemaRegistry {
403 self.received.lock().unwrap().clone()
404 }
405}
406
407impl Default for SchemaRecvTracker {
408 fn default() -> Self {
409 Self::new()
410 }
411}
412
413impl SchemaSource for SchemaRecvTracker {
414 fn get_schema(&self, id: SchemaHash) -> Option<Schema> {
415 self.get_received(&id)
416 }
417}
418
419impl std::fmt::Debug for SchemaRecvTracker {
420 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421 f.debug_struct("SchemaRecvTracker").finish_non_exhaustive()
422 }
423}
424
425pub struct ExtractedSchemas {
427 pub schemas: Vec<Schema>,
429
430 pub root: TypeRef,
432}
433
434pub fn extract_schemas(shape: &'static Shape) -> Result<ExtractedSchemas, SchemaExtractError> {
437 let mut ctx = ExtractCtx {
438 next_id: CycleSchemaIndex::first(),
439 schemas: IndexMap::new(),
440 assigned: HashMap::new(),
441 seen: HashSet::new(),
442 };
443 let root_mixed_ref = ctx.extract(shape)?;
444 let schemas: Vec<MixedSchema> = ctx.schemas.into_values().collect();
445 let (finalized, temp_to_final) = finalize_content_hashes(schemas)?;
446
447 let resolve = |mid: MixedId| -> SchemaHash {
448 match mid {
449 MixedId::Final(tid) => tid,
450 MixedId::Temp(t) => temp_to_final.get(&t).copied().unwrap_or(SchemaHash(0)),
451 }
452 };
453 let root_type_ref = root_mixed_ref.map(resolve);
454
455 Ok(ExtractedSchemas {
456 schemas: finalized,
457 root: root_type_ref,
458 })
459}
460
461fn resolve_mixed(id: MixedId, temp_to_final: &HashMap<CycleSchemaIndex, SchemaHash>) -> SchemaHash {
470 match id {
471 MixedId::Final(tid) => tid,
472 MixedId::Temp(t) => temp_to_final.get(&t).copied().unwrap_or(SchemaHash(0)),
473 }
474}
475
476#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
477enum ExtractKey {
478 Decl(DeclId),
479 AnonymousTupleArity(usize),
480}
481
482fn finalize_content_hashes(
493 schemas: Vec<MixedSchema>,
494) -> Result<(Vec<Schema>, HashMap<CycleSchemaIndex, SchemaHash>), SchemaExtractError> {
495 let temp_to_idx: HashMap<CycleSchemaIndex, usize> = schemas
497 .iter()
498 .enumerate()
499 .filter_map(|(i, s)| match s.id {
500 MixedId::Temp(t) => Some((t, i)),
501 MixedId::Final(_) => None,
502 })
503 .collect();
504
505 fn collect_refs(kind: &MixedSchemaKind) -> Vec<MixedId> {
506 let mut refs = Vec::new();
507 kind.for_each_type_ref(&mut |tr: &TypeRef<MixedId>| tr.collect_ids(&mut refs));
508 refs
509 }
510
511 let n = schemas.len();
513 let mut in_recursive_group: Vec<bool> = vec![false; n];
514
515 for (i, schema) in schemas.iter().enumerate() {
516 if matches!(schema.id, MixedId::Final(_)) {
517 continue; }
519 for r in collect_refs(&schema.kind) {
520 if let MixedId::Temp(t) = r
521 && let Some(&ref_idx) = temp_to_idx.get(&t)
522 && ref_idx >= i
523 {
524 in_recursive_group[i] = true;
525 in_recursive_group[ref_idx] = true;
526 }
527 }
528 }
529
530 let mut temp_to_final: HashMap<CycleSchemaIndex, SchemaHash> = HashMap::new();
532
533 for (i, schema) in schemas.iter().enumerate() {
535 if in_recursive_group[i] {
536 continue;
537 }
538 if let MixedId::Temp(temp) = schema.id {
539 let final_id = compute_content_hash(&schema.kind, &schema.type_params, &|mid| {
540 resolve_mixed(mid, &temp_to_final)
541 });
542 temp_to_final.insert(temp, final_id);
543 }
544 }
545
546 let mut i = 0;
548 while i < n {
549 if !in_recursive_group[i] {
550 i += 1;
551 continue;
552 }
553
554 let group_start = i;
555 while i < n && in_recursive_group[i] {
556 i += 1;
557 }
558 let group_end = i;
559
560 let group_temp_ids: HashSet<CycleSchemaIndex> = schemas[group_start..group_end]
562 .iter()
563 .filter_map(|s| match s.id {
564 MixedId::Temp(t) => Some(t),
565 _ => None,
566 })
567 .collect();
568
569 let mut prelim_hashes: Vec<SchemaHash> = Vec::new();
571 for schema in &schemas[group_start..group_end] {
572 let prelim =
573 compute_content_hash(&schema.kind, &schema.type_params, &|mid| match mid {
574 MixedId::Final(tid) => tid,
575 MixedId::Temp(t) => {
576 if group_temp_ids.contains(&t) {
577 SchemaHash(0) } else {
579 temp_to_final.get(&t).copied().unwrap_or(SchemaHash(0))
580 }
581 }
582 });
583 prelim_hashes.push(prelim);
584 }
585
586 let mut order: Vec<usize> = (0..prelim_hashes.len()).collect();
588 order.sort_by_key(|&i| prelim_hashes[i].0);
589
590 let mut group_hasher = blake3::Hasher::new();
592 for &idx in &order {
593 group_hasher.update(&prelim_hashes[idx].0.to_le_bytes());
594 }
595 let gh = group_hasher.finalize();
596 let group_hash = u64::from_le_bytes(gh.as_bytes()[0..8].try_into().unwrap());
597
598 for (position, &idx) in order.iter().enumerate() {
599 let mut fh = blake3::Hasher::new();
600 fh.update(&group_hash.to_le_bytes());
601 fh.update(&(position as u64).to_le_bytes());
602 let fo = fh.finalize();
603 let final_hash =
604 SchemaHash(u64::from_le_bytes(fo.as_bytes()[0..8].try_into().unwrap()));
605
606 if let MixedId::Temp(t) = schemas[group_start + idx].id {
607 temp_to_final.insert(t, final_hash);
608 }
609 }
610 }
611
612 let resolve = |mid: MixedId| -> Result<SchemaHash, SchemaExtractError> {
614 match mid {
615 MixedId::Final(tid) => Ok(tid),
616 MixedId::Temp(t) => temp_to_final
617 .get(&t)
618 .copied()
619 .ok_or(SchemaExtractError::UnresolvedTempId { temp_id: t }),
620 }
621 };
622
623 let mut resolve_type_ref =
624 |type_ref: TypeRef<MixedId>| -> Result<TypeRef<SchemaHash>, SchemaExtractError> {
625 type_ref.try_map(&resolve)
626 };
627
628 let mut seen_ids = HashSet::new();
629 let finalized: Vec<Schema> = schemas
630 .into_iter()
631 .map(|s| {
632 let type_id = resolve(s.id)?;
633 Ok(Schema {
634 id: type_id,
635 type_params: s.type_params,
636 kind: s.kind.try_map_type_refs(&mut resolve_type_ref)?,
637 })
638 })
639 .collect::<Result<Vec<_>, _>>()?
640 .into_iter()
641 .filter(|s| seen_ids.insert(s.id))
642 .collect();
643
644 Ok((finalized, temp_to_final))
645}
646
647struct ExtractCtx {
648 next_id: CycleSchemaIndex,
650 schemas: IndexMap<ExtractKey, MixedSchema>,
653 assigned: HashMap<ExtractKey, MixedId>,
656 seen: HashSet<&'static Shape>,
659}
660
661impl ExtractCtx {
662 fn id_for_key(&mut self, key: ExtractKey) -> MixedId {
664 if let Some(&id) = self.assigned.get(&key) {
665 return id;
666 }
667 let id = MixedId::Temp(self.next_id.next());
668 self.assigned.insert(key, id);
669 id
670 }
671
672 fn emit_schema(&mut self, key: ExtractKey, schema: MixedSchema) {
674 self.schemas.entry(key).or_insert(schema);
675 }
676
677 fn key_for_shape(&self, shape: &'static Shape) -> ExtractKey {
678 match anonymous_tuple_arity(shape) {
679 Some(arity) => ExtractKey::AnonymousTupleArity(arity),
680 None => ExtractKey::Decl(shape.decl_id),
681 }
682 }
683
684 fn type_ref_for_shape(
687 &mut self,
688 shape: &'static Shape,
689 param_map: &[(&'static Shape, TypeParamName)],
690 ) -> Result<TypeRef<MixedId>, SchemaExtractError> {
691 if let Some((_, name)) = param_map
692 .iter()
693 .find(|(param_shape, _)| shape.is_shape(param_shape))
694 {
695 self.extract(shape)?;
698 Ok(TypeRef::Var { name: name.clone() })
699 } else {
700 self.extract(shape)
701 }
702 }
703
704 fn extract(&mut self, shape: &'static Shape) -> Result<TypeRef<MixedId>, SchemaExtractError> {
707 if is_tx(shape) || is_rx(shape) {
709 let direction = if is_tx(shape) {
710 ChannelDirection::Tx
711 } else {
712 ChannelDirection::Rx
713 };
714 if let Some(inner) = shape.type_params.first() {
715 let elem_ref = self.extract(inner.shape)?;
716 let key = self.key_for_shape(shape);
717 let id = self.id_for_key(key);
718 let type_params = vec![TypeParamName("T".to_string())];
721 self.emit_schema(
722 key,
723 MixedSchema {
724 id,
725 type_params,
726 kind: SchemaKind::Channel {
727 direction,
728 element: TypeRef::Var {
729 name: TypeParamName("T".to_string()),
730 },
731 },
732 },
733 );
734 self.seen.insert(shape);
735 return Ok(TypeRef::Concrete {
736 type_id: id,
737 args: vec![elem_ref],
738 });
739 }
740 }
741
742 if shape.is_transparent()
744 && let Some(inner) = shape.inner
745 {
746 return self.extract(inner);
747 }
748
749 if let Def::Pointer(ptr_def) = shape.def
752 && let Some(pointee) = ptr_def.pointee
753 {
754 return self.extract(pointee);
755 }
756
757 let key = self.key_for_shape(shape);
758 let id = self.id_for_key(key);
759
760 if !self.seen.insert(shape) {
764 let args = self.extract_instantiation_args(shape)?;
767 return Ok(if args.is_empty() {
768 TypeRef::concrete(id)
769 } else {
770 TypeRef::generic(id, args)
771 });
772 }
773
774 let already_emitted = self.schemas.contains_key(&key);
777 if already_emitted {
778 let args = self.extract_instantiation_args(shape)?;
779 return Ok(if args.is_empty() {
780 TypeRef::concrete(id)
781 } else {
782 TypeRef::generic(id, args)
783 });
784 }
785
786 let param_map: Vec<(&'static Shape, TypeParamName)> = shape
789 .type_params
790 .iter()
791 .map(|tp| (tp.shape, TypeParamName(tp.name.to_string())))
792 .collect();
793 let type_param_names: Vec<TypeParamName> = shape
794 .type_params
795 .iter()
796 .map(|tp| TypeParamName(tp.name.to_string()))
797 .collect();
798
799 if let Some(scalar) = shape.scalar_type() {
802 self.emit_schema(
803 key,
804 MixedSchema {
805 id,
806 type_params: vec![],
807 kind: SchemaKind::Primitive {
808 primitive_type: scalar_to_primitive(scalar),
809 },
810 },
811 );
812 return Ok(TypeRef::concrete(id));
813 }
814
815 match shape.def {
818 Def::List(list_def) => {
819 if let Some(ScalarType::U8) = list_def.t().scalar_type() {
820 self.emit_schema(
821 key,
822 MixedSchema {
823 id,
824 type_params: vec![],
825 kind: SchemaKind::Primitive {
826 primitive_type: PrimitiveType::Bytes,
827 },
828 },
829 );
830 return Ok(TypeRef::concrete(id));
831 }
832 let elem_ref = self.type_ref_for_shape(list_def.t(), ¶m_map)?;
833 let args = self.extract_type_args(shape)?;
834 self.emit_schema(
835 key,
836 MixedSchema {
837 id,
838 type_params: type_param_names,
839 kind: SchemaKind::List { element: elem_ref },
840 },
841 );
842 return Ok(if args.is_empty() {
843 TypeRef::concrete(id)
844 } else {
845 TypeRef::generic(id, args)
846 });
847 }
848 Def::Array(array_def) => {
849 let elem_ref = self.type_ref_for_shape(array_def.t(), ¶m_map)?;
850 let args = self.extract_type_args(shape)?;
851 self.emit_schema(
852 key,
853 MixedSchema {
854 id,
855 type_params: type_param_names,
856 kind: SchemaKind::Array {
857 element: elem_ref,
858 length: array_def.n as u64,
859 },
860 },
861 );
862 return Ok(if args.is_empty() {
863 TypeRef::concrete(id)
864 } else {
865 TypeRef::generic(id, args)
866 });
867 }
868 Def::Slice(slice_def) => {
869 if let Some(ScalarType::U8) = slice_def.t().scalar_type() {
870 self.emit_schema(
871 key,
872 MixedSchema {
873 id,
874 type_params: vec![],
875 kind: SchemaKind::Primitive {
876 primitive_type: PrimitiveType::Bytes,
877 },
878 },
879 );
880 return Ok(TypeRef::concrete(id));
881 }
882 let elem_ref = self.type_ref_for_shape(slice_def.t(), ¶m_map)?;
883 let args = self.extract_type_args(shape)?;
884 self.emit_schema(
885 key,
886 MixedSchema {
887 id,
888 type_params: type_param_names,
889 kind: SchemaKind::List { element: elem_ref },
890 },
891 );
892 return Ok(if args.is_empty() {
893 TypeRef::concrete(id)
894 } else {
895 TypeRef::generic(id, args)
896 });
897 }
898 Def::Map(map_def) => {
899 let key_ref = self.type_ref_for_shape(map_def.k(), ¶m_map)?;
900 let val_ref = self.type_ref_for_shape(map_def.v(), ¶m_map)?;
901 let args = self.extract_type_args(shape)?;
902 self.emit_schema(
903 key,
904 MixedSchema {
905 id,
906 type_params: type_param_names,
907 kind: SchemaKind::Map {
908 key: key_ref,
909 value: val_ref,
910 },
911 },
912 );
913 return Ok(if args.is_empty() {
914 TypeRef::concrete(id)
915 } else {
916 TypeRef::generic(id, args)
917 });
918 }
919 Def::Set(set_def) => {
920 let elem_ref = self.type_ref_for_shape(set_def.t(), ¶m_map)?;
921 let args = self.extract_type_args(shape)?;
922 self.emit_schema(
923 key,
924 MixedSchema {
925 id,
926 type_params: type_param_names,
927 kind: SchemaKind::List { element: elem_ref },
928 },
929 );
930 return Ok(if args.is_empty() {
931 TypeRef::concrete(id)
932 } else {
933 TypeRef::generic(id, args)
934 });
935 }
936 Def::Option(opt_def) => {
937 let elem_ref = self.type_ref_for_shape(opt_def.t(), ¶m_map)?;
938 let args = self.extract_type_args(shape)?;
939 self.emit_schema(
940 key,
941 MixedSchema {
942 id,
943 type_params: type_param_names,
944 kind: SchemaKind::Option { element: elem_ref },
945 },
946 );
947 return Ok(if args.is_empty() {
948 TypeRef::concrete(id)
949 } else {
950 TypeRef::generic(id, args)
951 });
952 }
953 Def::Result(result_def) => {
954 let ok_ref = self.type_ref_for_shape(result_def.t(), ¶m_map)?;
955 let err_ref = self.type_ref_for_shape(result_def.e(), ¶m_map)?;
956 let args = self.extract_type_args(shape)?;
957 self.emit_schema(
958 key,
959 MixedSchema {
960 id,
961 type_params: type_param_names,
962 kind: SchemaKind::Enum {
963 name: shape.type_identifier.to_string(),
964 variants: vec![
965 VariantSchema {
966 name: "Ok".to_string(),
967 index: 0,
968 payload: VariantPayload::Newtype { type_ref: ok_ref },
969 },
970 VariantSchema {
971 name: "Err".to_string(),
972 index: 1,
973 payload: VariantPayload::Newtype { type_ref: err_ref },
974 },
975 ],
976 },
977 },
978 );
979 return Ok(if args.is_empty() {
980 TypeRef::concrete(id)
981 } else {
982 TypeRef::generic(id, args)
983 });
984 }
985 _ => {}
986 }
987
988 let kind = match shape.ty {
990 Type::User(UserType::Struct(struct_type)) => match struct_type.kind {
993 StructKind::Unit => {
994 let primitive_type = if is_infallible_shape(shape) {
995 PrimitiveType::Never
996 } else {
997 PrimitiveType::Unit
998 };
999 SchemaKind::Primitive { primitive_type }
1000 }
1001 StructKind::TupleStruct | StructKind::Tuple => {
1002 if let Some(arity) = anonymous_tuple_arity(shape) {
1003 let args = self.extract_instantiation_args(shape)?;
1004 let type_params = tuple_type_params(arity);
1005 let elements = type_params
1006 .iter()
1007 .cloned()
1008 .map(|name| TypeRef::Var { name })
1009 .collect();
1010 self.emit_schema(
1011 key,
1012 MixedSchema {
1013 id,
1014 type_params,
1015 kind: SchemaKind::Tuple { elements },
1016 },
1017 );
1018 return Ok(TypeRef::generic(id, args));
1019 }
1020 let mut elements = Vec::with_capacity(struct_type.fields.len());
1021 for f in struct_type.fields {
1022 elements.push(self.type_ref_for_shape(f.shape(), ¶m_map)?);
1023 }
1024 SchemaKind::Tuple { elements }
1025 }
1026 StructKind::Struct => {
1027 let mut fields = Vec::with_capacity(struct_type.fields.len());
1028 for f in struct_type.fields {
1029 fields.push(FieldSchema {
1030 name: f.name.to_string(),
1031 type_ref: self.type_ref_for_shape(f.shape(), ¶m_map)?,
1032 required: f.default.is_none(),
1033 });
1034 }
1035 SchemaKind::Struct {
1036 name: shape.type_identifier.to_string(),
1037 fields,
1038 }
1039 }
1040 },
1041 Type::User(UserType::Enum(enum_type)) => {
1043 let mut variants = Vec::with_capacity(enum_type.variants.len());
1044 for (i, v) in enum_type.variants.iter().enumerate() {
1045 let payload = match v.data.kind {
1046 StructKind::Unit => VariantPayload::Unit,
1047 StructKind::TupleStruct | StructKind::Tuple => {
1048 if v.data.fields.len() == 1 {
1049 VariantPayload::Newtype {
1050 type_ref: self
1051 .type_ref_for_shape(v.data.fields[0].shape(), ¶m_map)?,
1052 }
1053 } else {
1054 let mut types = Vec::with_capacity(v.data.fields.len());
1055 for f in v.data.fields {
1056 types.push(self.type_ref_for_shape(f.shape(), ¶m_map)?);
1057 }
1058 VariantPayload::Tuple { types }
1059 }
1060 }
1061 StructKind::Struct => {
1062 let mut fields = Vec::with_capacity(v.data.fields.len());
1063 for f in v.data.fields {
1064 fields.push(FieldSchema {
1065 name: f.name.to_string(),
1066 type_ref: self.type_ref_for_shape(f.shape(), ¶m_map)?,
1067 required: true,
1068 });
1069 }
1070 VariantPayload::Struct { fields }
1071 }
1072 };
1073 variants.push(VariantSchema {
1074 name: v.name.to_string(),
1075 index: i as u32,
1076 payload,
1077 });
1078 }
1079 SchemaKind::Enum {
1080 name: shape.type_identifier.to_string(),
1081 variants,
1082 }
1083 }
1084 Type::User(UserType::Opaque) => SchemaKind::Primitive {
1085 primitive_type: PrimitiveType::Payload,
1086 },
1087 other => {
1088 return Err(SchemaExtractError::UnhandledType {
1089 type_desc: format!("{other:?} for shape {shape} (def={:?})", shape.def),
1090 });
1091 }
1092 };
1093
1094 let args = self.extract_type_args(shape)?;
1095 self.emit_schema(
1096 key,
1097 MixedSchema {
1098 id,
1099 type_params: type_param_names,
1100 kind,
1101 },
1102 );
1103
1104 Ok(if args.is_empty() {
1105 TypeRef::concrete(id)
1106 } else {
1107 TypeRef::generic(id, args)
1108 })
1109 }
1110
1111 fn extract_type_args(
1115 &mut self,
1116 shape: &'static Shape,
1117 ) -> Result<Vec<TypeRef<MixedId>>, SchemaExtractError> {
1118 if shape.type_params.is_empty() {
1119 return Ok(vec![]);
1120 }
1121 let mut args = Vec::with_capacity(shape.type_params.len());
1122 for tp in shape.type_params {
1123 args.push(self.extract(tp.shape)?);
1124 }
1125 Ok(args)
1126 }
1127
1128 fn extract_instantiation_args(
1134 &mut self,
1135 shape: &'static Shape,
1136 ) -> Result<Vec<TypeRef<MixedId>>, SchemaExtractError> {
1137 if anonymous_tuple_arity(shape).is_some()
1138 && let Type::User(UserType::Struct(struct_type)) = shape.ty
1139 {
1140 let mut args = Vec::with_capacity(struct_type.fields.len());
1141 for field in struct_type.fields {
1142 args.push(self.extract(field.shape())?);
1143 }
1144 return Ok(args);
1145 }
1146 self.extract_type_args(shape)
1147 }
1148}
1149
1150fn anonymous_tuple_arity(shape: &'static Shape) -> Option<usize> {
1151 match shape.ty {
1152 Type::User(UserType::Struct(struct_type))
1153 if struct_type.kind == StructKind::Tuple && shape.type_identifier.starts_with('(') =>
1154 {
1155 Some(struct_type.fields.len())
1156 }
1157 _ => None,
1158 }
1159}
1160
1161fn tuple_type_params(arity: usize) -> Vec<TypeParamName> {
1162 (0..arity)
1163 .map(|index| TypeParamName(format!("T{index}")))
1164 .collect()
1165}
1166
1167fn is_infallible_shape(shape: &'static Shape) -> bool {
1168 shape.is_shape(<std::convert::Infallible as Facet<'static>>::SHAPE)
1169}
1170
1171fn scalar_to_primitive(scalar: ScalarType) -> PrimitiveType {
1172 match scalar {
1173 ScalarType::Unit => PrimitiveType::Unit,
1174 ScalarType::Bool => PrimitiveType::Bool,
1175 ScalarType::Char => PrimitiveType::Char,
1176 ScalarType::Str | ScalarType::String | ScalarType::CowStr => PrimitiveType::String,
1177 ScalarType::F32 => PrimitiveType::F32,
1178 ScalarType::F64 => PrimitiveType::F64,
1179 ScalarType::U8 => PrimitiveType::U8,
1180 ScalarType::U16 => PrimitiveType::U16,
1181 ScalarType::U32 => PrimitiveType::U32,
1182 ScalarType::U64 => PrimitiveType::U64,
1183 ScalarType::U128 => PrimitiveType::U128,
1184 ScalarType::USize => PrimitiveType::U64,
1185 ScalarType::I8 => PrimitiveType::I8,
1186 ScalarType::I16 => PrimitiveType::I16,
1187 ScalarType::I32 => PrimitiveType::I32,
1188 ScalarType::I64 => PrimitiveType::I64,
1189 ScalarType::I128 => PrimitiveType::I128,
1190 ScalarType::ISize => PrimitiveType::I64,
1191 ScalarType::ConstTypeId => PrimitiveType::U64,
1192 _ => PrimitiveType::Unit,
1193 }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199 use facet::Facet;
1200
1201 struct TestSchematic {
1202 direction: BindingDirection,
1203 shape: &'static Shape,
1204 attached: CborPayload,
1205 }
1206
1207 impl TestSchematic {
1208 fn new(direction: BindingDirection, shape: &'static Shape) -> Self {
1209 Self {
1210 direction,
1211 shape,
1212 attached: CborPayload::default(),
1213 }
1214 }
1215 }
1216
1217 impl Schematic for TestSchematic {
1218 fn direction(&self) -> BindingDirection {
1219 self.direction
1220 }
1221
1222 fn attach_schemas(&mut self, schemas: CborPayload) {
1223 self.attached = schemas;
1224 }
1225 }
1226
1227 #[test]
1229 fn type_ids_are_u64_content_hashes() {
1230 let id = SchemaHash(42);
1231 assert_eq!(id.0, 42);
1232 assert_eq!(id, SchemaHash(42));
1233 assert_ne!(id, SchemaHash(43));
1234 }
1235
1236 #[test]
1239 fn cbor_round_trip() {
1240 let schema = Schema {
1241 id: SchemaHash(1),
1242 type_params: vec![],
1243 kind: SchemaKind::Primitive {
1244 primitive_type: PrimitiveType::U32,
1245 },
1246 };
1247 let bytes = SchemaPayload {
1248 schemas: vec![schema.clone()],
1249 root: TypeRef::concrete(schema.id),
1250 }
1251 .to_cbor();
1252 let payload = SchemaPayload::from_cbor(&bytes.0).expect("should parse CBOR");
1253 assert_eq!(payload.schemas.len(), 1);
1254 assert_eq!(payload.schemas[0].id, schema.id);
1255 assert_eq!(payload.root, TypeRef::concrete(schema.id));
1256 }
1257
1258 #[test]
1260 fn primitive_u32() {
1261 let schemas = extract_schemas(<u32 as Facet>::SHAPE).unwrap().schemas;
1262 assert_eq!(schemas.len(), 1);
1263 assert!(matches!(
1264 schemas[0].kind,
1265 SchemaKind::Primitive {
1266 primitive_type: PrimitiveType::U32
1267 }
1268 ));
1269 }
1270
1271 #[test]
1272 fn primitive_string() {
1273 let schemas = extract_schemas(<String as Facet>::SHAPE).unwrap().schemas;
1274 assert_eq!(schemas.len(), 1);
1275 assert!(matches!(
1276 schemas[0].kind,
1277 SchemaKind::Primitive {
1278 primitive_type: PrimitiveType::String
1279 }
1280 ));
1281 }
1282
1283 #[test]
1284 fn primitive_bool() {
1285 let schemas = extract_schemas(<bool as Facet>::SHAPE).unwrap().schemas;
1286 assert_eq!(schemas.len(), 1);
1287 assert!(matches!(
1288 schemas[0].kind,
1289 SchemaKind::Primitive {
1290 primitive_type: PrimitiveType::Bool
1291 }
1292 ));
1293 }
1294
1295 #[test]
1297 fn simple_struct() {
1298 #[derive(Facet)]
1299 struct Point {
1300 x: f64,
1301 y: f64,
1302 }
1303
1304 let schemas = extract_schemas(Point::SHAPE).unwrap().schemas;
1305 assert!(schemas.len() >= 2);
1306
1307 let point_schema = schemas.last().unwrap();
1308 match &point_schema.kind {
1309 SchemaKind::Struct { name, fields } => {
1310 assert!(
1311 name.contains("Point"),
1312 "expected name to contain Point, got {name}"
1313 );
1314 assert_eq!(fields.len(), 2);
1315 assert_eq!(fields[0].name, "x");
1316 assert_eq!(fields[1].name, "y");
1317 assert!(fields[0].required);
1318 assert_eq!(fields[0].type_ref, fields[1].type_ref);
1319 }
1320 other => panic!("expected Struct, got {other:?}"),
1321 }
1322 }
1323
1324 #[test]
1326 fn simple_enum() {
1327 #[derive(Facet)]
1328 #[repr(u8)]
1329 enum Color {
1330 Red,
1331 Green,
1332 Blue,
1333 }
1334
1335 let schemas = extract_schemas(Color::SHAPE).unwrap().schemas;
1336 let color_schema = schemas.last().unwrap();
1337 match &color_schema.kind {
1338 SchemaKind::Enum { variants, .. } => {
1339 assert_eq!(variants.len(), 3);
1340 assert_eq!(variants[0].name, "Red");
1341 assert_eq!(variants[1].name, "Green");
1342 assert_eq!(variants[2].name, "Blue");
1343 assert!(matches!(variants[0].payload, VariantPayload::Unit));
1344 }
1345 other => panic!("expected Enum, got {other:?}"),
1346 }
1347 }
1348
1349 #[test]
1351 fn enum_with_payloads() {
1352 #[derive(Facet)]
1353 #[repr(u8)]
1354 #[allow(dead_code)]
1355 enum Shape {
1356 Circle(f64),
1357 Rect { w: f64, h: f64 },
1358 Empty,
1359 }
1360
1361 let schemas = extract_schemas(Shape::SHAPE).unwrap().schemas;
1362 let shape_schema = schemas.last().unwrap();
1363 match &shape_schema.kind {
1364 SchemaKind::Enum { variants, .. } => {
1365 assert_eq!(variants.len(), 3);
1366 assert!(matches!(
1367 variants[0].payload,
1368 VariantPayload::Newtype { .. }
1369 ));
1370 match &variants[1].payload {
1371 VariantPayload::Struct { fields } => {
1372 assert_eq!(fields.len(), 2);
1373 assert_eq!(fields[0].name, "w");
1374 assert_eq!(fields[1].name, "h");
1375 }
1376 other => panic!("expected Struct variant, got {other:?}"),
1377 }
1378 assert!(matches!(variants[2].payload, VariantPayload::Unit));
1379 }
1380 other => panic!("expected Enum, got {other:?}"),
1381 }
1382 }
1383
1384 #[test]
1386 fn container_vec() {
1387 let schemas = extract_schemas(<Vec<u32> as Facet>::SHAPE).unwrap().schemas;
1388 assert_eq!(schemas.len(), 2);
1389 assert!(matches!(
1390 schemas[0].kind,
1391 SchemaKind::Primitive {
1392 primitive_type: PrimitiveType::U32
1393 }
1394 ));
1395 assert!(matches!(schemas[1].kind, SchemaKind::List { .. }));
1396 }
1397
1398 #[test]
1400 fn container_option() {
1401 let schemas = extract_schemas(<Option<String> as Facet>::SHAPE)
1402 .unwrap()
1403 .schemas;
1404 assert_eq!(schemas.len(), 2);
1405 assert!(matches!(
1406 schemas[0].kind,
1407 SchemaKind::Primitive {
1408 primitive_type: PrimitiveType::String
1409 }
1410 ));
1411 assert!(matches!(schemas[1].kind, SchemaKind::Option { .. }));
1412 }
1413
1414 #[test]
1416 fn recursive_type_terminates() {
1417 #[derive(Facet)]
1418 struct Node {
1419 value: u32,
1420 next: Option<Box<Node>>,
1421 }
1422
1423 let schemas = extract_schemas(Node::SHAPE).unwrap().schemas;
1424 assert!(schemas.len() >= 2);
1425
1426 let node_schema = schemas.last().unwrap();
1427 assert!(matches!(node_schema.kind, SchemaKind::Struct { .. }));
1428 }
1429
1430 #[test]
1432 fn vec_u8_is_bytes() {
1433 let schemas = extract_schemas(<Vec<u8> as Facet>::SHAPE).unwrap().schemas;
1434 assert_eq!(schemas.len(), 1);
1435 assert!(matches!(
1436 schemas[0].kind,
1437 SchemaKind::Primitive {
1438 primitive_type: PrimitiveType::Bytes
1439 }
1440 ));
1441 }
1442
1443 #[test]
1444 fn slice_u8_is_bytes() {
1445 let schemas = extract_schemas(<&[u8] as Facet>::SHAPE).unwrap().schemas;
1446 assert_eq!(schemas.len(), 1);
1447 assert!(matches!(
1448 schemas[0].kind,
1449 SchemaKind::Primitive {
1450 primitive_type: PrimitiveType::Bytes
1451 }
1452 ));
1453 }
1454
1455 #[test]
1456 fn cbor_payload_is_bytes() {
1457 let schemas = extract_schemas(CborPayload::SHAPE).unwrap().schemas;
1458 assert_eq!(schemas.len(), 1);
1459 assert!(matches!(
1460 schemas[0].kind,
1461 SchemaKind::Primitive {
1462 primitive_type: PrimitiveType::Bytes
1463 }
1464 ));
1465 }
1466
1467 #[test]
1469 fn opaque_payload_is_payload_primitive() {
1470 let schemas = extract_schemas(crate::Payload::<'static>::SHAPE)
1471 .unwrap()
1472 .schemas;
1473 assert_eq!(schemas.len(), 1);
1474 assert!(matches!(
1475 schemas[0].kind,
1476 SchemaKind::Primitive {
1477 primitive_type: PrimitiveType::Payload
1478 }
1479 ));
1480 }
1481
1482 #[test]
1483 fn infallible_is_never_primitive() {
1484 let schemas = extract_schemas(<std::convert::Infallible as Facet>::SHAPE)
1485 .unwrap()
1486 .schemas;
1487 assert_eq!(schemas.len(), 1);
1488 assert!(matches!(
1489 schemas[0].kind,
1490 SchemaKind::Primitive {
1491 primitive_type: PrimitiveType::Never
1492 }
1493 ));
1494 }
1495
1496 #[test]
1498 fn deduplication_two_u32_fields() {
1499 #[derive(Facet)]
1500 struct TwoU32 {
1501 a: u32,
1502 b: u32,
1503 }
1504
1505 let schemas = extract_schemas(TwoU32::SHAPE).unwrap().schemas;
1506 let u32_count = schemas
1507 .iter()
1508 .filter(|s| {
1509 matches!(
1510 s.kind,
1511 SchemaKind::Primitive {
1512 primitive_type: PrimitiveType::U32
1513 }
1514 )
1515 })
1516 .count();
1517 assert_eq!(u32_count, 1, "u32 schema should appear exactly once");
1518 assert_eq!(schemas.len(), 2);
1519 }
1520
1521 #[test]
1523 fn container_map() {
1524 let schemas = extract_schemas(<std::collections::HashMap<String, u32> as Facet>::SHAPE)
1525 .unwrap()
1526 .schemas;
1527 let map_schema = schemas.last().unwrap();
1528 assert!(matches!(map_schema.kind, SchemaKind::Map { .. }));
1529 }
1530
1531 #[test]
1533 fn container_array() {
1534 let schemas = extract_schemas(<[u32; 4] as Facet>::SHAPE).unwrap().schemas;
1535 let arr_schema = schemas.last().unwrap();
1536 match &arr_schema.kind {
1537 SchemaKind::Array { length, .. } => assert_eq!(*length, 4),
1538 other => panic!("expected Array, got {other:?}"),
1539 }
1540 }
1541
1542 #[test]
1544 fn tuple_type() {
1545 let schemas = extract_schemas(<(u32, String) as Facet>::SHAPE)
1546 .unwrap()
1547 .schemas;
1548 let tuple_schema = schemas.last().unwrap();
1549 match &tuple_schema.kind {
1550 SchemaKind::Tuple { elements } => {
1551 assert_eq!(elements.len(), 2);
1552 assert_ne!(elements[0], elements[1]);
1553 }
1554 other => panic!("expected Tuple, got {other:?}"),
1555 }
1556 }
1557
1558 #[test]
1560 fn extract_schemas_returns_all_kinds() {
1561 #[derive(Facet)]
1562 struct Mixed {
1563 count: u32,
1564 tags: Vec<String>,
1565 pair: (u8, u8),
1566 }
1567
1568 let schemas = extract_schemas(Mixed::SHAPE).unwrap().schemas;
1569 assert!(schemas.len() >= 4);
1570 }
1571
1572 #[test]
1575 fn tracker_prepare_send_returns_payload_then_empty() {
1576 let mut tracker = SchemaSendTracker::new();
1577 let method = MethodId(1);
1578 let mut schematic = TestSchematic::new(BindingDirection::Args, <u32 as Facet>::SHAPE);
1579 let first = tracker
1580 .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1581 .unwrap();
1582 assert!(
1583 !first.is_empty(),
1584 "first prepare_send should return payload"
1585 );
1586 assert_eq!(schematic.attached.0, first.0);
1587 let second = tracker
1588 .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1589 .unwrap();
1590 assert!(
1591 second.is_empty(),
1592 "second prepare_send for same method should return empty"
1593 );
1594 assert!(schematic.attached.is_empty());
1595 }
1596
1597 #[test]
1600 fn tracker_prepare_send_includes_transitive_deps() {
1601 #[derive(Facet)]
1602 struct Outer {
1603 inner: u32,
1604 name: String,
1605 }
1606
1607 let mut tracker = SchemaSendTracker::new();
1608 let method = MethodId(1);
1609 let mut schematic = TestSchematic::new(BindingDirection::Args, Outer::SHAPE);
1610 let first = tracker
1611 .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1612 .unwrap();
1613 assert!(!first.is_empty(), "should return schemas");
1614 let parsed = SchemaPayload::from_cbor(&first.0).expect("should parse CBOR");
1615 assert!(
1616 parsed.schemas.len() >= 3,
1617 "should include transitive deps, got {}",
1618 parsed.schemas.len()
1619 );
1620
1621 schematic.shape = <u32 as Facet>::SHAPE;
1623 let again = tracker
1624 .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1625 .unwrap();
1626 assert!(
1627 again.is_empty(),
1628 "u32 was already sent as transitive dep, method already bound"
1629 );
1630 }
1631
1632 #[test]
1634 fn tracker_record_and_get_received() {
1635 let tracker = SchemaRecvTracker::new();
1636 let schemas = extract_schemas(<u32 as Facet>::SHAPE).unwrap().schemas;
1637 let id = schemas[0].id;
1638 assert!(tracker.get_received(&id).is_none());
1639 tracker
1640 .record_received(
1641 MethodId(7),
1642 BindingDirection::Args,
1643 SchemaPayload {
1644 schemas,
1645 root: TypeRef::concrete(id),
1646 },
1647 )
1648 .expect("first record should succeed");
1649 assert!(tracker.get_received(&id).is_some());
1650 assert_eq!(
1651 tracker.get_remote_args_root(MethodId(7)),
1652 Some(TypeRef::concrete(id))
1653 );
1654 }
1655
1656 #[test]
1659 fn type_ids_are_content_hashes() {
1660 let mut tracker = SchemaSendTracker::new();
1661 let extracted = tracker
1662 .extract_schemas(<(u32, String) as Facet>::SHAPE)
1663 .unwrap();
1664 let schemas = extracted.schemas;
1665 assert!(schemas.len() >= 3);
1666
1667 let mut tracker2 = SchemaSendTracker::new();
1669 let schemas2 = tracker2
1670 .extract_schemas(<(u32, String) as Facet>::SHAPE)
1671 .unwrap()
1672 .schemas;
1673 assert_eq!(schemas.len(), schemas2.len());
1674 for (a, b) in schemas.iter().zip(schemas2.iter()) {
1675 assert_eq!(a.id, b.id, "content hash should be deterministic");
1676 }
1677
1678 let mut tracker3 = SchemaSendTracker::new();
1680 let extracted3 = tracker3
1681 .extract_schemas(<(u64, String) as Facet>::SHAPE)
1682 .unwrap();
1683 assert_ne!(
1684 extracted.root, extracted3.root,
1685 "different types should produce different root refs"
1686 );
1687 }
1688
1689 #[test]
1691 fn primitive_content_hashes_are_stable() {
1692 let primitives = [
1695 PrimitiveType::Bool,
1696 PrimitiveType::U8,
1697 PrimitiveType::U16,
1698 PrimitiveType::U32,
1699 PrimitiveType::U64,
1700 PrimitiveType::U128,
1701 PrimitiveType::I8,
1702 PrimitiveType::I16,
1703 PrimitiveType::I32,
1704 PrimitiveType::I64,
1705 PrimitiveType::I128,
1706 PrimitiveType::F32,
1707 PrimitiveType::F64,
1708 PrimitiveType::Char,
1709 PrimitiveType::String,
1710 PrimitiveType::Unit,
1711 PrimitiveType::Never,
1712 PrimitiveType::Bytes,
1713 PrimitiveType::Payload,
1714 ];
1715
1716 let hashes: Vec<SchemaHash> = primitives
1718 .iter()
1719 .map(|p| {
1720 compute_content_hash(&SchemaKind::Primitive { primitive_type: *p }, &[], &|id| id)
1721 })
1722 .collect();
1723 let unique: HashSet<SchemaHash> = hashes.iter().copied().collect();
1724 assert_eq!(
1725 unique.len(),
1726 hashes.len(),
1727 "all primitive hashes must be unique"
1728 );
1729
1730 for (i, p) in primitives.iter().enumerate() {
1732 let hash2 =
1733 compute_content_hash(&SchemaKind::Primitive { primitive_type: *p }, &[], &|id| id);
1734 assert_eq!(hashes[i], hash2, "hash for {:?} must be deterministic", p);
1735 }
1736 }
1737
1738 #[test]
1740 fn struct_hash_is_deterministic() {
1741 #[derive(Facet)]
1742 struct Point {
1743 x: f64,
1744 y: f64,
1745 }
1746
1747 let schemas1 = extract_schemas(Point::SHAPE).unwrap().schemas;
1748 let schemas2 = extract_schemas(Point::SHAPE).unwrap().schemas;
1749 assert_eq!(
1750 schemas1.last().unwrap().id,
1751 schemas2.last().unwrap().id,
1752 "same struct must produce the same content hash"
1753 );
1754 }
1755
1756 #[test]
1758 fn recursive_type_hash_is_deterministic() {
1759 #[derive(Facet)]
1760 struct TreeNode {
1761 label: String,
1762 children: Vec<TreeNode>,
1763 }
1764
1765 let schemas1 = extract_schemas(TreeNode::SHAPE).unwrap().schemas;
1766 let schemas2 = extract_schemas(TreeNode::SHAPE).unwrap().schemas;
1767
1768 assert!(schemas1.len() >= 2);
1770
1771 let root1 = schemas1.last().unwrap().id;
1773 let root2 = schemas2.last().unwrap().id;
1774 assert_eq!(root1, root2, "recursive type hash must be deterministic");
1775
1776 for s in &schemas1 {
1778 assert_ne!(s.id.0, 0, "content hash must not be zero");
1779 }
1780 }
1781
1782 #[test]
1783 fn bidirectional_bindings_are_independent() {
1784 let mut tracker = SchemaSendTracker::new();
1785 let method = MethodId(1);
1786
1787 let mut args_schematic = TestSchematic::new(BindingDirection::Args, <u32 as Facet>::SHAPE);
1789 let args = tracker
1790 .attach_schemas_for_shape_if_needed(method, args_schematic.shape, &mut args_schematic)
1791 .unwrap();
1792 assert!(!args.is_empty(), "should send args");
1793 let args_parsed = SchemaPayload::from_cbor(&args.0).expect("parse args CBOR");
1794
1795 let mut response_schematic =
1797 TestSchematic::new(BindingDirection::Response, <String as Facet>::SHAPE);
1798 let response = tracker
1799 .attach_schemas_for_shape_if_needed(
1800 method,
1801 response_schematic.shape,
1802 &mut response_schematic,
1803 )
1804 .unwrap();
1805 assert!(!response.is_empty(), "should send response");
1806 let response_parsed = SchemaPayload::from_cbor(&response.0).expect("parse response CBOR");
1807 assert_ne!(args_parsed.root, response_parsed.root);
1808
1809 let recv_tracker = SchemaRecvTracker::new();
1811 recv_tracker
1812 .record_received(
1813 MethodId(42),
1814 BindingDirection::Args,
1815 SchemaPayload {
1816 schemas: extract_schemas(<u64 as Facet>::SHAPE).unwrap().schemas,
1817 root: TypeRef::concrete(SchemaHash(100)),
1818 },
1819 )
1820 .expect("record should succeed");
1821 recv_tracker
1822 .record_received(
1823 MethodId(42),
1824 BindingDirection::Response,
1825 SchemaPayload {
1826 schemas: vec![],
1827 root: TypeRef::concrete(SchemaHash(200)),
1828 },
1829 )
1830 .expect("record should succeed");
1831
1832 assert_eq!(
1833 recv_tracker.get_remote_args_root(MethodId(42)),
1834 Some(TypeRef::concrete(SchemaHash(100)))
1835 );
1836 assert_eq!(
1837 recv_tracker.get_remote_response_root(MethodId(42)),
1838 Some(TypeRef::concrete(SchemaHash(200)))
1839 );
1840 }
1841
1842 #[test]
1843 fn duplicate_schema_is_protocol_error() {
1844 let tracker = SchemaRecvTracker::new();
1845 let schemas = extract_schemas(<u32 as Facet>::SHAPE).unwrap().schemas;
1846 tracker
1847 .record_received(
1848 MethodId(9),
1849 BindingDirection::Args,
1850 SchemaPayload {
1851 schemas: schemas.clone(),
1852 root: TypeRef::concrete(schemas[0].id),
1853 },
1854 )
1855 .expect("first record should succeed");
1856 let err = tracker
1857 .record_received(
1858 MethodId(9),
1859 BindingDirection::Args,
1860 SchemaPayload {
1861 schemas: schemas.clone(),
1862 root: TypeRef::concrete(schemas[0].id),
1863 },
1864 )
1865 .expect_err("duplicate should fail");
1866 assert_eq!(err.type_id, schemas[0].id);
1867 }
1868
1869 #[test]
1870 fn send_tracker_reset_clears_all_state() {
1871 let mut tracker = SchemaSendTracker::new();
1872 let method = MethodId(1);
1873 let mut schematic = TestSchematic::new(BindingDirection::Args, <u32 as Facet>::SHAPE);
1874 let first = tracker
1875 .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1876 .unwrap();
1877 assert!(!first.is_empty(), "first should return payload");
1878
1879 tracker.reset();
1880
1881 let after_reset = tracker
1882 .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1883 .unwrap();
1884 assert!(
1885 !after_reset.is_empty(),
1886 "after reset, prepare_send should return payload again"
1887 );
1888 }
1889
1890 #[test]
1895 fn generic_vec_uses_var_in_body() {
1896 let schemas = extract_schemas(<Vec<u32> as Facet>::SHAPE).unwrap().schemas;
1897 let list_schema = schemas
1898 .iter()
1899 .find(|s| matches!(s.kind, SchemaKind::List { .. }))
1900 .unwrap();
1901 assert_eq!(
1902 list_schema.type_params.len(),
1903 1,
1904 "Vec should have 1 type param"
1905 );
1906 match &list_schema.kind {
1907 SchemaKind::List { element } => {
1908 assert!(
1909 matches!(element, TypeRef::Var { .. }),
1910 "element should be Var, got {element:?}"
1911 );
1912 }
1913 other => panic!("expected List, got {other:?}"),
1914 }
1915 }
1916
1917 #[test]
1918 fn generic_option_uses_var_in_body() {
1919 let schemas = extract_schemas(<Option<String> as Facet>::SHAPE)
1920 .unwrap()
1921 .schemas;
1922 let opt_schema = schemas
1923 .iter()
1924 .find(|s| matches!(s.kind, SchemaKind::Option { .. }))
1925 .unwrap();
1926 assert_eq!(
1927 opt_schema.type_params.len(),
1928 1,
1929 "Option should have 1 type param"
1930 );
1931 match &opt_schema.kind {
1932 SchemaKind::Option { element } => {
1933 assert!(
1934 matches!(element, TypeRef::Var { .. }),
1935 "element should be Var, got {element:?}"
1936 );
1937 }
1938 other => panic!("expected Option, got {other:?}"),
1939 }
1940 }
1941
1942 #[test]
1943 fn generic_tuple_uses_vars_in_body() {
1944 let schemas = extract_schemas(<(u32, String) as Facet>::SHAPE)
1945 .unwrap()
1946 .schemas;
1947 let tuple_schema = schemas
1948 .iter()
1949 .find(|s| matches!(s.kind, SchemaKind::Tuple { .. }))
1950 .unwrap();
1951 assert_eq!(
1952 tuple_schema.type_params.len(),
1953 2,
1954 "tuple arity 2 should have 2 type params"
1955 );
1956 match &tuple_schema.kind {
1957 SchemaKind::Tuple { elements } => {
1958 assert_eq!(elements.len(), 2);
1959 assert!(matches!(elements[0], TypeRef::Var { .. }));
1960 assert!(matches!(elements[1], TypeRef::Var { .. }));
1961 }
1962 other => panic!("expected Tuple, got {other:?}"),
1963 }
1964 }
1965
1966 #[test]
1967 fn generic_vox_error_uses_var_in_user_payload() {
1968 use crate::VoxError;
1969
1970 let schemas = extract_schemas(<VoxError<::core::convert::Infallible> as Facet>::SHAPE)
1971 .unwrap()
1972 .schemas;
1973 let vox_error_schema = schemas
1974 .iter()
1975 .find(|s| matches!(&s.kind, SchemaKind::Enum { name, .. } if name == "VoxError"))
1976 .expect("VoxError schema should be present");
1977 match &vox_error_schema.kind {
1978 SchemaKind::Enum { variants, .. } => {
1979 let user = variants
1980 .iter()
1981 .find(|variant| variant.name == "User")
1982 .expect("VoxError should have User variant");
1983 let VariantPayload::Newtype { type_ref } = &user.payload else {
1984 panic!("User variant should be newtype");
1985 };
1986 assert!(
1987 matches!(type_ref, TypeRef::Var { .. }),
1988 "User payload should be a type variable, got {type_ref:?}"
1989 );
1990 }
1991 other => panic!("expected enum, got {other:?}"),
1992 }
1993 }
1994
1995 #[test]
1996 fn vec_of_option_of_u32_deduplicates() {
1997 let schemas = extract_schemas(<Vec<Option<u32>> as Facet>::SHAPE)
2000 .unwrap()
2001 .schemas;
2002
2003 let list_count = schemas
2004 .iter()
2005 .filter(|s| matches!(s.kind, SchemaKind::List { .. }))
2006 .count();
2007 let option_count = schemas
2008 .iter()
2009 .filter(|s| matches!(s.kind, SchemaKind::Option { .. }))
2010 .count();
2011 assert_eq!(list_count, 1, "should have exactly 1 List schema");
2012 assert_eq!(option_count, 1, "should have exactly 1 Option schema");
2013 }
2014
2015 #[test]
2016 fn vec_u32_and_vec_string_share_one_list_schema() {
2017 #[derive(Facet)]
2018 struct Both {
2019 a: Vec<u32>,
2020 b: Vec<String>,
2021 }
2022
2023 let schemas = extract_schemas(Both::SHAPE).unwrap().schemas;
2024 let list_count = schemas
2025 .iter()
2026 .filter(|s| matches!(s.kind, SchemaKind::List { .. }))
2027 .count();
2028 assert_eq!(
2029 list_count, 1,
2030 "Vec<u32> and Vec<String> should share one List schema"
2031 );
2032 }
2033
2034 #[test]
2035 fn resolve_kind_substitutes_vars() {
2036 let schemas = extract_schemas(<Vec<u32> as Facet>::SHAPE).unwrap().schemas;
2037 let registry = build_registry(&schemas);
2038
2039 let root = schemas.last().unwrap();
2041 assert!(matches!(root.kind, SchemaKind::List { .. }));
2042
2043 let u32_schema = schemas
2045 .iter()
2046 .find(|s| {
2047 matches!(
2048 s.kind,
2049 SchemaKind::Primitive {
2050 primitive_type: PrimitiveType::U32
2051 }
2052 )
2053 })
2054 .unwrap();
2055 let type_ref = TypeRef::generic(root.id, vec![TypeRef::concrete(u32_schema.id)]);
2056
2057 let resolved = type_ref.resolve_kind(®istry).expect("should resolve");
2059 match &resolved {
2060 SchemaKind::List { element } => match element {
2061 TypeRef::Concrete { type_id, args } => {
2062 assert_eq!(*type_id, u32_schema.id);
2063 assert!(args.is_empty());
2064 }
2065 other => panic!("expected concrete after resolution, got {other:?}"),
2066 },
2067 other => panic!("expected List, got {other:?}"),
2068 }
2069 }
2070
2071 #[test]
2072 fn extract_result_tuple_root_preserves_ok_tuple() {
2073 use crate::VoxError;
2074
2075 let extracted = extract_schemas(
2076 <Result<(String, i32), VoxError<::core::convert::Infallible>> as Facet>::SHAPE,
2077 )
2078 .unwrap();
2079 let registry = build_registry(&extracted.schemas);
2080 let root = extracted
2081 .root
2082 .resolve_kind(®istry)
2083 .expect("result root should resolve");
2084
2085 let SchemaKind::Enum { variants, .. } = root else {
2086 panic!("expected Result enum root");
2087 };
2088 let ok_variant = variants
2089 .iter()
2090 .find(|variant| variant.name == "Ok")
2091 .expect("Result should have Ok variant");
2092 let VariantPayload::Newtype { type_ref } = &ok_variant.payload else {
2093 panic!("Ok variant should be newtype");
2094 };
2095 let ok_kind = type_ref
2096 .resolve_kind(®istry)
2097 .expect("Ok payload should resolve");
2098 match ok_kind {
2099 SchemaKind::Tuple { elements } => {
2100 assert_eq!(elements.len(), 2, "Ok tuple should have two elements");
2101 }
2102 other => panic!("expected Ok payload to be tuple, got {other:?}"),
2103 }
2104 }
2105
2106 #[test]
2107 fn result_ok_tuple_uses_generic_tuple_schema() {
2108 use crate::VoxError;
2109
2110 let result_shape =
2111 <Result<(String, i32), VoxError<::core::convert::Infallible>> as Facet>::SHAPE;
2112 let ok_shape = result_shape.type_params[0].shape;
2113 let extracted = extract_schemas(
2114 <Result<(String, i32), VoxError<::core::convert::Infallible>> as Facet>::SHAPE,
2115 )
2116 .unwrap();
2117 let TypeRef::Concrete { args, .. } = &extracted.root else {
2118 panic!("Result root should be concrete");
2119 };
2120 assert_eq!(
2121 args.len(),
2122 2,
2123 "Result root should have Ok and Err type args"
2124 );
2125 let TypeRef::Concrete { args: ok_args, .. } = &args[0] else {
2126 panic!("Ok type arg should be concrete tuple ref");
2127 };
2128 assert_eq!(
2129 ok_args.len(),
2130 2,
2131 "Ok tuple ref should carry concrete tuple element args; root={:?}; ok_shape={}; ok_shape_ty={:?}",
2132 extracted.root,
2133 ok_shape.type_identifier,
2134 ok_shape.ty
2135 );
2136 }
2137
2138 #[test]
2139 fn unary_tuple_root_preserves_nested_tuple() {
2140 let extracted = extract_schemas(<((i32, String),) as Facet>::SHAPE).unwrap();
2141 let registry = build_registry(&extracted.schemas);
2142
2143 let root = extracted
2144 .root
2145 .resolve_kind(®istry)
2146 .expect("root should resolve");
2147 let SchemaKind::Tuple { elements } = root else {
2148 panic!("expected unary tuple root");
2149 };
2150 assert_eq!(elements.len(), 1, "outer tuple should remain unary");
2151
2152 let inner = elements[0]
2153 .resolve_kind(®istry)
2154 .expect("inner tuple should resolve");
2155 match inner {
2156 SchemaKind::Tuple { elements } => {
2157 assert_eq!(elements.len(), 2, "inner tuple should remain binary");
2158 }
2159 other => panic!("expected inner tuple, got {other:?}"),
2160 }
2161
2162 let tuple_count = extracted
2163 .schemas
2164 .iter()
2165 .filter(|schema| matches!(schema.kind, SchemaKind::Tuple { .. }))
2166 .count();
2167 assert_eq!(tuple_count, 2, "should emit one tuple schema per arity");
2168 }
2169
2170 #[test]
2171 fn nested_generic_vec_of_vec_of_u32() {
2172 let schemas = extract_schemas(<Vec<Vec<u32>> as Facet>::SHAPE)
2174 .unwrap()
2175 .schemas;
2176 let list_count = schemas
2177 .iter()
2178 .filter(|s| matches!(s.kind, SchemaKind::List { .. }))
2179 .count();
2180 assert_eq!(
2181 list_count, 1,
2182 "Vec<Vec<u32>> should have exactly 1 List schema (Vec<T>)"
2183 );
2184 }
2185
2186 #[test]
2187 fn recursive_type_with_option_box() {
2188 #[derive(Facet)]
2189 struct Node {
2190 value: u32,
2191 next: Option<Box<Node>>,
2192 }
2193
2194 let schemas = extract_schemas(Node::SHAPE).unwrap().schemas;
2195 let option_count = schemas
2197 .iter()
2198 .filter(|s| matches!(s.kind, SchemaKind::Option { .. }))
2199 .count();
2200 assert_eq!(option_count, 1, "should have exactly 1 Option schema");
2201
2202 let opt_schema = schemas
2204 .iter()
2205 .find(|s| matches!(s.kind, SchemaKind::Option { .. }))
2206 .unwrap();
2207 match &opt_schema.kind {
2208 SchemaKind::Option { element } => {
2209 assert!(
2210 matches!(element, TypeRef::Var { .. }),
2211 "element should be Var"
2212 );
2213 }
2214 _ => unreachable!(),
2215 }
2216
2217 for s in &schemas {
2219 assert_ne!(s.id.0, 0, "content hash must not be zero: {:?}", s.kind);
2220 }
2221 }
2222
2223 #[test]
2224 fn map_schema_is_generic() {
2225 let schemas = extract_schemas(<std::collections::HashMap<String, u32> as Facet>::SHAPE)
2226 .unwrap()
2227 .schemas;
2228 let map_schema = schemas
2229 .iter()
2230 .find(|s| matches!(s.kind, SchemaKind::Map { .. }))
2231 .unwrap();
2232 assert_eq!(
2233 map_schema.type_params.len(),
2234 2,
2235 "HashMap should have 2 type params"
2236 );
2237 match &map_schema.kind {
2238 SchemaKind::Map { key, value } => {
2239 assert!(matches!(key, TypeRef::Var { .. }), "key should be Var");
2240 assert!(matches!(value, TypeRef::Var { .. }), "value should be Var");
2241 }
2242 _ => unreachable!(),
2243 }
2244 }
2245
2246 #[test]
2247 fn schema_payload_cbor_round_trip() {
2248 let payload = SchemaPayload {
2249 schemas: vec![],
2250 root: TypeRef::Concrete {
2251 type_id: SchemaHash(123),
2252 args: vec![TypeRef::concrete(SchemaHash(456))],
2253 },
2254 };
2255 let bytes = payload.to_cbor();
2256 let parsed = SchemaPayload::from_cbor(&bytes.0).expect("should parse CBOR");
2257 match &parsed.root {
2258 TypeRef::Concrete { type_id, args } => {
2259 assert_eq!(*type_id, SchemaHash(123));
2260 assert_eq!(args.len(), 1);
2261 match &args[0] {
2262 TypeRef::Concrete { type_id, args } => {
2263 assert_eq!(*type_id, SchemaHash(456));
2264 assert!(args.is_empty());
2265 }
2266 other => panic!("expected concrete arg, got {other:?}"),
2267 }
2268 }
2269 other => panic!("expected concrete root, got {other:?}"),
2270 }
2271 }
2272}