agentroot_core/db/
metadata.rs

1//! User-defined metadata system
2//!
3//! Supports typed metadata fields that users can add manually to documents.
4//! This complements the LLM-generated metadata and enables rich filtering/querying.
5
6use crate::error::{AgentRootError, Result};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Metadata value types
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13#[serde(tag = "type", content = "value")]
14pub enum MetadataValue {
15    /// Text string
16    Text(String),
17
18    /// Integer number
19    Integer(i64),
20
21    /// Floating point number
22    Float(f64),
23
24    /// Boolean value
25    Boolean(bool),
26
27    /// ISO 8601 timestamp
28    DateTime(String),
29
30    /// Array of strings (tags, labels)
31    Tags(Vec<String>),
32
33    /// Enum value (predefined set of options)
34    Enum { value: String, options: Vec<String> },
35
36    /// Qualitative measure (e.g., "high", "medium", "low")
37    Qualitative { value: String, scale: Vec<String> },
38
39    /// Quantitative measure with unit
40    Quantitative { value: f64, unit: String },
41
42    /// JSON object for complex nested data
43    Json(serde_json::Value),
44}
45
46impl MetadataValue {
47    /// Create a datetime value from timestamp
48    pub fn datetime_now() -> Self {
49        MetadataValue::DateTime(Utc::now().to_rfc3339())
50    }
51
52    /// Create a datetime value from a DateTime
53    pub fn datetime(dt: DateTime<Utc>) -> Self {
54        MetadataValue::DateTime(dt.to_rfc3339())
55    }
56
57    /// Create tags from strings
58    pub fn tags<I, S>(iter: I) -> Self
59    where
60        I: IntoIterator<Item = S>,
61        S: Into<String>,
62    {
63        MetadataValue::Tags(iter.into_iter().map(|s| s.into()).collect())
64    }
65
66    /// Create enum with validation
67    pub fn enum_value(value: impl Into<String>, options: Vec<String>) -> Result<Self> {
68        let value = value.into();
69        if !options.contains(&value) {
70            return Err(AgentRootError::InvalidInput(format!(
71                "Invalid enum value '{}'. Must be one of: {:?}",
72                value, options
73            )));
74        }
75        Ok(MetadataValue::Enum { value, options })
76    }
77
78    /// Create qualitative value
79    pub fn qualitative(value: impl Into<String>, scale: Vec<String>) -> Result<Self> {
80        let value = value.into();
81        if !scale.contains(&value) {
82            return Err(AgentRootError::InvalidInput(format!(
83                "Invalid qualitative value '{}'. Must be one of: {:?}",
84                value, scale
85            )));
86        }
87        Ok(MetadataValue::Qualitative { value, scale })
88    }
89
90    /// Create quantitative value
91    pub fn quantitative(value: f64, unit: impl Into<String>) -> Self {
92        MetadataValue::Quantitative {
93            value,
94            unit: unit.into(),
95        }
96    }
97}
98
99/// User-defined metadata for a document
100#[derive(Debug, Clone, Serialize, Deserialize, Default)]
101pub struct UserMetadata {
102    /// Field name -> value mapping
103    pub fields: HashMap<String, MetadataValue>,
104}
105
106impl UserMetadata {
107    /// Create empty metadata
108    pub fn new() -> Self {
109        Self {
110            fields: HashMap::new(),
111        }
112    }
113
114    /// Add a metadata field
115    pub fn add(&mut self, key: impl Into<String>, value: MetadataValue) -> &mut Self {
116        self.fields.insert(key.into(), value);
117        self
118    }
119
120    /// Get a metadata field
121    pub fn get(&self, key: &str) -> Option<&MetadataValue> {
122        self.fields.get(key)
123    }
124
125    /// Remove a metadata field
126    pub fn remove(&mut self, key: &str) -> Option<MetadataValue> {
127        self.fields.remove(key)
128    }
129
130    /// Check if a field exists
131    pub fn contains(&self, key: &str) -> bool {
132        self.fields.contains_key(key)
133    }
134
135    /// Serialize to JSON string
136    pub fn to_json(&self) -> Result<String> {
137        serde_json::to_string(&self.fields).map_err(|e| e.into())
138    }
139
140    /// Deserialize from JSON string
141    pub fn from_json(json: &str) -> Result<Self> {
142        let fields = serde_json::from_str(json)?;
143        Ok(Self { fields })
144    }
145
146    /// Merge with another metadata instance (other takes precedence)
147    pub fn merge(&mut self, other: &UserMetadata) {
148        for (key, value) in &other.fields {
149            self.fields.insert(key.clone(), value.clone());
150        }
151    }
152}
153
154/// Builder for constructing metadata
155pub struct MetadataBuilder {
156    metadata: UserMetadata,
157}
158
159impl MetadataBuilder {
160    /// Create a new builder
161    pub fn new() -> Self {
162        Self {
163            metadata: UserMetadata::new(),
164        }
165    }
166
167    /// Add text field
168    pub fn text(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
169        self.metadata.add(key, MetadataValue::Text(value.into()));
170        self
171    }
172
173    /// Add integer field
174    pub fn integer(mut self, key: impl Into<String>, value: i64) -> Self {
175        self.metadata.add(key, MetadataValue::Integer(value));
176        self
177    }
178
179    /// Add float field
180    pub fn float(mut self, key: impl Into<String>, value: f64) -> Self {
181        self.metadata.add(key, MetadataValue::Float(value));
182        self
183    }
184
185    /// Add boolean field
186    pub fn boolean(mut self, key: impl Into<String>, value: bool) -> Self {
187        self.metadata.add(key, MetadataValue::Boolean(value));
188        self
189    }
190
191    /// Add datetime field (now)
192    pub fn datetime_now(mut self, key: impl Into<String>) -> Self {
193        self.metadata.add(key, MetadataValue::datetime_now());
194        self
195    }
196
197    /// Add datetime field
198    pub fn datetime(mut self, key: impl Into<String>, dt: DateTime<Utc>) -> Self {
199        self.metadata.add(key, MetadataValue::datetime(dt));
200        self
201    }
202
203    /// Add tags field
204    pub fn tags<I, S>(mut self, key: impl Into<String>, tags: I) -> Self
205    where
206        I: IntoIterator<Item = S>,
207        S: Into<String>,
208    {
209        self.metadata.add(key, MetadataValue::tags(tags));
210        self
211    }
212
213    /// Add enum field
214    pub fn enum_value(
215        mut self,
216        key: impl Into<String>,
217        value: impl Into<String>,
218        options: Vec<String>,
219    ) -> Result<Self> {
220        let metadata_value = MetadataValue::enum_value(value, options)?;
221        self.metadata.add(key, metadata_value);
222        Ok(self)
223    }
224
225    /// Add qualitative field
226    pub fn qualitative(
227        mut self,
228        key: impl Into<String>,
229        value: impl Into<String>,
230        scale: Vec<String>,
231    ) -> Result<Self> {
232        let metadata_value = MetadataValue::qualitative(value, scale)?;
233        self.metadata.add(key, metadata_value);
234        Ok(self)
235    }
236
237    /// Add quantitative field
238    pub fn quantitative(
239        mut self,
240        key: impl Into<String>,
241        value: f64,
242        unit: impl Into<String>,
243    ) -> Self {
244        self.metadata
245            .add(key, MetadataValue::quantitative(value, unit));
246        self
247    }
248
249    /// Add JSON field
250    pub fn json(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
251        self.metadata.add(key, MetadataValue::Json(value));
252        self
253    }
254
255    /// Build the metadata
256    pub fn build(self) -> UserMetadata {
257        self.metadata
258    }
259}
260
261impl Default for MetadataBuilder {
262    fn default() -> Self {
263        Self::new()
264    }
265}
266
267/// Metadata query filter
268#[derive(Debug, Clone)]
269pub enum MetadataFilter {
270    /// Text equals
271    TextEq(String, String),
272
273    /// Text contains
274    TextContains(String, String),
275
276    /// Integer comparison
277    IntegerEq(String, i64),
278    IntegerGt(String, i64),
279    IntegerLt(String, i64),
280    IntegerRange(String, i64, i64),
281
282    /// Float comparison
283    FloatEq(String, f64),
284    FloatGt(String, f64),
285    FloatLt(String, f64),
286    FloatRange(String, f64, f64),
287
288    /// Boolean equals
289    BooleanEq(String, bool),
290
291    /// DateTime comparison
292    DateTimeAfter(String, String),
293    DateTimeBefore(String, String),
294    DateTimeRange(String, String, String),
295
296    /// Tags contains
297    TagsContain(String, String),
298    TagsContainAll(String, Vec<String>),
299    TagsContainAny(String, Vec<String>),
300
301    /// Enum equals
302    EnumEq(String, String),
303
304    /// Field exists
305    Exists(String),
306
307    /// AND combination
308    And(Vec<MetadataFilter>),
309
310    /// OR combination
311    Or(Vec<MetadataFilter>),
312
313    /// NOT
314    Not(Box<MetadataFilter>),
315}
316
317impl MetadataFilter {
318    /// Check if metadata matches this filter
319    pub fn matches(&self, metadata: &UserMetadata) -> bool {
320        match self {
321            MetadataFilter::TextEq(key, value) => {
322                matches!(metadata.get(key), Some(MetadataValue::Text(v)) if v == value)
323            }
324            MetadataFilter::TextContains(key, substring) => {
325                matches!(metadata.get(key), Some(MetadataValue::Text(v)) if v.contains(substring))
326            }
327            MetadataFilter::IntegerEq(key, value) => {
328                matches!(metadata.get(key), Some(MetadataValue::Integer(v)) if v == value)
329            }
330            MetadataFilter::IntegerGt(key, value) => {
331                matches!(metadata.get(key), Some(MetadataValue::Integer(v)) if v > value)
332            }
333            MetadataFilter::IntegerLt(key, value) => {
334                matches!(metadata.get(key), Some(MetadataValue::Integer(v)) if v < value)
335            }
336            MetadataFilter::IntegerRange(key, min, max) => {
337                matches!(metadata.get(key), Some(MetadataValue::Integer(v)) if v >= min && v <= max)
338            }
339            MetadataFilter::BooleanEq(key, value) => {
340                matches!(metadata.get(key), Some(MetadataValue::Boolean(v)) if v == value)
341            }
342            MetadataFilter::TagsContain(key, tag) => {
343                matches!(metadata.get(key), Some(MetadataValue::Tags(tags)) if tags.contains(tag))
344            }
345            MetadataFilter::TagsContainAll(key, search_tags) => {
346                matches!(metadata.get(key), Some(MetadataValue::Tags(tags))
347                    if search_tags.iter().all(|t| tags.contains(t)))
348            }
349            MetadataFilter::TagsContainAny(key, search_tags) => {
350                matches!(metadata.get(key), Some(MetadataValue::Tags(tags))
351                    if search_tags.iter().any(|t| tags.contains(t)))
352            }
353            MetadataFilter::EnumEq(key, value) => {
354                matches!(metadata.get(key), Some(MetadataValue::Enum { value: v, .. }) if v == value)
355            }
356            MetadataFilter::Exists(key) => metadata.contains(key),
357            MetadataFilter::And(filters) => filters.iter().all(|f| f.matches(metadata)),
358            MetadataFilter::Or(filters) => filters.iter().any(|f| f.matches(metadata)),
359            MetadataFilter::Not(filter) => !filter.matches(metadata),
360            _ => false, // Other filters need proper implementation
361        }
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn test_metadata_builder() {
371        let metadata = MetadataBuilder::new()
372            .text("author", "John Doe")
373            .integer("version", 2)
374            .float("score", 4.5)
375            .boolean("published", true)
376            .datetime_now("created_at")
377            .tags("labels", vec!["rust", "programming"])
378            .quantitative("size", 1024.0, "KB")
379            .build();
380
381        assert_eq!(
382            metadata.get("author"),
383            Some(&MetadataValue::Text("John Doe".to_string()))
384        );
385        assert_eq!(metadata.get("version"), Some(&MetadataValue::Integer(2)));
386        assert!(metadata.contains("created_at"));
387    }
388
389    #[test]
390    fn test_enum_validation() {
391        let result =
392            MetadataValue::enum_value("active", vec!["draft".to_string(), "published".to_string()]);
393        assert!(result.is_err());
394
395        let result = MetadataValue::enum_value(
396            "published",
397            vec!["draft".to_string(), "published".to_string()],
398        );
399        assert!(result.is_ok());
400    }
401
402    #[test]
403    fn test_metadata_filter() {
404        let metadata = MetadataBuilder::new()
405            .text("status", "published")
406            .tags("labels", vec!["rust", "tutorial"])
407            .integer("views", 100)
408            .build();
409
410        assert!(
411            MetadataFilter::TextEq("status".to_string(), "published".to_string())
412                .matches(&metadata)
413        );
414        assert!(
415            MetadataFilter::TagsContain("labels".to_string(), "rust".to_string())
416                .matches(&metadata)
417        );
418        assert!(MetadataFilter::IntegerGt("views".to_string(), 50).matches(&metadata));
419    }
420
421    #[test]
422    fn test_metadata_merge() {
423        let mut meta1 = MetadataBuilder::new()
424            .text("author", "Alice")
425            .integer("version", 1)
426            .build();
427
428        let meta2 = MetadataBuilder::new()
429            .text("author", "Bob")
430            .tags("labels", vec!["rust"])
431            .build();
432
433        meta1.merge(&meta2);
434
435        assert_eq!(
436            meta1.get("author"),
437            Some(&MetadataValue::Text("Bob".to_string()))
438        );
439        assert!(meta1.contains("labels"));
440        assert!(meta1.contains("version"));
441    }
442
443    #[test]
444    fn test_json_serialization() {
445        let metadata = MetadataBuilder::new()
446            .text("name", "Test")
447            .integer("count", 42)
448            .build();
449
450        let json = metadata.to_json().unwrap();
451        let restored = UserMetadata::from_json(&json).unwrap();
452
453        assert_eq!(metadata.fields, restored.fields);
454    }
455}