use crate::error::{AgentRootError, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", content = "value")]
pub enum MetadataValue {
Text(String),
Integer(i64),
Float(f64),
Boolean(bool),
DateTime(String),
Tags(Vec<String>),
Enum { value: String, options: Vec<String> },
Qualitative { value: String, scale: Vec<String> },
Quantitative { value: f64, unit: String },
Json(serde_json::Value),
}
impl MetadataValue {
pub fn datetime_now() -> Self {
MetadataValue::DateTime(Utc::now().to_rfc3339())
}
pub fn datetime(dt: DateTime<Utc>) -> Self {
MetadataValue::DateTime(dt.to_rfc3339())
}
pub fn tags<I, S>(iter: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
MetadataValue::Tags(iter.into_iter().map(|s| s.into()).collect())
}
pub fn enum_value(value: impl Into<String>, options: Vec<String>) -> Result<Self> {
let value = value.into();
if !options.contains(&value) {
return Err(AgentRootError::InvalidInput(format!(
"Invalid enum value '{}'. Must be one of: {:?}",
value, options
)));
}
Ok(MetadataValue::Enum { value, options })
}
pub fn qualitative(value: impl Into<String>, scale: Vec<String>) -> Result<Self> {
let value = value.into();
if !scale.contains(&value) {
return Err(AgentRootError::InvalidInput(format!(
"Invalid qualitative value '{}'. Must be one of: {:?}",
value, scale
)));
}
Ok(MetadataValue::Qualitative { value, scale })
}
pub fn quantitative(value: f64, unit: impl Into<String>) -> Self {
MetadataValue::Quantitative {
value,
unit: unit.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UserMetadata {
pub fields: HashMap<String, MetadataValue>,
}
impl UserMetadata {
pub fn new() -> Self {
Self {
fields: HashMap::new(),
}
}
pub fn add(&mut self, key: impl Into<String>, value: MetadataValue) -> &mut Self {
self.fields.insert(key.into(), value);
self
}
pub fn get(&self, key: &str) -> Option<&MetadataValue> {
self.fields.get(key)
}
pub fn remove(&mut self, key: &str) -> Option<MetadataValue> {
self.fields.remove(key)
}
pub fn contains(&self, key: &str) -> bool {
self.fields.contains_key(key)
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(&self.fields).map_err(|e| e.into())
}
pub fn from_json(json: &str) -> Result<Self> {
let fields = serde_json::from_str(json)?;
Ok(Self { fields })
}
pub fn merge(&mut self, other: &UserMetadata) {
for (key, value) in &other.fields {
self.fields.insert(key.clone(), value.clone());
}
}
}
pub struct MetadataBuilder {
metadata: UserMetadata,
}
impl MetadataBuilder {
pub fn new() -> Self {
Self {
metadata: UserMetadata::new(),
}
}
pub fn text(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.add(key, MetadataValue::Text(value.into()));
self
}
pub fn integer(mut self, key: impl Into<String>, value: i64) -> Self {
self.metadata.add(key, MetadataValue::Integer(value));
self
}
pub fn float(mut self, key: impl Into<String>, value: f64) -> Self {
self.metadata.add(key, MetadataValue::Float(value));
self
}
pub fn boolean(mut self, key: impl Into<String>, value: bool) -> Self {
self.metadata.add(key, MetadataValue::Boolean(value));
self
}
pub fn datetime_now(mut self, key: impl Into<String>) -> Self {
self.metadata.add(key, MetadataValue::datetime_now());
self
}
pub fn datetime(mut self, key: impl Into<String>, dt: DateTime<Utc>) -> Self {
self.metadata.add(key, MetadataValue::datetime(dt));
self
}
pub fn tags<I, S>(mut self, key: impl Into<String>, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.metadata.add(key, MetadataValue::tags(tags));
self
}
pub fn enum_value(
mut self,
key: impl Into<String>,
value: impl Into<String>,
options: Vec<String>,
) -> Result<Self> {
let metadata_value = MetadataValue::enum_value(value, options)?;
self.metadata.add(key, metadata_value);
Ok(self)
}
pub fn qualitative(
mut self,
key: impl Into<String>,
value: impl Into<String>,
scale: Vec<String>,
) -> Result<Self> {
let metadata_value = MetadataValue::qualitative(value, scale)?;
self.metadata.add(key, metadata_value);
Ok(self)
}
pub fn quantitative(
mut self,
key: impl Into<String>,
value: f64,
unit: impl Into<String>,
) -> Self {
self.metadata
.add(key, MetadataValue::quantitative(value, unit));
self
}
pub fn json(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.add(key, MetadataValue::Json(value));
self
}
pub fn build(self) -> UserMetadata {
self.metadata
}
}
impl Default for MetadataBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub enum MetadataFilter {
TextEq(String, String),
TextContains(String, String),
IntegerEq(String, i64),
IntegerGt(String, i64),
IntegerLt(String, i64),
IntegerRange(String, i64, i64),
FloatEq(String, f64),
FloatGt(String, f64),
FloatLt(String, f64),
FloatRange(String, f64, f64),
BooleanEq(String, bool),
DateTimeAfter(String, String),
DateTimeBefore(String, String),
DateTimeRange(String, String, String),
TagsContain(String, String),
TagsContainAll(String, Vec<String>),
TagsContainAny(String, Vec<String>),
EnumEq(String, String),
Exists(String),
And(Vec<MetadataFilter>),
Or(Vec<MetadataFilter>),
Not(Box<MetadataFilter>),
}
impl MetadataFilter {
pub fn matches(&self, metadata: &UserMetadata) -> bool {
match self {
MetadataFilter::TextEq(key, value) => {
matches!(metadata.get(key), Some(MetadataValue::Text(v)) if v == value)
}
MetadataFilter::TextContains(key, substring) => {
matches!(metadata.get(key), Some(MetadataValue::Text(v)) if v.contains(substring))
}
MetadataFilter::IntegerEq(key, value) => {
matches!(metadata.get(key), Some(MetadataValue::Integer(v)) if v == value)
}
MetadataFilter::IntegerGt(key, value) => {
matches!(metadata.get(key), Some(MetadataValue::Integer(v)) if v > value)
}
MetadataFilter::IntegerLt(key, value) => {
matches!(metadata.get(key), Some(MetadataValue::Integer(v)) if v < value)
}
MetadataFilter::IntegerRange(key, min, max) => {
matches!(metadata.get(key), Some(MetadataValue::Integer(v)) if v >= min && v <= max)
}
MetadataFilter::BooleanEq(key, value) => {
matches!(metadata.get(key), Some(MetadataValue::Boolean(v)) if v == value)
}
MetadataFilter::TagsContain(key, tag) => {
matches!(metadata.get(key), Some(MetadataValue::Tags(tags)) if tags.contains(tag))
}
MetadataFilter::TagsContainAll(key, search_tags) => {
matches!(metadata.get(key), Some(MetadataValue::Tags(tags))
if search_tags.iter().all(|t| tags.contains(t)))
}
MetadataFilter::TagsContainAny(key, search_tags) => {
matches!(metadata.get(key), Some(MetadataValue::Tags(tags))
if search_tags.iter().any(|t| tags.contains(t)))
}
MetadataFilter::EnumEq(key, value) => {
matches!(metadata.get(key), Some(MetadataValue::Enum { value: v, .. }) if v == value)
}
MetadataFilter::Exists(key) => metadata.contains(key),
MetadataFilter::And(filters) => filters.iter().all(|f| f.matches(metadata)),
MetadataFilter::Or(filters) => filters.iter().any(|f| f.matches(metadata)),
MetadataFilter::Not(filter) => !filter.matches(metadata),
_ => false, }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metadata_builder() {
let metadata = MetadataBuilder::new()
.text("author", "John Doe")
.integer("version", 2)
.float("score", 4.5)
.boolean("published", true)
.datetime_now("created_at")
.tags("labels", vec!["rust", "programming"])
.quantitative("size", 1024.0, "KB")
.build();
assert_eq!(
metadata.get("author"),
Some(&MetadataValue::Text("John Doe".to_string()))
);
assert_eq!(metadata.get("version"), Some(&MetadataValue::Integer(2)));
assert!(metadata.contains("created_at"));
}
#[test]
fn test_enum_validation() {
let result =
MetadataValue::enum_value("active", vec!["draft".to_string(), "published".to_string()]);
assert!(result.is_err());
let result = MetadataValue::enum_value(
"published",
vec!["draft".to_string(), "published".to_string()],
);
assert!(result.is_ok());
}
#[test]
fn test_metadata_filter() {
let metadata = MetadataBuilder::new()
.text("status", "published")
.tags("labels", vec!["rust", "tutorial"])
.integer("views", 100)
.build();
assert!(
MetadataFilter::TextEq("status".to_string(), "published".to_string())
.matches(&metadata)
);
assert!(
MetadataFilter::TagsContain("labels".to_string(), "rust".to_string())
.matches(&metadata)
);
assert!(MetadataFilter::IntegerGt("views".to_string(), 50).matches(&metadata));
}
#[test]
fn test_metadata_merge() {
let mut meta1 = MetadataBuilder::new()
.text("author", "Alice")
.integer("version", 1)
.build();
let meta2 = MetadataBuilder::new()
.text("author", "Bob")
.tags("labels", vec!["rust"])
.build();
meta1.merge(&meta2);
assert_eq!(
meta1.get("author"),
Some(&MetadataValue::Text("Bob".to_string()))
);
assert!(meta1.contains("labels"));
assert!(meta1.contains("version"));
}
#[test]
fn test_json_serialization() {
let metadata = MetadataBuilder::new()
.text("name", "Test")
.integer("count", 42)
.build();
let json = metadata.to_json().unwrap();
let restored = UserMetadata::from_json(&json).unwrap();
assert_eq!(metadata.fields, restored.fields);
}
}