Skip to main content

zerodds_web/
bridge.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! DDS-WEB Bridge zur DDS-DCPS Public API — Spec §7.4.
5//!
6//! Implementiert §7.4 §7.4 Class-Operationen
7//! Application + Participant + Topic + Pub/Sub + Writer/Reader von
8//! partial auf done.
9//!
10//! Spec-Quelle: OMG DDS-WEB 1.0 §7.4 (S. 17-58) — alle CRUD-
11//! Operationen des Web-Object-Models, das auf die DDS-Public-API
12//! abgebildet wird.
13//!
14//! # Schicht-Disziplin
15//!
16//! Wir definieren hier ein Trait [`DdsBackend`], das die zwei
17//! Schichten entkoppelt:
18//!
19//! * `crates/web/` definiert REST-Routes (siehe `rest.rs`) und
20//!   Web-Object-Model (siehe `model.rs`).
21//! * Eine konkrete Backend-Implementation in einer hoeheren Schicht
22//!   (z.B. einem Daemon-Crate) implementiert `DdsBackend` und
23//!   ruft die DDS-Public-API auf (`crates/dcps/`).
24//!
25//! Das Trait halten wir bewusst minimal — eine Method pro Operation,
26//! keine versteckte Async-Runtime. Caller, die async-Backends
27//! brauchen, koennen via `tokio::task::spawn_blocking` adaptieren.
28//!
29//! Cross-Ref: Spec §7.4 Tab 5 + §8.3.3 (PIM-zu-REST-Mapping).
30
31use alloc::string::String;
32use alloc::vec::Vec;
33
34use crate::access_control::Decision;
35
36/// Operations-Resultat: entweder ein neu erzeugter Resource-Identifier,
37/// eine Liste oder ein opaker Body.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum BackendResult {
40    /// Erfolgreiche CREATE/UPDATE — liefert neuen Resource-Pfad
41    /// (z.B. `app1/topics/Sensor`).
42    Created(String),
43    /// Erfolgreiche DELETE / no-content-Operation.
44    Deleted,
45    /// Liste von Resource-Pfaden (z.B. fuer `get_applications`).
46    List(Vec<String>),
47    /// Opaker Resource-Body als XML/JSON (Spec §8.1).
48    Body {
49        /// MIME-Type (z.B. `application/zerodds-web+xml`).
50        content_type: String,
51        /// Body-Bytes.
52        body: Vec<u8>,
53    },
54}
55
56/// Backend-Fehler.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum BackendError {
59    /// Resource nicht gefunden.
60    NotFound(String),
61    /// Resource existiert bereits.
62    Conflict(String),
63    /// Anfrage-Body nicht parsbar.
64    BadRequest(String),
65    /// AccessController hat Decision::Deny zurueckgegeben.
66    Forbidden,
67    /// Unerwarteter interner Fehler.
68    Internal(String),
69}
70
71/// Bridge-Trait — konkrete Backend-Implementation routet diese
72/// Method-Calls auf `crates/dcps/`-Public-API.
73///
74/// Spec §7.4 fordert genau diese Operationen pro Object-Type
75/// (Application/Participant/Topic/Publisher/Subscriber/DataWriter/
76/// DataReader/WaitSet). Wir buendeln sie thematisch.
77pub trait DdsBackend {
78    // ---- Application (Spec §7.4.1) ----------------------------------
79
80    /// `POST /dds/rest1/applications` — create_application.
81    ///
82    /// # Errors
83    /// `Conflict` wenn Application-Name bereits existiert.
84    fn create_application(&mut self, app_name: &str) -> Result<BackendResult, BackendError>;
85
86    /// `DELETE /dds/rest1/applications/{name}`.
87    ///
88    /// # Errors
89    /// `NotFound` wenn Name unbekannt.
90    fn delete_application(&mut self, app_name: &str) -> Result<BackendResult, BackendError>;
91
92    /// `GET /dds/rest1/applications` (Spec §7.4.1.4 fnmatch).
93    ///
94    /// # Errors
95    /// `Internal`.
96    fn list_applications(&self, pattern: &str) -> Result<BackendResult, BackendError>;
97
98    // ---- DomainParticipant (Spec §7.4.2) ----------------------------
99
100    /// `POST /dds/rest1/applications/{app}/domain_participants`.
101    ///
102    /// # Errors
103    /// `NotFound` / `Conflict`.
104    fn create_participant(
105        &mut self,
106        app_name: &str,
107        domain_id: u32,
108    ) -> Result<BackendResult, BackendError>;
109
110    /// `DELETE /dds/rest1/applications/{app}/domain_participants/{n}`.
111    ///
112    /// # Errors
113    /// `NotFound`.
114    fn delete_participant(
115        &mut self,
116        app_name: &str,
117        participant: &str,
118    ) -> Result<BackendResult, BackendError>;
119
120    // ---- Topic (Spec §7.4.3) ----------------------------------------
121
122    /// `POST .../topics`.
123    ///
124    /// # Errors
125    /// `NotFound` / `Conflict` / `BadRequest`.
126    fn create_topic(
127        &mut self,
128        app_name: &str,
129        participant: &str,
130        topic_name: &str,
131        type_name: &str,
132    ) -> Result<BackendResult, BackendError>;
133
134    // ---- DataWriter / DataReader (Spec §7.4.6 + §7.4.7) -------------
135
136    /// `POST .../data_writers`.
137    ///
138    /// # Errors
139    /// `NotFound` / `Conflict`.
140    fn create_data_writer(
141        &mut self,
142        app_name: &str,
143        participant: &str,
144        publisher: &str,
145        topic: &str,
146    ) -> Result<BackendResult, BackendError>;
147
148    /// `POST .../data_readers`.
149    ///
150    /// # Errors
151    /// `NotFound` / `Conflict`.
152    fn create_data_reader(
153        &mut self,
154        app_name: &str,
155        participant: &str,
156        subscriber: &str,
157        topic: &str,
158    ) -> Result<BackendResult, BackendError>;
159
160    // ---- Sample IO (Spec §7.4.6.5 + §7.4.7.5) -----------------------
161
162    /// `POST .../data_writers/{w}/write_sample_seq`.
163    ///
164    /// # Errors
165    /// `NotFound` / `BadRequest`.
166    fn write_sample(
167        &mut self,
168        app_name: &str,
169        participant: &str,
170        publisher: &str,
171        writer: &str,
172        body: &[u8],
173    ) -> Result<BackendResult, BackendError>;
174
175    /// `POST .../data_readers/{r}/read_sample_seq`.
176    ///
177    /// # Errors
178    /// `NotFound`.
179    fn read_samples(
180        &mut self,
181        app_name: &str,
182        participant: &str,
183        subscriber: &str,
184        reader: &str,
185        selector: Option<&str>,
186    ) -> Result<BackendResult, BackendError>;
187}
188
189/// Decision-Wrapper, der vor jedem Backend-Call die
190/// AccessController-Permission prueft.
191///
192/// Spec §7.3 Decision-Engine + §7.4 §7.4 Class-Operationen werden so
193/// orthogonal komponiert.
194///
195/// # Errors
196/// `BackendError::Forbidden` wenn `decision == Decision::Deny`.
197pub fn enforce(decision: Decision) -> Result<(), BackendError> {
198    match decision {
199        Decision::Permit => Ok(()),
200        Decision::Deny => Err(BackendError::Forbidden),
201    }
202}
203
204#[cfg(test)]
205#[allow(clippy::expect_used)]
206mod tests {
207    use super::*;
208    use crate::access_control::{Decision, Operation, Permissions, Rule};
209    use alloc::collections::BTreeMap;
210
211    /// Minimaler In-Memory-Backend fuer den Test (zaehlt nur
212    /// Operations-Calls, keine echte DDS-Bridge).
213    #[derive(Default)]
214    struct InMemoryBackend {
215        apps: BTreeMap<String, ()>,
216    }
217
218    impl DdsBackend for InMemoryBackend {
219        fn create_application(&mut self, n: &str) -> Result<BackendResult, BackendError> {
220            if self.apps.contains_key(n) {
221                return Err(BackendError::Conflict(n.to_string()));
222            }
223            self.apps.insert(n.to_string(), ());
224            Ok(BackendResult::Created(alloc::format!("/applications/{n}")))
225        }
226        fn delete_application(&mut self, n: &str) -> Result<BackendResult, BackendError> {
227            if self.apps.remove(n).is_none() {
228                return Err(BackendError::NotFound(n.to_string()));
229            }
230            Ok(BackendResult::Deleted)
231        }
232        fn list_applications(&self, _p: &str) -> Result<BackendResult, BackendError> {
233            Ok(BackendResult::List(self.apps.keys().cloned().collect()))
234        }
235        fn create_participant(&mut self, _: &str, _: u32) -> Result<BackendResult, BackendError> {
236            Ok(BackendResult::Created("p".to_string()))
237        }
238        fn delete_participant(&mut self, _: &str, _: &str) -> Result<BackendResult, BackendError> {
239            Ok(BackendResult::Deleted)
240        }
241        fn create_topic(
242            &mut self,
243            _: &str,
244            _: &str,
245            _: &str,
246            _: &str,
247        ) -> Result<BackendResult, BackendError> {
248            Ok(BackendResult::Created("t".to_string()))
249        }
250        fn create_data_writer(
251            &mut self,
252            _: &str,
253            _: &str,
254            _: &str,
255            _: &str,
256        ) -> Result<BackendResult, BackendError> {
257            Ok(BackendResult::Created("dw".to_string()))
258        }
259        fn create_data_reader(
260            &mut self,
261            _: &str,
262            _: &str,
263            _: &str,
264            _: &str,
265        ) -> Result<BackendResult, BackendError> {
266            Ok(BackendResult::Created("dr".to_string()))
267        }
268        fn write_sample(
269            &mut self,
270            _: &str,
271            _: &str,
272            _: &str,
273            _: &str,
274            _: &[u8],
275        ) -> Result<BackendResult, BackendError> {
276            Ok(BackendResult::Deleted)
277        }
278        fn read_samples(
279            &mut self,
280            _: &str,
281            _: &str,
282            _: &str,
283            _: &str,
284            _: Option<&str>,
285        ) -> Result<BackendResult, BackendError> {
286            Ok(BackendResult::List(Vec::new()))
287        }
288    }
289
290    #[test]
291    fn enforce_permit_allows_call() {
292        assert!(enforce(Decision::Permit).is_ok());
293    }
294
295    #[test]
296    fn enforce_deny_returns_forbidden() {
297        let r = enforce(Decision::Deny);
298        assert!(matches!(r, Err(BackendError::Forbidden)));
299    }
300
301    #[test]
302    fn permission_check_then_backend_call_chains_correctly() {
303        let perms = Permissions {
304            subject_name: "alice".to_string(),
305            default: Decision::Deny,
306            rules: alloc::vec![Rule::allow("*", alloc::vec![Operation::Admin])],
307        };
308        let mut backend = InMemoryBackend::default();
309        // Alice darf eine Application erzeugen.
310        let dec = perms.evaluate(Operation::Admin, "App1");
311        assert!(enforce(dec).is_ok());
312        let r = backend.create_application("App1").expect("ok");
313        assert!(matches!(r, BackendResult::Created(_)));
314    }
315
316    #[test]
317    fn create_then_delete_application_round_trip() {
318        let mut b = InMemoryBackend::default();
319        b.create_application("X").expect("ok");
320        let r = b.delete_application("X").expect("ok");
321        assert_eq!(r, BackendResult::Deleted);
322    }
323
324    #[test]
325    fn create_application_twice_yields_conflict() {
326        let mut b = InMemoryBackend::default();
327        b.create_application("X").expect("ok");
328        let err = b.create_application("X").expect_err("conflict");
329        assert!(matches!(err, BackendError::Conflict(_)));
330    }
331}