1use crate::error::{AgentRootError, Result};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13#[serde(tag = "type", content = "value")]
14pub enum MetadataValue {
15 Text(String),
17
18 Integer(i64),
20
21 Float(f64),
23
24 Boolean(bool),
26
27 DateTime(String),
29
30 Tags(Vec<String>),
32
33 Enum { value: String, options: Vec<String> },
35
36 Qualitative { value: String, scale: Vec<String> },
38
39 Quantitative { value: f64, unit: String },
41
42 Json(serde_json::Value),
44}
45
46impl MetadataValue {
47 pub fn datetime_now() -> Self {
49 MetadataValue::DateTime(Utc::now().to_rfc3339())
50 }
51
52 pub fn datetime(dt: DateTime<Utc>) -> Self {
54 MetadataValue::DateTime(dt.to_rfc3339())
55 }
56
57 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 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 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 pub fn quantitative(value: f64, unit: impl Into<String>) -> Self {
92 MetadataValue::Quantitative {
93 value,
94 unit: unit.into(),
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, Default)]
101pub struct UserMetadata {
102 pub fields: HashMap<String, MetadataValue>,
104}
105
106impl UserMetadata {
107 pub fn new() -> Self {
109 Self {
110 fields: HashMap::new(),
111 }
112 }
113
114 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 pub fn get(&self, key: &str) -> Option<&MetadataValue> {
122 self.fields.get(key)
123 }
124
125 pub fn remove(&mut self, key: &str) -> Option<MetadataValue> {
127 self.fields.remove(key)
128 }
129
130 pub fn contains(&self, key: &str) -> bool {
132 self.fields.contains_key(key)
133 }
134
135 pub fn to_json(&self) -> Result<String> {
137 serde_json::to_string(&self.fields).map_err(|e| e.into())
138 }
139
140 pub fn from_json(json: &str) -> Result<Self> {
142 let fields = serde_json::from_str(json)?;
143 Ok(Self { fields })
144 }
145
146 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
154pub struct MetadataBuilder {
156 metadata: UserMetadata,
157}
158
159impl MetadataBuilder {
160 pub fn new() -> Self {
162 Self {
163 metadata: UserMetadata::new(),
164 }
165 }
166
167 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 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 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 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 pub fn datetime_now(mut self, key: impl Into<String>) -> Self {
193 self.metadata.add(key, MetadataValue::datetime_now());
194 self
195 }
196
197 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 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 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 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 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 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 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#[derive(Debug, Clone)]
269pub enum MetadataFilter {
270 TextEq(String, String),
272
273 TextContains(String, String),
275
276 IntegerEq(String, i64),
278 IntegerGt(String, i64),
279 IntegerLt(String, i64),
280 IntegerRange(String, i64, i64),
281
282 FloatEq(String, f64),
284 FloatGt(String, f64),
285 FloatLt(String, f64),
286 FloatRange(String, f64, f64),
287
288 BooleanEq(String, bool),
290
291 DateTimeAfter(String, String),
293 DateTimeBefore(String, String),
294 DateTimeRange(String, String, String),
295
296 TagsContain(String, String),
298 TagsContainAll(String, Vec<String>),
299 TagsContainAny(String, Vec<String>),
300
301 EnumEq(String, String),
303
304 Exists(String),
306
307 And(Vec<MetadataFilter>),
309
310 Or(Vec<MetadataFilter>),
312
313 Not(Box<MetadataFilter>),
315}
316
317impl MetadataFilter {
318 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, }
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}