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}
228
229/// Which side of the schema comparison a missing schema was on.
230#[derive(Debug, Clone, Copy)]
231pub enum SchemaSide {
232    Remote,
233    Local,
234}
235
236impl fmt::Display for SchemaSide {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        match self {
239            SchemaSide::Remote => write!(f, "remote"),
240            SchemaSide::Local => write!(f, "local"),
241        }
242    }
243}
244
245impl TranslationError {
246    pub fn new(kind: TranslationErrorKind) -> Self {
247        Self {
248            path: SchemaPath::new(),
249            kind: Box::new(kind),
250        }
251    }
252
253    /// Prepend a path segment when propagating errors up from nested plan building.
254    pub fn with_path_prefix(mut self, segment: PathSegment) -> Self {
255        self.path.push_front(segment);
256        self
257    }
258}
259
260pub(crate) fn format_schema_rust(schema: &Schema, registry: &SchemaRegistry) -> String {
261    match &schema.kind {
262        SchemaKind::Struct { name, .. } | SchemaKind::Enum { name, .. } => name.clone(),
263        kind => format_schema_kind_rust(kind, registry),
264    }
265}
266
267pub(crate) fn format_type_ref_rust(type_ref: &TypeRef, registry: &SchemaRegistry) -> String {
268    match type_ref {
269        TypeRef::Var { name } => name.as_str().to_string(),
270        TypeRef::Concrete { type_id, args } => {
271            let Some(schema) = registry.get(type_id) else {
272                return format!("<missing:{type_id:?}>");
273            };
274            match &schema.kind {
275                SchemaKind::Struct { name, .. } | SchemaKind::Enum { name, .. } => {
276                    if args.is_empty() {
277                        name.clone()
278                    } else {
279                        let args = args
280                            .iter()
281                            .map(|arg| format_type_ref_rust(arg, registry))
282                            .collect::<Vec<_>>()
283                            .join(", ");
284                        format!("{name}<{args}>")
285                    }
286                }
287                kind => format_schema_kind_rust(kind, registry),
288            }
289        }
290    }
291}
292
293fn format_schema_kind_rust(kind: &SchemaKind, registry: &SchemaRegistry) -> String {
294    match kind {
295        SchemaKind::Struct { name, .. } | SchemaKind::Enum { name, .. } => name.clone(),
296        SchemaKind::Tuple { elements } => {
297            let elements = elements
298                .iter()
299                .map(|element| format_type_ref_rust(element, registry))
300                .collect::<Vec<_>>();
301            match elements.len() {
302                0 => "()".to_string(),
303                1 => format!("({},)", elements[0]),
304                _ => format!("({})", elements.join(", ")),
305            }
306        }
307        SchemaKind::List { element } => format!("Vec<{}>", format_type_ref_rust(element, registry)),
308        SchemaKind::Map { key, value } => format!(
309            "HashMap<{}, {}>",
310            format_type_ref_rust(key, registry),
311            format_type_ref_rust(value, registry)
312        ),
313        SchemaKind::Array { element, length } => {
314            format!("[{}; {length}]", format_type_ref_rust(element, registry))
315        }
316        SchemaKind::Option { element } => {
317            format!("Option<{}>", format_type_ref_rust(element, registry))
318        }
319        SchemaKind::Channel { direction, element } => format!(
320            "{}<{}>",
321            match direction {
322                ChannelDirection::Tx => "Tx",
323                ChannelDirection::Rx => "Rx",
324            },
325            format_type_ref_rust(element, registry)
326        ),
327        SchemaKind::Primitive { primitive_type } => match primitive_type {
328            PrimitiveType::Bool => "bool".to_string(),
329            PrimitiveType::U8 => "u8".to_string(),
330            PrimitiveType::U16 => "u16".to_string(),
331            PrimitiveType::U32 => "u32".to_string(),
332            PrimitiveType::U64 => "u64".to_string(),
333            PrimitiveType::U128 => "u128".to_string(),
334            PrimitiveType::I8 => "i8".to_string(),
335            PrimitiveType::I16 => "i16".to_string(),
336            PrimitiveType::I32 => "i32".to_string(),
337            PrimitiveType::I64 => "i64".to_string(),
338            PrimitiveType::I128 => "i128".to_string(),
339            PrimitiveType::F32 => "f32".to_string(),
340            PrimitiveType::F64 => "f64".to_string(),
341            PrimitiveType::Char => "char".to_string(),
342            PrimitiveType::String => "String".to_string(),
343            PrimitiveType::Unit => "()".to_string(),
344            PrimitiveType::Never => "never".to_string(),
345            PrimitiveType::Bytes => "Vec<u8>".to_string(),
346            PrimitiveType::Payload => "Payload".to_string(),
347        },
348    }
349}
350
351impl fmt::Display for TranslationError {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        if !self.path.segments.is_empty() {
354            write!(f, "at {}: ", self.path)?;
355        }
356
357        match &*self.kind {
358            TranslationErrorKind::NameMismatch {
359                remote,
360                local,
361                remote_rust,
362                local_rust,
363            } => {
364                write!(
365                    f,
366                    "type name mismatch: remote is `{remote_rust}`, local is `{local_rust}` (remote name `{}`, local name `{}`)",
367                    remote.name().unwrap_or("<anonymous>"),
368                    local.name().unwrap_or("<anonymous>"),
369                )
370            }
371            TranslationErrorKind::KindMismatch {
372                remote_rust,
373                local_rust,
374                ..
375            } => {
376                write!(
377                    f,
378                    "structural mismatch: remote is `{remote_rust}`, local is `{local_rust}`",
379                )
380            }
381            TranslationErrorKind::MissingRequiredField {
382                field,
383                remote_struct,
384            } => {
385                write!(
386                    f,
387                    "required field '{}' (type {:?}) missing from remote '{}'",
388                    field.name,
389                    field.type_ref,
390                    format_schema_rust(remote_struct, &SchemaRegistry::new()),
391                )?;
392                if let SchemaKind::Struct { fields, .. } = &remote_struct.kind {
393                    write!(f, " (remote has fields: ")?;
394                    for (i, rf) in fields.iter().enumerate() {
395                        if i > 0 {
396                            write!(f, ", ")?;
397                        }
398                        write!(f, "{}", rf.name)?;
399                    }
400                    write!(f, ")")?;
401                }
402                Ok(())
403            }
404            TranslationErrorKind::FieldTypeMismatch {
405                field_name,
406                remote_field_type,
407                local_field_type,
408                source,
409            } => {
410                write!(
411                    f,
412                    "field '{field_name}' type mismatch: remote is '{}', local is '{}': {source}",
413                    format_schema_rust(remote_field_type, &SchemaRegistry::new()),
414                    format_schema_rust(local_field_type, &SchemaRegistry::new()),
415                )
416            }
417            TranslationErrorKind::IncompatibleVariantPayload {
418                remote_variant,
419                local_variant,
420            } => {
421                write!(
422                    f,
423                    "variant '{}' payload mismatch: remote is {}, local is {}",
424                    remote_variant.name,
425                    variant_payload_str(&remote_variant.payload),
426                    variant_payload_str(&local_variant.payload),
427                )
428            }
429            TranslationErrorKind::SchemaNotFound { type_id, side } => {
430                write!(f, "{side} schema not found for type ID {type_id:?}")
431            }
432            TranslationErrorKind::TupleLengthMismatch {
433                remote_rust,
434                local_rust,
435                remote_len,
436                local_len,
437                ..
438            } => {
439                write!(
440                    f,
441                    "tuple length mismatch: remote `{remote_rust}` has {remote_len} elements, local `{local_rust}` has {local_len} elements",
442                )
443            }
444            TranslationErrorKind::UnresolvedVar { name, side } => {
445                write!(
446                    f,
447                    "unresolved type variable {name} on {side} side — Var substitution failed"
448                )
449            }
450        }
451    }
452}
453
454fn variant_payload_str(payload: &vox_schema::VariantPayload) -> &'static str {
455    match payload {
456        vox_schema::VariantPayload::Unit => "unit",
457        vox_schema::VariantPayload::Newtype { .. } => "newtype",
458        vox_schema::VariantPayload::Tuple { .. } => "tuple",
459        vox_schema::VariantPayload::Struct { .. } => "struct",
460    }
461}
462
463impl std::error::Error for TranslationError {}