Skip to content
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

Initial implementation #2

Merged
merged 13 commits into from
May 11, 2024
Prev Previous commit
Next Next commit
test: integrate sdk-test-data test for eval
  • Loading branch information
rasendubi committed Apr 30, 2024
commit 407c722e30b15473baae66ee4ea34822859a0734
2 changes: 2 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ impl<'a> EppoClient<'a> {
pub type SubjectAttributes = HashMap<String, AttributeValue>;

#[derive(Debug, Serialize, Deserialize, PartialEq, PartialOrd, From, Clone)]
#[serde(untagged)]
pub enum AttributeValue {
String(String),
Number(f64),
Boolean(bool),
Null,
}
impl From<&str> for AttributeValue {
fn from(value: &str) -> Self {
Expand Down
84 changes: 84 additions & 0 deletions src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,87 @@ impl Shard {
self.ranges.iter().any(|range| range.contains(h))
}
}

#[cfg(test)]
mod tests {
use std::fs::{self, File};

use serde::{Deserialize, Serialize};

use crate::{
sharder::Md5Sharder,
ufc::{Flag, TryParse, Ufc, Value, VariationType},
SubjectAttributes,
};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TestFile {
flag: String,
variation_type: VariationType,
default_value: TryParse<Value>,
subjects: Vec<TestSubject>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TestSubject {
subject_key: String,
subject_attributes: SubjectAttributes,
assignment: TryParse<Value>,
}

// Test files have different representation of Value for JSON. Whereas server returns a string
// that has to be further parsed, test files embed the JSON object directly.
//
// Therefore, if we failed to parse "assignment" field as one of the values, we fallback to
// AttributeValue::Json.
fn to_value(try_parse: TryParse<Value>) -> Value {
match try_parse {
TryParse::Parsed(v) => v,
TryParse::ParseFailed(json) => Value::String(serde_json::to_string(&json).unwrap()),
}
}

#[test]
fn evaluation_sdk_test_data() {
let config: Ufc =
serde_json::from_reader(File::open("tests/data/ufc/flags-v1.json").unwrap()).unwrap();

for entry in fs::read_dir("tests/data/ufc/tests/").unwrap() {
let entry = entry.unwrap();
println!("Processing test file: {:?}", entry.path());

let f = File::open(entry.path()).unwrap();
let test_file: TestFile = serde_json::from_reader(f).unwrap();

let flag: Option<&Flag> = config.flags.get(&test_file.flag).and_then(|x| x.into());

let default_assignment = to_value(test_file.default_value)
.to_assignment_value(test_file.variation_type)
.unwrap();

for subject in test_file.subjects {
print!("test subject {:?} ... ", subject.subject_key);
let result = flag.and_then(|f| {
f.eval(
&subject.subject_key,
&subject.subject_attributes,
&Md5Sharder,
)
});

let result_assingment = result
.as_ref()
.map(|(value, _event)| value)
.unwrap_or(&default_assignment);
let expected_assignment = to_value(subject.assignment)
.to_assignment_value(test_file.variation_type)
.unwrap();

assert_eq!(result_assingment, &expected_assignment);
println!("ok");
}
}
}
}
3 changes: 2 additions & 1 deletion src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ impl Operator {
}

Self::IsNull => {
let is_null = attribute.is_none();
let is_null =
attribute.is_none() || attribute.is_some_and(|v| v == &AttributeValue::Null);
match condition_value {
ConditionValue::Single(Value::Boolean(true)) => Some(is_null),
ConditionValue::Single(Value::Boolean(false)) => Some(!is_null),
Expand Down
10 changes: 9 additions & 1 deletion src/ufc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{client::AssignmentValue, rules::Rule};
pub struct Ufc {
// Value is wrapped in `TryParse` so that if we fail to parse one flag (e.g., new server
// format), we can still serve other flags.
flags: HashMap<String, TryParse<Flag>>,
pub flags: HashMap<String, TryParse<Flag>>,
}

/// `TryParse` allows the subfield to fail parsing without failing the parsing of the whole
Expand All @@ -22,6 +22,14 @@ pub enum TryParse<T> {
Parsed(T),
ParseFailed(serde_json::Value),
}
impl<T> From<TryParse<T>> for Result<T, serde_json::Value> {
fn from(value: TryParse<T>) -> Self {
match value {
TryParse::Parsed(v) => Ok(v),
TryParse::ParseFailed(v) => Err(v),
}
}
}
impl<T> From<TryParse<T>> for Option<T> {
fn from(value: TryParse<T>) -> Self {
match value {
Expand Down