sayiir_core/codec.rs
1//! Pluggable serialization codec traits.
2//!
3//! Every workflow carries a codec that serializes task inputs/outputs to
4//! [`Bytes`] and back. The trait hierarchy is:
5//!
6//! - [`Encoder`] / [`Decoder`] — marker traits
7//! - [`sealed::EncodeValue<T>`] / [`sealed::DecodeValue<T>`] — per-type encode/decode
8//! - [`Codec`] — blanket `Encoder + Decoder`
9//! - [`EnvelopeCodec`] — object-safe helpers for branch routing envelopes
10//!
11//! Concrete implementations live in `sayiir-runtime` (`JsonCodec`, `RkyvCodec`).
12
13use crate::error::BoxError;
14use bytes::Bytes;
15
16/// Sealed helper traits for codec implementations.
17/// These traits allow implementations to specify their own type bounds.
18///
19/// # Implementation Note
20///
21/// To implement `Encoder` or `Decoder`, you need to:
22/// 1. Implement the `Encoder` or `Decoder` trait (empty impl is fine)
23/// 2. Implement `sealed::EncodeValue<T>` or `sealed::DecodeValue<T>` with your desired bounds
24pub mod sealed {
25 use super::{BoxError, Bytes};
26
27 /// Helper trait for encoding with custom bounds.
28 pub trait EncodeValue<T>: Send + Sync + 'static {
29 /// Encode the value into bytes.
30 ///
31 /// # Errors
32 ///
33 /// Returns an error if serialization fails.
34 fn encode_value(&self, value: &T) -> Result<Bytes, BoxError>;
35 }
36
37 /// Helper trait for decoding with custom bounds.
38 pub trait DecodeValue<T>: Send + Sync + 'static {
39 /// Decode a value from bytes.
40 ///
41 /// # Errors
42 ///
43 /// Returns an error if deserialization fails.
44 fn decode_value(&self, bytes: Bytes) -> Result<T, BoxError>;
45 }
46}
47
48/// An encoder that can serialize a value into a byte stream.
49pub trait Encoder: Send + Sync + 'static {
50 /// Encode a value into bytes.
51 ///
52 /// # Errors
53 ///
54 /// Returns an error if serialization fails.
55 fn encode<T>(&self, value: &T) -> Result<Bytes, BoxError>
56 where
57 Self: sealed::EncodeValue<T>,
58 {
59 sealed::EncodeValue::encode_value(self, value)
60 }
61}
62
63/// A decoder that can deserialize a value from a byte stream.
64pub trait Decoder: Send + Sync + 'static {
65 /// Decode a value from bytes.
66 ///
67 /// # Errors
68 ///
69 /// Returns an error if deserialization fails.
70 fn decode<T>(&self, bytes: Bytes) -> Result<T, BoxError>
71 where
72 Self: sealed::DecodeValue<T>,
73 {
74 sealed::DecodeValue::decode_value(self, bytes)
75 }
76}
77
78/// A codec that can serialize and deserialize a value.
79pub trait Codec: Encoder + Decoder {}
80
81/// Blanket impl `Codec` for any type that implements Encoder and Decoder.
82impl<U> Codec for U where U: Encoder + Decoder {}
83
84/// Blanket implementations for `Arc<C>` to allow passing Arc-wrapped codecs.
85impl<C, T> sealed::EncodeValue<T> for std::sync::Arc<C>
86where
87 C: sealed::EncodeValue<T>,
88{
89 fn encode_value(&self, value: &T) -> Result<Bytes, BoxError> {
90 (**self).encode_value(value)
91 }
92}
93
94impl<C, T> sealed::DecodeValue<T> for std::sync::Arc<C>
95where
96 C: sealed::DecodeValue<T>,
97{
98 fn decode_value(&self, bytes: Bytes) -> Result<T, BoxError> {
99 (**self).decode_value(bytes)
100 }
101}
102
103impl<C> Encoder for std::sync::Arc<C> where C: Encoder {}
104
105impl<C> Decoder for std::sync::Arc<C> where C: Decoder {}
106
107/// Object-safe trait for branch envelope operations in the execution layer.
108///
109/// [`Codec`] is generic over `T`, which makes it non-object-safe — callers must
110/// know the concrete type at compile time. The execution layer, however, is
111/// type-erased: it shuffles opaque `Bytes` between tasks without knowing their
112/// Rust types, so it cannot call `Codec` directly.
113///
114/// `EnvelopeCodec` bridges this gap by exposing **byte-level** operations that
115/// the runtime executor can call through a trait object (`dyn EnvelopeCodec`).
116/// It abstracts the serialization format used for:
117/// - Deserializing routing keys from branch key functions
118/// - Constructing discriminated `{"branch": key, "result": value}` envelopes
119/// - Serializing named fork/join results
120///
121/// By default, `JsonCodec` implements this with `serde_json`. Other codecs
122/// (e.g. `RkyvCodec`) can return clear errors if envelope operations are
123/// unsupported.
124pub trait EnvelopeCodec: Send + Sync {
125 /// Decode a routing key (String) from bytes produced by a branch key function.
126 ///
127 /// # Errors
128 ///
129 /// Returns an error if the bytes cannot be decoded as a string.
130 fn decode_string(&self, bytes: &[u8]) -> Result<String, BoxError>;
131
132 /// Encode a branch envelope containing the routing key and result bytes.
133 ///
134 /// Produces a discriminated envelope (e.g. `{"branch": key, "result": value}`)
135 /// so downstream tasks know which branch produced the result.
136 ///
137 /// # Errors
138 ///
139 /// Returns an error if envelope construction or serialization fails.
140 fn encode_branch_envelope(&self, key: &str, result_bytes: &[u8]) -> Result<Bytes, BoxError>;
141
142 /// Serialize named branch results for a fork/join.
143 ///
144 /// Encodes a `Vec<(String, Bytes)>` of branch results into a single `Bytes`
145 /// value suitable for passing to a join task.
146 ///
147 /// # Errors
148 ///
149 /// Returns an error if serialization fails.
150 fn encode_named_results(&self, results: &[(String, Bytes)]) -> Result<Bytes, BoxError>;
151}
152
153/// Blanket implementation for `&C` to allow passing references generically.
154impl<C: EnvelopeCodec> EnvelopeCodec for &C {
155 fn decode_string(&self, bytes: &[u8]) -> Result<String, BoxError> {
156 (**self).decode_string(bytes)
157 }
158
159 fn encode_branch_envelope(&self, key: &str, result_bytes: &[u8]) -> Result<Bytes, BoxError> {
160 (**self).encode_branch_envelope(key, result_bytes)
161 }
162
163 fn encode_named_results(&self, results: &[(String, Bytes)]) -> Result<Bytes, BoxError> {
164 (**self).encode_named_results(results)
165 }
166}
167
168/// Blanket implementation for `Arc<C>` to allow passing Arc-wrapped envelope codecs.
169impl<C: EnvelopeCodec> EnvelopeCodec for std::sync::Arc<C> {
170 fn decode_string(&self, bytes: &[u8]) -> Result<String, BoxError> {
171 (**self).decode_string(bytes)
172 }
173
174 fn encode_branch_envelope(&self, key: &str, result_bytes: &[u8]) -> Result<Bytes, BoxError> {
175 (**self).encode_branch_envelope(key, result_bytes)
176 }
177
178 fn encode_named_results(&self, results: &[(String, Bytes)]) -> Result<Bytes, BoxError> {
179 (**self).encode_named_results(results)
180 }
181}