aimdb_core/codec.rs
1//! JSON codec for records (feature `remote`, no_std + alloc compatible).
2//!
3//! A record's value `T` often has to cross a type-erasure boundary into a wire
4//! format. AimX uses owned JSON bytes on its outbound record hot paths and
5//! `serde_json::Value` where a tree is still required; no_std connectors and
6//! local `record.latest()?.as_json()` calls use the same bridge. This module
7//! provides both forms without requiring `std` — `serde_json` runs on `alloc`
8//! alone, so embedded targets can opt in via the `remote` feature.
9//!
10//! Two layers:
11//!
12//! - [`RemoteSerialize`] — the capability trait. Blanket-implemented for every
13//! `serde` type, so any `T: Serialize + DeserializeOwned` gets a JSON codec
14//! for free. This is the AimX/connector analogue of the data-contract traits
15//! (`Streamable`, `Linkable`). It lives in `aimdb-core` rather than
16//! `aimdb-data-contracts` because that crate depends on `aimdb-core`, not the
17//! reverse — bounding a core method on `Streamable` would be a dependency
18//! cycle. Every `Streamable` type satisfies `RemoteSerialize` automatically.
19//!
20//! - [`JsonCodec`] — the object-safe, type-erased storage form, with the
21//! zero-sized [`SerdeJsonCodec`] implementation. A record stores
22//! `Option<Arc<dyn JsonCodec<T>>>`; the AimX read/write/subscribe paths and
23//! `RecordValue::as_json` route through it. This mirrors the connector
24//! layer's fused `SerializedSource` / `IngestFn` callbacks.
25
26use alloc::vec::Vec;
27use serde::{de::DeserializeOwned, Serialize};
28
29/// A record type that can be encoded to / decoded from the JSON wire format.
30///
31/// Blanket-implemented for every `T: Serialize + DeserializeOwned`, so opting a
32/// record into JSON via `with_remote_access()` requires no extra boilerplate.
33/// Implement `Serialize` + `Deserialize` (e.g. via `derive`) and the type is
34/// codec-ready.
35pub trait RemoteSerialize: Sized {
36 /// Serialize this value to a JSON value, or `None` on failure.
37 fn to_json(&self) -> Option<serde_json::Value>;
38
39 /// Deserialize a JSON value into `Self`, or `None` on schema mismatch.
40 fn from_json(value: &serde_json::Value) -> Option<Self>;
41
42 /// Serialize this value to owned JSON bytes.
43 ///
44 /// The default preserves compatibility with manual implementations by
45 /// routing through [`to_json`](Self::to_json). The blanket Serde
46 /// implementation overrides it with direct `T -> Vec<u8>` serialization,
47 /// avoiding an intermediate [`serde_json::Value`] tree.
48 fn to_json_vec(&self) -> Option<Vec<u8>> {
49 serde_json::to_vec(&self.to_json()?).ok()
50 }
51
52 /// Deserialize one complete JSON value from `bytes`.
53 ///
54 /// The default preserves compatibility with manual implementations by
55 /// parsing a [`serde_json::Value`] and calling
56 /// [`from_json`](Self::from_json). The blanket Serde implementation
57 /// overrides it with direct slice deserialization.
58 fn from_json_slice(bytes: &[u8]) -> Option<Self> {
59 let value = serde_json::from_slice(bytes).ok()?;
60 Self::from_json(&value)
61 }
62}
63
64impl<T> RemoteSerialize for T
65where
66 T: Serialize + DeserializeOwned,
67{
68 fn to_json(&self) -> Option<serde_json::Value> {
69 serde_json::to_value(self).ok()
70 }
71
72 fn from_json(value: &serde_json::Value) -> Option<Self> {
73 serde_json::from_value(value.clone()).ok()
74 }
75
76 fn to_json_vec(&self) -> Option<Vec<u8>> {
77 serde_json::to_vec(self).ok()
78 }
79
80 fn from_json_slice(bytes: &[u8]) -> Option<Self> {
81 serde_json::from_slice(bytes).ok()
82 }
83}
84
85/// Type-erased JSON codec for one record type.
86///
87/// Stored as `Arc<dyn JsonCodec<T>>` inside `TypedRecord<T, R>`, where the
88/// blanket `AnyRecord` impl cannot carry a `T: RemoteSerialize` bound (it must
89/// also cover non-serializable record types). `T` is fixed per record, so the
90/// trait is object-safe.
91pub trait JsonCodec<T>: Send + Sync {
92 /// Encode a typed value to JSON, or `None` on failure.
93 fn encode(&self, value: &T) -> Option<serde_json::Value>;
94
95 /// Decode a JSON value into `T`, or `None` on schema mismatch.
96 fn decode(&self, value: &serde_json::Value) -> Option<T>;
97
98 /// Encode a typed value into owned JSON bytes.
99 ///
100 /// Defaulted through [`encode`](Self::encode) so existing custom codecs
101 /// remain source-compatible and retain their exact JSON-tree semantics.
102 fn encode_to_vec(&self, value: &T) -> Option<Vec<u8>> {
103 serde_json::to_vec(&self.encode(value)?).ok()
104 }
105
106 /// Decode one complete JSON value from a byte slice.
107 ///
108 /// Defaulted through [`decode`](Self::decode) so existing custom codecs
109 /// remain source-compatible.
110 fn decode_slice(&self, bytes: &[u8]) -> Option<T> {
111 let value = serde_json::from_slice(bytes).ok()?;
112 self.decode(&value)
113 }
114}
115
116/// Zero-sized serde-backed [`JsonCodec`].
117///
118/// Constructed only under a `T: RemoteSerialize` bound (see
119/// `TypedRecord::with_remote_access`), so the erased `JsonCodec<T>` it yields is
120/// always valid.
121pub struct SerdeJsonCodec;
122
123impl<T: RemoteSerialize> JsonCodec<T> for SerdeJsonCodec {
124 fn encode(&self, value: &T) -> Option<serde_json::Value> {
125 value.to_json()
126 }
127
128 fn decode(&self, value: &serde_json::Value) -> Option<T> {
129 T::from_json(value)
130 }
131
132 fn encode_to_vec(&self, value: &T) -> Option<Vec<u8>> {
133 value.to_json_vec()
134 }
135
136 fn decode_slice(&self, bytes: &[u8]) -> Option<T> {
137 T::from_json_slice(bytes)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use alloc::string::String;
145 use alloc::vec;
146 use serde::{Deserialize, Serialize};
147 use serde_json::json;
148
149 #[derive(Debug, PartialEq, Serialize, Deserialize)]
150 struct Fixture {
151 name: String,
152 optional: Option<u32>,
153 bytes: Vec<u8>,
154 }
155
156 struct TreeOnlyCodec;
157
158 impl JsonCodec<Fixture> for TreeOnlyCodec {
159 fn encode(&self, value: &Fixture) -> Option<serde_json::Value> {
160 serde_json::to_value(value).ok()
161 }
162
163 fn decode(&self, value: &serde_json::Value) -> Option<Fixture> {
164 serde_json::from_value(value.clone()).ok()
165 }
166 }
167
168 #[derive(Debug, PartialEq)]
169 struct ManualRemote(u64);
170
171 // Deliberately implements only the original required methods. This type
172 // does not implement Serde, proving the new byte methods are additive.
173 impl RemoteSerialize for ManualRemote {
174 fn to_json(&self) -> Option<serde_json::Value> {
175 Some(json!({ "value": self.0 }))
176 }
177
178 fn from_json(value: &serde_json::Value) -> Option<Self> {
179 Some(Self(value.get("value")?.as_u64()?))
180 }
181 }
182
183 fn fixture() -> Fixture {
184 Fixture {
185 name: "cảm biến \"ngoài trời\"".into(),
186 optional: None,
187 bytes: vec![0, 1, 127, 255],
188 }
189 }
190
191 #[test]
192 fn tree_only_codec_gets_default_byte_compatibility() {
193 let value = fixture();
194 let bytes = TreeOnlyCodec.encode_to_vec(&value).unwrap();
195
196 assert_eq!(
197 serde_json::from_slice::<serde_json::Value>(&bytes).unwrap(),
198 TreeOnlyCodec.encode(&value).unwrap()
199 );
200 assert_eq!(TreeOnlyCodec.decode_slice(&bytes), Some(value));
201 }
202
203 #[test]
204 fn manual_remote_serialize_gets_default_byte_compatibility() {
205 let value = ManualRemote(42);
206 let bytes =
207 <SerdeJsonCodec as JsonCodec<ManualRemote>>::encode_to_vec(&SerdeJsonCodec, &value)
208 .unwrap();
209
210 assert_eq!(bytes, br#"{"value":42}"#);
211 assert_eq!(
212 <SerdeJsonCodec as JsonCodec<ManualRemote>>::decode_slice(&SerdeJsonCodec, &bytes),
213 Some(value)
214 );
215 }
216
217 #[test]
218 fn serde_codec_byte_path_matches_json_value_semantics() {
219 let value = fixture();
220 let bytes =
221 <SerdeJsonCodec as JsonCodec<Fixture>>::encode_to_vec(&SerdeJsonCodec, &value).unwrap();
222
223 assert_eq!(
224 serde_json::from_slice::<serde_json::Value>(&bytes).unwrap(),
225 <SerdeJsonCodec as JsonCodec<Fixture>>::encode(&SerdeJsonCodec, &value).unwrap()
226 );
227 assert_eq!(
228 <SerdeJsonCodec as JsonCodec<Fixture>>::decode_slice(&SerdeJsonCodec, &bytes),
229 Some(value)
230 );
231 }
232
233 #[test]
234 fn serde_codec_rejects_malformed_trailing_and_mismatched_bytes() {
235 for invalid in [
236 br#"{"name":"truncated""#.as_slice(),
237 br#"{"name":"ok","optional":null,"bytes":[]} trailing"#.as_slice(),
238 br#"{"name":7,"optional":null,"bytes":[]}"#.as_slice(),
239 ] {
240 assert!(
241 <SerdeJsonCodec as JsonCodec<Fixture>>::decode_slice(&SerdeJsonCodec, invalid)
242 .is_none()
243 );
244 }
245 }
246}