Skip to content

Commit f12ed5c

Browse files
committed
feat(codec): introduce JSON codec for records with no_std support
- Added `RemoteSerialize` trait for JSON serialization/deserialization, blanket-implemented for all `serde` types. - Implemented `JsonCodec<T>` trait for type-erased JSON encoding/decoding. - Introduced `SerdeJsonCodec` as a zero-sized implementation of `JsonCodec`. - Updated `TypedRecord` to use a single `remote_codec` field instead of separate serializer/deserializer closures. - Modified `with_remote_access` to require `RemoteSerialize` and enable JSON codec installation. - Updated `RecordValue` to utilize the new codec for JSON serialization. - Removed the deprecated `with_read_only_serialization` method. - Added documentation for the new codec functionality and its usage.
1 parent a214e30 commit f12ed5c

7 files changed

Lines changed: 687 additions & 113 deletions

File tree

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ test:
104104
cargo test --package aimdb-core --no-default-features --features "alloc,profiling"
105105
@printf "$(YELLOW) → Testing aimdb-core (no_std + alloc + metrics)$(NC)\n"
106106
cargo test --package aimdb-core --no-default-features --features "alloc,metrics"
107+
@printf "$(YELLOW) → Testing aimdb-core (no_std + alloc + json-serialize)$(NC)\n"
108+
cargo test --package aimdb-core --no-default-features --features "alloc,json-serialize"
107109
@printf "$(YELLOW) → Testing aimdb-core remote module$(NC)\n"
108110
cargo test --package aimdb-core --lib --features "std" remote::
109111
@printf "$(YELLOW) → Testing tokio adapter$(NC)\n"
@@ -167,6 +169,8 @@ clippy:
167169
cargo clippy --package aimdb-data-contracts --no-default-features --features alloc -- -D warnings
168170
@printf "$(YELLOW) → Clippy on aimdb-core (no_std + alloc)$(NC)\n"
169171
cargo clippy --package aimdb-core --no-default-features --features alloc --all-targets -- -D warnings
172+
@printf "$(YELLOW) → Clippy on aimdb-core (no_std + alloc + json-serialize)$(NC)\n"
173+
cargo clippy --package aimdb-core --no-default-features --features "alloc,json-serialize" --all-targets -- -D warnings
170174
@printf "$(YELLOW) → Clippy on aimdb-core (std)$(NC)\n"
171175
cargo clippy --package aimdb-core --features "std,tracing,metrics" --all-targets -- -D warnings
172176
@printf "$(YELLOW) → Clippy on tokio adapter$(NC)\n"
@@ -253,6 +257,8 @@ test-embedded:
253257
cargo check --package aimdb-data-contracts --target thumbv7em-none-eabihf --no-default-features --features alloc
254258
@printf "$(YELLOW) → Checking aimdb-core (no_std minimal) on thumbv7em-none-eabihf target$(NC)\n"
255259
cargo check --package aimdb-core --target thumbv7em-none-eabihf --no-default-features --features alloc
260+
@printf "$(YELLOW) → Checking aimdb-core (no_std + alloc + json-serialize) on thumbv7em-none-eabihf target$(NC)\n"
261+
cargo check --package aimdb-core --target thumbv7em-none-eabihf --no-default-features --features "alloc,json-serialize"
256262
@printf "$(YELLOW) → Checking aimdb-core (no_std/embassy) on thumbv7em-none-eabihf target$(NC)\n"
257263
cargo check --package aimdb-core --target thumbv7em-none-eabihf --no-default-features --features alloc
258264
@printf "$(YELLOW) → Checking aimdb-embassy-adapter on thumbv7em-none-eabihf target$(NC)\n"

aimdb-core/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,19 @@ std = [
2323
"serde",
2424
"thiserror",
2525
"anyhow",
26-
"serde_json",
26+
"json-serialize",
2727
"tokio",
2828
"aimdb-executor/std",
2929
]
3030

3131
# Heap allocation in no_std environments
3232
alloc = ["serde"] # Enable heap in no_std
3333

34+
# JSON codec (`crate::codec`): serde_json-backed `RemoteSerialize` / `JsonCodec`.
35+
# no_std-compatible (serde_json runs on alloc); opt in on embedded targets to
36+
# get `record.latest()?.as_json()` without std/AimX. `std` enables it for AimX.
37+
json-serialize = ["alloc", "serde_json"]
38+
3439
# Observability features (available on both std/no_std)
3540
tracing = ["dep:tracing"] # Works in both std and no_std environments
3641
defmt = ["dep:defmt"] # Embedded logging via probe (no_std)

aimdb-core/src/codec.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
}

aimdb-core/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ extern crate alloc;
2020

2121
pub mod buffer;
2222
pub mod builder;
23+
#[cfg(feature = "json-serialize")]
24+
pub mod codec;
2325
pub mod connector;
2426
pub mod context;
2527
pub mod database;
@@ -80,6 +82,10 @@ pub use transport::{Connector, ConnectorConfig, PublishError};
8082
pub use typed_api::{Consumer, Producer, RecordRegistrar, RecordT, StageKind};
8183
pub use typed_record::{AnyRecord, AnyRecordExt, TypedRecord};
8284

85+
// JSON codec (feature `json-serialize`, no_std + alloc compatible)
86+
#[cfg(feature = "json-serialize")]
87+
pub use codec::{JsonCodec, RemoteSerialize, SerdeJsonCodec};
88+
8389
// Stage profiling exports (feature-gated)
8490
#[cfg(feature = "profiling")]
8591
pub use profiling::{RecordProfilingMetrics, StageMetrics, StageProfilingInfo};

aimdb-core/src/typed_api.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -487,10 +487,12 @@ where
487487
self
488488
}
489489

490-
/// Enables JSON serialization for remote access (std only)
490+
/// Installs the JSON codec for this record (feature `json-serialize`)
491491
///
492-
/// Configures this record to support the `record.get` protocol method.
493-
/// Requires `T: serde::Serialize`.
492+
/// Enables `record.latest()?.as_json()`, and on `std` the AimX `record.get`
493+
/// / `set` / `subscribe` protocol. Requires `T: RemoteSerialize`
494+
/// (blanket-impl'd for every `Serialize + DeserializeOwned` type). Works on
495+
/// no_std + alloc.
494496
///
495497
/// # Example
496498
/// ```rust,ignore
@@ -499,10 +501,10 @@ where
499501
/// .with_remote_access(); // Enable remote queries
500502
/// });
501503
/// ```
502-
#[cfg(feature = "std")]
504+
#[cfg(feature = "json-serialize")]
503505
pub fn with_remote_access(&'a mut self) -> &'a mut Self
504506
where
505-
T: serde::Serialize + serde::de::DeserializeOwned,
507+
T: crate::codec::RemoteSerialize + 'static,
506508
{
507509
self.rec.with_remote_access();
508510
self

0 commit comments

Comments
 (0)