zerodds-webdds-bridge 1.0.0-rc.7.1

OMG DDS-WEB 1.0 gateway daemon: REST-PSM (zerodds-web) + SOAP-PSM (zerodds-soap), one process, backed by zerodds-dcps.
Documentation

zerodds-webdds-bridge

License: Apache-2.0 docs.rs

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 by zerodds-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_qos on the live zerodds-dcps entity, zerodds_dcps::Entity), write/read for samples, create/update/wait/ delete/list for WaitSets (§7.4.9's long-poll WaitSet::get).
  • qos_update — turns a PUT body's inline <qos> container (zerodds_xml::EntityQos), or a <qos_profile>name</qos_profile> reference resolved through the daemon's own QosLibrary/QosProfile metadata store (override-wins merge when both are present), into the target entity's concrete zerodds_dcps::qos value, merged onto that entity's own current QoS.
  • contained_entities — parses the nested <publishers>/ <subscribers>/<data_writers>/<data_readers> a create_participant/update_participant/create_publisher/ update_publisher/create_subscriber/update_subscriber body 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::gateway reconciles 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 .idl file, or from a peer's TypeLookup discovery, into a SerializedTypeSupport plus a DynamicType reflective view.
  • dynxml — converts a DynamicData sample to/from the DDS-WEB §8.1 XML representation, driven by the type's own descriptor.
  • metadata::MetadataStore — §7.3.1 Root metadata (QosLibrary/QosProfile/Type CRUD) — not a DCPS entity, so it is not part of DdsBackend.
  • wsdl_bridge — builds the SOAP WSDL from the fixed DDS-WEB object-model operations plus every config-named IDL interface'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-Length and chunked request bodies with a size cap, keep-alive). No framework dependency.
  • daemon — config/CLI/security/server, the same shape as every other zerodds-*-bridged daemon.

Layer position

Layer 5 — bridges/gateways. The caller layer for zerodds-web + zerodds-zerodds-soap.

Quickstart

zerodds-webdds-bridged --config /etc/zerodds/webdds-bridged.yaml

or, for a quick local run without a config file:

zerodds-webdds-bridged --listen 127.0.0.1:8080 --domain 0 \
  --type 'Demo::Sensor=/path/to/sensor.idl'

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
curl -si -X POST "$API/applications" -H "$ACCEPT" -H "$CTYPE" \
  -d '<application><name>app1</name></application>'
# -> 201 Created, Location: applications/app1

# 2. DomainParticipant on domain 0
curl -si -X POST "$API/applications/app1/domain_participants" -H "$ACCEPT" -H "$CTYPE" \
  -d '<domain_participant><domain_id>0</domain_id></domain_participant>'
# -> 201 Created, Location: .../domain_participants/p1

# 3. Topic, naming a type this daemon's `types:` config resolved
curl -si -X POST "$API/applications/app1/domain_participants/p1/topics" -H "$ACCEPT" -H "$CTYPE" \
  -d '<topic><name>Sensor</name><type_name>Demo::Sensor</type_name></topic>'

# 4. Publisher + DataWriter
curl -si -X POST "$API/applications/app1/domain_participants/p1/publishers" -H "$ACCEPT"
# -> Location: .../publishers/pub1
curl -si -X POST "$API/applications/app1/domain_participants/p1/publishers/pub1/data_writers" -H "$ACCEPT" -H "$CTYPE" \
  -d '<data_writer><topic_name>Sensor</topic_name></data_writer>'
# -> 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.
curl -si -X POST "$API/applications/app1/domain_participants/p1/publishers/pub1/data_writers/dw1" -H "$ACCEPT" -H "$CTYPE" \
  -d '<data><sensor_id>7</sensor_id><value>3.5</value></data>'
# -> 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.
curl -si -X PUT "$API/applications/app1/domain_participants/p1/publishers/pub1/data_writers/dw1" -H "$ACCEPT" -H "$CTYPE" \
  -d '<data_writer><qos><deadline><period><sec>2</sec><nanosec>0</nanosec></period></deadline></qos></data_writer>'
# -> 204 No Content

REST: subscribe and read

curl -si -X POST "$API/applications/app1/domain_participants/p1/subscribers" -H "$ACCEPT"
# -> Location: .../subscribers/sub1
curl -si -X POST "$API/applications/app1/domain_participants/p1/subscribers/sub1/data_readers" -H "$ACCEPT" -H "$CTYPE" \
  -d '<data_reader><topic_name>Sensor</topic_name></data_reader>'
# -> Location: .../data_readers/dr1

curl -si "$API/applications/app1/domain_participants/p1/subscribers/sub1/data_readers/dr1" -H "$ACCEPT"
# -> 200 OK, <read_sample_seq><data>...</data>...</read_sample_seq>

WaitSet: long-poll for new data (Spec §7.4.9)

curl -si -X POST "$API/applications/app1/waitsets" -H "$ACCEPT" -H "$CTYPE" \
  -d '<waitset><name>ws1</name><readers><item>applications/app1/domain_participants/p1/subscribers/sub1/data_readers/dr1</item></readers></waitset>'

# Blocks up to ?timeout=<seconds> (default 10) for a condition to
# trigger; an empty <list/> means the timeout elapsed, not an error.
curl -si "$API/applications/app1/waitsets/ws1?timeout=30" -H "$ACCEPT"

SOAP: the same object model, a SOAP envelope

curl -si -X POST http://127.0.0.1:8080/soap \
  -H 'Content-Type: application/soap+xml' \
  -d '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
        <soap:Body>
          <CreateApplication><name>app1</name></CreateApplication>
        </soap:Body>
      </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

curl -s http://127.0.0.1:8080/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 and tests/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 real zerodds-idlc generate --lang rust binding (zerodds-rpc-rust-e2e's Calculator), not a hand-written Replier.
  • update_*'s <qos_profile>name</qos_profile> reference and contained-entity sync. Was: only an inline <qos> resolved; create_participant/create_publisher/create_subscriber read no contained entities, and their update_* counterparts synced none. Now: crate::qos_update::resolve_entity_qos/resolve_qos_profile_ref resolve a <qos_profile> reference through the daemon's own QosLibrary/QosProfile metadata store (override-wins merge with an inline <qos>, Spec's own precedent for QoS composition); crate::contained_entities parses 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's sync_publishers/sync_subscribers/ sync_data_writers/sync_data_readers add/re-QoS/remove by name on every create_*/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 by crates/webdds-bridge/src/qos_update.rs and crates/webdds-bridge/src/contained_entities.rs's own test modules, plus crate::gateway's update_participant_creates_and_reconciles_contained_entities e2e 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 neither Accept nor Content-Type was checked at all — a request naming any other representation, or none, was silently served anyway. Closed by crate::rest_service::check_representation_headers (400 on a missing Accept, 406 on one naming no served representation, 415 on a Content-Type naming one), wired into dispatch() 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

cargo test -p zerodds-webdds-bridge                       # 96 unit tests
cargo test -p zerodds-webdds-bridge --test daemon_e2e      # 4 integration tests
cargo test -p zerodds-webdds-bridge --test soap_rpc_e2e     # 2 integration tests (SOAP↔DDS-RPC↔Replier)
cargo test -p zerodds-webdds-bridge --test example_yaml_loadback  # 2 integration tests

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.