pub(crate) mod errors;
use std::collections::HashMap;
use crate::entity::Entity;
use crate::errors::Result;
use crate::models::Segment;
use crate::models::TargetingRule;
use crate::Value;
use errors::{CheckOperatorErrorDetail, SegmentEvaluationError};
pub(crate) fn find_applicable_segment_rule_for_entity(
segments: &HashMap<String, Segment>,
segment_rules: impl Iterator<Item = TargetingRule>,
entity: &impl Entity,
) -> Result<Option<TargetingRule>> {
let mut targeting_rules = segment_rules.collect::<Vec<_>>();
targeting_rules.sort_by(|a, b| a.order.cmp(&b.order));
for targeting_rule in targeting_rules.into_iter() {
if targeting_rule_applies_to_entity(segments, &targeting_rule, entity)? {
return Ok(Some(targeting_rule));
}
}
Ok(None)
}
fn targeting_rule_applies_to_entity(
segments: &HashMap<String, Segment>,
targeting_rule: &TargetingRule,
entity: &impl Entity,
) -> std::result::Result<bool, SegmentEvaluationError> {
let rules = &targeting_rule.rules;
for rule in rules.iter() {
let rule_applies = segment_applies_to_entity(segments, &rule.segments, entity)?;
if rule_applies {
return Ok(true);
}
}
Ok(false)
}
fn segment_applies_to_entity(
segments: &HashMap<String, Segment>,
segment_ids: &[String],
entity: &impl Entity,
) -> std::result::Result<bool, SegmentEvaluationError> {
for segment_id in segment_ids.iter() {
let segment = segments
.get(segment_id)
.ok_or(SegmentEvaluationError::SegmentIdNotFound(
segment_id.clone(),
))?;
let applies = belong_to_segment(segment, entity.get_attributes())?;
if applies {
return Ok(true);
}
}
Ok(false)
}
fn belong_to_segment(
segment: &Segment,
attrs: HashMap<String, Value>,
) -> std::result::Result<bool, SegmentEvaluationError> {
for rule in segment.rules.iter() {
let operator = &rule.operator;
let attr_name = &rule.attribute_name;
let attr_value = attrs.get(attr_name);
if attr_value.is_none() {
return Ok(false);
}
let rule_result = match attr_value {
None => {
println!("Warning: Operation '{attr_name}' '{operator}' '[...]' failed to evaluate: '{attr_name}' not found in entity");
false
}
Some(attr_value) => {
let candidate = rule
.values
.iter()
.find_map(|value| match check_operator(attr_value, operator, value) {
Ok(true) => Some(Ok::<_, SegmentEvaluationError>(())),
Ok(false) => None,
Err(e) => Some(Err((e, segment, rule, value).into())),
})
.transpose()?;
candidate.is_some()
}
};
if !rule_result {
return Ok(false);
}
}
Ok(true)
}
fn check_operator(
attribute_value: &Value,
operator: &str,
reference_value: &str,
) -> std::result::Result<bool, CheckOperatorErrorDetail> {
match operator {
"is" => match attribute_value {
Value::String(data) => Ok(*data == reference_value),
Value::Boolean(data) => Ok(*data == reference_value.parse::<bool>()?),
Value::Float64(data) => Ok(*data == reference_value.parse::<f64>()?),
Value::UInt64(data) => Ok(*data == reference_value.parse::<u64>()?),
Value::Int64(data) => Ok(*data == reference_value.parse::<i64>()?),
},
"contains" => match attribute_value {
Value::String(data) => Ok(data.contains(reference_value)),
_ => Err(CheckOperatorErrorDetail::StringExpected),
},
"startsWith" => match attribute_value {
Value::String(data) => Ok(data.starts_with(reference_value)),
_ => Err(CheckOperatorErrorDetail::StringExpected),
},
"endsWith" => match attribute_value {
Value::String(data) => Ok(data.ends_with(reference_value)),
_ => Err(CheckOperatorErrorDetail::StringExpected),
},
"greaterThan" => match attribute_value {
Value::Float64(data) => Ok(*data > reference_value.parse()?),
Value::UInt64(data) => Ok(*data > reference_value.parse()?),
Value::Int64(data) => Ok(*data > reference_value.parse()?),
_ => Err(CheckOperatorErrorDetail::EntityAttrNotANumber),
},
"lesserThan" => match attribute_value {
Value::Float64(data) => Ok(*data < reference_value.parse()?),
Value::UInt64(data) => Ok(*data < reference_value.parse()?),
Value::Int64(data) => Ok(*data < reference_value.parse()?),
_ => Err(CheckOperatorErrorDetail::EntityAttrNotANumber),
},
"greaterThanEquals" => match attribute_value {
Value::Float64(data) => Ok(*data >= reference_value.parse()?),
Value::UInt64(data) => Ok(*data >= reference_value.parse()?),
Value::Int64(data) => Ok(*data >= reference_value.parse()?),
_ => Err(CheckOperatorErrorDetail::EntityAttrNotANumber),
},
"lesserThanEquals" => match attribute_value {
Value::Float64(data) => Ok(*data <= reference_value.parse()?),
Value::UInt64(data) => Ok(*data <= reference_value.parse()?),
Value::Int64(data) => Ok(*data <= reference_value.parse()?),
_ => Err(CheckOperatorErrorDetail::EntityAttrNotANumber),
},
_ => Err(CheckOperatorErrorDetail::OperatorNotImplemented),
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::errors::{EntityEvaluationError, Error};
use crate::models::{ConfigValue, Segment, SegmentRule, Segments, TargetingRule};
use rstest::*;
#[fixture]
fn segments() -> HashMap<String, Segment> {
HashMap::from([(
"some_segment_id_1".into(),
Segment {
_name: "".into(),
segment_id: "some_segment_id_1".into(),
_description: "".into(),
_tags: None,
rules: vec![SegmentRule {
attribute_name: "name".into(),
operator: "is".into(),
values: vec!["heinz".into()],
}],
},
)])
}
#[fixture]
fn segment_rules() -> Vec<TargetingRule> {
vec![TargetingRule {
rules: vec![Segments {
segments: vec!["some_segment_id_1".into()],
}],
value: ConfigValue(serde_json::Value::Number((-48).into())),
order: 0,
rollout_percentage: Some(ConfigValue(serde_json::Value::Number((100).into()))),
}]
}
#[rstest]
fn test_attribute_not_found(
segments: HashMap<String, Segment>,
segment_rules: Vec<TargetingRule>,
) {
let entity = crate::tests::GenericEntity {
id: "a2".into(),
attributes: HashMap::from([("name2".into(), Value::from("heinz".to_string()))]),
};
let rule =
find_applicable_segment_rule_for_entity(&segments, segment_rules.into_iter(), &entity);
let rule = rule.unwrap();
assert!(rule.is_none())
}
#[rstest]
fn test_invalid_segment_id(segments: HashMap<String, Segment>) {
let entity = crate::tests::GenericEntity {
id: "a2".into(),
attributes: HashMap::from([("name".into(), Value::from(42.0))]),
};
let segment_rules = vec![TargetingRule {
rules: vec![Segments {
segments: vec!["non_existing_segment_id".into()],
}],
value: ConfigValue(serde_json::Value::Number((-48).into())),
order: 0,
rollout_percentage: Some(ConfigValue(serde_json::Value::Number((100).into()))),
}];
let rule =
find_applicable_segment_rule_for_entity(&segments, segment_rules.into_iter(), &entity);
let e = rule.unwrap_err();
assert!(matches!(e, Error::EntityEvaluationError(_)));
let Error::EntityEvaluationError(EntityEvaluationError(
SegmentEvaluationError::SegmentIdNotFound(ref segment_id),
)) = e
else {
panic!("Error type mismatch!");
};
assert_eq!(segment_id, "non_existing_segment_id");
}
#[rstest]
fn test_operator_failed(segments: HashMap<String, Segment>, segment_rules: Vec<TargetingRule>) {
let entity = crate::tests::GenericEntity {
id: "a2".into(),
attributes: HashMap::from([("name".into(), Value::from(42.0))]),
};
let rule =
find_applicable_segment_rule_for_entity(&segments, segment_rules.into_iter(), &entity);
let e = rule.unwrap_err();
assert!(matches!(e, Error::EntityEvaluationError(_)));
let Error::EntityEvaluationError(EntityEvaluationError(
SegmentEvaluationError::SegmentEvaluationFailed(ref error),
)) = e
else {
panic!("Error type mismatch!");
};
assert_eq!(error.segment_id, "some_segment_id_1");
assert_eq!(error.segment_rule_attribute_name, "name");
assert_eq!(error.value, "heinz");
}
}