#[cfg(test)]
mod tests;
use std::{borrow::Cow, collections::HashMap};
use serde_json::{json, Map, Value};
pub use superposition_types::api::config::MergeStrategy;
use superposition_types::{
logic::evaluate_local_cohorts, Config, ConfigFilter, Context, DimensionInfo,
ExtendedMap, Overrides, PrefixList,
};
struct ConfigRef<'a> {
pub contexts: Vec<&'a Context>,
pub overrides: Cow<'a, HashMap<String, Overrides>>,
pub default_configs: ExtendedMap,
pub dimensions: &'a HashMap<String, DimensionInfo>,
}
impl<'a> ConfigFilter<'a> for ConfigRef<'a> {
type Context = &'a Context;
type Dimensions = &'a HashMap<String, DimensionInfo>;
fn into_parts(
self,
) -> (
Vec<Self::Context>,
Cow<'a, HashMap<String, Overrides>>,
ExtendedMap,
Self::Dimensions,
) {
(
self.contexts,
self.overrides,
self.default_configs,
self.dimensions,
)
}
fn from_parts(
contexts: Vec<Self::Context>,
overrides: Cow<'a, HashMap<String, Overrides>>,
default_configs: ExtendedMap,
dimensions: Self::Dimensions,
) -> Self {
Self {
contexts,
overrides,
default_configs,
dimensions,
}
}
}
pub fn eval_config(
mut config: Config,
query_data: Map<String, Value>,
merge_strategy: MergeStrategy,
filter_prefixes: Option<Vec<String>>,
filter_exclude_prefixes: Option<Vec<String>>,
) -> Map<String, Value> {
let filter_prefixes = PrefixList::from(filter_prefixes);
let filter_exclude_prefixes = PrefixList::from(filter_exclude_prefixes);
if !filter_prefixes.is_empty() || !filter_exclude_prefixes.is_empty() {
config = config.filter_by_prefix(&filter_prefixes, &filter_exclude_prefixes);
}
let modified_query_data = evaluate_local_cohorts(&config.dimensions, query_data);
let overrides_map: Map<String, Value> = get_overrides(
&modified_query_data,
config.contexts.iter().collect(),
Cow::Owned(config.overrides),
&merge_strategy,
);
let result = apply_overrides_on_default_config(
config.default_configs,
overrides_map,
merge_strategy,
);
result.into_inner()
}
#[allow(clippy::too_many_arguments)]
pub fn eval(
default_configs: ExtendedMap,
contexts: &[Context],
overrides: &HashMap<String, Overrides>,
dimensions: &HashMap<String, DimensionInfo>,
query_data: Map<String, Value>,
merge_strategy: MergeStrategy,
filter_prefixes: Option<Vec<String>>,
filter_exclude_prefixes: Option<Vec<String>>,
) -> Map<String, Value> {
let mut config = ConfigRef {
default_configs,
contexts: contexts.iter().collect(),
overrides: Cow::Borrowed(overrides),
dimensions,
};
let filter_prefixes = PrefixList::from(filter_prefixes);
let filter_exclude_prefixes = PrefixList::from(filter_exclude_prefixes);
if !filter_prefixes.is_empty() || !filter_exclude_prefixes.is_empty() {
config = config.filter_by_prefix(&filter_prefixes, &filter_exclude_prefixes);
}
let modified_query_data = evaluate_local_cohorts(config.dimensions, query_data);
let overrides_map: Map<String, Value> = get_overrides(
&modified_query_data,
config.contexts,
config.overrides,
&merge_strategy,
);
let result = apply_overrides_on_default_config(
config.default_configs,
overrides_map,
merge_strategy,
);
result.into_inner()
}
pub fn merge(doc: &mut Value, patch: Value) {
match (doc, patch) {
(Value::Object(map), Value::Object(obj)) => {
for (key, value) in obj {
merge(map.entry(key).or_insert(Value::Null), value);
}
}
(doc, patch) => *doc = patch,
}
}
fn get_overrides(
query_data: &Map<String, Value>,
mut contexts: Vec<&Context>,
mut overrides: Cow<'_, HashMap<String, Overrides>>,
merge_strategy: &MergeStrategy,
) -> Map<String, Value> {
let mut final_consumer_context: HashMap<&str, &str> = HashMap::new();
contexts.retain(|&context| {
if !superposition_types::apply(&context.condition, query_data) {
return false;
}
final_consumer_context.insert(
context.override_with_keys.get_key().as_str(),
context.id.as_str(),
);
true
});
let mut required_overrides: Value = json!({});
for context in contexts {
let override_key = context.override_with_keys.get_key();
let is_last = final_consumer_context.get(override_key.as_str())
== Some(&context.id.as_str());
let overriden_value = match (&mut overrides, is_last) {
(Cow::Owned(map), true) => map.remove(override_key).map(Cow::Owned),
(map, _) => map.get(override_key).map(Cow::Borrowed),
};
let Some(overriden_value) = overriden_value else {
continue;
};
match merge_strategy {
MergeStrategy::REPLACE => {
if let Some(doc) = required_overrides.as_object_mut() {
for (key, value) in overriden_value.into_owned().into_inner() {
doc.insert(key, value);
}
}
}
MergeStrategy::MERGE => merge(
&mut required_overrides,
Value::Object(overriden_value.into_owned().into_inner()),
),
}
}
match required_overrides {
Value::Object(map) => map,
_ => Map::new(),
}
}
fn apply_overrides_on_default_config(
mut default_config: ExtendedMap,
overrides: Map<String, Value>,
merge_strategy: MergeStrategy,
) -> ExtendedMap {
overrides.into_iter().for_each(|(key, val)| {
if let Some(og_val) = default_config.get_mut(&key) {
match merge_strategy {
MergeStrategy::REPLACE => {
default_config.insert(key, val);
}
MergeStrategy::MERGE => merge(og_val, val),
}
} else {
log::error!("Config: found non-default_config key: {key} in overrides");
}
});
default_config
}