Skip to main content

qubit_fs/metadata/
user_metadata.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Provider-neutral user metadata.
9
10use std::collections::BTreeMap;
11use std::fmt::Debug;
12use std::fmt::Formatter;
13use std::fmt::Result as FmtResult;
14
15use crate::error::FsError;
16use crate::error::FsErrorKind;
17use crate::error::FsOperation;
18
19/// An ordered string-to-string metadata map with safe structural formatting.
20///
21/// # Examples
22///
23/// ```
24/// use qubit_fs::metadata::UserMetadata;
25///
26/// let metadata = UserMetadata::new().with("content-language", "en")?;
27/// assert_eq!(Some("en"), metadata.get("content-language"));
28/// # Ok::<(), qubit_fs::FsError>(())
29/// ```
30#[derive(Clone, Default, Eq, PartialEq)]
31pub struct UserMetadata(
32    /// Ordered metadata pairs retained without automatic value formatting.
33    BTreeMap<String, String>,
34);
35
36impl UserMetadata {
37    /// Creates empty metadata.
38    #[must_use]
39    #[inline]
40    pub const fn new() -> Self {
41        Self(BTreeMap::new())
42    }
43
44    /// Adds one metadata pair.
45    ///
46    /// # Errors
47    /// Returns an invalid-options error when the key resembles credential
48    /// material.
49    pub fn with(mut self, key: &str, value: &str) -> Result<Self, FsError> {
50        if crate::uri::query_pair_is_sensitive(key) {
51            return Err(FsError::new(
52                FsErrorKind::InvalidOptions,
53                FsOperation::Other,
54                "credential-like metadata keys are forbidden",
55            ));
56        }
57        self.0.insert(key.to_owned(), value.to_owned());
58        Ok(self)
59    }
60
61    /// Returns the value associated with a key.
62    #[must_use]
63    #[inline]
64    pub fn get(&self, key: &str) -> Option<&str> {
65        self.0.get(key).map(String::as_str)
66    }
67
68    /// Returns whether the map contains no metadata pairs.
69    #[must_use]
70    #[inline]
71    pub fn is_empty(&self) -> bool {
72        self.0.is_empty()
73    }
74
75    /// Returns whether a metadata key is present.
76    #[must_use]
77    #[inline]
78    pub fn contains_key(&self, key: &str) -> bool {
79        self.0.contains_key(key)
80    }
81
82    /// Returns an iterator over metadata pairs.
83    #[inline]
84    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
85        self.0.iter().map(|(key, value)| (key.as_str(), value.as_str()))
86    }
87}
88
89impl Debug for UserMetadata {
90    #[inline]
91    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
92        formatter
93            .debug_struct("UserMetadata")
94            .field("keys", &self.0.keys().collect::<Vec<_>>())
95            .finish()
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::UserMetadata;
102
103    #[test]
104    fn metadata_debug_and_iteration_are_executed_at_runtime() {
105        let metadata = UserMetadata::new()
106            .with("provider", "test")
107            .expect("ordinary metadata key should be accepted");
108        assert_eq!(metadata.get("provider"), Some("test"));
109        assert!(metadata.contains_key("provider"));
110        assert_eq!(metadata.iter().collect::<Vec<_>>(), vec![("provider", "test")]);
111        assert!(format!("{metadata:?}").contains("UserMetadata"));
112    }
113}