Skip to main content

zerodds_webdds_bridge/
metadata.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Spec §7.3.1 `Root` metadata operations that are not DCPS entities:
5//! `create/update/delete/get_qos_library`, `create/delete/get_qos_profile`,
6//! `create/delete/get_type`. A `QosLibrary`/`QosProfile`/registered
7//! `Type` is a named document the daemon stores and serves back — no
8//! DCPS round-trip, so it stays out of `zerodds_web::DdsBackend` (whose
9//! doc comment scopes it to §7.4 entity operations).
10//!
11//! In-memory, process-lifetime store. Concurrency: one `Mutex` per
12//! collection, held only for the duration of the map operation.
13
14use std::collections::BTreeMap;
15use std::sync::Mutex;
16
17/// A stored resource: its XML body plus the content type it was stored
18/// under (mirrors `zerodds_web::bridge::BackendResult::Body`).
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct StoredDocument {
21    /// MIME type, e.g. `application/dds-web+xml`.
22    pub content_type: String,
23    /// Raw body bytes.
24    pub body: Vec<u8>,
25}
26
27/// Error from the metadata store.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum MetadataError {
30    /// The name already exists (create).
31    Conflict(String),
32    /// The name does not exist (update/delete/get).
33    NotFound(String),
34}
35
36impl std::fmt::Display for MetadataError {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            Self::Conflict(n) => write!(f, "`{n}` already exists"),
40            Self::NotFound(n) => write!(f, "`{n}` not found"),
41        }
42    }
43}
44
45impl std::error::Error for MetadataError {}
46
47/// One named collection (QoS libraries, or the profiles of one
48/// library, or the registered types) — `create` refuses a duplicate
49/// name, `update`/`delete`/`get` refuse a missing one.
50#[derive(Default)]
51pub struct NamedStore(Mutex<BTreeMap<String, StoredDocument>>);
52
53impl NamedStore {
54    /// Empty store.
55    #[must_use]
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// # Errors
61    /// [`MetadataError::Conflict`] if `name` already exists.
62    pub fn create(&self, name: &str, doc: StoredDocument) -> Result<(), MetadataError> {
63        let mut map = self
64            .0
65            .lock()
66            .unwrap_or_else(std::sync::PoisonError::into_inner);
67        if map.contains_key(name) {
68            return Err(MetadataError::Conflict(name.to_string()));
69        }
70        map.insert(name.to_string(), doc);
71        Ok(())
72    }
73
74    /// # Errors
75    /// [`MetadataError::NotFound`] if `name` does not exist.
76    pub fn update(&self, name: &str, doc: StoredDocument) -> Result<(), MetadataError> {
77        let mut map = self
78            .0
79            .lock()
80            .unwrap_or_else(std::sync::PoisonError::into_inner);
81        if !map.contains_key(name) {
82            return Err(MetadataError::NotFound(name.to_string()));
83        }
84        map.insert(name.to_string(), doc);
85        Ok(())
86    }
87
88    /// # Errors
89    /// [`MetadataError::NotFound`] if `name` does not exist.
90    pub fn delete(&self, name: &str) -> Result<(), MetadataError> {
91        let mut map = self
92            .0
93            .lock()
94            .unwrap_or_else(std::sync::PoisonError::into_inner);
95        if map.remove(name).is_none() {
96            return Err(MetadataError::NotFound(name.to_string()));
97        }
98        Ok(())
99    }
100
101    /// # Errors
102    /// [`MetadataError::NotFound`] if `name` does not exist.
103    pub fn get(&self, name: &str) -> Result<StoredDocument, MetadataError> {
104        let map = self
105            .0
106            .lock()
107            .unwrap_or_else(std::sync::PoisonError::into_inner);
108        map.get(name)
109            .cloned()
110            .ok_or_else(|| MetadataError::NotFound(name.to_string()))
111    }
112
113    /// Every stored name, in order.
114    #[must_use]
115    pub fn names(&self) -> Vec<String> {
116        let map = self
117            .0
118            .lock()
119            .unwrap_or_else(std::sync::PoisonError::into_inner);
120        map.keys().cloned().collect()
121    }
122}
123
124/// The three Spec §7.3.1 metadata collections a `Gateway` serves beside
125/// its DCPS-backed `DdsBackend` operations.
126#[derive(Default)]
127pub struct MetadataStore {
128    /// `Root::{create,update,delete,get}_qos_library`.
129    pub qos_libraries: NamedStore,
130    /// `QosLibrary::{create,update,delete,get}_qos_profile`, keyed
131    /// `<library>/<profile>` — the profile namespace is per-library
132    /// (Spec §7.3.1), so a flat map needs the composite key.
133    pub qos_profiles: NamedStore,
134    /// `Root::{create,delete,get}_type`.
135    pub types: NamedStore,
136}
137
138impl MetadataStore {
139    /// Empty store.
140    #[must_use]
141    pub fn new() -> Self {
142        Self::default()
143    }
144
145    /// The composite key `qos_profiles` uses.
146    #[must_use]
147    pub fn profile_key(library: &str, profile: &str) -> String {
148        format!("{library}/{profile}")
149    }
150}
151
152#[cfg(test)]
153#[allow(clippy::unwrap_used, clippy::expect_used)]
154mod tests {
155    use super::*;
156
157    fn doc(text: &str) -> StoredDocument {
158        StoredDocument {
159            content_type: "application/dds-web+xml".to_string(),
160            body: text.as_bytes().to_vec(),
161        }
162    }
163
164    #[test]
165    fn create_then_get_round_trips() {
166        let s = NamedStore::new();
167        s.create("HighQos", doc("<qos_library/>")).unwrap();
168        assert_eq!(s.get("HighQos").unwrap(), doc("<qos_library/>"));
169    }
170
171    #[test]
172    fn create_twice_is_conflict() {
173        let s = NamedStore::new();
174        s.create("A", doc("x")).unwrap();
175        assert_eq!(
176            s.create("A", doc("y")).unwrap_err(),
177            MetadataError::Conflict("A".to_string())
178        );
179    }
180
181    #[test]
182    fn update_missing_is_not_found() {
183        let s = NamedStore::new();
184        assert_eq!(
185            s.update("A", doc("x")).unwrap_err(),
186            MetadataError::NotFound("A".to_string())
187        );
188    }
189
190    #[test]
191    fn delete_removes_and_get_then_fails() {
192        let s = NamedStore::new();
193        s.create("A", doc("x")).unwrap();
194        s.delete("A").unwrap();
195        assert!(matches!(s.get("A"), Err(MetadataError::NotFound(_))));
196    }
197
198    #[test]
199    fn names_lists_every_entry() {
200        let s = NamedStore::new();
201        s.create("B", doc("x")).unwrap();
202        s.create("A", doc("x")).unwrap();
203        assert_eq!(s.names(), vec!["A".to_string(), "B".to_string()]);
204    }
205
206    #[test]
207    fn profile_key_is_library_slash_profile() {
208        assert_eq!(MetadataStore::profile_key("Lib", "High"), "Lib/High");
209    }
210
211    #[test]
212    fn profiles_of_different_libraries_do_not_collide() {
213        let store = MetadataStore::new();
214        store
215            .qos_profiles
216            .create(&MetadataStore::profile_key("LibA", "P"), doc("a"))
217            .unwrap();
218        store
219            .qos_profiles
220            .create(&MetadataStore::profile_key("LibB", "P"), doc("b"))
221            .unwrap();
222        assert_eq!(
223            store
224                .qos_profiles
225                .get(&MetadataStore::profile_key("LibA", "P"))
226                .unwrap(),
227            doc("a")
228        );
229    }
230}