Skip to main content

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}