Skip to main content

zerodds_web/
model.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! WebDDS Object Model — Spec §7.
5
6use alloc::string::String;
7use alloc::vec::Vec;
8
9use crate::session::SessionId;
10use crate::status::ReturnStatus;
11
12/// Spec §7.3.1 — `WebDDS::Root` Singleton. Verwaltet alle Objects.
13#[derive(Debug, Default)]
14pub struct WebDdsRoot {
15    /// Alle registrierten Applications.
16    pub applications: Vec<Application>,
17    /// Alle registrierten Clients.
18    pub clients: Vec<Client>,
19}
20
21impl WebDdsRoot {
22    /// Spec §7.3.1.1 — `create_application`. Liefert
23    /// `OBJECT_ALREADY_EXISTS` wenn Application mit gleichem Namen
24    /// existiert.
25    ///
26    /// # Errors
27    /// Siehe [`ReturnStatus`].
28    pub fn create_application(&mut self, app: Application) -> Result<SessionId, ReturnStatus> {
29        if self.applications.iter().any(|a| a.name == app.name) {
30            return Err(ReturnStatus::ObjectAlreadyExists);
31        }
32        let token = alloc::format!("session-{}", self.applications.len() + 1);
33        self.applications.push(app);
34        Ok(SessionId::new(token))
35    }
36
37    /// Spec §7.3.1.2 — `delete_application`.
38    ///
39    /// # Errors
40    /// `INVALID_OBJECT` wenn Name nicht existiert.
41    pub fn delete_application(&mut self, name: &str) -> Result<(), ReturnStatus> {
42        let idx = self
43            .applications
44            .iter()
45            .position(|a| a.name == name)
46            .ok_or(ReturnStatus::InvalidObject)?;
47        self.applications.remove(idx);
48        Ok(())
49    }
50
51    /// Spec §7.3.1.3 — `get_applications` mit fnmatch-Pattern.
52    ///
53    /// `expression` ist ein POSIX-fnmatch-Pattern (`*` = beliebig,
54    /// `?` = ein Zeichen). Wir implementieren ein vereinfachtes Subset
55    /// mit `*`-Wildcard.
56    #[must_use]
57    pub fn get_applications(&self, expression: &str) -> Vec<&Application> {
58        self.applications
59            .iter()
60            .filter(|a| fnmatch_simple(expression, &a.name))
61            .collect()
62    }
63}
64
65/// Spec §7.3 — `WebDDS::Application` Entity.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Application {
68    /// Name (eindeutig im Root-Scope).
69    pub name: String,
70    /// DomainParticipants der Application.
71    pub participants: Vec<DomainParticipant>,
72}
73
74/// Spec §7.3.1 — `WebDDS::Client` (User-Principal).
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct Client {
77    /// Client-API-Key.
78    pub client_api_key: String,
79}
80
81/// Spec §7.3 — `WebDDS::DomainParticipant`.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct DomainParticipant {
84    /// Name (eindeutig pro Application).
85    pub name: String,
86    /// `domain_id` (Spec §7.3 Tab).
87    pub domain_id: i32,
88}
89
90/// POSIX-fnmatch-Subset (nur `*`-Wildcard, keine `?`/`[...]`-Klassen).
91fn fnmatch_simple(pattern: &str, name: &str) -> bool {
92    if pattern == "*" {
93        return true;
94    }
95    // Einfacher Prefix*-Suffix-Matcher.
96    if let Some(idx) = pattern.find('*') {
97        let (prefix, suffix) = pattern.split_at(idx);
98        let suffix = &suffix[1..]; // skip '*'.
99        return name.starts_with(prefix) && name.ends_with(suffix);
100    }
101    pattern == name
102}
103
104#[cfg(test)]
105#[allow(clippy::expect_used)]
106mod tests {
107    use super::*;
108
109    fn app(name: &str) -> Application {
110        Application {
111            name: String::from(name),
112            participants: Vec::new(),
113        }
114    }
115
116    #[test]
117    fn create_application_returns_session_id() {
118        // Spec §7.3.1.1.
119        let mut root = WebDdsRoot::default();
120        let s = root.create_application(app("app1")).expect("ok");
121        assert!(s.is_valid());
122    }
123
124    #[test]
125    fn duplicate_application_yields_object_already_exists() {
126        // Spec — same name → OBJECT_ALREADY_EXISTS.
127        let mut root = WebDdsRoot::default();
128        root.create_application(app("dup")).expect("first ok");
129        assert_eq!(
130            root.create_application(app("dup")),
131            Err(ReturnStatus::ObjectAlreadyExists)
132        );
133    }
134
135    #[test]
136    fn delete_application_removes_existing() {
137        let mut root = WebDdsRoot::default();
138        root.create_application(app("a")).expect("ok");
139        assert_eq!(root.delete_application("a"), Ok(()));
140        assert!(root.applications.is_empty());
141    }
142
143    #[test]
144    fn delete_unknown_application_yields_invalid_object() {
145        let mut root = WebDdsRoot::default();
146        assert_eq!(
147            root.delete_application("missing"),
148            Err(ReturnStatus::InvalidObject)
149        );
150    }
151
152    #[test]
153    fn get_applications_with_wildcard_matches_all() {
154        // Spec §7.3.1.3 — fnmatch.
155        let mut root = WebDdsRoot::default();
156        root.create_application(app("a1")).expect("ok");
157        root.create_application(app("a2")).expect("ok");
158        let all = root.get_applications("*");
159        assert_eq!(all.len(), 2);
160    }
161
162    #[test]
163    fn get_applications_with_prefix_pattern_matches_subset() {
164        let mut root = WebDdsRoot::default();
165        root.create_application(app("temp_sensor")).expect("ok");
166        root.create_application(app("temp_actuator")).expect("ok");
167        root.create_application(app("pressure_sensor")).expect("ok");
168        let temps = root.get_applications("temp_*");
169        assert_eq!(temps.len(), 2);
170    }
171
172    #[test]
173    fn get_applications_with_exact_match() {
174        let mut root = WebDdsRoot::default();
175        root.create_application(app("exactly")).expect("ok");
176        root.create_application(app("else")).expect("ok");
177        let one = root.get_applications("exactly");
178        assert_eq!(one.len(), 1);
179        assert_eq!(one[0].name, "exactly");
180    }
181
182    #[test]
183    fn application_carries_participants() {
184        let a = Application {
185            name: String::from("trader"),
186            participants: alloc::vec![
187                DomainParticipant {
188                    name: String::from("p0"),
189                    domain_id: 0,
190                },
191                DomainParticipant {
192                    name: String::from("p1"),
193                    domain_id: 1,
194                },
195            ],
196        };
197        assert_eq!(a.participants.len(), 2);
198        assert_eq!(a.participants[0].domain_id, 0);
199    }
200}