use std::collections::BTreeMap;
use std::sync::Mutex;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredDocument {
pub content_type: String,
pub body: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetadataError {
Conflict(String),
NotFound(String),
}
impl std::fmt::Display for MetadataError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Conflict(n) => write!(f, "`{n}` already exists"),
Self::NotFound(n) => write!(f, "`{n}` not found"),
}
}
}
impl std::error::Error for MetadataError {}
#[derive(Default)]
pub struct NamedStore(Mutex<BTreeMap<String, StoredDocument>>);
impl NamedStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn create(&self, name: &str, doc: StoredDocument) -> Result<(), MetadataError> {
let mut map = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if map.contains_key(name) {
return Err(MetadataError::Conflict(name.to_string()));
}
map.insert(name.to_string(), doc);
Ok(())
}
pub fn update(&self, name: &str, doc: StoredDocument) -> Result<(), MetadataError> {
let mut map = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !map.contains_key(name) {
return Err(MetadataError::NotFound(name.to_string()));
}
map.insert(name.to_string(), doc);
Ok(())
}
pub fn delete(&self, name: &str) -> Result<(), MetadataError> {
let mut map = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if map.remove(name).is_none() {
return Err(MetadataError::NotFound(name.to_string()));
}
Ok(())
}
pub fn get(&self, name: &str) -> Result<StoredDocument, MetadataError> {
let map = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.get(name)
.cloned()
.ok_or_else(|| MetadataError::NotFound(name.to_string()))
}
#[must_use]
pub fn names(&self) -> Vec<String> {
let map = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.keys().cloned().collect()
}
}
#[derive(Default)]
pub struct MetadataStore {
pub qos_libraries: NamedStore,
pub qos_profiles: NamedStore,
pub types: NamedStore,
}
impl MetadataStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn profile_key(library: &str, profile: &str) -> String {
format!("{library}/{profile}")
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn doc(text: &str) -> StoredDocument {
StoredDocument {
content_type: "application/dds-web+xml".to_string(),
body: text.as_bytes().to_vec(),
}
}
#[test]
fn create_then_get_round_trips() {
let s = NamedStore::new();
s.create("HighQos", doc("<qos_library/>")).unwrap();
assert_eq!(s.get("HighQos").unwrap(), doc("<qos_library/>"));
}
#[test]
fn create_twice_is_conflict() {
let s = NamedStore::new();
s.create("A", doc("x")).unwrap();
assert_eq!(
s.create("A", doc("y")).unwrap_err(),
MetadataError::Conflict("A".to_string())
);
}
#[test]
fn update_missing_is_not_found() {
let s = NamedStore::new();
assert_eq!(
s.update("A", doc("x")).unwrap_err(),
MetadataError::NotFound("A".to_string())
);
}
#[test]
fn delete_removes_and_get_then_fails() {
let s = NamedStore::new();
s.create("A", doc("x")).unwrap();
s.delete("A").unwrap();
assert!(matches!(s.get("A"), Err(MetadataError::NotFound(_))));
}
#[test]
fn names_lists_every_entry() {
let s = NamedStore::new();
s.create("B", doc("x")).unwrap();
s.create("A", doc("x")).unwrap();
assert_eq!(s.names(), vec!["A".to_string(), "B".to_string()]);
}
#[test]
fn profile_key_is_library_slash_profile() {
assert_eq!(MetadataStore::profile_key("Lib", "High"), "Lib/High");
}
#[test]
fn profiles_of_different_libraries_do_not_collide() {
let store = MetadataStore::new();
store
.qos_profiles
.create(&MetadataStore::profile_key("LibA", "P"), doc("a"))
.unwrap();
store
.qos_profiles
.create(&MetadataStore::profile_key("LibB", "P"), doc("b"))
.unwrap();
assert_eq!(
store
.qos_profiles
.get(&MetadataStore::profile_key("LibA", "P"))
.unwrap(),
doc("a")
);
}
}