Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

No changes yet.
### Added

- **Context-Aware Deserializers (Design 026)**: Inbound connector deserializers can now receive a `RuntimeContext<R>` for platform-independent timestamps and logging
- New `.with_deserializer(|ctx, bytes| ...)` API on `InboundConnectorBuilder` provides `RuntimeContext<R>` to deserialization closures
- New `.with_deserializer_raw(|bytes| ...)` for plain bytes-only deserialization when context is unnecessary
- `DeserializerKind` enum enforces mutual exclusivity between raw and context-aware deserializers
- `Router::route()` propagates optional runtime context to context-aware routes
- Design document: 026 (Context-Aware Deserializers)

### Changed

- **aimdb-core**: Breaking API changes to `InboundConnectorLink`, `Router`, and `RouterBuilder` to support `DeserializerKind` (see [aimdb-core/CHANGELOG.md](aimdb-core/CHANGELOG.md))
- **aimdb-mqtt-connector**: Updated router dispatch for new `route()` signature
- **aimdb-knx-connector**: Updated router dispatch for new `route()` signature
- **aimdb-websocket-connector**: Updated router dispatch for new `route()` signature
- All connector examples updated to use new `.with_deserializer(|_ctx, bytes| ...)` signature

## [1.0.0] - 2026-03-16

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion _external/embassy
Submodule embassy updated 112 files
19 changes: 18 additions & 1 deletion aimdb-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

No changes yet.
### Added

- **Context-Aware Deserializers (Design 026)**: Inbound connector deserializers can now receive a `RuntimeContext<R>` for platform-independent timestamps and logging during deserialization
- New `ContextDeserializerFn` type alias for context-aware type-erased deserializer callbacks
- New `DeserializerKind` enum (`Raw` / `Context`) to enforce mutual exclusivity between plain and context-aware deserializers
- `.with_deserializer(|ctx, bytes| ...)` now accepts a context-aware closure receiving `RuntimeContext<R>`
- `.with_deserializer_raw(|bytes| ...)` added for plain bytes-only deserialization (no context needed)
- `Router::route()` now accepts an optional type-erased runtime context (`Option<&Arc<dyn Any + Send + Sync>>`)
- Context deserializer routes are gracefully skipped when no context is provided

### Changed

- **Breaking**: `InboundConnectorLink::deserializer` field type changed from `DeserializerFn` to `DeserializerKind`
- **Breaking**: `InboundConnectorLink::new()` now takes `DeserializerKind` instead of `DeserializerFn`
- **Breaking**: `Router::route()` signature changed to accept an additional `ctx` parameter
- **Breaking**: `RouterBuilder::from_routes()` and `RouterBuilder::add_route()` now take `DeserializerKind` instead of `DeserializerFn`
- **Breaking**: `.with_deserializer()` on `InboundConnectorBuilder` now expects `Fn(RuntimeContext<R>, &[u8]) -> Result<T, String>` instead of `Fn(&[u8]) -> Result<T, String>` — use `.with_deserializer_raw()` for the previous bytes-only signature
- `AimDb::collect_inbound_routes()` return type updated to use `DeserializerKind`

## [1.0.0] - 2026-03-11

Expand Down
2 changes: 1 addition & 1 deletion aimdb-core/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1450,7 +1450,7 @@ impl<R: aimdb_executor::Spawn + 'static> AimDb<R> {
) -> Vec<(
String,
Box<dyn crate::connector::ProducerTrait>,
crate::connector::DeserializerFn,
crate::connector::DeserializerKind,
)> {
let mut routes = Vec::new();

Expand Down
50 changes: 39 additions & 11 deletions aimdb-core/src/connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,34 @@ impl ConnectorLink {
pub type DeserializerFn =
Arc<dyn Fn(&[u8]) -> Result<Box<dyn core::any::Any + Send>, String> + Send + Sync>;

/// Type alias for context-aware type-erased deserializer callbacks
///
/// Like `DeserializerFn`, but receives a type-erased runtime context
/// for platform-independent timestamps and logging during deserialization.
///
/// The first argument is the type-erased runtime (as `Arc<dyn Any + Send + Sync>`),
/// which is downcast to the concrete runtime type via `RuntimeContext::extract_from_any`.
pub type ContextDeserializerFn = Arc<
dyn Fn(
Arc<dyn core::any::Any + Send + Sync>,
&[u8],
) -> Result<Box<dyn core::any::Any + Send>, String>
+ Send
+ Sync,
>;

/// Which deserializer variant is registered for an inbound link
///
/// Enforces mutual exclusivity between raw bytes-only deserializers
/// and context-aware deserializers.
#[derive(Clone)]
pub enum DeserializerKind {
/// Plain bytes-only deserializer (from `.with_deserializer_raw()`)
Raw(DeserializerFn),
/// Context-aware deserializer (from `.with_deserializer()`)
Context(ContextDeserializerFn),
}

/// Type alias for producer factory callback (alloc feature)
///
/// Takes Arc<dyn Any> (which contains AimDb<R>) and returns a boxed ProducerTrait.
Expand Down Expand Up @@ -646,12 +674,12 @@ pub struct InboundConnectorLink {

/// Deserialization callback that converts bytes to typed values
///
/// This is a type-erased function that takes `&[u8]` and returns
/// `Result<Box<dyn Any + Send>, String>`. The spawned task will
/// downcast to the concrete type before producing.
/// Either a plain bytes-only deserializer (`Raw`) or a context-aware
/// deserializer (`Context`) that receives `RuntimeContext` for timestamps
/// and logging.
///
/// Available in both `std` and `no_std` (with `alloc` feature) environments.
pub deserializer: DeserializerFn,
pub deserializer: DeserializerKind,

/// Producer creation callback (alloc feature)
///
Expand Down Expand Up @@ -700,7 +728,7 @@ impl Debug for InboundConnectorLink {

impl InboundConnectorLink {
/// Creates a new inbound connector link from a URL and deserializer
pub fn new(url: ConnectorUrl, deserializer: DeserializerFn) -> Self {
pub fn new(url: ConnectorUrl, deserializer: DeserializerKind) -> Self {
Self {
url,
config: Vec::new(),
Expand Down Expand Up @@ -1153,25 +1181,25 @@ mod tests {

#[test]
fn test_inbound_connector_link_resolve_topic_default() {
use super::{ConnectorUrl, DeserializerFn, InboundConnectorLink};
use super::{ConnectorUrl, DeserializerFn, DeserializerKind, InboundConnectorLink};

let url = ConnectorUrl::parse("mqtt://sensors/temperature").unwrap();
let deserializer: DeserializerFn =
Arc::new(|_| Ok(Box::new(()) as Box<dyn core::any::Any + Send>));
let link = InboundConnectorLink::new(url, deserializer);
let link = InboundConnectorLink::new(url, DeserializerKind::Raw(deserializer));

// No resolver configured, should return static topic from URL
assert_eq!(link.resolve_topic(), "sensors/temperature");
}

#[test]
fn test_inbound_connector_link_resolve_topic_dynamic() {
use super::{ConnectorUrl, DeserializerFn, InboundConnectorLink};
use super::{ConnectorUrl, DeserializerFn, DeserializerKind, InboundConnectorLink};

let url = ConnectorUrl::parse("mqtt://sensors/default").unwrap();
let deserializer: DeserializerFn =
Arc::new(|_| Ok(Box::new(()) as Box<dyn core::any::Any + Send>));
let mut link = InboundConnectorLink::new(url, deserializer);
let mut link = InboundConnectorLink::new(url, DeserializerKind::Raw(deserializer));

// Configure dynamic resolver
link.topic_resolver = Some(Arc::new(|| Some("sensors/dynamic/kitchen".into())));
Expand All @@ -1182,12 +1210,12 @@ mod tests {

#[test]
fn test_inbound_connector_link_resolve_topic_fallback() {
use super::{ConnectorUrl, DeserializerFn, InboundConnectorLink};
use super::{ConnectorUrl, DeserializerFn, DeserializerKind, InboundConnectorLink};

let url = ConnectorUrl::parse("mqtt://sensors/fallback").unwrap();
let deserializer: DeserializerFn =
Arc::new(|_| Ok(Box::new(()) as Box<dyn core::any::Any + Send>));
let mut link = InboundConnectorLink::new(url, deserializer);
let mut link = InboundConnectorLink::new(url, DeserializerKind::Raw(deserializer));

// Configure resolver that returns None
link.topic_resolver = Some(Arc::new(|| None));
Expand Down
Loading
Loading