Skip to main content

arkhe_kernel/state/
traits.rs

1//! Sealed traits for kernel-managed types.
2//!
3//! `Component`, `Action`, `Event` are derive-only. Domains reach `Action`
4//! through `#[derive(arkhe_macros::ArkheAction)]`, which generates the
5//! `_sealed::Sealed` + `ActionDeriv` impls; the kernel's blanket
6//! `impl<T: ActionDeriv + ActionCompute> Action for T` then supplies the
7//! postcard-canonical default methods. `Component` and `Event` keep the
8//! single-trait shape until their derive macros land alongside the
9//! snapshot integration.
10//!
11//! `_sealed::Sealed` is `#[doc(hidden)] pub`. The Rust language has no
12//! true sealing primitive — this is the documented convention. Manual
13//! `impl _sealed::Sealed` from outside the crate is technically possible
14//! but auditable and against contract; A11 grade rests on the macro
15//! being the canonical (and documented) path.
16
17use crate::abi::TypeCode;
18
19#[doc(hidden)]
20pub mod _sealed {
21    pub trait Sealed {}
22}
23
24/// Deserialization failure for canonical-bytes round-trip.
25#[non_exhaustive]
26#[derive(Clone, Debug)]
27pub enum DeserializeError {
28    /// Stored bytes carry a different `SCHEMA_VERSION` than the type
29    /// being decoded into.
30    SchemaVersionMismatch {
31        /// `SCHEMA_VERSION` declared on the target type.
32        expected: u32,
33        /// `version` argument supplied by the caller.
34        got: u32,
35    },
36    /// Postcard refused to decode the bytes (truncated, invalid tag,
37    /// type-shape mismatch).
38    PayloadMalformed,
39    /// `TypeCode` not present in the registry consulted at decode time.
40    UnknownTypeCode {
41        /// The unrecognized `TypeCode`.
42        observed: TypeCode,
43    },
44    /// Encoded length exceeded the configured per-Action / per-Component
45    /// byte budget.
46    LengthExceedsBudget,
47}
48
49impl core::fmt::Display for DeserializeError {
50    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
51        match self {
52            Self::SchemaVersionMismatch { expected, got } => {
53                write!(
54                    f,
55                    "schema version mismatch: expected {}, got {}",
56                    expected, got
57                )
58            }
59            Self::PayloadMalformed => write!(f, "payload malformed"),
60            Self::UnknownTypeCode { observed } => {
61                write!(f, "unknown type code: {:?}", observed)
62            }
63            Self::LengthExceedsBudget => write!(f, "deserialization length exceeds budget"),
64        }
65    }
66}
67
68impl std::error::Error for DeserializeError {}
69
70/// Component — derive-only via `#[derive(arkhe_macros::ArkheComponent)]`.
71/// Default methods use postcard for the canonical-bytes round trip;
72/// the macro only needs to emit `Sealed` + the const declarations.
73pub trait Component:
74    _sealed::Sealed + serde::Serialize + serde::de::DeserializeOwned + 'static
75{
76    /// Stable dispatch identifier for this component type. Set by the
77    /// `#[arkhe(type_code = N, ...)]` attribute on the deriving struct.
78    const TYPE_CODE: TypeCode;
79    /// Version tag accompanying canonical bytes. Bumping invalidates
80    /// older serialized payloads at decode time.
81    const SCHEMA_VERSION: u32;
82
83    /// Postcard-canonical byte encoding. Default implementation uses
84    /// `postcard::to_allocvec` against the deriving type's serde impl.
85    fn canonical_bytes(&self) -> Vec<u8>
86    where
87        Self: Sized,
88    {
89        postcard::to_allocvec(self).expect("postcard encode self for Component::canonical_bytes")
90    }
91
92    /// Inverse of [`canonical_bytes`](Component::canonical_bytes).
93    /// Returns [`DeserializeError::SchemaVersionMismatch`] if `version`
94    /// does not equal [`Component::SCHEMA_VERSION`].
95    fn from_bytes(version: u32, bytes: &[u8]) -> Result<Box<Self>, DeserializeError>
96    where
97        Self: Sized,
98    {
99        if version != Self::SCHEMA_VERSION {
100            return Err(DeserializeError::SchemaVersionMismatch {
101                expected: Self::SCHEMA_VERSION,
102                got: version,
103            });
104        }
105        postcard::from_bytes::<Self>(bytes)
106            .map(Box::new)
107            .map_err(|_| DeserializeError::PayloadMalformed)
108    }
109
110    /// Approximate byte size — defaults to `canonical_bytes().len()`.
111    /// Override only if a cheaper estimate is required.
112    fn approx_size(&self) -> usize
113    where
114        Self: Sized,
115    {
116        self.canonical_bytes().len()
117    }
118}
119
120/// Derive-emitted half of `Action`. Carries the constants and the
121/// serde bounds that `Action`'s blanket impl needs to drive postcard.
122/// `#[derive(arkhe_macros::ArkheAction)]` is the only sanctioned path.
123pub trait ActionDeriv:
124    _sealed::Sealed + serde::Serialize + serde::de::DeserializeOwned + 'static
125{
126    /// Stable dispatch identifier. Set via `#[arkhe(type_code = N, ...)]`.
127    const TYPE_CODE: TypeCode;
128    /// Version tag for canonical bytes. Bumping invalidates older
129    /// serialized bodies.
130    const SCHEMA_VERSION: u32;
131}
132
133/// User-written half of `Action`. The deterministic effect-list
134/// computation is the only domain logic the kernel runs.
135pub trait ActionCompute: 'static {
136    /// Translate this action into a list of [`Op`](super::op::Op)s the
137    /// kernel will then authorize, dispatch, and apply. **Must be
138    /// pure** — A11 SOCIAL-CONTRACT until the subset-Rust checker
139    /// promotes it to MACHINE-CHECKED.
140    fn compute(&self, ctx: &super::context::ActionContext) -> Vec<super::op::Op>;
141}
142
143/// `Action` — kernel-facing trait. Composed automatically by the
144/// blanket below: any `T: ActionDeriv + ActionCompute` is `Action`.
145/// External code never implements this directly.
146///
147/// Default method bodies use postcard for the canonical-bytes round
148/// trip (R3v3-Δ2). `approx_size` defaults to the encoded length;
149/// override only if a cheaper estimate is required.
150pub trait Action: ActionDeriv + ActionCompute {
151    /// Postcard-canonical byte encoding. See
152    /// [`Component::canonical_bytes`] for the contract; identical
153    /// shape applies here.
154    fn canonical_bytes(&self) -> Vec<u8>
155    where
156        Self: Sized,
157    {
158        postcard::to_allocvec(self).expect("postcard encode self for canonical_bytes")
159    }
160
161    /// Inverse of [`canonical_bytes`](Action::canonical_bytes).
162    /// `SchemaVersionMismatch` on unequal version.
163    fn from_bytes(version: u32, bytes: &[u8]) -> Result<Box<Self>, DeserializeError>
164    where
165        Self: Sized,
166    {
167        if version != Self::SCHEMA_VERSION {
168            return Err(DeserializeError::SchemaVersionMismatch {
169                expected: Self::SCHEMA_VERSION,
170                got: version,
171            });
172        }
173        postcard::from_bytes::<Self>(bytes)
174            .map(Box::new)
175            .map_err(|_| DeserializeError::PayloadMalformed)
176    }
177
178    /// Approximate byte size — defaults to encoded length.
179    fn approx_size(&self) -> usize
180    where
181        Self: Sized,
182    {
183        self.canonical_bytes().len()
184    }
185}
186
187impl<T: ActionDeriv + ActionCompute> Action for T {}
188
189/// Event — derive-only via `#[derive(arkhe_macros::ArkheEvent)]`. Same
190/// postcard-default shape as `Component`; user types must additionally
191/// `#[derive(Debug, serde::Serialize, serde::Deserialize)]`.
192pub trait Event:
193    _sealed::Sealed + std::fmt::Debug + serde::Serialize + serde::de::DeserializeOwned + 'static
194{
195    /// Stable dispatch identifier. Set via `#[arkhe(type_code = N, ...)]`.
196    const TYPE_CODE: TypeCode;
197    /// Version tag for canonical bytes.
198    const SCHEMA_VERSION: u32;
199
200    /// Postcard-canonical byte encoding.
201    fn canonical_bytes(&self) -> Vec<u8>
202    where
203        Self: Sized,
204    {
205        postcard::to_allocvec(self).expect("postcard encode self for Event::canonical_bytes")
206    }
207
208    /// Inverse of [`canonical_bytes`](Event::canonical_bytes).
209    fn from_bytes(version: u32, bytes: &[u8]) -> Result<Box<Self>, DeserializeError>
210    where
211        Self: Sized,
212    {
213        if version != Self::SCHEMA_VERSION {
214            return Err(DeserializeError::SchemaVersionMismatch {
215                expected: Self::SCHEMA_VERSION,
216                got: version,
217            });
218        }
219        postcard::from_bytes::<Self>(bytes)
220            .map(Box::new)
221            .map_err(|_| DeserializeError::PayloadMalformed)
222    }
223
224    /// Approximate byte size — defaults to encoded length.
225    fn approx_size(&self) -> usize
226    where
227        Self: Sized,
228    {
229        self.canonical_bytes().len()
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn deserialize_error_display_includes_versions() {
239        let e = DeserializeError::SchemaVersionMismatch {
240            expected: 1,
241            got: 2,
242        };
243        let s = format!("{}", e);
244        assert!(s.contains("expected 1"));
245        assert!(s.contains("got 2"));
246    }
247
248    #[test]
249    fn deserialize_error_payload_malformed_displays() {
250        let e = DeserializeError::PayloadMalformed;
251        assert_eq!(format!("{}", e), "payload malformed");
252    }
253
254    #[test]
255    fn deserialize_error_implements_std_error() {
256        fn assert_err<E: std::error::Error>() {}
257        assert_err::<DeserializeError>();
258    }
259
260    #[test]
261    fn sealed_trait_is_implementable_within_crate() {
262        // Crate-internal impl is permitted; this test compiles only if the
263        // seal module is reachable from this test scope.
264        struct CrateInternalProof;
265        impl _sealed::Sealed for CrateInternalProof {}
266        let _ = CrateInternalProof;
267    }
268}