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

feat: added newtype for condition and override in context apis #146

Merged
merged 1 commit into from
Aug 1, 2024

Conversation

pratikmishra356
Copy link
Collaborator

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

Endpoint Method Request body Response Body
API GET/POST, etc request response

Possible Issues in the future

NA

@pratikmishra356 pratikmishra356 requested a review from a team as a code owner July 2, 2024 11:30
@pratikmishra356 pratikmishra356 linked an issue Jul 2, 2024 that may be closed by this pull request
@Datron
Copy link
Collaborator

Datron commented Jul 3, 2024

@pratikmishra356 Can we add tests to check these validations?

Copy link
Collaborator

@Datron Datron left a 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?

AppError::ValidationError(msg)
| AppError::BadArgument(msg)
| AppError::NotFound(msg) => msg.to_owned(),
AppError::UnexpectedError(_) => String::from("Something went wrong"),
Copy link
Collaborator

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

Copy link
Collaborator

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

Copy link
Collaborator Author

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> {
Copy link
Collaborator

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| {
Copy link
Collaborator

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))?;
Copy link
Collaborator

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++?

Copy link
Collaborator

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);
Copy link
Collaborator

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") {
Copy link
Collaborator

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?

Suggested change
if let Some(Value::Object(res_context)) = res.get("context") {
if res.get("context").is_none() {

Throw error if true, process if false

@pratikmishra356 pratikmishra356 force-pushed the api-request-validation branch 6 times, most recently from 9da99ba to 1590454 Compare July 9, 2024 09:32
@pratikmishra356 pratikmishra356 force-pushed the api-request-validation branch 2 times, most recently from f50ff60 to 390606b Compare July 16, 2024 11:53
@@ -74,3 +77,276 @@ impl FromRequest for User {
}
}
}

#[derive(Debug, Clone, PartialEq)]
Copy link
Collaborator

@ayushjain17 ayushjain17 Jul 16, 2024

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:

Suggested change
#[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

Copy link
Collaborator

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");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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() {
Copy link
Collaborator

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.

Copy link
Collaborator Author

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],
Copy link
Collaborator

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.

Copy link
Collaborator Author

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

Copy link
Collaborator

@ayushjain17 ayushjain17 left a 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

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)],
Copy link
Collaborator

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

Suggested change
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()),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here also,

Suggested change
context: json!(req.context.clone()),
context: Value::Object(req.context.clone()),

experiment
.context
.as_object()
.unwrap_or(&Map::new())
Copy link
Collaborator

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

Suggested change
.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())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Suggested change
.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())],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here as well

Suggested change
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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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();
Copy link
Collaborator

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

Suggested change
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to maintain consistency

Suggested change
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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to maintain consistency

Suggested change
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());
Copy link
Collaborator

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

Suggested change
let ctx_condition = json!(req.context.into_inner());
let ctx_condition = Value::Object(req.context.into_inner());

Copy link
Collaborator

@Datron Datron left a 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);
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

@pratikmishra356 pratikmishra356 force-pushed the api-request-validation branch 2 times, most recently from 1cffea5 to 50b0507 Compare August 1, 2024 11:03
@mahatoankitkumar mahatoankitkumar merged commit 66ad741 into main Aug 1, 2024
4 checks passed
@mahatoankitkumar mahatoankitkumar deleted the api-request-validation branch August 1, 2024 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add new type for condition and override in context
4 participants