solti_model/resource/
annotations.rs1use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{ModelError, ModelResult, validation};
12
13const ANNOTATIONS_MAX_TOTAL_BYTES: usize = 256 * 1024;
14
15#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20#[cfg_attr(
21 feature = "schema",
22 schemars(schema_with = "crate::schema::annotations")
23)]
24#[serde(transparent)]
25pub struct Annotations(BTreeMap<String, String>);
26
27impl Annotations {
28 #[inline]
30 pub fn new() -> Self {
31 Self(BTreeMap::new())
32 }
33
34 #[inline]
36 pub fn len(&self) -> usize {
37 self.0.len()
38 }
39
40 #[inline]
42 pub fn is_empty(&self) -> bool {
43 self.0.is_empty()
44 }
45
46 #[inline]
48 pub fn insert<K, V>(&mut self, key: K, value: V) -> &mut Self
49 where
50 K: Into<String>,
51 V: Into<String>,
52 {
53 self.0.insert(key.into(), value.into());
54 self
55 }
56
57 #[inline]
59 pub fn get(&self, key: &str) -> Option<&str> {
60 self.0.get(key).map(String::as_str)
61 }
62
63 #[inline]
65 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
66 self.0
67 .iter()
68 .map(|(key, value)| (key.as_str(), value.as_str()))
69 }
70
71 pub fn validate(&self) -> ModelResult<()> {
79 let mut total_bytes = 0_usize;
80 for (key, value) in &self.0 {
81 validation::validate_qualified_name("annotation key", key)?;
82 total_bytes = total_bytes
83 .saturating_add(key.len())
84 .saturating_add(value.len());
85 }
86 if total_bytes > ANNOTATIONS_MAX_TOTAL_BYTES {
87 return Err(ModelError::Invalid(
88 format!(
89 "annotations total size {total_bytes} bytes exceeds max {ANNOTATIONS_MAX_TOTAL_BYTES}"
90 )
91 .into(),
92 ));
93 }
94 Ok(())
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn serde_roundtrip_preserves_entries_and_arbitrary_values() {
104 let mut annotations = Annotations::new();
105 annotations.insert("example.io/note", "spaces, JSON: {\"ok\":true}");
106 annotations.validate().unwrap();
107
108 let json = serde_json::to_string(&annotations).unwrap();
109 let back: Annotations = serde_json::from_str(&json).unwrap();
110
111 assert_eq!(back, annotations);
112 }
113
114 #[test]
115 fn validation_rejects_invalid_keys_and_oversized_payloads() {
116 let mut annotations = Annotations::new();
117 annotations.insert("example.io/bad key", "value");
118 assert!(annotations.validate().is_err());
119
120 let mut annotations = Annotations::new();
121 annotations.insert("example.io/data", "x".repeat(ANNOTATIONS_MAX_TOTAL_BYTES));
122 assert!(annotations.validate().is_err());
123 }
124}