use std::collections::HashMap;
use std::collections::HashSet;
use std::string::ToString;
use std::sync::Arc;
use cheetah_string::CheetahString;
use tracing::info;
use crate::common::attribute::Attribute;
use crate::ModelContractViolation;
pub struct AttributeUtil;
impl AttributeUtil {
#[allow(clippy::map_entry)]
pub fn alter_current_attributes(
create: bool,
all: &HashMap<CheetahString, Arc<dyn Attribute>>,
current_attributes: &HashMap<CheetahString, CheetahString>,
new_attributes: &HashMap<CheetahString, CheetahString>,
) -> Result<HashMap<CheetahString, CheetahString>, ModelContractViolation> {
let mut init = HashMap::new();
let mut add = HashMap::new();
let mut update = HashMap::new();
let mut delete = HashMap::new();
let mut keys = HashSet::new();
for (key, value) in new_attributes {
let real_key = Self::real_key(key)?;
Self::validate(&real_key)?;
Self::duplication_check(&mut keys, &real_key)?;
if create {
if key.starts_with('+') {
init.insert(real_key, value.clone());
} else {
return Err(ModelContractViolation::AttributeCreateRequiresAdd);
}
} else if key.starts_with('+') {
if !current_attributes.contains_key(&real_key) {
add.insert(real_key, value.clone());
} else {
update.insert(real_key, value.clone());
}
} else if key.starts_with('-') {
if !current_attributes.contains_key(&real_key) {
return Err(ModelContractViolation::AttributeDeleteTargetsMissingKey);
}
delete.insert(real_key, value.clone());
} else {
return Err(ModelContractViolation::AttributeOperationKeyHasUnsupportedForm);
}
}
Self::validate_alter(all, &init, true, false)?;
Self::validate_alter(all, &add, false, false)?;
Self::validate_alter(all, &update, false, false)?;
Self::validate_alter(all, &delete, false, true)?;
info!("add: {:?}, update: {:?}, delete: {:?}", add, update, delete);
let mut final_attributes = current_attributes.clone();
for (k, v) in init {
final_attributes.insert(k, v);
}
for (k, v) in add {
final_attributes.insert(k, v);
}
for (k, v) in update {
final_attributes.insert(k, v);
}
for k in delete.keys() {
final_attributes.remove(k);
}
Ok(final_attributes)
}
fn duplication_check(keys: &mut HashSet<String>, key: &str) -> Result<(), ModelContractViolation> {
if !keys.insert(key.to_string()) {
return Err(ModelContractViolation::AttributeOperationSetContainsDuplicateKey);
}
Ok(())
}
fn validate(kv_attribute: &str) -> Result<(), ModelContractViolation> {
if kv_attribute.is_empty() {
return Err(ModelContractViolation::AttributeOperationKeyIsInvalid);
}
if kv_attribute.contains('+') {
return Err(ModelContractViolation::AttributeOperationKeyIsInvalid);
}
if kv_attribute.contains('-') {
return Err(ModelContractViolation::AttributeOperationKeyIsInvalid);
}
Ok(())
}
fn validate_alter(
all: &HashMap<CheetahString, Arc<dyn Attribute>>,
alter: &HashMap<CheetahString, CheetahString>,
init: bool,
delete: bool,
) -> Result<(), ModelContractViolation> {
for (key, value) in alter {
let attribute = match all.get(key) {
Some(attr) => attr,
None => return Err(ModelContractViolation::AttributeOperationTargetsUnsupportedKey),
};
if !init && !attribute.is_changeable() {
return Err(ModelContractViolation::AttributeUpdateTargetsImmutableAttribute);
}
if !delete {
attribute
.verify(value)
.map_err(|_| ModelContractViolation::AttributeValueDoesNotSatisfyRules)?;
}
}
Ok(())
}
fn real_key(key: &str) -> Result<CheetahString, ModelContractViolation> {
if key.len() < 2 {
return Err(ModelContractViolation::AttributeOperationKeyIsInvalid);
}
let Some(real_key) = key.strip_prefix('+').or_else(|| key.strip_prefix('-')) else {
return Err(ModelContractViolation::AttributeOperationKeyHasUnsupportedForm);
};
if real_key.is_empty() {
return Err(ModelContractViolation::AttributeOperationKeyIsInvalid);
}
Ok(real_key.to_string().into())
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use cheetah_string::CheetahString;
use crate::common::attribute::bool_attribute::BooleanAttribute;
use crate::common::attribute::string_attribute::StringAttribute;
use crate::common::attribute::Attribute;
use crate::ModelContractViolation;
use super::*;
fn all_attributes() -> HashMap<CheetahString, Arc<dyn Attribute>> {
let mut all = HashMap::new();
all.insert(
CheetahString::from("immutable"),
Arc::new(StringAttribute::new("immutable".into(), false)) as Arc<dyn Attribute>,
);
all.insert(
CheetahString::from("mutable"),
Arc::new(StringAttribute::new("mutable".into(), true)) as Arc<dyn Attribute>,
);
all.insert(
CheetahString::from("remove"),
Arc::new(StringAttribute::new("remove".into(), true)) as Arc<dyn Attribute>,
);
all.insert(
CheetahString::from("added"),
Arc::new(StringAttribute::new("added".into(), true)) as Arc<dyn Attribute>,
);
all.insert(
CheetahString::from("enabled"),
Arc::new(BooleanAttribute::new("enabled".into(), true, false)) as Arc<dyn Attribute>,
);
all
}
#[test]
fn alter_current_attributes_create_only_supports_add() {
let all = HashMap::new();
let current_attributes = HashMap::new();
let mut new_attributes = HashMap::new();
new_attributes.insert("-key1".into(), "".into());
let result = AttributeUtil::alter_current_attributes(true, &all, ¤t_attributes, &new_attributes);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"topic creation attribute operation must use the add form"
);
}
#[test]
fn alter_current_attributes_delete_nonexistent_key() {
let all = HashMap::new();
let current_attributes = HashMap::new();
let mut new_attributes = HashMap::new();
new_attributes.insert("-key1".into(), "".into());
let result = AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &new_attributes);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"attribute delete operation targets a missing key"
);
}
#[test]
fn alter_current_attributes_wrong_format_key() {
let all = HashMap::new();
let current_attributes = HashMap::new();
let mut new_attributes = HashMap::new();
new_attributes.insert("key1".into(), "value1".into());
let result = AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &new_attributes);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"attribute operation key has an unsupported form"
);
}
#[test]
fn immutable_attributes_can_be_created_but_not_updated_or_deleted() {
let all = all_attributes();
let current_attributes = HashMap::new();
let create = HashMap::from([(CheetahString::from("+immutable"), CheetahString::from("initial"))]);
let created = AttributeUtil::alter_current_attributes(true, &all, ¤t_attributes, &create).unwrap();
assert_eq!(created.get("immutable"), Some(&CheetahString::from("initial")));
let update = HashMap::from([(CheetahString::from("+immutable"), CheetahString::from("changed"))]);
assert_eq!(
AttributeUtil::alter_current_attributes(false, &all, &created, &update),
Err(ModelContractViolation::AttributeUpdateTargetsImmutableAttribute)
);
let delete = HashMap::from([(CheetahString::from("-immutable"), CheetahString::new())]);
assert_eq!(
AttributeUtil::alter_current_attributes(false, &all, &created, &delete),
Err(ModelContractViolation::AttributeUpdateTargetsImmutableAttribute)
);
}
#[test]
fn mixed_add_update_delete_returns_all_remaining_attributes() {
let all = all_attributes();
let current_attributes = HashMap::from([
(CheetahString::from("mutable"), CheetahString::from("old")),
(CheetahString::from("remove"), CheetahString::from("gone")),
(CheetahString::from("enabled"), CheetahString::from("true")),
]);
let operations = HashMap::from([
(CheetahString::from("+mutable"), CheetahString::from("new")),
(CheetahString::from("+added"), CheetahString::from("value")),
(CheetahString::from("-remove"), CheetahString::from("unused")),
]);
let expected = HashMap::from([
(CheetahString::from("mutable"), CheetahString::from("new")),
(CheetahString::from("added"), CheetahString::from("value")),
(CheetahString::from("enabled"), CheetahString::from("true")),
]);
assert_eq!(
AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &operations).unwrap(),
expected
);
}
#[test]
fn unsupported_keys_and_invalid_boolean_values_are_rejected() {
let all = all_attributes();
let current_attributes = HashMap::new();
let unsupported = HashMap::from([(CheetahString::from("+missing"), CheetahString::from("value"))]);
assert_eq!(
AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &unsupported),
Err(ModelContractViolation::AttributeOperationTargetsUnsupportedKey)
);
let invalid_boolean = HashMap::from([(CheetahString::from("+enabled"), CheetahString::from("yes"))]);
assert_eq!(
AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &invalid_boolean),
Err(ModelContractViolation::AttributeValueDoesNotSatisfyRules)
);
}
#[test]
fn opposing_operations_for_one_key_are_rejected_as_duplicates() {
let all = all_attributes();
let current_attributes = HashMap::from([(CheetahString::from("mutable"), CheetahString::from("old"))]);
let operations = HashMap::from([
(CheetahString::from("+mutable"), CheetahString::from("new")),
(CheetahString::from("-mutable"), CheetahString::from("ignored")),
]);
assert_eq!(
AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &operations),
Err(ModelContractViolation::AttributeOperationSetContainsDuplicateKey)
);
}
#[test]
fn deleting_a_boolean_attribute_does_not_validate_the_delete_value() {
let all = all_attributes();
let current_attributes = HashMap::from([(CheetahString::from("enabled"), CheetahString::from("true"))]);
let delete = HashMap::from([(CheetahString::from("-enabled"), CheetahString::from("not-a-boolean"))]);
assert!(
AttributeUtil::alter_current_attributes(false, &all, ¤t_attributes, &delete)
.unwrap()
.is_empty()
);
}
}