rocketmq_model/common/attribute/
attribute_util.rs1use std::collections::HashMap;
16use std::collections::HashSet;
17use std::string::ToString;
18use std::sync::Arc;
19
20use cheetah_string::CheetahString;
21use tracing::info;
22
23use crate::common::attribute::Attribute;
24use crate::ModelContractViolation;
25
26pub struct AttributeUtil;
28
29impl AttributeUtil {
30 #[allow(clippy::map_entry)]
43 pub fn alter_current_attributes(
44 create: bool,
45 all: &HashMap<CheetahString, Arc<dyn Attribute>>,
46 current_attributes: &HashMap<CheetahString, CheetahString>,
47 new_attributes: &HashMap<CheetahString, CheetahString>,
48 ) -> Result<HashMap<CheetahString, CheetahString>, ModelContractViolation> {
49 let mut init = HashMap::new();
50 let mut add = HashMap::new();
51 let mut update = HashMap::new();
52 let mut delete = HashMap::new();
53 let mut keys = HashSet::new();
54
55 for (key, value) in new_attributes {
57 let real_key = Self::real_key(key)?;
58
59 Self::validate(&real_key)?;
60 Self::duplication_check(&mut keys, &real_key)?;
61
62 if create {
63 if key.starts_with('+') {
64 init.insert(real_key, value.clone());
65 } else {
66 return Err(ModelContractViolation::AttributeCreateRequiresAdd);
67 }
68 } else if key.starts_with('+') {
69 if !current_attributes.contains_key(&real_key) {
70 add.insert(real_key, value.clone());
71 } else {
72 update.insert(real_key, value.clone());
73 }
74 } else if key.starts_with('-') {
75 if !current_attributes.contains_key(&real_key) {
76 return Err(ModelContractViolation::AttributeDeleteTargetsMissingKey);
77 }
78 delete.insert(real_key, value.clone());
79 } else {
80 return Err(ModelContractViolation::AttributeOperationKeyHasUnsupportedForm);
81 }
82 }
83
84 Self::validate_alter(all, &init, true, false)?;
86 Self::validate_alter(all, &add, false, false)?;
87 Self::validate_alter(all, &update, false, false)?;
88 Self::validate_alter(all, &delete, false, true)?;
89
90 info!("add: {:?}, update: {:?}, delete: {:?}", add, update, delete);
91
92 let mut final_attributes = current_attributes.clone();
94
95 for (k, v) in init {
97 final_attributes.insert(k, v);
98 }
99
100 for (k, v) in add {
101 final_attributes.insert(k, v);
102 }
103
104 for (k, v) in update {
105 final_attributes.insert(k, v);
106 }
107
108 for k in delete.keys() {
109 final_attributes.remove(k);
110 }
111
112 Ok(final_attributes)
113 }
114
115 fn duplication_check(keys: &mut HashSet<String>, key: &str) -> Result<(), ModelContractViolation> {
117 if !keys.insert(key.to_string()) {
118 return Err(ModelContractViolation::AttributeOperationSetContainsDuplicateKey);
119 }
120 Ok(())
121 }
122
123 fn validate(kv_attribute: &str) -> Result<(), ModelContractViolation> {
125 if kv_attribute.is_empty() {
126 return Err(ModelContractViolation::AttributeOperationKeyIsInvalid);
127 }
128
129 if kv_attribute.contains('+') {
130 return Err(ModelContractViolation::AttributeOperationKeyIsInvalid);
131 }
132
133 if kv_attribute.contains('-') {
134 return Err(ModelContractViolation::AttributeOperationKeyIsInvalid);
135 }
136
137 Ok(())
138 }
139
140 fn validate_alter(
142 all: &HashMap<CheetahString, Arc<dyn Attribute>>,
143 alter: &HashMap<CheetahString, CheetahString>,
144 init: bool,
145 delete: bool,
146 ) -> Result<(), ModelContractViolation> {
147 for (key, value) in alter {
148 let attribute = match all.get(key) {
149 Some(attr) => attr,
150 None => return Err(ModelContractViolation::AttributeOperationTargetsUnsupportedKey),
151 };
152
153 if !init && !attribute.is_changeable() {
154 return Err(ModelContractViolation::AttributeUpdateTargetsImmutableAttribute);
155 }
156
157 if !delete {
158 attribute
159 .verify(value)
160 .map_err(|_| ModelContractViolation::AttributeValueDoesNotSatisfyRules)?;
161 }
162 }
163
164 Ok(())
165 }
166
167 fn real_key(key: &str) -> Result<CheetahString, ModelContractViolation> {
169 if key.len() < 2 {
170 return Err(ModelContractViolation::AttributeOperationKeyIsInvalid);
171 }
172
173 let Some(real_key) = key.strip_prefix('+').or_else(|| key.strip_prefix('-')) else {
174 return Err(ModelContractViolation::AttributeOperationKeyHasUnsupportedForm);
175 };
176 if real_key.is_empty() {
177 return Err(ModelContractViolation::AttributeOperationKeyIsInvalid);
178 }
179 Ok(real_key.to_string().into())
180 }
181}
182
183#[cfg(test)]
184mod tests {
185
186 use std::sync::Arc;
187
188 use cheetah_string::CheetahString;
189
190 use crate::common::attribute::bool_attribute::BooleanAttribute;
191 use crate::common::attribute::string_attribute::StringAttribute;
192 use crate::common::attribute::Attribute;
193 use crate::ModelContractViolation;
194
195 use super::*;
196
197 fn all_attributes() -> HashMap<CheetahString, Arc<dyn Attribute>> {
198 let mut all = HashMap::new();
199 all.insert(
200 CheetahString::from("immutable"),
201 Arc::new(StringAttribute::new("immutable".into(), false)) as Arc<dyn Attribute>,
202 );
203 all.insert(
204 CheetahString::from("mutable"),
205 Arc::new(StringAttribute::new("mutable".into(), true)) as Arc<dyn Attribute>,
206 );
207 all.insert(
208 CheetahString::from("remove"),
209 Arc::new(StringAttribute::new("remove".into(), true)) as Arc<dyn Attribute>,
210 );
211 all.insert(
212 CheetahString::from("added"),
213 Arc::new(StringAttribute::new("added".into(), true)) as Arc<dyn Attribute>,
214 );
215 all.insert(
216 CheetahString::from("enabled"),
217 Arc::new(BooleanAttribute::new("enabled".into(), true, false)) as Arc<dyn Attribute>,
218 );
219 all
220 }
221
222 #[test]
223 fn alter_current_attributes_create_only_supports_add() {
224 let all = HashMap::new();
225 let current_attributes = HashMap::new();
226 let mut new_attributes = HashMap::new();
227 new_attributes.insert("-key1".into(), "".into());
228
229 let result = AttributeUtil::alter_current_attributes(true, &all, ¤t_attributes, &new_attributes);
230 assert!(result.is_err());
231 assert_eq!(
232 result.unwrap_err().to_string(),
233 "topic creation attribute operation must use the add form"
234 );
235 }
236
237 #[test]
238 fn alter_current_attributes_delete_nonexistent_key() {
239 let all = HashMap::new();
240 let current_attributes = HashMap::new();
241 let mut new_attributes = HashMap::new();
242 new_attributes.insert("-key1".into(), "".into());
243
244 let result = AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &new_attributes);
245 assert!(result.is_err());
246 assert_eq!(
247 result.unwrap_err().to_string(),
248 "attribute delete operation targets a missing key"
249 );
250 }
251
252 #[test]
253 fn alter_current_attributes_wrong_format_key() {
254 let all = HashMap::new();
255 let current_attributes = HashMap::new();
256 let mut new_attributes = HashMap::new();
257 new_attributes.insert("key1".into(), "value1".into());
258
259 let result = AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &new_attributes);
260 assert!(result.is_err());
261 assert_eq!(
262 result.unwrap_err().to_string(),
263 "attribute operation key has an unsupported form"
264 );
265 }
266
267 #[test]
268 fn immutable_attributes_can_be_created_but_not_updated_or_deleted() {
269 let all = all_attributes();
270 let current_attributes = HashMap::new();
271 let create = HashMap::from([(CheetahString::from("+immutable"), CheetahString::from("initial"))]);
272
273 let created = AttributeUtil::alter_current_attributes(true, &all, ¤t_attributes, &create).unwrap();
274 assert_eq!(created.get("immutable"), Some(&CheetahString::from("initial")));
275
276 let update = HashMap::from([(CheetahString::from("+immutable"), CheetahString::from("changed"))]);
277 assert_eq!(
278 AttributeUtil::alter_current_attributes(false, &all, &created, &update),
279 Err(ModelContractViolation::AttributeUpdateTargetsImmutableAttribute)
280 );
281
282 let delete = HashMap::from([(CheetahString::from("-immutable"), CheetahString::new())]);
283 assert_eq!(
284 AttributeUtil::alter_current_attributes(false, &all, &created, &delete),
285 Err(ModelContractViolation::AttributeUpdateTargetsImmutableAttribute)
286 );
287 }
288
289 #[test]
290 fn mixed_add_update_delete_returns_all_remaining_attributes() {
291 let all = all_attributes();
292 let current_attributes = HashMap::from([
293 (CheetahString::from("mutable"), CheetahString::from("old")),
294 (CheetahString::from("remove"), CheetahString::from("gone")),
295 (CheetahString::from("enabled"), CheetahString::from("true")),
296 ]);
297 let operations = HashMap::from([
298 (CheetahString::from("+mutable"), CheetahString::from("new")),
299 (CheetahString::from("+added"), CheetahString::from("value")),
300 (CheetahString::from("-remove"), CheetahString::from("unused")),
301 ]);
302 let expected = HashMap::from([
303 (CheetahString::from("mutable"), CheetahString::from("new")),
304 (CheetahString::from("added"), CheetahString::from("value")),
305 (CheetahString::from("enabled"), CheetahString::from("true")),
306 ]);
307
308 assert_eq!(
309 AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &operations).unwrap(),
310 expected
311 );
312 }
313
314 #[test]
315 fn unsupported_keys_and_invalid_boolean_values_are_rejected() {
316 let all = all_attributes();
317 let current_attributes = HashMap::new();
318
319 let unsupported = HashMap::from([(CheetahString::from("+missing"), CheetahString::from("value"))]);
320 assert_eq!(
321 AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &unsupported),
322 Err(ModelContractViolation::AttributeOperationTargetsUnsupportedKey)
323 );
324
325 let invalid_boolean = HashMap::from([(CheetahString::from("+enabled"), CheetahString::from("yes"))]);
326 assert_eq!(
327 AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &invalid_boolean),
328 Err(ModelContractViolation::AttributeValueDoesNotSatisfyRules)
329 );
330 }
331
332 #[test]
333 fn opposing_operations_for_one_key_are_rejected_as_duplicates() {
334 let all = all_attributes();
335 let current_attributes = HashMap::from([(CheetahString::from("mutable"), CheetahString::from("old"))]);
336 let operations = HashMap::from([
337 (CheetahString::from("+mutable"), CheetahString::from("new")),
338 (CheetahString::from("-mutable"), CheetahString::from("ignored")),
339 ]);
340
341 assert_eq!(
342 AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &operations),
343 Err(ModelContractViolation::AttributeOperationSetContainsDuplicateKey)
344 );
345 }
346
347 #[test]
348 fn deleting_a_boolean_attribute_does_not_validate_the_delete_value() {
349 let all = all_attributes();
350 let current_attributes = HashMap::from([(CheetahString::from("enabled"), CheetahString::from("true"))]);
351 let delete = HashMap::from([(CheetahString::from("-enabled"), CheetahString::from("not-a-boolean"))]);
352
353 assert!(
354 AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &delete)
355 .unwrap()
356 .is_empty()
357 );
358 }
359}