made_core/value_objects/
attributes.rs1use std::collections::BTreeMap;
9use std::fmt;
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14use crate::error::DomainError;
15
16pub const MAX_ATTRIBUTES_KEYS: usize = 1024;
18
19#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct Attributes(BTreeMap<String, Value>);
22
23impl Attributes {
24 pub fn new(entries: BTreeMap<String, Value>) -> Result<Self, DomainError> {
25 if entries.len() > MAX_ATTRIBUTES_KEYS {
26 return Err(DomainError::OutOfRange {
27 field: "attributes.keys",
28 value: entries.len() as f64,
29 min: 0.0,
30 max: MAX_ATTRIBUTES_KEYS as f64,
31 });
32 }
33 for key in entries.keys() {
34 if key.trim().is_empty() {
35 return Err(DomainError::EmptyField {
36 field: "attributes.key",
37 });
38 }
39 }
40 Ok(Self(entries))
41 }
42
43 #[must_use]
44 pub fn empty() -> Self {
45 Self::default()
46 }
47
48 #[must_use]
49 pub fn len(&self) -> usize {
50 self.0.len()
51 }
52
53 #[must_use]
54 pub fn is_empty(&self) -> bool {
55 self.0.is_empty()
56 }
57
58 #[must_use]
59 pub fn get(&self, key: &str) -> Option<&Value> {
60 self.0.get(key)
61 }
62
63 #[must_use]
64 pub fn as_map(&self) -> &BTreeMap<String, Value> {
65 &self.0
66 }
67
68 #[must_use]
69 pub fn into_inner(self) -> BTreeMap<String, Value> {
70 self.0
71 }
72}
73
74impl fmt::Display for Attributes {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 write!(f, "Attributes({} keys)", self.0.len())
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use serde_json::json;
84
85 fn entry(key: &str, value: Value) -> BTreeMap<String, Value> {
86 let mut m = BTreeMap::new();
87 m.insert(key.to_owned(), value);
88 m
89 }
90
91 #[test]
92 fn empty_is_allowed() {
93 assert!(Attributes::empty().is_empty());
94 }
95
96 #[test]
97 fn blank_key_is_rejected() {
98 let err = Attributes::new(entry(" ", json!(true))).unwrap_err();
99 assert!(matches!(
100 err,
101 DomainError::EmptyField {
102 field: "attributes.key"
103 }
104 ));
105 }
106
107 #[test]
108 fn too_many_keys_is_rejected() {
109 let mut m = BTreeMap::new();
110 for i in 0..=MAX_ATTRIBUTES_KEYS {
111 m.insert(format!("k{i}"), json!(i));
112 }
113 assert!(matches!(
114 Attributes::new(m).unwrap_err(),
115 DomainError::OutOfRange {
116 field: "attributes.keys",
117 ..
118 }
119 ));
120 }
121
122 #[test]
123 fn arbitrary_payload_is_accepted() {
124 let attrs = Attributes::new(entry(
125 "alert",
126 json!({"source": "grafana", "severity": "p1"}),
127 ))
128 .unwrap();
129 assert_eq!(attrs.len(), 1);
130 assert_eq!(attrs.get("alert").unwrap()["severity"], json!("p1"));
131 }
132
133 #[test]
134 fn serde_is_transparent() {
135 let a = Attributes::new(entry("k", json!("v"))).unwrap();
136 assert_eq!(serde_json::to_string(&a).unwrap(), r#"{"k":"v"}"#);
137 }
138}