use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct AgentMeta {
pub entries: Vec<MetaEntry>,
}
impl<'de> Deserialize<'de> for AgentMeta {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Raw {
#[serde(default)]
entries: Vec<MetaEntry>,
}
Ok(AgentMeta::new(Raw::deserialize(deserializer)?.entries))
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct MetaEntry {
pub key: String,
pub value: String,
}
impl MetaEntry {
pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
Self {
key: key.into(),
value: value.into(),
}
}
}
impl AgentMeta {
pub fn new<I: IntoIterator<Item = MetaEntry>>(entries: I) -> Self {
let mut order: Vec<String> = Vec::new();
let mut values: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for e in entries {
let key = e.key.trim().to_string();
if key.is_empty() {
continue;
}
let value = e.value.trim().to_string();
if !values.contains_key(&key) {
order.push(key.clone());
}
values.insert(key, value); }
let entries = order
.into_iter()
.map(|key| {
let value = values.remove(&key).unwrap_or_default();
MetaEntry { key, value }
})
.collect();
Self { entries }
}
pub fn upsert(&mut self, key: impl Into<String>, value: impl Into<String>) -> bool {
let key = key.into().trim().to_string();
if key.is_empty() {
return false;
}
let value = value.into().trim().to_string();
match self.entries.iter_mut().find(|e| e.key == key) {
Some(e) if e.value == value => false,
Some(e) => {
e.value = value;
true
}
None => {
self.entries.push(MetaEntry { key, value });
true
}
}
}
pub fn remove(&mut self, key: &str) -> bool {
let key = key.trim();
let before = self.entries.len();
self.entries.retain(|e| e.key != key);
self.entries.len() != before
}
pub fn get(&self, key: &str) -> Option<&str> {
let key = key.trim();
self.entries
.iter()
.find(|e| e.key == key)
.map(|e| e.value.as_str())
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn e(key: &str, value: &str) -> MetaEntry {
MetaEntry::new(key, value)
}
#[test]
fn new_trims_drops_empty_keys_and_dedups_last_wins() {
let m = AgentMeta::new([
e(" Name ", " Alice "),
e("Email", "alice@example.com"),
e(" ", "orphan value"), e("Name", "Alice Smith"), ]);
assert_eq!(
m.entries,
vec![e("Name", "Alice Smith"), e("Email", "alice@example.com"),]
);
}
#[test]
fn round_trips_through_json() {
let m = AgentMeta::new([e("Dept", "Finance")]);
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, r#"{"entries":[{"key":"Dept","value":"Finance"}]}"#);
let back: AgentMeta = serde_json::from_str(&json).unwrap();
assert_eq!(back, m);
}
#[test]
fn empty_round_trips() {
let m = AgentMeta::default();
assert_eq!(serde_json::to_string(&m).unwrap(), r#"{"entries":[]}"#);
assert!(m.is_empty());
}
#[test]
fn upsert_inserts_overwrites_and_noops() {
let mut m = AgentMeta::default();
assert!(m.upsert("Name", "Alice")); assert!(!m.upsert("Name", "Alice")); assert!(m.upsert("Name", "Bob")); assert!(!m.upsert(" ", "x")); assert!(m.upsert(" Email ", " a@b.com ")); assert_eq!(m.get("Name"), Some("Bob"));
assert_eq!(m.get("Email"), Some("a@b.com"));
assert_eq!(m.entries.len(), 2);
}
#[test]
fn remove_reports_change() {
let mut m = AgentMeta::new([e("Name", "Alice"), e("Dept", "IT")]);
assert!(m.remove("Name"));
assert!(!m.remove("Name"));
assert_eq!(m.entries, vec![e("Dept", "IT")]);
}
#[test]
fn accepts_unknown_fields_for_forward_compat() {
let json = r#"{"entries":[{"key":"Name","value":"Alice"}],"set_by":"admin"}"#;
let m: AgentMeta = serde_json::from_str(json).unwrap();
assert_eq!(m.entries, vec![e("Name", "Alice")]);
}
#[test]
fn deserialize_normalises_untrusted_input() {
let json = r#"{"entries":[
{"key":" Name ","value":" Alice "},
{"key":"","value":"orphan"},
{"key":"Name","value":"Bob"}
]}"#;
let m: AgentMeta = serde_json::from_str(json).unwrap();
assert_eq!(m.entries, vec![e("Name", "Bob")]);
}
#[test]
fn deserialize_defaults_missing_entries() {
let m: AgentMeta = serde_json::from_str("{}").unwrap();
assert!(m.is_empty());
}
}