Skip to main content

wedeo_core/
metadata.rs

1/// Ordered metadata container using `Vec<(String, String)>` for deterministic
2/// ordering (required for FATE test parity). Case-insensitive key lookup.
3#[derive(Debug, Clone, Default)]
4pub struct Metadata {
5    entries: Vec<(String, String)>,
6}
7
8impl Metadata {
9    pub fn new() -> Self {
10        Self {
11            entries: Vec::new(),
12        }
13    }
14
15    /// Set a metadata key-value pair. If the key already exists (case-insensitive),
16    /// the existing value is replaced. Otherwise, the pair is appended.
17    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
18        let key = key.into();
19        let value = value.into();
20        if let Some(entry) = self
21            .entries
22            .iter_mut()
23            .find(|(k, _)| k.eq_ignore_ascii_case(&key))
24        {
25            entry.1 = value;
26        } else {
27            self.entries.push((key, value));
28        }
29    }
30
31    /// Get the value for a key (case-insensitive).
32    pub fn get(&self, key: &str) -> Option<&str> {
33        self.entries
34            .iter()
35            .find(|(k, _)| k.eq_ignore_ascii_case(key))
36            .map(|(_, v)| v.as_str())
37    }
38
39    /// Remove a key (case-insensitive). Returns the removed value if found.
40    pub fn remove(&mut self, key: &str) -> Option<String> {
41        if let Some(pos) = self
42            .entries
43            .iter()
44            .position(|(k, _)| k.eq_ignore_ascii_case(key))
45        {
46            Some(self.entries.remove(pos).1)
47        } else {
48            None
49        }
50    }
51
52    /// Iterate over all entries in insertion order.
53    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
54        self.entries.iter().map(|(k, v)| (k.as_str(), v.as_str()))
55    }
56
57    /// Number of entries.
58    pub fn len(&self) -> usize {
59        self.entries.len()
60    }
61
62    /// Returns true if empty.
63    pub fn is_empty(&self) -> bool {
64        self.entries.is_empty()
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_metadata_basic() {
74        let mut m = Metadata::new();
75        m.set("title", "Test Video");
76        m.set("artist", "Wedeo");
77        assert_eq!(m.get("title"), Some("Test Video"));
78        assert_eq!(m.get("TITLE"), Some("Test Video")); // case-insensitive
79        assert_eq!(m.len(), 2);
80    }
81
82    #[test]
83    fn test_metadata_overwrite() {
84        let mut m = Metadata::new();
85        m.set("title", "First");
86        m.set("TITLE", "Second"); // should overwrite
87        assert_eq!(m.get("title"), Some("Second"));
88        assert_eq!(m.len(), 1);
89    }
90
91    #[test]
92    fn test_metadata_order() {
93        let mut m = Metadata::new();
94        m.set("b", "2");
95        m.set("a", "1");
96        m.set("c", "3");
97        let keys: Vec<&str> = m.iter().map(|(k, _)| k).collect();
98        assert_eq!(keys, vec!["b", "a", "c"]); // insertion order preserved
99    }
100
101    #[test]
102    fn test_metadata_remove() {
103        let mut m = Metadata::new();
104        m.set("key", "value");
105        assert_eq!(m.remove("KEY"), Some("value".to_string()));
106        assert!(m.is_empty());
107    }
108}