Skip to main content

rocketmq_model/common/attribute/
attribute_util.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
26/// Utility for working with topic attributes
27pub struct AttributeUtil;
28
29impl AttributeUtil {
30    /// Alter the current attributes based on new attribute requests
31    ///
32    /// # Arguments
33    ///
34    /// * `create` - If true, we're creating new attributes, otherwise updating existing ones
35    /// * `all` - Map of all supported attributes
36    /// * `current_attributes` - Current attribute values
37    /// * `new_attributes` - New attribute operations (prefixed with + or -)
38    ///
39    /// # Returns
40    ///
41    /// A Result containing the final attribute map or an error
42    #[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        // Process new attribute operations
56        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        // Validate all operations
85        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        // Create final attribute map
93        let mut final_attributes = current_attributes.clone();
94
95        // Apply changes
96        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    /// Check for key duplication in the operation set
116    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    /// Validate attribute key format
124    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    /// Validate attribute operations
141    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    /// Extract the real key by removing the prefix (+ or -)
168    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, &current_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, &current_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, &current_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, &current_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, &current_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, &current_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, &current_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, &current_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, &current_attributes, &delete)
355                .unwrap()
356                .is_empty()
357        );
358    }
359}