-
Notifications
You must be signed in to change notification settings - Fork 21
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
feat: added newtype for condition and override in context apis #146
Conversation
@pratikmishra356 Can we add tests to check these validations? |
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.
Are all validations for context and overrides included here or more expected in another PR? Can we add tests to check the negative cases for validations so we can guarantee that they work in the future too?
crates/service_utils/src/result.rs
Outdated
AppError::ValidationError(msg) | ||
| AppError::BadArgument(msg) | ||
| AppError::NotFound(msg) => msg.to_owned(), | ||
AppError::UnexpectedError(_) => String::from("Something went wrong"), |
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.
Lets stop doing "Something went wrong". Need a better error message here
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.
Also log and throw errors if appropriate
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.
@Datron The string returned from message function should be same as what string returned using error macro.
For logging , wherever we pass error in these macro like bad_argument!(err) -> we are logging error there itself. Like you can check new function.
This function just helps in getting the message which Apperror sends , so that we can pass the same message into other error types like here serde deserialisation error
pub struct Override(Map<String, Value>); | ||
|
||
impl Override { | ||
pub fn new(override_map: Map<String, Value>) -> superposition::Result<Self> { |
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.
Should we validate the schema of the override value as well using json schema?
impl Condition { | ||
pub fn new(condition_map: Map<String, Value>) -> superposition::Result<Self> { | ||
if !condition_map.is_empty() { | ||
jsonlogic::apply(&json!(condition_map), &json!({})).map_err(|msg| { |
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.
We can use jsonlogic::compile here if we want to check validity instead of applying it
) -> superposition::Result<()> { | ||
use dimensions::dsl; | ||
let context = extract_dimensions(context)?; | ||
let context = extract_dimensions(&json!(**condition))?; |
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.
derive_more has an auto deref trait. Can we use that to avoid our code looking like C++?
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.
Can we also add a to_json() method on newtypes? Skip one more step here
let ctx_condition = Value::Object(req.context.to_owned()); | ||
let ctx_override: Value = req.r#override.to_owned().into(); | ||
let ctx_condition = req.context.to_owned(); | ||
let condition_val = json!(*ctx_condition); |
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.
Use derive_more Deref to avoid this.
fn construct_new_payload(req_payload: &Map<String, Value>) -> web::Json<PutReq> { | ||
fn construct_new_payload( | ||
req_payload: &Map<String, Value>, | ||
) -> superposition::Result<web::Json<PutReq>> { | ||
let mut res = req_payload.clone(); | ||
res.remove("to_be_deleted"); | ||
res.remove("override_id"); | ||
res.remove("id"); | ||
if let Some(Value::Object(res_context)) = res.get("context") { |
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.
Can we do negative checks first to avoid this nesting?
if let Some(Value::Object(res_context)) = res.get("context") { | |
if res.get("context").is_none() { |
Throw error if true, process if false
9da99ba
to
1590454
Compare
f50ff60
to
390606b
Compare
@@ -74,3 +77,276 @@ impl FromRequest for User { | |||
} | |||
} | |||
} | |||
|
|||
#[derive(Debug, Clone, PartialEq)] |
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.
can we use something like this:
#[derive(Debug, Clone, PartialEq)] | |
#[derive(Debug, Clone, PartialEq)] | |
pub enum ValidationType { | |
DEFAULT, | |
EXPERIMENTAL, | |
#[cfg(feature = "db_type_validation_disabled")] | |
DB, | |
} | |
pub fn get_db_validation_type() -> ValidationType { | |
#[cfg(feature = "db_type_validation_disabled")] | |
return ValidationType::DB; | |
#[cfg(not(feature = "db_type_validation_disabled"))] | |
return ValidationType::DEFAULT; | |
} |
we can utilise rust features here, and anyone who has old version of code can go ahead with db_type_validation_disabled
feature (or whatever you want to call it) as newer versions will not have discrepancy in DB data
wherever you want to use ValidationType::DB
, just use this function get_db_validation_type
directly, which will resolve the value based on the feature which the user of superposition requires
later on, when we are sure that data is not longer corrupt in DB, we can get the feature removed altogether
PS: db_type_validation_disabled
should be a superposition_types level feature
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.
places having a switch case can use something like:
#[cfg(feature = "db_type_validation_disabled")]
ValidationType::DB => (),
.and_then(|val| val.as_object()) | ||
.map_or_else( | ||
|| { | ||
log::error!("construct new payload Context not present"); |
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.
log::error!("construct new payload Context not present"); | |
log::error!("construct new payload: Context not present"); |
let mut contexts = Vec::new(); | ||
let mut overrides: HashMap<String, Overrides> = HashMap::new(); | ||
|
||
for (id, condition, priority_, override_id, override_) in contexts_vec.iter() { |
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.
Could had used fold here also.
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.
can be used , but it gives no advantage over for loop and readability reduces as multiple variables are being used here
pub default_configs: Map<String, Value>, | ||
} | ||
|
||
#[derive(Serialize, Clone, Deserialize)] | ||
pub struct Context { | ||
pub id: String, | ||
pub condition: Value, | ||
pub condition: Condition, | ||
pub priority: i32, | ||
pub override_with_keys: [String; 1], |
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.
Maybe we can make a type alias for override_with_keys
also.
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.
@mahatoankitkumar [String; 1]
seems enough for this validation and scope of this pr is only for context and overrides
16cd13e
to
c82dee6
Compare
48f963c
to
fadcfee
Compare
5af29db
to
eb4e3a2
Compare
eb4e3a2
to
17c9a95
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.
Can we avoid json! macro and specify Value::Object directly and keep parameter or variables name same as condition for Condition types to avoid inconsistency
crates/service_utils/src/helpers.rs
Outdated
Some(conditions_json) => conditions_json | ||
.as_array() | ||
.ok_or(result::AppError::BadArgument("Error extracting dimensions, failed parsing conditions as an array. Ensure the context provided obeys the rules of JSON logic".into()))? | ||
.clone(), | ||
None => vec![context_json.clone()], | ||
None => vec![json!(*context)], |
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.
can we make it direct instead of using json macro here
None => vec![json!(*context)], | |
None => vec![Value::Object(*context)], |
@@ -264,7 +270,7 @@ async fn create( | |||
override_keys: unique_override_keys.to_vec(), | |||
traffic_percentage: 0, | |||
status: ExperimentStatusType::CREATED, | |||
context: req.context.clone(), | |||
context: json!(req.context.clone()), |
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.
here also,
context: json!(req.context.clone()), | |
context: Value::Object(req.context.clone()), |
experiment | ||
.context | ||
.as_object() | ||
.unwrap_or(&Map::new()) |
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.
can we do something like this over here
.unwrap_or(&Map::new()) | |
.map_or_else(|| Map::new(), |ctx| ctx.clone()) |
else, in the current implementation, in the None case, we are creating a new Map and then right after that cloning it, which doesn't make sense
active_experiment | ||
.context | ||
.as_object() | ||
.unwrap_or(&Map::new()) |
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.
same here
.unwrap_or(&Map::new()) | |
.map_or_else(|| Map::new(), |ctx| ctx.clone()) |
@@ -222,7 +221,7 @@ pub fn add_variant_dimension_to_ctx( | |||
"Failed parsing conditions as an array. Ensure the context provided obeys the rules of JSON logic" | |||
))? | |||
.clone(), | |||
None => vec![context_json.clone()], | |||
None => vec![json!(context.clone())], |
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.
here as well
None => vec![json!(context.clone())], | |
None => vec![Value::Object(context.clone())], |
@@ -198,7 +200,7 @@ pub fn validate_experiment( | |||
} | |||
|
|||
pub fn add_variant_dimension_to_ctx( | |||
context_json: &Value, | |||
context_json: &Condition, |
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.
context_json: &Condition, | |
context: &Condition, |
let context = context_json.as_object().ok_or(bad_argument!( | ||
"Context not an object. Ensure the context provided obeys the rules of JSON logic" | ||
))?; | ||
let context: Map<String, Value> = context_json.to_owned().into(); |
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.
wouldn't it be better to go this way, after the above comment change is added
let context: Map<String, Value> = context_json.to_owned().into(); | |
let context_json = context.clone().into_inner(); |
let mut expected_condition = Map::new(); | ||
expected_condition.insert("foo".to_string(), json!("bar")); | ||
expected_condition.insert("bar".to_string(), json!({ "baz": "baz"})); | ||
let condition = Cac::<Condition>::try_from(expected_condition) |
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.
to maintain consistency
let condition = Cac::<Condition>::try_from(expected_condition) | |
let context = Cac::<Condition>::try_from(expected_condition) |
@@ -24,10 +24,10 @@ type DBConnection = PooledConnection<ConnectionManager<PgConnection>>; | |||
|
|||
pub fn validate_condition_with_functions( | |||
conn: &mut DBConnection, | |||
context: &Value, | |||
condition: &Condition, |
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.
to maintain consistency
condition: &Condition, | |
context: &Condition, |
@@ -399,7 +401,7 @@ fn r#move( | |||
) -> superposition::Result<PutResp> { | |||
use contexts::dsl; | |||
let req = req.into_inner(); | |||
let ctx_condition = Value::Object(req.context); | |||
let ctx_condition = json!(req.context.into_inner()); |
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.
can we let it be Value::Object itself
let ctx_condition = json!(req.context.into_inner()); | |
let ctx_condition = Value::Object(req.context.into_inner()); |
17c9a95
to
28574d7
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.
few nitpicks but overall looks good
@@ -74,3 +77,307 @@ impl FromRequest for User { | |||
} | |||
} | |||
} | |||
|
|||
#[derive(Clone, Debug, PartialEq, Copy, Serialize)] | |||
pub struct Cac<T>(T); |
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.
Can we implement deref trait instead of into_inner
?
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.
@Datron into_inner gives the type wrapped inside i.e T,
Deref trait mainly dereference the the value ,
and also Deref wont work in this case as Deref on CAC requires copy trait for Condition , and we cant have Copy trait for condition as it wraps serde Map which does not have the Copy trait either
cannot move out of dereference of `Cac<superposition_types::Condition>`
move occurs because value has type `superposition_types::Condition`, which does not implement the `Copy` trait
1cffea5
to
50b0507
Compare
50b0507
to
bc32633
Compare
bc32633
to
8edfa52
Compare
Problem
Request Data Validation was not present in context apis
Solution
Created newtype for condition and override and added validation there
Environment variable changes
NA
Pre-deployment activity
NA
Post-deployment activity
NA
API changes
Possible Issues in the future
NA