Skip to main content

vox_postcard/
plan.rs

1use std::collections::HashMap;
2
3use facet_core::{Shape, Type, UserType};
4use vox_schema::{
5    FieldSchema, PrimitiveType, Schema, SchemaHash, SchemaKind, SchemaRegistry, TypeRef,
6    VariantPayload, VariantSchema,
7};
8
9use crate::error::{PathSegment, SchemaSide, TranslationError, TranslationErrorKind};
10
11/// A precomputed plan for deserializing postcard bytes into a local type.
12///
13/// This is a recursive enum that mirrors the shape of data. Each variant
14/// carries sub-plans for its children, so translation works through
15/// arbitrarily nested containers (Vec, Option, Map, etc.).
16#[derive(Debug)]
17pub enum TranslationPlan {
18    /// Identity — no translation needed. Used for leaves (scalars, etc.)
19    /// and for container elements when remote and local types match.
20    Identity,
21
22    /// Struct or tuple-struct: field reordering, skipping unknown, filling defaults.
23    Struct {
24        /// One op per remote field, in remote wire order.
25        field_ops: Vec<FieldOp>,
26        /// Nested plans for fields that need translation, keyed by local field index.
27        nested: HashMap<usize, TranslationPlan>,
28    },
29
30    /// Enum: variant remapping + per-variant field plans.
31    Enum {
32        /// For each remote variant index, the local variant index (or None if unknown).
33        variant_map: Vec<Option<usize>>,
34        /// Per-variant field plans, keyed by remote variant index.
35        variant_plans: HashMap<usize, TranslationPlan>,
36        /// Nested plans for newtype/tuple variant inner types, keyed by local variant index.
37        nested: HashMap<usize, TranslationPlan>,
38    },
39
40    /// Tuple: positional elements with nested plans.
41    Tuple {
42        field_ops: Vec<FieldOp>,
43        nested: HashMap<usize, TranslationPlan>,
44    },
45
46    /// List/Vec/Slice: element type needs translation.
47    List { element: Box<TranslationPlan> },
48
49    /// Option: inner type needs translation.
50    Option { inner: Box<TranslationPlan> },
51
52    /// Map: key and/or value types need translation.
53    Map {
54        key: Box<TranslationPlan>,
55        value: Box<TranslationPlan>,
56    },
57
58    /// Fixed-size array: element type needs translation.
59    Array { element: Box<TranslationPlan> },
60
61    /// Pointer (Box, Arc, etc.): pointee needs translation.
62    Pointer { pointee: Box<TranslationPlan> },
63}
64
65#[derive(Debug)]
66pub enum FieldOp {
67    /// Read this remote field into local field at `local_index`.
68    Read { local_index: usize },
69    /// Skip this remote field (not present in local type).
70    Skip { type_ref: TypeRef },
71}
72
73/// A schema set: the root schema (with Vars resolved) + the registry.
74#[derive(Debug)]
75pub struct SchemaSet {
76    pub root: Schema,
77    pub registry: SchemaRegistry,
78}
79
80impl SchemaSet {
81    /// Build a SchemaSet from a raw list of schemas (e.g. received from the wire).
82    /// The root is the last schema. Its kind is used as-is (no Var resolution).
83    pub fn from_schemas(schemas: Vec<Schema>) -> Self {
84        let root = schemas.last().cloned().expect("empty schema list");
85        let registry = vox_schema::build_registry(&schemas);
86        SchemaSet { root, registry }
87    }
88
89    /// Build a SchemaSet from extracted root/schemas data.
90    /// The root TypeRef is used to resolve any Var references in the root schema.
91    pub fn from_root_and_schemas(root: TypeRef, schemas: Vec<Schema>) -> Self {
92        let registry = vox_schema::build_registry(&schemas);
93        let root_kind = root
94            .resolve_kind(&registry)
95            .expect("root schema must be in registry");
96        let root_id = match &root {
97            TypeRef::Concrete { type_id, .. } => *type_id,
98            TypeRef::Var { .. } => unreachable!("root type ref is never a Var"),
99        };
100        let root = Schema {
101            id: root_id,
102            type_params: vec![],
103            kind: root_kind,
104        };
105        SchemaSet { root, registry }
106    }
107}
108
109/// Input to `build_plan`: identifies which side is remote and which is local.
110pub struct PlanInput<'a> {
111    pub remote: &'a SchemaSet,
112    pub local: &'a SchemaSet,
113}
114
115/// Build the trivial identity plan from a local Shape alone.
116/// Every field maps 1:1, no skips, no defaults. Used when no remote schema
117/// is available (same types on both sides).
118pub fn build_identity_plan(shape: &'static Shape) -> TranslationPlan {
119    match shape.ty {
120        Type::User(UserType::Struct(struct_type)) => {
121            let field_ops = (0..struct_type.fields.len())
122                .map(|i| FieldOp::Read { local_index: i })
123                .collect();
124            TranslationPlan::Struct {
125                field_ops,
126                nested: HashMap::new(),
127            }
128        }
129        Type::User(UserType::Enum(enum_type)) => {
130            let variant_map = (0..enum_type.variants.len()).map(Some).collect();
131            let mut variant_plans = HashMap::new();
132            for (i, variant) in enum_type.variants.iter().enumerate() {
133                let field_ops = (0..variant.data.fields.len())
134                    .map(|j| FieldOp::Read { local_index: j })
135                    .collect();
136                variant_plans.insert(
137                    i,
138                    TranslationPlan::Struct {
139                        field_ops,
140                        nested: HashMap::new(),
141                    },
142                );
143            }
144            TranslationPlan::Enum {
145                variant_map,
146                variant_plans,
147                nested: HashMap::new(),
148            }
149        }
150        _ => TranslationPlan::Identity,
151    }
152}
153
154/// Build a translation plan by comparing remote and local schemas.
155///
156/// Both sides are represented as schemas — the same extraction logic
157/// (channel unwrapping, transparent wrappers, etc.) has already run on
158/// both. This avoids mismatches between schema representation and raw
159/// Shape inspection.
160// r[impl schema.translation.field-matching]
161// r[impl schema.translation.skip-unknown]
162// r[impl schema.translation.fill-defaults]
163// r[impl schema.translation.reorder]
164// r[impl schema.errors.early-detection]
165pub fn build_plan(input: &PlanInput) -> Result<TranslationPlan, TranslationError> {
166    let remote = &input.remote.root;
167    let local = &input.local.root;
168
169    // Validate type names match for nominal types (struct/enum).
170    if let (Some(remote_name), Some(local_name)) = (remote.name(), local.name())
171        && remote_name != local_name
172    {
173        return Err(TranslationError::new(TranslationErrorKind::NameMismatch {
174            remote: remote.clone(),
175            local: local.clone(),
176            remote_rust: crate::error::format_schema_rust(remote, &input.remote.registry),
177            local_rust: crate::error::format_schema_rust(local, &input.local.registry),
178        }));
179    }
180
181    if is_byte_buffer_kind(&remote.kind, &input.remote.registry)
182        && is_byte_buffer_kind(&local.kind, &input.local.registry)
183    {
184        return Ok(TranslationPlan::Identity);
185    }
186
187    match (&remote.kind, &local.kind) {
188        (
189            SchemaKind::Struct {
190                fields: remote_fields,
191                ..
192            },
193            SchemaKind::Struct {
194                fields: local_fields,
195                ..
196            },
197        ) => build_struct_plan(remote_fields, local_fields, remote, local, input),
198        (
199            SchemaKind::Enum {
200                variants: remote_variants,
201                ..
202            },
203            SchemaKind::Enum {
204                variants: local_variants,
205                ..
206            },
207        ) => build_enum_plan(remote_variants, local_variants, remote, local, input),
208        (
209            SchemaKind::Tuple {
210                elements: remote_elements,
211            },
212            SchemaKind::Tuple {
213                elements: local_elements,
214            },
215        ) => build_tuple_plan(remote_elements, local_elements, remote, local, input),
216        // Container types — recurse into element/value types
217        (
218            SchemaKind::List {
219                element: remote_elem,
220            },
221            SchemaKind::List {
222                element: local_elem,
223            },
224        ) => {
225            let element_plan = nested_plan(remote_elem, local_elem, input)?;
226            Ok(TranslationPlan::List {
227                element: Box::new(element_plan.unwrap_or(TranslationPlan::Identity)),
228            })
229        }
230        (
231            SchemaKind::Option {
232                element: remote_elem,
233            },
234            SchemaKind::Option {
235                element: local_elem,
236            },
237        ) => {
238            let inner_plan = nested_plan(remote_elem, local_elem, input)?;
239            Ok(TranslationPlan::Option {
240                inner: Box::new(inner_plan.unwrap_or(TranslationPlan::Identity)),
241            })
242        }
243        (
244            SchemaKind::Map {
245                key: remote_key,
246                value: remote_val,
247            },
248            SchemaKind::Map {
249                key: local_key,
250                value: local_val,
251            },
252        ) => {
253            let key_plan = nested_plan(remote_key, local_key, input)?;
254            let val_plan = nested_plan(remote_val, local_val, input)?;
255            Ok(TranslationPlan::Map {
256                key: Box::new(key_plan.unwrap_or(TranslationPlan::Identity)),
257                value: Box::new(val_plan.unwrap_or(TranslationPlan::Identity)),
258            })
259        }
260        (
261            SchemaKind::Array {
262                element: remote_elem,
263                ..
264            },
265            SchemaKind::Array {
266                element: local_elem,
267                ..
268            },
269        ) => {
270            let element_plan = nested_plan(remote_elem, local_elem, input)?;
271            Ok(TranslationPlan::Array {
272                element: Box::new(element_plan.unwrap_or(TranslationPlan::Identity)),
273            })
274        }
275        (
276            SchemaKind::Channel {
277                direction: remote_direction,
278                ..
279            },
280            SchemaKind::Channel {
281                direction: local_direction,
282                ..
283            },
284        ) if remote_direction == local_direction => Ok(TranslationPlan::Identity),
285        (
286            SchemaKind::Primitive {
287                primitive_type: remote_primitive,
288            },
289            SchemaKind::Primitive {
290                primitive_type: local_primitive,
291            },
292        ) if remote_primitive == local_primitive => Ok(TranslationPlan::Identity),
293        // Kind mismatch
294        _ => Err(TranslationError::new(TranslationErrorKind::KindMismatch {
295            remote: remote.clone(),
296            local: local.clone(),
297            remote_rust: crate::error::format_schema_rust(remote, &input.remote.registry),
298            local_rust: crate::error::format_schema_rust(local, &input.local.registry),
299        })),
300    }
301}
302
303/// Build a nested plan for two TypeRefs looked up in their respective registries.
304/// Handles generic types by resolving Var references using the TypeRef's args.
305fn nested_plan(
306    remote_type_ref: &TypeRef,
307    local_type_ref: &TypeRef,
308    input: &PlanInput,
309) -> Result<Option<TranslationPlan>, TranslationError> {
310    let resolve_schema = |type_ref: &TypeRef, registry: &SchemaRegistry, side: SchemaSide| {
311        let type_id = match type_ref {
312            TypeRef::Concrete { type_id, .. } => *type_id,
313            TypeRef::Var { name } => {
314                return Err(TranslationError::new(TranslationErrorKind::UnresolvedVar {
315                    name: format!("{name:?}"),
316                    side,
317                }));
318            }
319        };
320        let kind = type_ref.resolve_kind(registry).ok_or_else(|| {
321            TranslationError::new(TranslationErrorKind::SchemaNotFound { type_id, side })
322        })?;
323        let base = registry.get(&type_id).ok_or_else(|| {
324            TranslationError::new(TranslationErrorKind::SchemaNotFound { type_id, side })
325        })?;
326        Ok(Schema {
327            id: base.id,
328            type_params: vec![],
329            kind,
330        })
331    };
332
333    let remote_schema =
334        resolve_schema(remote_type_ref, &input.remote.registry, SchemaSide::Remote)?;
335    let local_schema = resolve_schema(local_type_ref, &input.local.registry, SchemaSide::Local)?;
336
337    let sub_input = PlanInput {
338        remote: &SchemaSet {
339            root: remote_schema,
340            registry: input.remote.registry.clone(),
341        },
342        local: &SchemaSet {
343            root: local_schema,
344            registry: input.local.registry.clone(),
345        },
346    };
347    build_plan(&sub_input).map(Some)
348}
349
350fn is_byte_buffer_kind(kind: &SchemaKind, registry: &SchemaRegistry) -> bool {
351    match kind {
352        SchemaKind::Primitive {
353            primitive_type: PrimitiveType::Bytes,
354        } => true,
355        SchemaKind::List { element } => matches!(
356            element.resolve_kind(registry),
357            Some(SchemaKind::Primitive {
358                primitive_type: PrimitiveType::U8,
359            })
360        ),
361        _ => false,
362    }
363}
364
365fn build_struct_plan(
366    remote_fields: &[FieldSchema],
367    local_fields: &[FieldSchema],
368    remote_schema: &Schema,
369    _local_schema: &Schema,
370    input: &PlanInput,
371) -> Result<TranslationPlan, TranslationError> {
372    let mut field_ops = Vec::with_capacity(remote_fields.len());
373    let mut nested = HashMap::new();
374    let mut matched_local = vec![false; local_fields.len()];
375
376    for remote_field in remote_fields {
377        if let Some((local_idx, local_field)) = local_fields
378            .iter()
379            .enumerate()
380            .find(|(_, f)| f.name == remote_field.name)
381        {
382            matched_local[local_idx] = true;
383            field_ops.push(FieldOp::Read {
384                local_index: local_idx,
385            });
386
387            // r[impl schema.translation.type-compat]
388            let nested_plan = nested_plan(&remote_field.type_ref, &local_field.type_ref, input)
389                .map_err(|e| e.with_path_prefix(PathSegment::Field(remote_field.name.clone())))?;
390            if let Some(plan) = nested_plan {
391                nested.insert(local_idx, plan);
392            }
393        } else {
394            field_ops.push(FieldOp::Skip {
395                type_ref: remote_field.type_ref.clone(),
396            });
397        }
398    }
399
400    // r[impl schema.errors.missing-required]
401    for (i, matched) in matched_local.iter().enumerate() {
402        if !matched && local_fields[i].required {
403            return Err(TranslationError::new(
404                TranslationErrorKind::MissingRequiredField {
405                    field: local_fields[i].clone(),
406                    remote_struct: remote_schema.clone(),
407                },
408            ));
409        }
410    }
411
412    Ok(TranslationPlan::Struct { field_ops, nested })
413}
414
415fn build_tuple_plan(
416    remote_elements: &[TypeRef<SchemaHash>],
417    local_elements: &[TypeRef<SchemaHash>],
418    remote_schema: &Schema,
419    local_schema: &Schema,
420    input: &PlanInput,
421) -> Result<TranslationPlan, TranslationError> {
422    if remote_elements.len() != local_elements.len() {
423        return Err(TranslationError::new(
424            TranslationErrorKind::TupleLengthMismatch {
425                remote: remote_schema.clone(),
426                local: local_schema.clone(),
427                remote_rust: crate::error::format_schema_rust(
428                    remote_schema,
429                    &input.remote.registry,
430                ),
431                local_rust: crate::error::format_schema_rust(local_schema, &input.local.registry),
432                remote_len: remote_elements.len(),
433                local_len: local_elements.len(),
434            },
435        ));
436    }
437
438    let mut field_ops = Vec::with_capacity(remote_elements.len());
439    let mut nested = HashMap::new();
440
441    for (i, (remote_elem, local_elem)) in remote_elements
442        .iter()
443        .zip(local_elements.iter())
444        .enumerate()
445    {
446        field_ops.push(FieldOp::Read { local_index: i });
447
448        let nested_plan = nested_plan(remote_elem, local_elem, input)
449            .map_err(|e| e.with_path_prefix(PathSegment::Index(i)))?;
450        if let Some(plan) = nested_plan {
451            nested.insert(i, plan);
452        }
453    }
454
455    Ok(TranslationPlan::Tuple { field_ops, nested })
456}
457
458// r[impl schema.translation.enum]
459// r[impl schema.translation.enum.missing-variant]
460// r[impl schema.translation.enum.payload-compat]
461fn build_enum_plan(
462    remote_variants: &[VariantSchema],
463    local_variants: &[VariantSchema],
464    _remote_schema: &Schema,
465    _local_schema: &Schema,
466    input: &PlanInput,
467) -> Result<TranslationPlan, TranslationError> {
468    let mut variant_map = Vec::with_capacity(remote_variants.len());
469    let mut variant_plans = HashMap::new();
470    let mut nested = HashMap::new();
471
472    for (remote_idx, remote_variant) in remote_variants.iter().enumerate() {
473        if let Some((local_idx, local_variant)) = local_variants
474            .iter()
475            .enumerate()
476            .find(|(_, v)| v.name == remote_variant.name)
477        {
478            variant_map.push(Some(local_idx));
479
480            match (&remote_variant.payload, &local_variant.payload) {
481                // Both struct variants — build a per-variant field plan
482                (
483                    VariantPayload::Struct {
484                        fields: remote_fields,
485                    },
486                    VariantPayload::Struct {
487                        fields: local_fields,
488                    },
489                ) => {
490                    let variant_field_ops: Vec<FieldOp> = remote_fields
491                        .iter()
492                        .map(|rf| {
493                            if let Some((local_field_idx, _)) = local_fields
494                                .iter()
495                                .enumerate()
496                                .find(|(_, f)| f.name == rf.name)
497                            {
498                                FieldOp::Read {
499                                    local_index: local_field_idx,
500                                }
501                            } else {
502                                FieldOp::Skip {
503                                    type_ref: rf.type_ref.clone(),
504                                }
505                            }
506                        })
507                        .collect();
508                    variant_plans.insert(
509                        remote_idx,
510                        TranslationPlan::Struct {
511                            field_ops: variant_field_ops,
512                            nested: HashMap::new(),
513                        },
514                    );
515                }
516                // Both newtype — build a nested plan for the inner type
517                (
518                    VariantPayload::Newtype {
519                        type_ref: remote_inner_ref,
520                    },
521                    VariantPayload::Newtype {
522                        type_ref: local_inner_ref,
523                    },
524                ) => {
525                    let inner_plan = nested_plan(remote_inner_ref, local_inner_ref, input)
526                        .map_err(|e| {
527                            e.with_path_prefix(PathSegment::Variant(remote_variant.name.clone()))
528                        })?;
529                    if let Some(plan) = inner_plan {
530                        nested.insert(local_idx, plan);
531                    }
532                }
533                // Both tuple — check arity matches and build nested plans
534                (
535                    VariantPayload::Tuple {
536                        types: remote_types,
537                    },
538                    VariantPayload::Tuple { types: local_types },
539                ) => {
540                    let tuple_plan = build_tuple_plan(
541                        remote_types,
542                        local_types,
543                        _remote_schema,
544                        _local_schema,
545                        input,
546                    )
547                    .map_err(|e| {
548                        e.with_path_prefix(PathSegment::Variant(remote_variant.name.clone()))
549                    })?;
550                    variant_plans.insert(remote_idx, tuple_plan);
551                }
552                (VariantPayload::Unit, VariantPayload::Unit) => {}
553                // Payload kind mismatch within a variant
554                _ => {
555                    return Err(TranslationError::new(
556                        TranslationErrorKind::IncompatibleVariantPayload {
557                            remote_variant: remote_variant.clone(),
558                            local_variant: local_variant.clone(),
559                        },
560                    )
561                    .with_path_prefix(PathSegment::Variant(remote_variant.name.clone())));
562                }
563            }
564        } else {
565            // r[impl schema.translation.enum.unknown-variant]
566            variant_map.push(None);
567        }
568    }
569
570    Ok(TranslationPlan::Enum {
571        variant_map,
572        variant_plans,
573        nested,
574    })
575}