-
Notifications
You must be signed in to change notification settings - Fork 2
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
refactor(core): move PrecomputedConfiguration into core #110
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,29 +2,58 @@ use std::collections::HashMap; | |
use std::sync::Arc; | ||
|
||
use chrono::{DateTime, Utc}; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
use crate::eval::get_assignment; | ||
use crate::ufc::Assignment; | ||
use crate::ufc::{AssignmentFormat, Environment}; | ||
use crate::{Attributes, Configuration, EvaluationError, Str}; | ||
use crate::ufc::{Assignment, AssignmentFormat, Environment, ValueWire, VariationType}; | ||
use crate::{Attributes, Configuration, Str}; | ||
|
||
#[derive(Debug)] | ||
#[derive(Debug, Serialize, Deserialize)] | ||
#[serde(rename_all = "camelCase")] | ||
pub struct PrecomputedConfiguration { | ||
pub created_at: DateTime<Utc>, | ||
pub format: AssignmentFormat, | ||
pub environment: Environment, | ||
pub flags: HashMap<String, Result<Assignment, EvaluationError>>, | ||
created_at: DateTime<Utc>, | ||
/// `format` is always `AssignmentFormat::Precomputed`. | ||
format: AssignmentFormat, | ||
// Environment might be missing if configuration was absent during evaluation. | ||
environment: Option<Environment>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no good default environment value, so decided to |
||
flags: HashMap<String, PrecomputedAssignment>, | ||
} | ||
|
||
impl PrecomputedConfiguration { | ||
pub fn empty(environment_name: &str) -> Self { | ||
Self { | ||
created_at: Utc::now(), | ||
format: AssignmentFormat::Precomputed, | ||
environment: Environment { | ||
name: environment_name.into(), | ||
#[derive(Debug, Serialize, Deserialize)] | ||
#[serde(rename_all = "camelCase")] | ||
struct PrecomputedAssignment { | ||
variation_type: VariationType, | ||
variation_value: ValueWire, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One diff here is that JSON assignment are now encoded as strings — following UFC flags config approach |
||
|
||
do_log: bool, | ||
// If `do_log` is false, the client doesn’t need the field below. | ||
#[serde(default, skip_serializing_if = "Option::is_none")] | ||
allocation_key: Option<Str>, | ||
#[serde(default, skip_serializing_if = "Option::is_none")] | ||
variation_key: Option<Str>, | ||
#[serde(default, skip_serializing_if = "Option::is_none")] | ||
extra_logging: Option<HashMap<String, String>>, | ||
Comment on lines
+30
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decided to skip serializing these if |
||
} | ||
|
||
impl From<Assignment> for PrecomputedAssignment { | ||
fn from(assignment: Assignment) -> PrecomputedAssignment { | ||
match assignment.event { | ||
Some(event) => PrecomputedAssignment { | ||
variation_type: assignment.value.variation_type(), | ||
variation_value: assignment.value.variation_value(), | ||
do_log: true, | ||
allocation_key: Some(event.base.allocation.clone()), | ||
variation_key: Some(event.base.variation.clone()), | ||
extra_logging: Some(event.base.extra_logging.clone()), | ||
}, | ||
None => PrecomputedAssignment { | ||
variation_type: assignment.value.variation_type(), | ||
variation_value: assignment.value.variation_value(), | ||
do_log: false, | ||
allocation_key: None, | ||
variation_key: None, | ||
extra_logging: None, | ||
}, | ||
flags: HashMap::new(), | ||
} | ||
} | ||
} | ||
|
@@ -33,53 +62,50 @@ pub fn get_precomputed_assignments( | |
configuration: Option<&Configuration>, | ||
subject_key: &Str, | ||
subject_attributes: &Arc<Attributes>, | ||
early_exit: bool, | ||
now: DateTime<Utc>, | ||
) -> PrecomputedConfiguration { | ||
if let Some(config) = configuration { | ||
let mut flags = HashMap::new(); | ||
|
||
for key in config.flags.compiled.flags.keys() { | ||
match get_assignment( | ||
Some(config), | ||
key, | ||
&subject_key, | ||
&subject_attributes, | ||
None, | ||
now, | ||
) { | ||
Ok(Some(assignment)) => { | ||
flags.insert(key.clone(), Ok(assignment)); | ||
} | ||
Ok(None) => continue, | ||
Err(e) => { | ||
eprintln!("Failed to evaluate assignment for key {}: {:?}", key, e); | ||
if early_exit { | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
||
log::trace!(target: "eppo", | ||
subject_key, | ||
assignments:serde = flags; | ||
"evaluated precomputed assignments"); | ||
PrecomputedConfiguration { | ||
let Some(configuration) = configuration else { | ||
log::warn!(target: "eppo", | ||
subject_key; | ||
"evaluating a flag before Eppo configuration has been fetched"); | ||
return PrecomputedConfiguration { | ||
created_at: now, | ||
format: AssignmentFormat::Precomputed, | ||
environment: { | ||
Environment { | ||
name: config.flags.compiled.environment.name.clone(), | ||
} | ||
}, | ||
flags, | ||
environment: None, | ||
flags: HashMap::new(), | ||
}; | ||
}; | ||
|
||
let mut flags = HashMap::new(); | ||
|
||
for key in configuration.flags.compiled.flags.keys() { | ||
match get_assignment( | ||
Some(configuration), | ||
key, | ||
&subject_key, | ||
&subject_attributes, | ||
None, | ||
now, | ||
) { | ||
Ok(Some(assignment)) => { | ||
flags.insert(key.clone(), assignment.into()); | ||
} | ||
Ok(None) => {} | ||
Err(e) => { | ||
eprintln!("Failed to evaluate assignment for key {}: {:?}", key, e); | ||
} | ||
} | ||
} else { | ||
log::warn!(target: "eppo", | ||
subject_key; | ||
"evaluating a flag before Eppo configuration has been fetched"); | ||
PrecomputedConfiguration::empty("unknown") | ||
} | ||
|
||
log::trace!(target: "eppo", | ||
subject_key, | ||
assignments:serde = flags; | ||
"evaluated precomputed assignments"); | ||
PrecomputedConfiguration { | ||
created_at: now, | ||
format: AssignmentFormat::Precomputed, | ||
environment: Some(configuration.flags.compiled.environment.clone()), | ||
flags, | ||
} | ||
} | ||
|
||
|
@@ -88,9 +114,8 @@ mod tests { | |
use chrono::Utc; | ||
|
||
use crate::{ | ||
eval::get_precomputed_assignments, | ||
ufc::{UniversalFlagConfig, VariationType}, | ||
Attributes, Configuration, SdkMetadata, | ||
eval::get_precomputed_assignments, ufc::UniversalFlagConfig, Attributes, Configuration, | ||
SdkMetadata, | ||
}; | ||
use std::{fs, sync::Arc}; | ||
|
||
|
@@ -122,7 +147,6 @@ mod tests { | |
Some(&configuration), | ||
&subject_key, | ||
&subject_attributes, | ||
false, | ||
now, | ||
); | ||
|
||
|
@@ -140,38 +164,4 @@ mod tests { | |
); | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_precomputed_assignment_early_exit() { | ||
let mut configuration = setup_test_config(); | ||
let num_good_flags = configuration.flags.compiled.flags.len(); | ||
|
||
// Add a flag that will cause an evaluation error | ||
configuration.flags.compiled.flags.insert( | ||
"error-flag".to_string(), | ||
Ok(crate::ufc::Flag { | ||
variation_type: VariationType::String, | ||
allocations: vec![].into_boxed_slice(), | ||
}), | ||
); | ||
|
||
let subject_key = "test-subject-1".into(); | ||
let subject_attributes = Arc::new(Attributes::new()); | ||
let now = Utc::now(); | ||
|
||
// Get assignments with early exit | ||
let precomputed_with_early_exit = get_precomputed_assignments( | ||
Some(&configuration), | ||
&subject_key, | ||
&subject_attributes, | ||
true, | ||
now, | ||
); | ||
|
||
// Verify we have fewer entries due to early exit | ||
assert!( | ||
precomputed_with_early_exit.flags.len() < num_good_flags, | ||
"Early exit should stop processing on first error" | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -117,14 +117,12 @@ impl Evaluator { | |
&self, | ||
subject_key: &Str, | ||
subject_attributes: &Arc<Attributes>, | ||
early_exit: bool, | ||
) -> PrecomputedConfiguration { | ||
let configuration = self.get_configuration(); | ||
get_precomputed_assignments( | ||
configuration.as_ref().map(AsRef::as_ref), | ||
&subject_key, | ||
&subject_attributes, | ||
early_exit, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 ok your philosophy is not to implement something without a use, I can understand that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't say it's my "philosophy" but in this case I'm pretty skeptical that we ever see a use case for early exit (given we spec'ed precomputed format and that it shouldn't early exit). |
||
Utc::now(), | ||
) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,8 @@ use crate::{events::AssignmentEvent, Str}; | |
|
||
use crate::ufc::VariationType; | ||
|
||
use super::ValueWire; | ||
|
||
/// Result of assignment evaluation. | ||
#[derive(Debug, Serialize, Clone)] | ||
#[serde(rename_all = "camelCase")] | ||
|
@@ -17,7 +19,7 @@ pub struct Assignment { | |
} | ||
|
||
/// Enum representing values assigned to a subject as a result of feature flag evaluation. | ||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] | ||
#[derive(Debug, Serialize, Deserialize, Clone)] | ||
#[serde(tag = "type", content = "value", rename_all = "SCREAMING_SNAKE_CASE")] | ||
pub enum AssignmentValue { | ||
/// A string value. | ||
|
@@ -29,10 +31,38 @@ pub enum AssignmentValue { | |
/// A boolean value. | ||
Boolean(bool), | ||
/// Arbitrary JSON value. | ||
Json(Arc<serde_json::Value>), | ||
Json { | ||
raw: Str, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Saving raw json value here. This will also allow us to return raw string in some SDKs if users want to use their own parser. |
||
parsed: Arc<serde_json::Value>, | ||
}, | ||
} | ||
|
||
impl PartialEq for AssignmentValue { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cool! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. haha, |
||
// Compare ignoring Json::raw. | ||
fn eq(&self, other: &Self) -> bool { | ||
match (self, other) { | ||
(AssignmentValue::String(v1), AssignmentValue::String(v2)) => v1 == v2, | ||
(AssignmentValue::Integer(v1), AssignmentValue::Integer(v2)) => v1 == v2, | ||
(AssignmentValue::Numeric(v1), AssignmentValue::Numeric(v2)) => v1 == v2, | ||
(AssignmentValue::Boolean(v1), AssignmentValue::Boolean(v2)) => v1 == v2, | ||
( | ||
AssignmentValue::Json { parsed: v1, .. }, | ||
AssignmentValue::Json { parsed: v2, .. }, | ||
) => v1 == v2, | ||
_ => false, | ||
} | ||
} | ||
} | ||
|
||
impl AssignmentValue { | ||
pub fn from_json(value: serde_json::Value) -> Result<AssignmentValue, serde_json::Error> { | ||
let raw = serde_json::to_string(&value)?; | ||
Ok(AssignmentValue::Json { | ||
raw: raw.into(), | ||
parsed: Arc::new(value), | ||
}) | ||
} | ||
|
||
/// Checks if the assignment value is of type String. | ||
/// | ||
/// # Returns | ||
|
@@ -189,7 +219,7 @@ impl AssignmentValue { | |
/// # use eppo_core::ufc::AssignmentValue; | ||
/// use serde_json::json; | ||
/// | ||
/// let value = AssignmentValue::Json(json!({ "key": "value" }).into()); | ||
/// let value = AssignmentValue::from_json(json!({ "key": "value" }).into()).unwrap(); | ||
/// assert_eq!(value.is_json(), true); | ||
/// ``` | ||
pub fn is_json(&self) -> bool { | ||
|
@@ -205,12 +235,12 @@ impl AssignmentValue { | |
/// # use eppo_core::ufc::AssignmentValue; | ||
/// use serde_json::json; | ||
/// | ||
/// let value = AssignmentValue::Json(json!({ "key": "value" }).into()); | ||
/// let value = AssignmentValue::from_json(json!({ "key": "value" }).into()).unwrap(); | ||
/// assert_eq!(value.as_json(), Some(&json!({ "key": "value" }))); | ||
/// ``` | ||
pub fn as_json(&self) -> Option<&serde_json::Value> { | ||
match self { | ||
Self::Json(v) => Some(v), | ||
Self::Json { raw: _, parsed } => Some(parsed), | ||
_ => None, | ||
} | ||
} | ||
|
@@ -224,12 +254,12 @@ impl AssignmentValue { | |
/// # use eppo_core::ufc::AssignmentValue; | ||
/// use serde_json::json; | ||
/// | ||
/// let value = AssignmentValue::Json(json!({ "key": "value" }).into()); | ||
/// let value = AssignmentValue::from_json(json!({ "key": "value" }).into()).unwrap(); | ||
/// assert_eq!(value.to_json(), Some(json!({ "key": "value" }).into())); | ||
/// ``` | ||
pub fn to_json(self) -> Option<Arc<serde_json::Value>> { | ||
match self { | ||
Self::Json(v) => Some(v), | ||
Self::Json { raw: _, parsed } => Some(parsed), | ||
_ => None, | ||
} | ||
} | ||
|
@@ -252,31 +282,21 @@ impl AssignmentValue { | |
AssignmentValue::Integer(_) => VariationType::Integer, | ||
AssignmentValue::Numeric(_) => VariationType::Numeric, | ||
AssignmentValue::Boolean(_) => VariationType::Boolean, | ||
AssignmentValue::Json(_) => VariationType::Json, | ||
AssignmentValue::Json { .. } => VariationType::Json, | ||
} | ||
} | ||
|
||
/// Returns the raw value of the variation. | ||
/// | ||
/// # Returns | ||
/// - A JSON Value containing the variation value. | ||
/// | ||
/// # Examples | ||
/// ``` | ||
/// # use eppo_core::ufc::AssignmentValue; | ||
/// # use serde_json::json; | ||
/// let value = AssignmentValue::String("example".into()); | ||
/// assert_eq!(value.variation_value(), json!("example")); | ||
/// ``` | ||
pub fn variation_value(&self) -> serde_json::Value { | ||
pub(crate) fn variation_value(&self) -> ValueWire { | ||
match self { | ||
AssignmentValue::String(s) => serde_json::Value::String(s.to_string()), | ||
AssignmentValue::Integer(i) => serde_json::Value::Number((*i).into()), | ||
AssignmentValue::Numeric(n) => serde_json::Value::Number( | ||
serde_json::Number::from_f64(*n).unwrap_or_else(|| serde_json::Number::from(0)), | ||
), | ||
AssignmentValue::Boolean(b) => serde_json::Value::Bool(*b), | ||
AssignmentValue::Json(j) => j.as_ref().clone(), | ||
AssignmentValue::String(s) => ValueWire::String(s.clone()), | ||
AssignmentValue::Integer(i) => ValueWire::Number(*i as f64), | ||
AssignmentValue::Numeric(n) => ValueWire::Number(*n), | ||
AssignmentValue::Boolean(b) => ValueWire::Boolean(*b), | ||
AssignmentValue::Json { raw, parsed: _ } => ValueWire::String(raw.clone()), | ||
} | ||
} | ||
} | ||
|
@@ -296,10 +316,12 @@ mod pyo3_impl { | |
AssignmentValue::Integer(i) => i.to_object(py), | ||
AssignmentValue::Numeric(n) => n.to_object(py), | ||
AssignmentValue::Boolean(b) => b.to_object(py), | ||
AssignmentValue::Json(j) => match serde_pyobject::to_pyobject(py, j) { | ||
Ok(it) => it.unbind(), | ||
Err(err) => return Err(err.0), | ||
}, | ||
AssignmentValue::Json { raw: _, parsed } => { | ||
match serde_pyobject::to_pyobject(py, parsed) { | ||
Ok(it) => it.unbind(), | ||
Err(err) => return Err(err.0), | ||
} | ||
} | ||
}; | ||
Ok(obj) | ||
} | ||
|
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.
Another change here is that all fields of
PrecomputedConfiguration
are private toeppo_core
: edge server shouldn't need to tinker with them and clients would have their own parsers.Basically, the only way to interact with
PrecomputedConfiguration
is via serialize/deserialize.This should allow us tweaking the type here without being afraid of breaking the edge server.
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.
Good idea! That's a nice way to signal where it should used, noted.