use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::{ModelError, ModelResult, validation};
const ANNOTATIONS_MAX_TOTAL_BYTES: usize = 256 * 1024;
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "schema",
schemars(schema_with = "crate::schema::annotations")
)]
#[serde(transparent)]
pub struct Annotations(BTreeMap<String, String>);
impl Annotations {
#[inline]
pub fn new() -> Self {
Self(BTreeMap::new())
}
#[inline]
pub fn len(&self) -> usize {
self.0.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline]
pub fn insert<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: Into<String>,
V: Into<String>,
{
self.0.insert(key.into(), value.into());
self
}
#[inline]
pub fn get(&self, key: &str) -> Option<&str> {
self.0.get(key).map(String::as_str)
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.0
.iter()
.map(|(key, value)| (key.as_str(), value.as_str()))
}
pub fn validate(&self) -> ModelResult<()> {
let mut total_bytes = 0_usize;
for (key, value) in &self.0 {
validation::validate_qualified_name("annotation key", key)?;
total_bytes = total_bytes
.saturating_add(key.len())
.saturating_add(value.len());
}
if total_bytes > ANNOTATIONS_MAX_TOTAL_BYTES {
return Err(ModelError::Invalid(
format!(
"annotations total size {total_bytes} bytes exceeds max {ANNOTATIONS_MAX_TOTAL_BYTES}"
)
.into(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde_roundtrip_preserves_entries_and_arbitrary_values() {
let mut annotations = Annotations::new();
annotations.insert("example.io/note", "spaces, JSON: {\"ok\":true}");
annotations.validate().unwrap();
let json = serde_json::to_string(&annotations).unwrap();
let back: Annotations = serde_json::from_str(&json).unwrap();
assert_eq!(back, annotations);
}
#[test]
fn validation_rejects_invalid_keys_and_oversized_payloads() {
let mut annotations = Annotations::new();
annotations.insert("example.io/bad key", "value");
assert!(annotations.validate().is_err());
let mut annotations = Annotations::new();
annotations.insert("example.io/data", "x".repeat(ANNOTATIONS_MAX_TOTAL_BYTES));
assert!(annotations.validate().is_err());
}
}