Skip to main content

zerodds_web/
rest.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! REST-PSM URI-Routing — Spec §8.3.1 + §8.3.3.
5
6use alloc::string::String;
7
8/// Spec §8.3.1 — URI-Prefix `/dds/rest1`.
9pub const REST_PREFIX: &str = "/dds/rest1";
10
11/// HTTP-Method (Spec §8.3.2 erlaubt POST/PUT/GET/DELETE + HEAD wie GET).
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum RestMethod {
14    /// `POST` — create operations.
15    Post,
16    /// `PUT` — update operations.
17    Put,
18    /// `GET` — read operations.
19    Get,
20    /// `DELETE` — delete operations.
21    Delete,
22    /// `HEAD` — like GET but no body (Spec §8.3.3).
23    Head,
24}
25
26/// Spec §8.3.3 Tab 5 — alle WebDDS-Operations als parametrisierte
27/// Routen.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum RestRoute {
30    // Root operations.
31    /// `POST /applications/` — Root::create_application.
32    CreateApplication,
33    /// `DELETE /applications/<appname>` — Root::delete_application.
34    DeleteApplication {
35        /// Application-Name.
36        app: String,
37    },
38    /// `GET /applications` — Root::get_applications.
39    GetApplications,
40    /// `POST /types` — Root::create_type.
41    CreateType,
42    /// `DELETE /types/<typename>` — Root::delete_type.
43    DeleteType {
44        /// Type-Name.
45        type_name: String,
46    },
47    /// `GET /types` — Root::get_types.
48    GetTypes,
49    /// `POST /qos_libraries` — Root::create_qos_library.
50    CreateQosLibrary,
51    /// `PUT /qos_libraries/<qosLibName>` — Root::update_qos_library.
52    UpdateQosLibrary {
53        /// QosLibrary-Name.
54        qos_lib: String,
55    },
56    /// `DELETE /qos_libraries/<qosLibName>` — Root::delete_qos_library.
57    DeleteQosLibrary {
58        /// QosLibrary-Name.
59        qos_lib: String,
60    },
61    /// `GET /qos_libraries` — Root::get_qos_libraries.
62    GetQosLibraries,
63
64    // QosLibrary operations.
65    /// `POST /qos_libraries/<qosLibName>/qos_profiles` —
66    /// QosLibrary::create_qos_profile.
67    CreateQosProfile {
68        /// QosLibrary-Name.
69        qos_lib: String,
70    },
71    /// `PUT /qos_libraries/<qosLibName>/qos_profiles/<qosProfileName>` —
72    /// QosLibrary::update_qos_profile.
73    UpdateQosProfile {
74        /// QosLibrary-Name.
75        qos_lib: String,
76        /// QosProfile-Name.
77        profile: String,
78    },
79    /// `DELETE /qos_libraries/<qosLibName>/qos_profiles/<qosProfileName>` —
80    /// QosLibrary::delete_qos_profile.
81    DeleteQosProfile {
82        /// QosLibrary-Name.
83        qos_lib: String,
84        /// QosProfile-Name.
85        profile: String,
86    },
87    /// `GET /qos_libraries/<qosLibName>/qos_profiles` —
88    /// QosLibrary::get_qos_profiles.
89    GetQosProfiles {
90        /// QosLibrary-Name.
91        qos_lib: String,
92    },
93
94    // Application operations.
95    /// `POST /applications/<appname>/domain_participants` —
96    /// Application::create_participant.
97    CreateParticipant {
98        /// Application-Name.
99        app: String,
100    },
101    /// `PUT /applications/<appname>/domain_participants/<partname>` —
102    /// Application::update_participant.
103    UpdateParticipant {
104        /// Application-Name.
105        app: String,
106        /// Participant-Name.
107        participant: String,
108    },
109    /// `DELETE /applications/<appname>/domain_participants/<partname>` —
110    /// Application::delete_participant.
111    DeleteParticipant {
112        /// Application-Name.
113        app: String,
114        /// Participant-Name.
115        participant: String,
116    },
117    /// `GET /applications/<appname>/domain_participants` —
118    /// Application::get_participants.
119    GetParticipants {
120        /// Application-Name.
121        app: String,
122    },
123    /// `POST /applications/<appname>/waitsets` —
124    /// Application::create_waitset.
125    CreateWaitset {
126        /// Application-Name.
127        app: String,
128    },
129    /// `GET /applications/<appname>/waitsets/<waitsetname>` —
130    /// Waitset::get.
131    GetWaitset {
132        /// Application-Name.
133        app: String,
134        /// Waitset-Name.
135        waitset: String,
136    },
137
138    // Participant operations.
139    /// `POST /applications/<a>/domain_participants/<p>/topics/` —
140    /// Participant::create_topic.
141    CreateTopic {
142        /// Application-Name.
143        app: String,
144        /// Participant-Name.
145        participant: String,
146    },
147    /// `POST /applications/<a>/domain_participants/<p>/publishers` —
148    /// Participant::create_publisher.
149    CreatePublisher {
150        /// Application-Name.
151        app: String,
152        /// Participant-Name.
153        participant: String,
154    },
155    /// `POST /applications/<a>/domain_participants/<p>/subscribers` —
156    /// Participant::create_subscriber.
157    CreateSubscriber {
158        /// Application-Name.
159        app: String,
160        /// Participant-Name.
161        participant: String,
162    },
163
164    // DataWriter / DataReader operations.
165    /// `POST /applications/<a>/domain_participants/<p>/publishers/<pub>/data_writers/<dwname>` —
166    /// DataWriter::write.
167    DataWriterWrite {
168        /// Application-Name.
169        app: String,
170        /// Participant-Name.
171        participant: String,
172        /// Publisher-Name.
173        publisher: String,
174        /// DataWriter-Name.
175        data_writer: String,
176    },
177    /// `GET /applications/<a>/domain_participants/<p>/subscribers/<sub>/data_readers/<drname>` —
178    /// DataReader::read.
179    DataReaderRead {
180        /// Application-Name.
181        app: String,
182        /// Participant-Name.
183        participant: String,
184        /// Subscriber-Name.
185        subscriber: String,
186        /// DataReader-Name.
187        data_reader: String,
188    },
189
190    /// Beliebige andere URI, die zu keinem Spec-Pattern matched.
191    Unknown {
192        /// Original-Pfad ohne Prefix.
193        path: String,
194    },
195}
196
197/// Parst Method + URI in eine [`RestRoute`].
198///
199/// Erwartet die URI **ohne** Query-String. Caller muss URLs
200/// vorher percent-decoded haben (Spec §8.3.2 verlangt
201/// percent-encoding fuer non-ASCII).
202///
203/// # Errors
204/// Liefert `Err(RouteError::MissingPrefix)` wenn URI nicht mit
205/// `/dds/rest1` beginnt.
206pub fn parse_route(method: RestMethod, uri: &str) -> Result<RestRoute, RouteError> {
207    let path = uri
208        .strip_prefix(REST_PREFIX)
209        .ok_or(RouteError::MissingPrefix)?;
210    let segments: alloc::vec::Vec<&str> = path
211        .trim_start_matches('/')
212        .trim_end_matches('/')
213        .split('/')
214        .filter(|s| !s.is_empty())
215        .collect();
216    Ok(match (method, segments.as_slice()) {
217        // Root.
218        (RestMethod::Post, ["applications"]) => RestRoute::CreateApplication,
219        (RestMethod::Get, ["applications"]) => RestRoute::GetApplications,
220        (RestMethod::Delete, ["applications", app]) => RestRoute::DeleteApplication {
221            app: String::from(*app),
222        },
223        // Types.
224        (RestMethod::Post, ["types"]) => RestRoute::CreateType,
225        (RestMethod::Get, ["types"]) => RestRoute::GetTypes,
226        (RestMethod::Delete, ["types", t]) => RestRoute::DeleteType {
227            type_name: String::from(*t),
228        },
229        // QoS-Library.
230        (RestMethod::Post, ["qos_libraries"]) => RestRoute::CreateQosLibrary,
231        (RestMethod::Get, ["qos_libraries"]) => RestRoute::GetQosLibraries,
232        (RestMethod::Put, ["qos_libraries", lib]) => RestRoute::UpdateQosLibrary {
233            qos_lib: String::from(*lib),
234        },
235        (RestMethod::Delete, ["qos_libraries", lib]) => RestRoute::DeleteQosLibrary {
236            qos_lib: String::from(*lib),
237        },
238        // QoS-Profiles.
239        (RestMethod::Post, ["qos_libraries", lib, "qos_profiles"]) => RestRoute::CreateQosProfile {
240            qos_lib: String::from(*lib),
241        },
242        (RestMethod::Get, ["qos_libraries", lib, "qos_profiles"]) => RestRoute::GetQosProfiles {
243            qos_lib: String::from(*lib),
244        },
245        (RestMethod::Put, ["qos_libraries", lib, "qos_profiles", p]) => {
246            RestRoute::UpdateQosProfile {
247                qos_lib: String::from(*lib),
248                profile: String::from(*p),
249            }
250        }
251        (RestMethod::Delete, ["qos_libraries", lib, "qos_profiles", p]) => {
252            RestRoute::DeleteQosProfile {
253                qos_lib: String::from(*lib),
254                profile: String::from(*p),
255            }
256        }
257        // Application.
258        (RestMethod::Post, ["applications", app, "domain_participants"]) => {
259            RestRoute::CreateParticipant {
260                app: String::from(*app),
261            }
262        }
263        (RestMethod::Get, ["applications", app, "domain_participants"]) => {
264            RestRoute::GetParticipants {
265                app: String::from(*app),
266            }
267        }
268        (RestMethod::Put, ["applications", app, "domain_participants", part]) => {
269            RestRoute::UpdateParticipant {
270                app: String::from(*app),
271                participant: String::from(*part),
272            }
273        }
274        (RestMethod::Delete, ["applications", app, "domain_participants", part]) => {
275            RestRoute::DeleteParticipant {
276                app: String::from(*app),
277                participant: String::from(*part),
278            }
279        }
280        // Waitset.
281        (RestMethod::Post, ["applications", app, "waitsets"]) => RestRoute::CreateWaitset {
282            app: String::from(*app),
283        },
284        (RestMethod::Get, ["applications", app, "waitsets", ws]) => RestRoute::GetWaitset {
285            app: String::from(*app),
286            waitset: String::from(*ws),
287        },
288        // Participant — Topic / Publisher / Subscriber.
289        (RestMethod::Post, ["applications", app, "domain_participants", part, "topics"]) => {
290            RestRoute::CreateTopic {
291                app: String::from(*app),
292                participant: String::from(*part),
293            }
294        }
295        (
296            RestMethod::Post,
297            [
298                "applications",
299                app,
300                "domain_participants",
301                part,
302                "publishers",
303            ],
304        ) => RestRoute::CreatePublisher {
305            app: String::from(*app),
306            participant: String::from(*part),
307        },
308        (
309            RestMethod::Post,
310            [
311                "applications",
312                app,
313                "domain_participants",
314                part,
315                "subscribers",
316            ],
317        ) => RestRoute::CreateSubscriber {
318            app: String::from(*app),
319            participant: String::from(*part),
320        },
321        // DataWriter::write.
322        (
323            RestMethod::Post,
324            [
325                "applications",
326                app,
327                "domain_participants",
328                part,
329                "publishers",
330                pubn,
331                "data_writers",
332                dw,
333            ],
334        ) => RestRoute::DataWriterWrite {
335            app: String::from(*app),
336            participant: String::from(*part),
337            publisher: String::from(*pubn),
338            data_writer: String::from(*dw),
339        },
340        // DataReader::read.
341        (
342            RestMethod::Get,
343            [
344                "applications",
345                app,
346                "domain_participants",
347                part,
348                "subscribers",
349                sub,
350                "data_readers",
351                dr,
352            ],
353        ) => RestRoute::DataReaderRead {
354            app: String::from(*app),
355            participant: String::from(*part),
356            subscriber: String::from(*sub),
357            data_reader: String::from(*dr),
358        },
359        _ => RestRoute::Unknown {
360            path: String::from(path),
361        },
362    })
363}
364
365/// Routing-Fehler.
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub enum RouteError {
368    /// URI hat nicht den Prefix `/dds/rest1`.
369    MissingPrefix,
370}
371
372impl core::fmt::Display for RouteError {
373    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
374        match self {
375            Self::MissingPrefix => f.write_str("URI must start with /dds/rest1"),
376        }
377    }
378}
379
380#[cfg(feature = "std")]
381impl std::error::Error for RouteError {}
382
383#[cfg(test)]
384#[allow(clippy::expect_used, clippy::panic)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn rest_prefix_constant_matches_spec() {
390        assert_eq!(REST_PREFIX, "/dds/rest1");
391    }
392
393    #[test]
394    fn missing_prefix_yields_error() {
395        assert_eq!(
396            parse_route(RestMethod::Get, "/foo/bar"),
397            Err(RouteError::MissingPrefix)
398        );
399    }
400
401    #[test]
402    fn create_application_route() {
403        // Spec §8.3.3 Tab 5 — POST /applications/.
404        let r = parse_route(RestMethod::Post, "/dds/rest1/applications").expect("ok");
405        assert_eq!(r, RestRoute::CreateApplication);
406    }
407
408    #[test]
409    fn get_applications_route() {
410        let r = parse_route(RestMethod::Get, "/dds/rest1/applications").expect("ok");
411        assert_eq!(r, RestRoute::GetApplications);
412    }
413
414    #[test]
415    fn delete_application_route_extracts_app_name() {
416        let r = parse_route(RestMethod::Delete, "/dds/rest1/applications/myapp").expect("ok");
417        assert_eq!(
418            r,
419            RestRoute::DeleteApplication {
420                app: String::from("myapp")
421            }
422        );
423    }
424
425    #[test]
426    fn create_participant_route_extracts_app_name() {
427        let r = parse_route(
428            RestMethod::Post,
429            "/dds/rest1/applications/myapp/domain_participants",
430        )
431        .expect("ok");
432        assert_eq!(
433            r,
434            RestRoute::CreateParticipant {
435                app: String::from("myapp")
436            }
437        );
438    }
439
440    #[test]
441    fn delete_participant_route_extracts_app_and_participant() {
442        let r = parse_route(
443            RestMethod::Delete,
444            "/dds/rest1/applications/myapp/domain_participants/p1",
445        )
446        .expect("ok");
447        assert_eq!(
448            r,
449            RestRoute::DeleteParticipant {
450                app: String::from("myapp"),
451                participant: String::from("p1")
452            }
453        );
454    }
455
456    #[test]
457    fn data_writer_write_route_extracts_4_segments() {
458        let r = parse_route(
459            RestMethod::Post,
460            "/dds/rest1/applications/app/domain_participants/p/publishers/pub/data_writers/dw",
461        )
462        .expect("ok");
463        assert_eq!(
464            r,
465            RestRoute::DataWriterWrite {
466                app: String::from("app"),
467                participant: String::from("p"),
468                publisher: String::from("pub"),
469                data_writer: String::from("dw"),
470            }
471        );
472    }
473
474    #[test]
475    fn data_reader_read_route_extracts_4_segments() {
476        let r = parse_route(
477            RestMethod::Get,
478            "/dds/rest1/applications/app/domain_participants/p/subscribers/sub/data_readers/dr",
479        )
480        .expect("ok");
481        assert_eq!(
482            r,
483            RestRoute::DataReaderRead {
484                app: String::from("app"),
485                participant: String::from("p"),
486                subscriber: String::from("sub"),
487                data_reader: String::from("dr"),
488            }
489        );
490    }
491
492    #[test]
493    fn qos_profile_routes_extract_lib_and_profile_name() {
494        let create = parse_route(
495            RestMethod::Post,
496            "/dds/rest1/qos_libraries/myLib/qos_profiles",
497        )
498        .expect("ok");
499        assert_eq!(
500            create,
501            RestRoute::CreateQosProfile {
502                qos_lib: String::from("myLib")
503            }
504        );
505        let put = parse_route(
506            RestMethod::Put,
507            "/dds/rest1/qos_libraries/myLib/qos_profiles/highQos",
508        )
509        .expect("ok");
510        assert_eq!(
511            put,
512            RestRoute::UpdateQosProfile {
513                qos_lib: String::from("myLib"),
514                profile: String::from("highQos")
515            }
516        );
517    }
518
519    #[test]
520    fn waitset_routes_extract_app_and_waitset() {
521        let create =
522            parse_route(RestMethod::Post, "/dds/rest1/applications/app/waitsets").expect("ok");
523        assert_eq!(
524            create,
525            RestRoute::CreateWaitset {
526                app: String::from("app")
527            }
528        );
529        let get =
530            parse_route(RestMethod::Get, "/dds/rest1/applications/app/waitsets/ws1").expect("ok");
531        assert_eq!(
532            get,
533            RestRoute::GetWaitset {
534                app: String::from("app"),
535                waitset: String::from("ws1"),
536            }
537        );
538    }
539
540    #[test]
541    fn topic_publisher_subscriber_create_routes() {
542        for (segment, expected_variant_test) in [
543            ("topics", "topic"),
544            ("publishers", "publisher"),
545            ("subscribers", "subscriber"),
546        ] {
547            let uri = alloc::format!("/dds/rest1/applications/app/domain_participants/p/{segment}");
548            let r = parse_route(RestMethod::Post, &uri).expect("ok");
549            // Sanity-Check: keine Unknown-Variante.
550            assert!(
551                !matches!(r, RestRoute::Unknown { .. }),
552                "{expected_variant_test} -> got {r:?}"
553            );
554        }
555    }
556
557    #[test]
558    fn unknown_paths_yield_unknown_variant() {
559        let r = parse_route(RestMethod::Get, "/dds/rest1/foobar").expect("ok");
560        match r {
561            RestRoute::Unknown { path } => assert_eq!(path, "/foobar"),
562            _ => panic!("expected unknown"),
563        }
564    }
565
566    #[test]
567    fn types_routes_match_root_pattern() {
568        // Spec §8.3.3 — /types ohne <appname>-Prefix.
569        let create = parse_route(RestMethod::Post, "/dds/rest1/types").expect("ok");
570        assert_eq!(create, RestRoute::CreateType);
571        let delete = parse_route(RestMethod::Delete, "/dds/rest1/types/MyType").expect("ok");
572        assert_eq!(
573            delete,
574            RestRoute::DeleteType {
575                type_name: String::from("MyType")
576            }
577        );
578    }
579}