-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add evaluation context #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
e9ab450 to
c6b5a56
Compare
c6b5a56 to
4f0a0fb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good overall. Left a handful of comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: Shipping the mapper layer for with the engine for a while makes sense to me. That's what we did with the Python engine at first, and it was easier to review and integrate. This also explains why the SDK metadata structs belong to here.
src/engine_eval/mappers.rs
Outdated
| conditions: vec![Condition { | ||
| operator: ConditionOperator::In, | ||
| property: "$.identity.identifier".to_string(), | ||
| value: identifiers.join(","), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we stick with the full serde route, I suggest serialising to a JSON list to support identifiers containing the , separator:
| value: identifiers.join(","), | |
| value: serde_json::to_string(identifiers.join), |
Otherwise, I'd prefer to avoid extra marshalling:
| value: identifiers.join(","), | |
| value: ConditionValue::Values(identifiers), |
src/engine_eval/context.rs
Outdated
| // Helper function to deserialize value that can be a string or array | ||
| fn deserialize_condition_value<'de, D>(deserializer: D) -> Result<String, D::Error> | ||
| where | ||
| D: serde::Deserializer<'de>, | ||
| { | ||
| use serde_json::Value; | ||
| let value: Value = serde::Deserialize::deserialize(deserializer)?; | ||
| Ok(match value { | ||
| Value::String(s) => s, | ||
| Value::Array(_) | Value::Object(_) | Value::Number(_) | Value::Bool(_) => { | ||
| // Serialize non-string values back to JSON string | ||
| serde_json::to_string(&value).unwrap_or_default() | ||
| } | ||
| Value::Null => String::new(), | ||
| }) | ||
| } | ||
|
|
||
| /// Represents a condition for segment rule evaluation. | ||
| #[derive(Clone, Debug, Serialize, Deserialize)] | ||
| pub struct Condition { | ||
| /// The operator for this condition. | ||
| pub operator: ConditionOperator, | ||
| /// The property to evaluate (can be a JSONPath expression starting with $.). | ||
| pub property: String, | ||
| /// The value to compare against (can be a string or serialized JSON). | ||
| #[serde(deserialize_with = "deserialize_condition_value")] | ||
| pub value: String, | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding an untagged enum to support native vectors instead:
| // Helper function to deserialize value that can be a string or array | |
| fn deserialize_condition_value<'de, D>(deserializer: D) -> Result<String, D::Error> | |
| where | |
| D: serde::Deserializer<'de>, | |
| { | |
| use serde_json::Value; | |
| let value: Value = serde::Deserialize::deserialize(deserializer)?; | |
| Ok(match value { | |
| Value::String(s) => s, | |
| Value::Array(_) | Value::Object(_) | Value::Number(_) | Value::Bool(_) => { | |
| // Serialize non-string values back to JSON string | |
| serde_json::to_string(&value).unwrap_or_default() | |
| } | |
| Value::Null => String::new(), | |
| }) | |
| } | |
| /// Represents a condition for segment rule evaluation. | |
| #[derive(Clone, Debug, Serialize, Deserialize)] | |
| pub struct Condition { | |
| /// The operator for this condition. | |
| pub operator: ConditionOperator, | |
| /// The property to evaluate (can be a JSONPath expression starting with $.). | |
| pub property: String, | |
| /// The value to compare against (can be a string or serialized JSON). | |
| #[serde(deserialize_with = "deserialize_condition_value")] | |
| pub value: String, | |
| } | |
| // Represents a segment condition value that can be a string (including a JSON-encoded array), or a native vector of strings. | |
| #[derive(Clone, Debug, Serialize, Deserialize)] | |
| #[serde(untagged)] | |
| enum ConditionValue { | |
| Value(String), | |
| Values(Vec<String>), | |
| } | |
| /// Represents a condition for segment rule evaluation. | |
| #[derive(Clone, Debug, Serialize, Deserialize)] | |
| pub struct Condition { | |
| /// The operator for this condition. | |
| pub operator: ConditionOperator, | |
| /// The property to evaluate (can be a JSONPath expression starting with $.). | |
| pub property: String, | |
| /// The value to compare against. | |
| pub value: ConditionValue, | |
| } |
| let entry = entry.expect("Failed to read directory entry"); | ||
| let path = entry.path(); | ||
|
|
||
| // Only process JSON and JSONC files | ||
| let extension = path.extension().and_then(|s| s.to_str()); | ||
| if extension != Some("json") && extension != Some("jsonc") { | ||
| continue; | ||
| } | ||
|
|
||
| let test_name = path | ||
| .file_name() | ||
| .and_then(|n| n.to_str()) | ||
| .unwrap_or("unknown"); | ||
|
|
||
| test_count += 1; | ||
|
|
||
| // Read the test file | ||
| let test_data = | ||
| fs::read_to_string(&path).expect(&format!("Failed to read test file: {:?}", path)); | ||
|
|
||
| // Strip comments if it's a JSONC file | ||
| let json_string = if extension == Some("jsonc") { | ||
| let mut stripped = String::new(); | ||
| StripComments::new(test_data.as_bytes()) | ||
| .read_to_string(&mut stripped) | ||
| .expect(&format!("Failed to strip comments from: {}", test_name)); | ||
| stripped | ||
| } else { | ||
| test_data | ||
| }; | ||
|
|
||
| // Parse the JSON | ||
| let test_json: serde_json::Value = serde_json::from_str(&json_string) | ||
| .expect(&format!("Failed to parse JSON for: {}", test_name)); | ||
|
|
||
| // Deserialize the context | ||
| let context_result: Result<EngineEvaluationContext, _> = | ||
| serde_json::from_value(test_json["context"].clone()); | ||
|
|
||
| if context_result.is_err() { | ||
| println!( | ||
| "FAIL {}: Failed to deserialize context: {:?}", | ||
| test_name, | ||
| context_result.err() | ||
| ); | ||
| failed += 1; | ||
| failed_tests.push(test_name.to_string()); | ||
| continue; | ||
| } | ||
|
|
||
| let context = context_result.unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: For cleaner test discovery, this could've been a macro, or a build script.
| if actual_flag.enabled != expected_flag.enabled { | ||
| println!( | ||
| "FAIL {}: Flag '{}' enabled mismatch - got {}, expected {}", | ||
| test_name, flag_name, actual_flag.enabled, expected_flag.enabled | ||
| ); | ||
| success = false; | ||
| } | ||
|
|
||
| if actual_flag.value != expected_flag.value { | ||
| println!( | ||
| "FAIL {}: Flag '{}' value mismatch - got {:?}, expected {:?}", | ||
| test_name, flag_name, actual_flag.value, expected_flag.value | ||
| ); | ||
| success = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's important to compare reasons as well:
| if actual_flag.enabled != expected_flag.enabled { | |
| println!( | |
| "FAIL {}: Flag '{}' enabled mismatch - got {}, expected {}", | |
| test_name, flag_name, actual_flag.enabled, expected_flag.enabled | |
| ); | |
| success = false; | |
| } | |
| if actual_flag.value != expected_flag.value { | |
| println!( | |
| "FAIL {}: Flag '{}' value mismatch - got {:?}, expected {:?}", | |
| test_name, flag_name, actual_flag.value, expected_flag.value | |
| ); | |
| success = false; | |
| if actual_flag.enabled != expected_flag.enabled { | |
| println!( | |
| "FAIL {}: Flag '{}' enabled mismatch - got {}, expected {}", | |
| test_name, flag_name, actual_flag.enabled, expected_flag.enabled | |
| ); | |
| success = false; | |
| } | |
| if actual_flag.value != expected_flag.value { | |
| println!( | |
| "FAIL {}: Flag '{}' value mismatch - got {:?}, expected {:?}", | |
| test_name, flag_name, actual_flag.value, expected_flag.value | |
| ); | |
| success = false; | |
| if actual_flag.reason != expected_flag.reason { | |
| println!( | |
| "FAIL {}: Flag '{}' reason mismatch - got {:?}, expected {:?}", | |
| test_name, flag_name, actual_flag.reason, expected_flag.reason | |
| ); | |
| success = false; |
| ); | ||
| success = false; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about segment names?
- Implement ConditionValue enum to handle string or array values - Supports Single(String) and Multiple(Vec<String>) variants - Custom deserializer handles JSON arrays, JSON array strings, and comma-separated strings - Helper methods: as_string(), as_vec(), contains_string() - Simplify IN operator implementation - Use ConditionValue's contains_string() for string matching - Remove redundant JSON array parsing logic
- Fix collapsible_if warning: collapse nested if statements - Fix manual_strip warning: use strip_suffix instead of manual slicing - Fix unwrap_or_default warning: use or_default() instead of or_insert_with(Vec::new)
No description provided.