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