Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **`ConnectorBuilder::owns_scheme` — a connector may declare that it must be the
only one registered under its scheme.** Routes are collected by scheme alone
(`collect_inbound_routes`, `collect_outbound_routes`, `pump_source`,
`pump_sink`, `pump_client`), so two connectors that each collect their
scheme's routes both claim *all* of them: every `link_to` gets two publishers,
and nothing in a route says which connector it belongs to. `AimDbBuilder::build`
now rejects that before building any connector, with an error naming the
scheme. Defaults to `false`, so nothing changes for a connector that collects
no routes — a session *server* binds its own listener, and two of them under
one scheme remain two endpoints onto one dispatch. `aimdb-knx-connector` opts
in; MQTT and the session clients are candidates but are left alone for now.
- **`AimDbHandle::shutdown(&self)` / `is_closed()`.** The shutdown contract a
foreign-language binding needs, moved into the crate whose thread it is
about; pinned by `aimdb-sync/tests/shutdown_contract_test.rs`. `detach(self)`
Expand Down
11 changes: 0 additions & 11 deletions Cargo.lock

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

26 changes: 13 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ build:
@printf "$(YELLOW) → Building persistence SQLite backend$(NC)\n"
cargo build --package aimdb-persistence-sqlite
@printf "$(YELLOW) → Building KNX connector$(NC)\n"
cargo build --package aimdb-knx-connector --features "std,tokio-runtime"
cargo build --package aimdb-knx-connector --no-default-features --features "std"
@printf "$(YELLOW) → Building WebSocket connector (server + client)$(NC)\n"
cargo build --package aimdb-websocket-connector --features "server,client"
@printf "$(YELLOW) → Building UDS connector$(NC)\n"
Expand Down Expand Up @@ -212,7 +212,7 @@ test:
@printf "$(YELLOW) → Testing MQTT connector (tokio + rustls)$(NC)\n"
cargo test --package aimdb-mqtt-connector --features "std,tokio-runtime,tokio-rustls"
@printf "$(YELLOW) → Testing KNX connector$(NC)\n"
cargo test --package aimdb-knx-connector --features "std,tokio-runtime"
cargo test --package aimdb-knx-connector --no-default-features --features "std"
@printf "$(YELLOW) → Testing WebSocket connector (server + client: unit, real-socket e2e, AimDB round-trip)$(NC)\n"
cargo test --package aimdb-websocket-connector --features "server,client"
@printf "$(YELLOW) → Testing WebSocket connector client-only build$(NC)\n"
Expand Down Expand Up @@ -323,9 +323,9 @@ clippy:
@printf "$(YELLOW) → Clippy on persistence SQLite backend$(NC)\n"
cargo clippy --package aimdb-persistence-sqlite --all-targets -- -D warnings
@printf "$(YELLOW) → Clippy on KNX connector (std)$(NC)\n"
cargo clippy --package aimdb-knx-connector --features "std,tokio-runtime" --all-targets -- -D warnings
@printf "$(YELLOW) → Clippy on KNX connector (embassy)$(NC)\n"
cargo clippy --package aimdb-knx-connector --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime" -- -D warnings
cargo clippy --package aimdb-knx-connector --no-default-features --features "std" --all-targets -- -D warnings
@printf "$(YELLOW) → Clippy on KNX connector (neutral, no_std+alloc)$(NC)\n"
cargo clippy --package aimdb-knx-connector --target thumbv7em-none-eabihf --no-default-features --features "connector" -- -D warnings
@printf "$(YELLOW) → Clippy on MQTT connector (tokio, no TLS backend)$(NC)\n"
cargo clippy --package aimdb-mqtt-connector --features "std,tokio-runtime" --all-targets -- -D warnings
@printf "$(YELLOW) → Clippy on MQTT connector (tokio + native-tls)$(NC)\n"
Expand All @@ -336,8 +336,8 @@ clippy:
cargo clippy --package aimdb-mqtt-connector --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime,defmt" -- -D warnings
@printf "$(YELLOW) → Clippy on MQTT connector (embassy + TLS + defmt)$(NC)\n"
cargo clippy --package aimdb-mqtt-connector --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime,embassy-tls,defmt" -- -D warnings
@printf "$(YELLOW) → Clippy on KNX connector (embassy + defmt)$(NC)\n"
cargo clippy --package aimdb-knx-connector --target thumbv7em-none-eabihf --no-default-features --features "embassy-runtime,defmt" -- -D warnings
@printf "$(YELLOW) → Clippy on KNX connector (neutral + defmt)$(NC)\n"
cargo clippy --package aimdb-knx-connector --target thumbv7em-none-eabihf --no-default-features --features "connector,defmt" -- -D warnings
@printf "$(YELLOW) → Clippy on WebSocket connector$(NC)\n"
cargo clippy --package aimdb-websocket-connector --features "tokio-runtime,client" --all-targets -- -D warnings
@printf "$(YELLOW) → Clippy on UDS connector$(NC)\n"
Expand Down Expand Up @@ -377,7 +377,7 @@ doc:
cargo doc --package aimdb-tokio-adapter --features "tokio-runtime,tracing,observability,net" --no-deps
cargo doc --package aimdb-sync --no-deps
cargo doc --package aimdb-mqtt-connector --features "std,tokio-runtime" --no-deps
cargo doc --package aimdb-knx-connector --features "std,tokio-runtime" --no-deps
cargo doc --package aimdb-knx-connector --no-default-features --features "std" --no-deps
cargo doc --package aimdb-codegen --no-deps
cargo doc --package aimdb-cli --no-deps
cargo doc --package aimdb-mcp --no-deps
Expand All @@ -398,7 +398,7 @@ doc:
cargo doc --package aimdb-core --no-default-features --features alloc --no-deps
cargo doc --package aimdb-embassy-adapter --features "embassy-runtime,net" --no-deps
cargo doc --package aimdb-mqtt-connector --no-default-features --features "embassy-runtime" --no-deps
cargo doc --package aimdb-knx-connector --no-default-features --features "embassy-runtime" --no-deps
cargo doc --package aimdb-knx-connector --no-default-features --features "connector" --no-deps
cargo doc --package aimdb-serial-connector --no-default-features --features "connector" --no-deps
cargo doc --package aimdb-tcp-connector --no-default-features --features "connector" --no-deps
@cp -r target/doc/* target/doc-final/embedded/
Expand Down Expand Up @@ -464,10 +464,10 @@ test-embedded:
cargo check --package aimdb-mqtt-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime"
@printf "$(YELLOW) → Checking aimdb-mqtt-connector (Embassy + defmt) on thumbv7em-none-eabihf target$(NC)\n"
cargo check --package aimdb-mqtt-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime,defmt"
@printf "$(YELLOW) → Checking aimdb-knx-connector (Embassy) on thumbv7em-none-eabihf target$(NC)\n"
cargo check --package aimdb-knx-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime"
@printf "$(YELLOW) → Checking aimdb-knx-connector (Embassy + defmt) on thumbv7em-none-eabihf target$(NC)\n"
cargo check --package aimdb-knx-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "embassy-runtime,defmt"
@printf "$(YELLOW) → Checking aimdb-knx-connector (neutral, no_std+alloc) on thumbv7em-none-eabihf target$(NC)\n"
cargo check --package aimdb-knx-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "connector"
@printf "$(YELLOW) → Checking aimdb-knx-connector (neutral + defmt) on thumbv7em-none-eabihf target$(NC)\n"
cargo check --package aimdb-knx-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "connector,defmt"
@printf "$(YELLOW) → Checking aimdb-serial-connector (Embassy: full no_std AimX serial client+server) on thumbv7em-none-eabihf target$(NC)\n"
cargo check --package aimdb-serial-connector --target thumbv7em-none-eabihf --target-dir $(EMBEDDED_CHECK_TARGET_DIR) --no-default-features --features "_test-embassy"
@printf "$(YELLOW) → Checking aimdb-serial-connector (Embassy + defmt) on thumbv7em-none-eabihf target$(NC)\n"
Expand Down
60 changes: 52 additions & 8 deletions aimdb-codegen/src/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,10 @@ pub fn generate_main_rs(state: &ArchitectureState, binary_name: &str) -> Option<
.iter()
.filter_map(|c| match c.protocol.as_str() {
"mqtt" => Some(quote! { use aimdb_mqtt_connector::MqttConnector; }),
"knx" => Some(quote! { use aimdb_knx_connector::KnxConnector; }),
"knx" => Some(quote! {
use aimdb_knx_connector::{Channels, KnxConnector};
use aimdb_tokio_adapter::net::{TokioDelay, TokioNet};
}),
"ws" => Some(quote! { use aimdb_websocket_connector::WebSocketConnector; }),
_ => None,
})
Expand All @@ -266,7 +269,19 @@ pub fn generate_main_rs(state: &ArchitectureState, binary_name: &str) -> Option<
let default = &c.default;
let ctor: TokenStream = match c.protocol.as_str() {
"mqtt" => quote! { MqttConnector::new(&#var_ident) },
"knx" => quote! { KnxConnector::new(&#var_ident) },
// The adapter owns the socket and the clock; the channels are
// the binary's, in a block-scoped `static`.
"knx" => quote! {
{
static KNX_CHANNELS: Channels = Channels::new();
KnxConnector::new(
TokioNet::udp(std::net::Ipv4Addr::UNSPECIFIED),
TokioDelay,
&#var_ident,
&KNX_CHANNELS,
)
}
},
"ws" => quote! {
WebSocketConnector::new()
.bind(#var_ident.parse::<std::net::SocketAddr>()
Expand Down Expand Up @@ -474,6 +489,12 @@ pub fn generate_binary_cargo_toml(state: &ArchitectureState, binary_name: &str)
let has_knx = bin.external_connectors.iter().any(|c| c.protocol == "knx");
let has_ws = bin.external_connectors.iter().any(|c| c.protocol == "ws");

let tokio_adapter_features = if has_knx {
"[\"tokio-runtime\", \"net\"]"
} else {
"[\"tokio-runtime\"]"
};

let mut optional_connector_deps = String::new();
if has_mqtt {
optional_connector_deps.push_str(
Expand All @@ -482,7 +503,9 @@ pub fn generate_binary_cargo_toml(state: &ArchitectureState, binary_name: &str)
}
if has_knx {
optional_connector_deps.push_str(
"aimdb-knx-connector = { version = \"0.5\", features = [\"tokio-runtime\"] }\n",
"# critical-section-std-impl: the KNX channels need an impl, and only \
the binary may pick one.\n\
aimdb-knx-connector = { version = \"0.5\", features = [\"std\", \"critical-section-std-impl\"] }\n",
);
}
if has_ws {
Expand All @@ -506,7 +529,7 @@ path = \"src/main.rs\"\n\
[dependencies]\n\
{common_crate_dep} = {{ path = \"../{common_crate_name}\" }}\n\
aimdb-core = {{ version = \"0.5\" }}\n\
aimdb-tokio-adapter = {{ version = \"0.5\", features = [\"tokio-runtime\"] }}\n\
aimdb-tokio-adapter = {{ version = \"0.5\", features = {tokio_adapter_features} }}\n\
{optional_connector_deps}\
tokio = {{ version = \"1\", features = [\"full\"] }}\n\
tracing = \"0.1\"\n\
Expand Down Expand Up @@ -1303,6 +1326,12 @@ pub fn generate_hub_cargo_toml(state: &ArchitectureState) -> String {
.iter()
.any(|r| r.connectors.iter().any(|c| c.protocol == "ws"));

let tokio_adapter_features = if has_knx {
"[\"tokio-runtime\", \"net\"]"
} else {
"[\"tokio-runtime\"]"
};

let mut connector_deps = String::new();
if has_mqtt {
connector_deps.push_str(
Expand All @@ -1311,7 +1340,9 @@ pub fn generate_hub_cargo_toml(state: &ArchitectureState) -> String {
}
if has_knx {
connector_deps.push_str(
"aimdb-knx-connector = { version = \"0.5\", features = [\"tokio-runtime\"] }\n",
"# critical-section-std-impl: the KNX channels need an impl, and only \
the binary may pick one.\n\
aimdb-knx-connector = { version = \"0.5\", features = [\"std\", \"critical-section-std-impl\"] }\n",
);
}
if has_ws {
Expand All @@ -1338,7 +1369,7 @@ path = \"src/main.rs\"\n\
{common_crate_name} = {{ path = \"../{common_crate_name}\" }}\n\
aimdb-core = {{ version = \"0.5\" }}\n\
aimdb-data-contracts = {{ version = \"0.5\", features = [\"linkable\"] }}\n\
aimdb-tokio-adapter = {{ version = \"0.5\", features = [\"tokio-runtime\"] }}\n\
aimdb-tokio-adapter = {{ version = \"0.5\", features = {tokio_adapter_features} }}\n\
{connector_deps}\
tokio = {{ version = \"1\", features = [\"full\"] }}\n\
tracing = \"0.1\"\n\
Expand Down Expand Up @@ -1379,7 +1410,10 @@ pub fn generate_hub_main_rs(state: &ArchitectureState) -> String {
v.push(quote! { use aimdb_mqtt_connector::MqttConnector; });
}
if has_knx {
v.push(quote! { use aimdb_knx_connector::KnxConnector; });
v.push(quote! {
use aimdb_knx_connector::{Channels, KnxConnector};
use aimdb_tokio_adapter::net::{TokioDelay, TokioNet};
});
}
if has_ws {
v.push(quote! { use aimdb_websocket_connector::WebSocketConnector; });
Expand Down Expand Up @@ -1421,7 +1455,17 @@ pub fn generate_hub_main_rs(state: &ArchitectureState) -> String {
v.push(quote! { .with_connector(MqttConnector::new(&mqtt_url)) });
}
if has_knx {
v.push(quote! { .with_connector(KnxConnector::new(&knx_gateway)) });
v.push(quote! {
.with_connector({
static KNX_CHANNELS: Channels = Channels::new();
KnxConnector::new(
TokioNet::udp(std::net::Ipv4Addr::UNSPECIFIED),
TokioDelay,
&knx_gateway,
&KNX_CHANNELS,
)
})
});
}
if has_ws {
v.push(quote! { .with_connector(WebSocketConnector::new().bind(ws_bind).path("/ws")) });
Expand Down
39 changes: 39 additions & 0 deletions aimdb-core/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,45 @@ impl AimDbBuilder {
// transport, applies the security policy's writable marking, and drives
// the shared session engine. See `with_connector`'s docs.

// A connector that claims every route for its scheme must be the only
// one registered under it (see `ConnectorBuilder::owns_scheme`).
// Checked before any `build`, so the error names the misconfiguration
// instead of surfacing later as duplicated or misdirected traffic.
// Keyless `ConfigError`s: the mistake is the db's, not a record's.
let mut duplicate_schemes: Vec<crate::error::ConfigError> = Vec::new();
for (i, builder) in self.connector_builders.iter().enumerate() {
if !builder.owns_scheme() {
continue;
}
let first = self.connector_builders[..i]
.iter()
.position(|earlier| earlier.scheme() == builder.scheme());
// Only the second registration reports; a third would repeat it.
if first.is_some()
&& !duplicate_schemes
.iter()
.any(|e| e.url.as_deref() == Some(builder.scheme()))
{
duplicate_schemes.push(crate::error::ConfigError::new(
"",
Some(builder.scheme().into()),
alloc::format!(
"More than one connector registered for scheme '{}'. Routes are \
collected by scheme alone, so each connector would claim all of \
them: every `link_to` would publish twice, and the routes cannot \
be divided between the two. Register one connector for this \
scheme, or give one of them a distinct scheme.",
builder.scheme()
),
));
}
}
if !duplicate_schemes.is_empty() {
return Err(DbError::InvalidConfiguration {
errors: duplicate_schemes,
});
}

// Collect connector futures. Connector builders return a
// `Vec<BoxFuture>` for the runner to drive — there is no connector
// object to keep.
Expand Down
22 changes: 22 additions & 0 deletions aimdb-core/src/connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,28 @@ pub trait ConnectorBuilder: Send + Sync {
/// will be registered under. Used for routing `.link_from()` and `.link_to()`
/// declarations to the appropriate connector.
fn scheme(&self) -> &str;

/// Whether registering a second connector under this scheme is an error.
///
/// Say `true` when [`build`](Self::build) claims every route for its
/// scheme — [`collect_inbound_routes`](crate::AimDb::collect_inbound_routes),
/// [`collect_outbound_routes`](crate::AimDb::collect_outbound_routes),
/// and `crate::session`'s `pump_source`, `pump_sink` and `pump_client`
/// (left unlinked: that module is behind `connector-session`, and this
/// trait is not) all filter by scheme alone, so two such connectors each
/// collect *all* of it: every `link_to` gets two publishers, and the routes
/// cannot be divided between the two endpoints because nothing in a route
/// names which connector it belongs to. That misconfiguration is otherwise
/// silent, and it fails as duplicated or misdirected traffic at runtime
/// rather than at build.
///
/// Leave it `false` — the default — for a connector that only serves what
/// it is given, such as a session *server*: it binds its own listener and
/// collects no routes, so two of them under one scheme are two endpoints
/// onto the same dispatch, which is useful rather than broken.
fn owns_scheme(&self) -> bool {
false
}
}

#[cfg(test)]
Expand Down
Loading