zerodds-webdds-bridge
The OMG Web-Enabled DDS 1.0 (formal/2014-12-01) gateway daemon,
zerodds-webdds-bridged. Serves the REST PSM (§8.3, crates/web) and
the SIMPLE-WSDL-SOAP PSM (§8.4, crates/zerodds-soap) from one process
on one HTTP port — two platform mappings of the same WebDDS object
model (Spec §1.5), backed by zerodds-dcps's public typed API.
crates/web and crates/zerodds-soap are pure no_std + alloc
libraries: routing, status-code mapping, XML element-tag registries,
SOAP envelope/fault/WSDL codecs. Neither implements an HTTP server, a
DdsBackend, or a body codec — crates/web's own crate doc says as
much. This crate is that caller layer.
What's inside
gateway::Gateway—impl zerodds_web::bridge::DdsBackend, backed byzerodds-dcps's typed API (DomainParticipant,Topic,Publisher/Subscriber,DataWriter/DataReader,WaitSet). Covers every §7.4 entity operation: create/update/delete/list for Application/Participant/Topic/Publisher/Subscriber/DataWriter/ DataReader (update =set_qoson the livezerodds-dcpsentity,zerodds_dcps::Entity), write/read for samples, create/update/wait/ delete/list for WaitSets (§7.4.9's long-pollWaitSet::get).qos_update— turns aPUTbody's inline<qos>container (zerodds_xml::EntityQos), or a<qos_profile>name</qos_profile>reference resolved through the daemon's ownQosLibrary/QosProfilemetadata store (override-wins merge when both are present), into the target entity's concretezerodds_dcps::qosvalue, merged onto that entity's own current QoS.contained_entities— parses the nested<publishers>/<subscribers>/<data_writers>/<data_readers>acreate_participant/update_participant/create_publisher/update_publisher/create_subscriber/update_subscriberbody may carry (Spec §7.4.3.1/§7.4.3.2/§7.4.4.7/§7.4.4.8/§7.4.4.11/§7.4.4.12);crate::gatewayreconciles the live DCPS entities against the parsed set by name (add, re-apply QoS, or remove).types_registry::TypeRegistry— resolves a topic's wire type from a config-named.idlfile, or from a peer's TypeLookup discovery, into aSerializedTypeSupportplus aDynamicTypereflective view.dynxml— converts aDynamicDatasample to/from the DDS-WEB §8.1 XML representation, driven by the type's own descriptor.metadata::MetadataStore— §7.3.1Rootmetadata (QosLibrary/QosProfile/Type CRUD) — not a DCPS entity, so it is not part ofDdsBackend.wsdl_bridge— builds the SOAP WSDL from the fixed DDS-WEB object-model operations plus every config-named IDLinterface's own operations (zerodds_lang_rules::adapter::interface).rest_service/soap_service— request dispatch.http1— a hand-rolled HTTP/1.1 engine (request line + headers,Content-Lengthand chunked request bodies with a size cap, keep-alive). No framework dependency.daemon— config/CLI/security/server, the same shape as every otherzerodds-*-bridgeddaemon.
Layer position
Layer 5 — bridges/gateways. The caller layer for zerodds-web +
zerodds-zerodds-soap.
Quickstart
or, for a quick local run without a config file:
Every REST request needs Accept: application/dds-web+xml (Spec
§8.3.5 Tab 7 — the only representation this gateway serves; a missing
Accept is 400, one naming no served representation is 406, and a
Content-Type naming anything else is 415 —
crate::rest_service::check_representation_headers). A request with a
body also names it as Content-Type: curl -d alone sends
application/x-www-form-urlencoded, which is 415. The examples below set
both once and pass them via -H.
REST: create an application, a participant, a topic, write a sample
API=http://127.0.0.1:8080/dds/rest1
ACCEPT='Accept: application/dds-web+xml'
CTYPE='Content-Type: application/dds-web+xml'
# 1. Application
# -> 201 Created, Location: applications/app1
# 2. DomainParticipant on domain 0
# -> 201 Created, Location: .../domain_participants/p1
# 3. Topic, naming a type this daemon's `types:` config resolved
# 4. Publisher + DataWriter
# -> Location: .../publishers/pub1
# -> Location: .../data_writers/dw1
# 5. Write a sample — the body is the sample itself, converted through
# the type's own descriptor (crate::dynxml), never an opaque guess.
# -> 200 OK (a write creates no new resource, unlike the POSTs above)
# 6. Update the DataWriter's QoS in place (Spec §7.4.5.2) — merged onto
# its current QoS (crate::qos_update), so a <qos> naming only
# DEADLINE leaves every other policy untouched. A <qos_profile> name
# instead of/beside <qos> resolves through the daemon's own
# QosLibrary/QosProfile metadata store.
# -> 204 No Content
REST: subscribe and read
# -> Location: .../subscribers/sub1
# -> Location: .../data_readers/dr1
# -> 200 OK, <read_sample_seq><data>...</data>...</read_sample_seq>
WaitSet: long-poll for new data (Spec §7.4.9)
# Blocks up to ?timeout=<seconds> (default 10) for a condition to
# trigger; an empty <list/> means the timeout elapsed, not an error.
SOAP: the same object model, a SOAP envelope
WriteSample/ReadSample's data field carries the sample XML, the
same representation REST uses, so a client that already builds REST
bodies reuses that code for SOAP.
WSDL
Lists every DDS-WEB object-model operation (CreateApplication,
WriteSample, ...) plus every operation of every interface in a
config-named interfaces: IDL file. A business interface's own
operation is dispatched over SOAP too — crate::rpc_dispatch builds
its In/Out structs from the operation's parameters (DDS-RPC 1.0
§7.5.1.1.4/§7.5.1.1.5), sends the request through a Requester on the
daemon's own DDS-RPC client DomainParticipant (domain:), and
converts the reply back to the SOAP body the same way crate::dynxml
converts a topic sample. WSDL and runtime read the same
zerodds_lang_rules::adapter::interface model, so an operation the
WSDL lists is always one the daemon can dispatch, and one it cannot
(neither a builtin nor a configured interface's operation) returns a
named env:Receiver fault, never a silent no-op.
DDS-RPC 1.0 §7.4.3/§7.5.1.1 topic granularity: one request/reply topic
pair per interface (service_name = the interface's qualified
name, e.g. demo_Calculator), shared across all its operations — the
Basic mapping's Call/Return union, keyed per operation by
operation_hash (§7.5.1.1.4), with a per-operation Result union
carrying either the Out struct or a declared raises exception
(§7.5.1.1.5). SOAP dispatch selects the union branch for the named
operation; every synthesized wire type is @final (classic-CDR
shape, no DHEADER), matching what zerodds-idlc generate --lang rust/--lang cpp themselves emit for an interface
(crates/lang-rules/src/emit/interface_rust.rs). tests/soap_rpc_e2e.rs
proves this against a real zerodds-idlc-generated Replier
(zerodds-rpc-rust-e2e's Calculator binding, on a second, independent
DomainParticipant) — not a hand-written stand-in — so interoperability
with a strictly spec-conformant generated client/server pair is a
measured property, not an assumption.
Configuration
See crates/webdds-bridge/src/daemon/config.rs for the full schema
(man 5 zerodds-webdds-bridged.yaml) and
packaging/linux/configs/webdds-bridged.yaml.example for a worked
example.
Open points, closed
Three points this README used to list as open (documented, not silent gaps) are closed as of this round; each is kept here with what closed it, per project convention (a claim of "done" names its evidence).
- DDS-RPC topic granularity. Was: one topic pair per operation.
Now: one topic pair per interface, DDS-RPC 1.0 §7.4.3/§7.5.1.1, as
the "WSDL" section above describes. Closed by
crate::rpc_dispatch's rewrite andtests/soap_rpc_e2e.rs::soap_add_reaches_a_generated_replier_and_returns_its_reply/soap_div_by_zero_surfaces_the_declared_exception, both against a realzerodds-idlc generate --lang rustbinding (zerodds-rpc-rust-e2e'sCalculator), not a hand-writtenReplier. update_*'s<qos_profile>name</qos_profile>reference and contained-entity sync. Was: only an inline<qos>resolved;create_participant/create_publisher/create_subscriberread no contained entities, and theirupdate_*counterparts synced none. Now:crate::qos_update::resolve_entity_qos/resolve_qos_profile_refresolve a<qos_profile>reference through the daemon's ownQosLibrary/QosProfilemetadata store (override-wins merge with an inline<qos>, Spec's own precedent for QoS composition);crate::contained_entitiesparses nested<publishers>/<subscribers>/<data_writers>/<data_readers>(§7.4.3.1/§7.4.3.2/§7.4.4.7/§7.4.4.8/§7.4.4.11/§7.4.4.12);crate::gateway'ssync_publishers/sync_subscribers/sync_data_writers/sync_data_readersadd/re-QoS/remove by name on everycreate_*/update_*call, gating on whether the container element is present (absent = leave untouched, empty = delete every contained entity of that kind) rather than on whether QoS was named. Closed bycrates/webdds-bridge/src/qos_update.rsandcrates/webdds-bridge/src/contained_entities.rs's own test modules, pluscrate::gateway'supdate_participant_creates_and_reconciles_contained_entitiese2e test (real DCPS entities, offline domain).- Optional data representations / content negotiation. DDS-WEB 1.0
§8.2/§8.3.5 (Tab 7/Tab 8) define exactly one representation,
application/dds-web+xml— confirmed by full-text reading of the spec PDF; there is no JSON representation to implement. What was open: the media type string was wrong everywhere in this codebase (application/zerodds-web+xml, fixed in six files — spec conformance bug, not a design choice), and neitherAcceptnorContent-Typewas checked at all — a request naming any other representation, or none, was silently served anyway. Closed bycrate::rest_service::check_representation_headers(400 on a missingAccept, 406 on one naming no served representation, 415 on aContent-Typenaming one), wired intodispatch()ahead of every other check, and its own test module's four new tests.
Security
Reuses the bridge fleet's common substrate, zerodds-bridge-security
(TLS, auth.mode: none|bearer|jwt|mtls, an ACL), rather than
zerodds_web::access_control's own §7.3 AccessController/
Permissions engine — both model the same Spec §7.3 decision shape,
and this daemon follows the fleet's shared implementation for
consistency (uniform config schema, one crate for a CVE fix to land
in). The ACL's resource unit is the Application name, not the DDS
topic name zerodds_bridge_security::Acl is shaped for — see
daemon::config's module doc for why.
Testing
daemon_e2e.rs runs the daemon in-process on a leased DDS domain,
drives it over a real TcpStream, and verifies a write with a
second, independent DomainParticipant that receives the sample
over real RTPS/UDP — not through the daemon's own bookkeeping.