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}