Skip to main content

vox_postcard/
error.rs

1use std::fmt;
2
3use vox_schema::{
4    ChannelDirection, FieldSchema, PrimitiveType, Schema, SchemaHash, SchemaKind, SchemaRegistry,
5    TypeRef, VariantSchema,
6};
7
8#[derive(Debug)]
9pub enum SerializeError {
10    UnsupportedType(String),
11    ReflectError(String),
12}
13
14impl fmt::Display for SerializeError {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        match self {
17            Self::UnsupportedType(ty) => write!(f, "unsupported type: {ty}"),
18            Self::ReflectError(msg) => write!(f, "reflect error: {msg}"),
19        }
20    }
21}
22
23impl std::error::Error for SerializeError {}
24
25#[derive(Debug)]
26pub enum DeserializeError {
27    UnexpectedEof {
28        pos: usize,
29    },
30    VarintOverflow {
31        pos: usize,
32    },
33    InvalidBool {
34        pos: usize,
35        got: u8,
36    },
37    InvalidUtf8 {
38        pos: usize,
39    },
40    InvalidOptionTag {
41        pos: usize,
42        got: u8,
43    },
44    InvalidEnumDiscriminant {
45        pos: usize,
46        index: u64,
47        variant_count: usize,
48    },
49    UnsupportedType(String),
50    ReflectError(String),
51    UnknownVariant {
52        remote_index: usize,
53    },
54    TrailingBytes {
55        pos: usize,
56        len: usize,
57    },
58    Custom(String),
59    /// A protocol-level error: missing schemas, missing tracker, etc.
60    // r[impl schema.exchange.required]
61    Protocol(String),
62}
63
64impl fmt::Display for DeserializeError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Self::UnexpectedEof { pos } => write!(f, "unexpected EOF at byte {pos}"),
68            Self::VarintOverflow { pos } => write!(f, "varint overflow at byte {pos}"),
69            Self::InvalidBool { pos, got } => write!(f, "invalid bool 0x{got:02x} at byte {pos}"),
70            Self::InvalidUtf8 { pos } => write!(f, "invalid UTF-8 at byte {pos}"),
71            Self::InvalidOptionTag { pos, got } => {
72                write!(f, "invalid option tag 0x{got:02x} at byte {pos}")
73            }
74            Self::InvalidEnumDiscriminant {
75                pos,
76                index,
77                variant_count,
78            } => {
79                write!(
80                    f,
81                    "enum discriminant {index} out of range (0..{variant_count}) at byte {pos}"
82                )
83            }
84            Self::UnsupportedType(ty) => write!(f, "unsupported type: {ty}"),
85            Self::ReflectError(msg) => write!(f, "reflect error: {msg}"),
86            Self::UnknownVariant { remote_index } => {
87                write!(f, "unknown remote enum variant index {remote_index}")
88            }
89            Self::TrailingBytes { pos, len } => {
90                write!(
91                    f,
92                    "trailing bytes: {remaining} at byte {pos}",
93                    remaining = len - pos
94                )
95            }
96            Self::Custom(msg) => write!(f, "{msg}"),
97            Self::Protocol(msg) => write!(f, "protocol error: {msg}"),
98        }
99    }
100}
101
102impl DeserializeError {
103    pub fn protocol(msg: &str) -> Self {
104        Self::Protocol(msg.to_string())
105    }
106}
107
108impl std::error::Error for DeserializeError {}
109
110/// Path from a root type to a specific location in the schema tree.
111///
112/// Formatted as `RootType.field.nested_field` or `RootType::Variant.field`.
113#[derive(Debug, Clone, Default)]
114pub struct SchemaPath {
115    segments: Vec<PathSegment>,
116}
117
118/// One segment in a schema path.
119#[derive(Debug, Clone)]
120pub enum PathSegment {
121    /// A struct field: `.field_name`
122    Field(String),
123    /// An enum variant: `::VariantName`
124    Variant(String),
125    /// A tuple element: `.0`, `.1`, etc.
126    Index(usize),
127}
128
129impl SchemaPath {
130    pub fn new() -> Self {
131        Self {
132            segments: Vec::new(),
133        }
134    }
135
136    /// Prepend a segment to the front of the path.
137    pub fn push_front(&mut self, segment: PathSegment) {
138        self.segments.insert(0, segment);
139    }
140
141    pub fn with_front(mut self, segment: PathSegment) -> Self {
142        self.push_front(segment);
143        self
144    }
145}
146
147impl fmt::Display for SchemaPath {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        for segment in &self.segments {
150            match segment {
151                PathSegment::Field(name) => write!(f, ".{name}")?,
152                PathSegment::Variant(name) => write!(f, "::{name}")?,
153                PathSegment::Index(i) => write!(f, ".{i}")?,
154            }
155        }
156        Ok(())
157    }
158}
159
160// r[impl schema.errors.content]
161// r[impl schema.errors.early-detection]
162#[derive(Debug)]
163pub struct TranslationError {
164    /// Path from the root type to the error site.
165    pub path: SchemaPath,
166    /// The specific incompatibility.
167    pub kind: Box<TranslationErrorKind>,
168}
169
170#[derive(Debug)]
171pub enum TranslationErrorKind {
172    /// The root type names don't match.
173    NameMismatch {
174        remote: Schema,
175        local: Schema,
176        remote_rust: String,
177        local_rust: String,
178    },
179    /// The structural kinds don't match (e.g. remote is enum, local is struct).
180    KindMismatch {
181        remote: Schema,
182        local: Schema,
183        remote_rust: String,
184        local_rust: String,
185    },
186    // r[impl schema.errors.missing-required]
187    /// A required local field has no corresponding remote field and no default.
188    MissingRequiredField {
189        /// The local field that's missing from the remote schema.
190        field: FieldSchema,
191        /// The remote struct schema (so you can see what fields it does have).
192        remote_struct: Schema,
193    },
194    // r[impl schema.errors.type-mismatch]
195    /// A field exists in both types but the nested types are incompatible.
196    FieldTypeMismatch {
197        field_name: String,
198        remote_field_type: Schema,
199        local_field_type: Schema,
200        /// The nested error that explains what exactly is incompatible.
201        source: Box<TranslationError>,
202    },
203    /// Enum variant payloads are incompatible (e.g. unit vs struct).
204    IncompatibleVariantPayload {
205        remote_variant: VariantSchema,
206        local_variant: VariantSchema,
207    },
208    /// A type ID referenced by the remote schema was not found in the registry.
209    SchemaNotFound {
210        type_id: SchemaHash,
211        /// Which side was missing it.
212        side: SchemaSide,
213    },
214    /// Tuple lengths don't match.
215    TupleLengthMismatch {
216        remote: Schema,
217        local: Schema,
218        remote_rust: String,
219        local_rust: String,
220        remote_len: usize,
221        local_len: usize,
222    },
223    /// A type variable (Var) appeared where a concrete type was expected.
224    /// This means Var substitution didn't happen — a bug in the extraction
225    /// or plan building pipeline.
226    UnresolvedVar { name: String, side: SchemaSide },
227    /// Two recursive types reach the same `(remote, local)` pair through a
228    /// cycle, but the remote and local schemas at that pair are not
229    /// structurally identical. Self-recursion with matching schemas is
230    /// fine (the IR layer's `CallSelf` op closes the loop); mutual or
231    /// asymmetric recursion would need a back-reference plan that
232    /// `build_plan` does not yet emit.
233    RecursiveTypeMismatch {
234        remote: Schema,
235        local: Schema,
236        remote_rust: String,
237        local_rust: String,
238    },
239}
240
241/// Which side of the schema comparison a missing schema was on.
242#[derive(Debug, Clone, Copy)]
243pub enum SchemaSide {
244    Remote,
245    Local,
246}
247
248impl fmt::Display for SchemaSide {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        match self {
251            SchemaSide::Remote => write!(f, "remote"),
252            SchemaSide::Local => write!(f, "local"),
253        }
254    }
255}
256
257impl TranslationError {
258    pub fn new(kind: TranslationErrorKind) -> Self {
259        Self {
260            path: SchemaPath::new(),
261            kind: Box::new(kind),
262        }
263    }
264
265    /// Prepend a path segment when propagating errors up from nested plan building.
266    pub fn with_path_prefix(mut self, segment: PathSegment) -> Self {
267        self.path.push_front(segment);
268        self
269    }
270}
271
272pub(crate) fn format_schema_rust(schema: &Schema, registry: &SchemaRegistry) -> String {
273    match &schema.kind {
274        SchemaKind::Struct { name, .. } | SchemaKind::Enum { name, .. } => name.clone(),
275        kind => format_schema_kind_rust(kind, registry),
276    }
277}
278
279pub(crate) fn format_type_ref_rust(type_ref: &TypeRef, registry: &SchemaRegistry) -> String {
280    match type_ref {
281        TypeRef::Var { name } => name.as_str().to_string(),
282        TypeRef::Concrete { type_id, args } => {
283            let Some(schema) = registry.get(type_id) else {
284                return format!("<missing:{type_id:?}>");
285            };
286            match &schema.kind {
287                SchemaKind::Struct { name, .. } | SchemaKind::Enum { name, .. } => {
288                    if args.is_empty() {
289                        name.clone()
290                    } else {
291                        let args = args
292                            .iter()
293                            .map(|arg| format_type_ref_rust(arg, registry))
294                            .collect::<Vec<_>>()
295                            .join(", ");
296                        format!("{name}<{args}>")
297                    }
298                }
299                kind => format_schema_kind_rust(kind, registry),
300            }
301        }
302    }
303}
304
305fn format_schema_kind_rust(kind: &SchemaKind, registry: &SchemaRegistry) -> String {
306    match kind {
307        SchemaKind::Struct { name, .. } | SchemaKind::Enum { name, .. } => name.clone(),
308        SchemaKind::Tuple { elements } => {
309            let elements = elements
310                .iter()
311                .map(|element| format_type_ref_rust(element, registry))
312                .collect::<Vec<_>>();
313            match elements.len() {
314                0 => "()".to_string(),
315                1 => format!("({},)", elements[0]),
316                _ => format!("({})", elements.join(", ")),
317            }
318        }
319        SchemaKind::List { element } => format!("Vec<{}>", format_type_ref_rust(element, registry)),
320        SchemaKind::Map { key, value } => format!(
321            "HashMap<{}, {}>",
322            format_type_ref_rust(key, registry),
323            format_type_ref_rust(value, registry)
324        ),
325        SchemaKind::Array { element, length } => {
326            format!("[{}; {length}]", format_type_ref_rust(element, registry))
327        }
328        SchemaKind::Option { element } => {
329            format!("Option<{}>", format_type_ref_rust(element, registry))
330        }
331        SchemaKind::Channel { direction, element } => format!(
332            "{}<{}>",
333            match direction {
334                ChannelDirection::Tx => "Tx",
335                ChannelDirection::Rx => "Rx",
336            },
337            format_type_ref_rust(element, registry)
338        ),
339        SchemaKind::Primitive { primitive_type } => match primitive_type {
340            PrimitiveType::Bool => "bool".to_string(),
341            PrimitiveType::U8 => "u8".to_string(),
342            PrimitiveType::U16 => "u16".to_string(),
343            PrimitiveType::U32 => "u32".to_string(),
344            PrimitiveType::U64 => "u64".to_string(),
345            PrimitiveType::U128 => "u128".to_string(),
346            PrimitiveType::I8 => "i8".to_string(),
347            PrimitiveType::I16 => "i16".to_string(),
348            PrimitiveType::I32 => "i32".to_string(),
349            PrimitiveType::I64 => "i64".to_string(),
350            PrimitiveType::I128 => "i128".to_string(),
351            PrimitiveType::F32 => "f32".to_string(),
352            PrimitiveType::F64 => "f64".to_string(),
353            PrimitiveType::Char => "char".to_string(),
354            PrimitiveType::String => "String".to_string(),
355            PrimitiveType::Unit => "()".to_string(),
356            PrimitiveType::Never => "never".to_string(),
357            PrimitiveType::Bytes => "Vec<u8>".to_string(),
358            PrimitiveType::Payload => "Payload".to_string(),
359        },
360    }
361}
362
363impl fmt::Display for TranslationError {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        if !self.path.segments.is_empty() {
366            write!(f, "at {}: ", self.path)?;
367        }
368
369        match &*self.kind {
370            TranslationErrorKind::NameMismatch {
371                remote,
372                local,
373                remote_rust,
374                local_rust,
375            } => {
376                write!(
377                    f,
378                    "type name mismatch: remote is `{remote_rust}`, local is `{local_rust}` (remote name `{}`, local name `{}`)",
379                    remote.name().unwrap_or("<anonymous>"),
380                    local.name().unwrap_or("<anonymous>"),
381                )
382            }
383            TranslationErrorKind::KindMismatch {
384                remote_rust,
385                local_rust,
386                ..
387            } => {
388                write!(
389                    f,
390                    "structural mismatch: remote is `{remote_rust}`, local is `{local_rust}`",
391                )
392            }
393            TranslationErrorKind::MissingRequiredField {
394                field,
395                remote_struct,
396            } => {
397                write!(
398                    f,
399                    "required field '{}' (type {:?}) missing from remote '{}'",
400                    field.name,
401                    field.type_ref,
402                    format_schema_rust(remote_struct, &SchemaRegistry::new()),
403                )?;
404                if let SchemaKind::Struct { fields, .. } = &remote_struct.kind {
405                    write!(f, " (remote has fields: ")?;
406                    for (i, rf) in fields.iter().enumerate() {
407                        if i > 0 {
408                            write!(f, ", ")?;
409                        }
410                        write!(f, "{}", rf.name)?;
411                    }
412                    write!(f, ")")?;
413                }
414                Ok(())
415            }
416            TranslationErrorKind::FieldTypeMismatch {
417                field_name,
418                remote_field_type,
419                local_field_type,
420                source,
421            } => {
422                write!(
423                    f,
424                    "field '{field_name}' type mismatch: remote is '{}', local is '{}': {source}",
425                    format_schema_rust(remote_field_type, &SchemaRegistry::new()),
426                    format_schema_rust(local_field_type, &SchemaRegistry::new()),
427                )
428            }
429            TranslationErrorKind::IncompatibleVariantPayload {
430                remote_variant,
431                local_variant,
432            } => {
433                write!(
434                    f,
435                    "variant '{}' payload mismatch: remote is {}, local is {}",
436                    remote_variant.name,
437                    variant_payload_str(&remote_variant.payload),
438                    variant_payload_str(&local_variant.payload),
439                )
440            }
441            TranslationErrorKind::SchemaNotFound { type_id, side } => {
442                write!(f, "{side} schema not found for type ID {type_id:?}")
443            }
444            TranslationErrorKind::TupleLengthMismatch {
445                remote_rust,
446                local_rust,
447                remote_len,
448                local_len,
449                ..
450            } => {
451                write!(
452                    f,
453                    "tuple length mismatch: remote `{remote_rust}` has {remote_len} elements, local `{local_rust}` has {local_len} elements",
454                )
455            }
456            TranslationErrorKind::UnresolvedVar { name, side } => {
457                write!(
458                    f,
459                    "unresolved type variable {name} on {side} side — Var substitution failed"
460                )
461            }
462            TranslationErrorKind::RecursiveTypeMismatch {
463                remote_rust,
464                local_rust,
465                ..
466            } => {
467                write!(
468                    f,
469                    "recursive type mismatch: cycle reaches `{remote_rust}` (remote) paired with `{local_rust}` (local), and the two are not structurally identical — only self-recursion with matching schemas is supported today",
470                )
471            }
472        }
473    }
474}
475
476fn variant_payload_str(payload: &vox_schema::VariantPayload) -> &'static str {
477    match payload {
478        vox_schema::VariantPayload::Unit => "unit",
479        vox_schema::VariantPayload::Newtype { .. } => "newtype",
480        vox_schema::VariantPayload::Tuple { .. } => "tuple",
481        vox_schema::VariantPayload::Struct { .. } => "struct",
482    }
483}
484
485impl std::error::Error for TranslationError {}