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: reducing context tool #44

Merged
merged 4 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
351 changes: 344 additions & 7 deletions crates/context_aware_config/src/api/config/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,23 @@ use std::{collections::HashMap, str::FromStr};
use super::helpers::{
filter_config_by_dimensions, filter_config_by_prefix, filter_context,
};

use super::types::Config;
use crate::db::schema::{
contexts::dsl as ctxt, default_configs::dsl as def_conf, event_log::dsl as event_log,
use super::types::{Config, Context};
use crate::api::context::{
delete_context_api, hash, put, validate_dimensions_and_calculate_priority, PutReq,
};
use crate::api::dimension::get_all_dimension_schema_map;
use crate::{
db::schema::{
contexts::dsl as ctxt, default_configs::dsl as def_conf,
event_log::dsl as event_log,
},
helpers::json_to_sorted_string,
};
use actix_http::header::{HeaderName, HeaderValue};
use actix_web::{get, web::Query, HttpRequest, HttpResponse, Scope};
use actix_web::web;
use actix_web::{
error::ErrorBadRequest, get, put, web::Query, HttpRequest, HttpResponse, Scope,
};
use cac_client::{eval_cac, eval_cac_with_reasoning, MergeStrategy};
use chrono::{DateTime, NaiveDateTime, TimeZone, Timelike, Utc};
use diesel::{
Expand All @@ -22,13 +32,17 @@ use serde_json::{json, Map, Value};
use service_utils::service::types::DbConnection;
use service_utils::{bad_argument, db_error, unexpected_error};

use service_utils::result as superposition;
use itertools::Itertools;
use jsonschema::JSONSchema;
use service_utils::helpers::extract_dimensions;
use service_utils::result::{self as superposition, AppError};
use superposition_types::User;
use uuid::Uuid;

pub fn endpoints() -> Scope {
Scope::new("")
.service(get)
.service(get_resolved_config)
.service(reduce_context)
.service(get_filtered_config)
}

Expand Down Expand Up @@ -147,6 +161,329 @@ async fn generate_cac(
})
}

fn generate_subsets(map: &Map<String, Value>) -> Vec<Map<String, Value>> {
let mut subsets = Vec::new();
let keys: Vec<String> = map.keys().cloned().collect_vec();
let all_subsets_keys = generate_subsets_keys(keys);

for subset_keys in &all_subsets_keys {
if subset_keys.len() >= 0 {
let mut subset_map = Map::new();

for key in subset_keys {
if let Some(value) = map.get(key) {
subset_map.insert(key.to_string(), value.clone());
}
}

subsets.push(subset_map);
}
}

subsets
}

fn generate_subsets_keys(keys: Vec<String>) -> Vec<Vec<String>> {
let mut res = vec![[].to_vec()];
for element in keys {
let len = res.len();
for ind in 0..len {
let mut sub = res[ind].clone();
sub.push(element.clone());
res.push(sub);
}
}
res
}

fn reduce(
contexts_overrides_values: Vec<(Context, Map<String, Value>, Value, String)>,
default_config_val: &Value,
) -> superposition::Result<Vec<Map<String, Value>>> {
let mut dimensions: Vec<Map<String, Value>> = Vec::new();
for (context, overrides, key_val, override_id) in contexts_overrides_values {
let mut ct_dimensions = extract_dimensions(&context.condition)?;
ct_dimensions.insert("key_val".to_string(), key_val);
let request_payload = json!({
"override": overrides,
"context": context.condition,
"id": context.id,
"to_be_deleted": overrides.is_empty(),
"override_id": override_id,
});
ct_dimensions.insert("req_payload".to_string(), request_payload);
dimensions.push(ct_dimensions);
}

//adding default config value
let mut default_config_map = Map::new();
default_config_map.insert("key_val".to_string(), default_config_val.to_owned());
dimensions.push(default_config_map);

Sauravcv98 marked this conversation as resolved.
Show resolved Hide resolved
/*
We now have dimensions array, which is a vector of elements representing each context present where each element is a type of Map<String,Value> which contains the following
1. all the dimensions and value of those dimensions in the context
2. key_val, which is the value of the override key for which we are trying to reduce
3. A req_payload which contains the details of the context like, context_id, override_id, the context_condition, new overrides (without containing the key that has to be reduced)
{
dimension1_in_context : value_of_dimension1_in_context,
dimension2_in_context : value_of_dimension2_in_context,
.
.
key_val: value of the override key that we are trying to reduce
req_payload : {
override : new_overrides(without the key that is to be reduced)
context : context_condition
id : context_id
to_be_deleted : if new_overrides is empty then delete this context
}
}

We have also sorted this dimensions vector in descending order based on the priority of the dimensions in that context
and in this vector the default config will be at the end of the list as it has no dimensions and it's priority is the least

Now we iterate from start and then pick an element and generate all subsets of that element keys excluding the req_payload and key_val
i.e we only generate different subsets of dimensions of that context along with the value of those dimensions in that context

Next we check if in the vector we find any other element c2 whose dimensions is part of the subsets of the parent element c1
if dimensions_subsets_of_c1 contains dimensions_of_c2

if the value of the override key is same in both c1 and c2 then we can reduce or remove that key in the override of c1
so we mark the can_be_reduce to be true, and then update the dimensions vector.

but if we find any other element c3 whose dimensions is a subset of c1_dimensions but the value is not the same
then that means we can't reduce this key from c1, because in resolve if we remove it from c1 it will pick the value form c3 which is different.
So if we find this element c3 before any other element which is a subset of c1 with the same value, then we can't reduce this key for c1 so we break
and continue with the next element.
Here "before" means the element with higher priority comes first with a subset of c1 but differnt override value for the key
*/
for (c1_index, dimensions_of_c1_with_payload) in dimensions.clone().iter().enumerate()
{
let mut dimensions_of_c1 = dimensions_of_c1_with_payload.clone();
dimensions_of_c1.remove("req_payload");
let override_val_of_key_in_c1 = dimensions_of_c1.remove("key_val");
let dimensions_subsets_of_c1 = generate_subsets(&dimensions_of_c1);
for (c2_index, dimensions_in_c2_with_payload) in dimensions.iter().enumerate() {
let mut dimensions_of_c2 = dimensions_in_c2_with_payload.clone();
dimensions_of_c2.remove("req_payload");
let override_val_of_key_in_c2 = dimensions_of_c2.remove("key_val");
if c2_index != c1_index {
if dimensions_subsets_of_c1.contains(&dimensions_of_c2) {
if override_val_of_key_in_c1 == override_val_of_key_in_c2 {
let mut temp_c1 = dimensions_of_c1_with_payload.to_owned();
temp_c1.insert("can_be_reduced".to_string(), Value::Bool(true));
dimensions[c1_index] = temp_c1;
break;
} else if override_val_of_key_in_c2.is_some() {
break;
}
}
}
}
}
Ok(dimensions)
}

fn get_contextids_from_overrideid(
contexts: Vec<Context>,
overrides: Map<String, Value>,
key_val: Value,
override_id: &str,
) -> superposition::Result<Vec<(Context, Map<String, Value>, Value, String)>> {
let mut res: Vec<(Context, Map<String, Value>, Value, String)> = Vec::new();
for ct in contexts {
let ct_dimensions = extract_dimensions(&ct.condition)?;
if ct_dimensions.contains_key("variantIds") {
continue;
}
let override_keys = &ct.override_with_keys;
if override_keys.contains(&override_id.to_owned()) {
res.push((
ct,
overrides.clone(),
key_val.clone(),
override_id.to_string(),
));
}
}
Ok(res)
}

fn construct_new_payload(req_payload: &Map<String, Value>) -> web::Json<PutReq> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not take a mutable cloned reference as an argument and skip the first line?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hey actually, i'll have to do the same where i'm calling also
Like i would have to clone it over there and send it.

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") {
if let Some(Value::Object(res_override)) = res.get("override") {
return web::Json(PutReq {
context: res_context.to_owned(),
r#override: res_override.to_owned(),
});
}
}
web::Json(PutReq {
context: Map::new(),
r#override: Map::new(),
})
}

async fn reduce_context_key(
user: User,
conn: &mut PooledConnection<ConnectionManager<PgConnection>>,
mut og_contexts: Vec<Context>,
Sauravcv98 marked this conversation as resolved.
Show resolved Hide resolved
mut og_overrides: Map<String, Value>,
check_key: &str,
dimension_schema_map: &HashMap<String, (JSONSchema, i32)>,
default_config: Map<String, Value>,
is_approve: bool,
) -> superposition::Result<Config> {
let default_config_val =
default_config
.get(check_key)
.ok_or(AppError::BadArgument(format!(
"{} not found in default config",
check_key
)))?;
let mut contexts_overrides_values = Vec::new();

for (override_id, override_value) in og_overrides.clone() {
match override_value {
Value::Object(mut override_obj) => {
if let Some(value_of_check_key) = override_obj.remove(check_key) {
let context_arr = get_contextids_from_overrideid(
og_contexts.clone(),
override_obj,
value_of_check_key.clone(),
&override_id,
)?;
contexts_overrides_values.extend(context_arr);
}
}
_ => (),
}
}

contexts_overrides_values.sort_by(|a, b| {
(validate_dimensions_and_calculate_priority(
"context",
&(b.0).condition,
dimension_schema_map,
)
.unwrap())
Sauravcv98 marked this conversation as resolved.
Show resolved Hide resolved
.cmp(
&(validate_dimensions_and_calculate_priority(
"context",
&(a.0).condition,
dimension_schema_map,
)
.unwrap()),
Sauravcv98 marked this conversation as resolved.
Show resolved Hide resolved
)
});

let resolved_dimensions = reduce(contexts_overrides_values, default_config_val)?;
for rd in resolved_dimensions {
match (
rd.get("can_be_reduced"),
rd.get("req_payload"),
rd.get("req_payload").and_then(|v| v.get("id")),
rd.get("req_payload").and_then(|v| v.get("override_id")),
rd.get("req_payload").and_then(|v| v.get("to_be_deleted")),
rd.get("req_payload").and_then(|v| v.get("override")),
) {
(
Some(Value::Bool(true)),
Some(Value::Object(request_payload)),
Some(Value::String(cid)),
Some(Value::String(oid)),
Some(Value::Bool(to_be_deleted)),
Some(override_val),
) => {
if *to_be_deleted {
if is_approve {
let _ = delete_context_api(cid.clone(), user.clone(), conn).await;
}
og_contexts.retain(|x| x.id != *cid);
} else {
if is_approve {
let _ = delete_context_api(cid.clone(), user.clone(), conn).await;
let put_req = construct_new_payload(request_payload);
let _ = put(put_req, conn, false, &user);
}

let new_id = hash(override_val);
og_overrides.insert(new_id.clone(), override_val.clone());

let mut ctx_index = 0;
let mut delete_old_oid = true;

for (ind, ctx) in og_contexts.iter().enumerate() {
if ctx.id == *cid {
ctx_index = ind;
} else if ctx.override_with_keys.contains(oid) {
delete_old_oid = false;
}
}

let mut elem = og_contexts[ctx_index].clone();
elem.override_with_keys = [new_id];
og_contexts[ctx_index] = elem;

if delete_old_oid {
og_overrides.remove(oid);
}
}
}
_ => continue,
}
}

Ok(Config {
contexts: og_contexts,
overrides: og_overrides,
default_configs: default_config,
})
}

#[put("/reduce")]
async fn reduce_context(
Copy link
Collaborator

Choose a reason for hiding this comment

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

@Sauravcv98 is there any specific reason we kept this api under config and not context ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hey this actually reduces the entire Config, even the response of this is of type Config

Copy link
Collaborator

Choose a reason for hiding this comment

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

ok make sense ,
can you rename the function to reduce_config ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah true, was thinking the same
Will rename this to reduce_config

req: HttpRequest,
user: User,
db_conn: DbConnection,
) -> superposition::Result<HttpResponse> {
let DbConnection(mut conn) = db_conn;
let is_approve = req
.headers()
.get("x-approve")
.and_then(|value| value.to_str().ok().and_then(|s| s.parse::<bool>().ok()))
.unwrap_or(false);

let dimensions_schema_map = get_all_dimension_schema_map(&mut conn)?;
let mut config = generate_cac(&mut conn).await?;
let default_config = (config.default_configs).clone();
for (key, val) in default_config {
let contexts = config.contexts;
let overrides = config.overrides;
let default_config = config.default_configs;
config = reduce_context_key(
user.clone(),
&mut conn,
contexts.clone(),
overrides.clone(),
key.as_str(),
&dimensions_schema_map,
default_config.clone(),
is_approve,
)
.await?;
if is_approve {
config = generate_cac(&mut conn).await?;
}
}

Ok(HttpResponse::Ok().json(config))
}

#[get("")]
async fn get(
req: HttpRequest,
Expand Down