|
| 1 | +//! JSON codec for records (feature `json-serialize`, 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 (std) crosses into `serde_json::Value`; so can a no_std |
| 5 | +//! connector or a local `record.latest()?.as_json()` call. This module |
| 6 | +//! provides that bridge without requiring `std` — `serde_json` runs on `alloc` |
| 7 | +//! alone, so embedded targets can opt in via the `json-serialize` feature. |
| 8 | +//! |
| 9 | +//! Two layers: |
| 10 | +//! |
| 11 | +//! - [`RemoteSerialize`] — the capability trait. Blanket-implemented for every |
| 12 | +//! `serde` type, so any `T: Serialize + DeserializeOwned` gets a JSON codec |
| 13 | +//! for free. This is the AimX/connector analogue of the data-contract traits |
| 14 | +//! (`Streamable`, `Linkable`). It lives in `aimdb-core` rather than |
| 15 | +//! `aimdb-data-contracts` because that crate depends on `aimdb-core`, not the |
| 16 | +//! reverse — bounding a core method on `Streamable` would be a dependency |
| 17 | +//! cycle. Every `Streamable` type satisfies `RemoteSerialize` automatically. |
| 18 | +//! |
| 19 | +//! - [`JsonCodec`] — the object-safe, type-erased storage form, with the |
| 20 | +//! zero-sized [`SerdeJsonCodec`] implementation. A record stores |
| 21 | +//! `Option<Arc<dyn JsonCodec<T>>>`; the AimX read/write/subscribe paths and |
| 22 | +//! `RecordValue::as_json` route through it. This mirrors the connector |
| 23 | +//! layer's `SerializerFn` / `DeserializerFn`. |
| 24 | +
|
| 25 | +use serde::{de::DeserializeOwned, Serialize}; |
| 26 | + |
| 27 | +/// A record type that can be encoded to / decoded from the JSON wire format. |
| 28 | +/// |
| 29 | +/// Blanket-implemented for every `T: Serialize + DeserializeOwned`, so opting a |
| 30 | +/// record into JSON via `with_remote_access()` requires no extra boilerplate. |
| 31 | +/// Implement `Serialize` + `Deserialize` (e.g. via `derive`) and the type is |
| 32 | +/// codec-ready. |
| 33 | +pub trait RemoteSerialize: Sized { |
| 34 | + /// Serialize this value to a JSON value, or `None` on failure. |
| 35 | + fn to_json(&self) -> Option<serde_json::Value>; |
| 36 | + |
| 37 | + /// Deserialize a JSON value into `Self`, or `None` on schema mismatch. |
| 38 | + fn from_json(value: &serde_json::Value) -> Option<Self>; |
| 39 | +} |
| 40 | + |
| 41 | +impl<T> RemoteSerialize for T |
| 42 | +where |
| 43 | + T: Serialize + DeserializeOwned, |
| 44 | +{ |
| 45 | + fn to_json(&self) -> Option<serde_json::Value> { |
| 46 | + serde_json::to_value(self).ok() |
| 47 | + } |
| 48 | + |
| 49 | + fn from_json(value: &serde_json::Value) -> Option<Self> { |
| 50 | + serde_json::from_value(value.clone()).ok() |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +/// Type-erased JSON codec for one record type. |
| 55 | +/// |
| 56 | +/// Stored as `Arc<dyn JsonCodec<T>>` inside `TypedRecord<T, R>`, where the |
| 57 | +/// blanket `AnyRecord` impl cannot carry a `T: RemoteSerialize` bound (it must |
| 58 | +/// also cover non-serializable record types). `T` is fixed per record, so the |
| 59 | +/// trait is object-safe. |
| 60 | +pub trait JsonCodec<T>: Send + Sync { |
| 61 | + /// Encode a typed value to JSON, or `None` on failure. |
| 62 | + fn encode(&self, value: &T) -> Option<serde_json::Value>; |
| 63 | + |
| 64 | + /// Decode a JSON value into `T`, or `None` on schema mismatch. |
| 65 | + fn decode(&self, value: &serde_json::Value) -> Option<T>; |
| 66 | +} |
| 67 | + |
| 68 | +/// Zero-sized serde-backed [`JsonCodec`]. |
| 69 | +/// |
| 70 | +/// Constructed only under a `T: RemoteSerialize` bound (see |
| 71 | +/// `TypedRecord::with_remote_access`), so the erased `JsonCodec<T>` it yields is |
| 72 | +/// always valid. |
| 73 | +pub struct SerdeJsonCodec; |
| 74 | + |
| 75 | +impl<T: RemoteSerialize> JsonCodec<T> for SerdeJsonCodec { |
| 76 | + fn encode(&self, value: &T) -> Option<serde_json::Value> { |
| 77 | + value.to_json() |
| 78 | + } |
| 79 | + |
| 80 | + fn decode(&self, value: &serde_json::Value) -> Option<T> { |
| 81 | + T::from_json(value) |
| 82 | + } |
| 83 | +} |
0 commit comments