use std::collections::HashMap;
use log::debug;
use serde_json::{Map, Value};
use superposition_core::experiment::{
ExperimentConfig, ExperimentGroups, FfiExperimentGroup,
};
use superposition_core::{Experiments, FfiExperiment};
use superposition_sdk::types::{
ExperimentStatusType as SDKExperimentStatusType, GroupType as SdkGroupType,
};
use superposition_types::database::models::cac::{DependencyGraph, DimensionType};
use superposition_types::database::models::experimentation::{
Bucket, Buckets, ExperimentStatusType, GroupType, Variant, VariantType, Variants,
};
use superposition_types::{
Cac, Condition, Config, Context, DimensionInfo, Exp, ExtendedMap, OverrideWithKeys,
Overrides,
};
use crate::{conversions, types::*};
pub struct ConversionUtils;
impl ConversionUtils {
pub fn convert_get_config_response(
response: superposition_sdk::operation::get_config::GetConfigOutput,
) -> Result<Config> {
debug!("Converting get_config response to superposition_types::Config");
let default_configs = conversions::hashmap_to_map(response.default_configs);
let overrides = response
.overrides
.into_iter()
.map(|(k, v)| {
let override_values = conversions::hashmap_to_map(v);
let override_obj = Cac::<Overrides>::try_from(override_values)
.map_err(|e| SuperpositionError::SerializationError(e.to_string()))?;
Ok((k, override_obj.into_inner()))
})
.collect::<Result<HashMap<String, Overrides>>>()?;
let contexts = response
.contexts
.into_iter()
.map(|context_partial| {
let condition_map =
conversions::hashmap_to_map(context_partial.condition);
let condition =
Cac::<Condition>::try_from(condition_map).map_err(|e| {
SuperpositionError::SerializationError(format!(
"Invalid condition: {}",
e
))
})?;
let override_with_keys =
OverrideWithKeys::try_from(context_partial.override_with_keys)
.map_err(|e| {
SuperpositionError::SerializationError(format!(
"Invalid override_with_keys: {e}",
))
})?;
Ok(Context {
id: context_partial.id,
condition: condition.into_inner(),
priority: context_partial.priority,
weight: context_partial.weight,
override_with_keys,
})
})
.collect::<Result<Vec<Context>>>()?;
let dimensions = response
.dimensions
.into_iter()
.map(|(key, dimension_info)| {
let schema = conversions::hashmap_to_map(dimension_info.schema);
let dim_info = DimensionInfo {
schema: ExtendedMap::from(schema),
position: dimension_info.position,
dimension_type: Self::try_dimension_type(
dimension_info.dimension_type,
)?,
dependency_graph: DependencyGraph(dimension_info.dependency_graph),
value_compute_function_name: dimension_info
.value_compute_function_name,
};
Ok((key, dim_info))
})
.collect::<Result<HashMap<String, DimensionInfo>>>()?;
let config = Config {
contexts,
overrides,
default_configs: default_configs.into(),
dimensions,
};
debug!("Successfully converted config with {} contexts, {} overrides, {} default configs",
config.contexts.len(), config.overrides.len(), config.default_configs.len());
Ok(config)
}
fn try_dimension_type(
dim_type: superposition_sdk::types::DimensionType,
) -> Result<DimensionType> {
match dim_type {
superposition_sdk::types::DimensionType::RemoteCohort(cohort_based_on) => {
Ok(DimensionType::RemoteCohort(cohort_based_on))
}
superposition_sdk::types::DimensionType::LocalCohort(cohort_based_on) => {
Ok(DimensionType::LocalCohort(cohort_based_on))
}
superposition_sdk::types::DimensionType::Regular => {
Ok(DimensionType::Regular {})
}
_ => Err(SuperpositionError::SerializationError(
"Unknown dimension type".to_string(),
)),
}
}
pub fn convert_value_to_config(map: &Map<String, Value>) -> Result<Config> {
let contexts =
map.get("contexts")
.and_then(|v| v.as_array())
.ok_or_else(|| {
SuperpositionError::ConfigError(
"Missing or invalid 'contexts' field".to_string(),
)
})?;
let parsed_contexts: Result<Vec<Context>> = contexts
.iter()
.map(|context_val| {
let context_obj = context_val.as_object().ok_or_else(|| {
SuperpositionError::ConfigError(
"Context must be an object".to_string(),
)
})?;
let id = context_obj
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| {
SuperpositionError::ConfigError(
"Missing or invalid 'id' field in context".to_string(),
)
})?
.to_string();
let priority = context_obj
.get("priority")
.and_then(|v| v.as_i64())
.ok_or_else(|| {
SuperpositionError::ConfigError(
"Missing or invalid 'priority' field in context".to_string(),
)
})? as i32;
let weight = context_obj
.get("weight")
.and_then(|v| v.as_i64())
.ok_or_else(|| {
SuperpositionError::ConfigError(
"Missing or invalid 'weight' field in context".to_string(),
)
})? as i32;
let override_with_keys: Vec<String> = context_obj
.get("override_with_keys")
.and_then(|v| v.as_array())
.ok_or_else(|| {
SuperpositionError::ConfigError(
"Missing or invalid 'override_with_keys' field in context"
.to_string(),
)
})?
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
let override_with_keys = OverrideWithKeys::try_from(override_with_keys)
.map_err(|e| {
SuperpositionError::ConfigError(format!(
"Invalid override_with_keys: {e}",
))
})?;
let condition_map = context_obj
.get("condition")
.and_then(|v| v.as_object())
.ok_or_else(|| {
SuperpositionError::ConfigError(
"Missing or invalid 'condition' field in context".to_string(),
)
})?
.clone();
let condition = Cac::<Condition>::try_from(condition_map)
.map_err(|e| {
SuperpositionError::SerializationError(format!(
"Invalid condition: {}",
e
))
})?
.into_inner();
Ok(Context {
id,
condition,
priority,
weight,
override_with_keys,
})
})
.collect();
let contexts = parsed_contexts?;
let overrides_obj = map
.get("overrides")
.and_then(|v| v.as_object())
.ok_or_else(|| {
SuperpositionError::ConfigError(
"Missing or invalid 'overrides' field".to_string(),
)
})?;
let mut overrides: HashMap<String, Overrides> = HashMap::new();
for (key, value) in overrides_obj {
let override_map = value
.as_object()
.ok_or_else(|| {
SuperpositionError::ConfigError(format!(
"Override '{}' must be an object",
key
))
})?
.clone();
let override_obj = Cac::<Overrides>::try_from(override_map)
.map_err(|e| {
SuperpositionError::SerializationError(format!(
"Invalid override '{}': {}",
key, e
))
})?
.into_inner();
overrides.insert(key.clone(), override_obj);
}
let default_configs = map
.get("default_configs")
.and_then(|v| v.as_object())
.ok_or_else(|| {
SuperpositionError::ConfigError(
"Missing or invalid 'default_configs' field".to_string(),
)
})?
.clone();
let dimensions = map
.get("dimensions")
.and_then(|v| v.as_object())
.map(|dim| {
dim.iter()
.map(|(key, value)| {
let dim_info: DimensionInfo =
serde_json::from_value(value.clone()).map_err(|e| {
SuperpositionError::SerializationError(format!(
"Invalid dimension info for '{}': {}",
key, e
))
})?;
Ok((key.clone(), dim_info))
})
.collect::<Result<HashMap<String, DimensionInfo>>>()
})
.unwrap_or_else(|| Ok(HashMap::new()))?;
Ok(Config {
contexts,
overrides,
default_configs: default_configs.into(),
dimensions,
})
}
pub fn convert_experiments_response(
response: Vec<superposition_sdk::types::ExperimentResponse>,
) -> Result<Experiments> {
debug!("Converting experiments response");
let mut trimmed_exp_list: Experiments = Vec::new();
for exp in response {
let condition_map = conversions::hashmap_to_map(exp.context);
let mut variants: Variants = Variants::new(vec![]);
for variant in exp.variants {
let variant_type = match variant.variant_type {
superposition_sdk::types::VariantType::Control => {
VariantType::CONTROL
}
superposition_sdk::types::VariantType::Experimental => {
VariantType::EXPERIMENTAL
}
_ => {
return Err(SuperpositionError::SerializationError(
"Unknown variant type".to_string(),
))
}
};
let overrides_map = conversions::hashmap_to_map(variant.overrides);
let override_ = Exp::<Overrides>::try_from(overrides_map)
.map_err(|e| SuperpositionError::SerializationError(e.to_string()))?;
let variant_value = Variant {
id: variant.id,
variant_type,
context_id: variant.context_id,
override_id: variant.override_id,
overrides: override_,
};
variants.push(variant_value);
}
let context = Exp::<Condition>::try_from(condition_map)
.map_err(|e| {
SuperpositionError::SerializationError(format!(
"Invalid condition: {}",
e
))
})?
.into_inner();
let status = match exp.status {
SDKExperimentStatusType::Created => ExperimentStatusType::CREATED,
SDKExperimentStatusType::Inprogress => ExperimentStatusType::INPROGRESS,
SDKExperimentStatusType::Paused => ExperimentStatusType::PAUSED,
SDKExperimentStatusType::Concluded => ExperimentStatusType::CONCLUDED,
SDKExperimentStatusType::Discarded => ExperimentStatusType::DISCARDED,
_ => {
return Err(SuperpositionError::SerializationError(
"Unknown experiment status".to_string(),
))
}
};
let experiment = FfiExperiment {
id: exp.id,
context,
variants,
traffic_percentage: exp.traffic_percentage as u8,
status,
};
trimmed_exp_list.push(experiment);
}
Ok(trimmed_exp_list)
}
pub fn convert_experiment_groups_response(
response: Vec<superposition_sdk::types::ExperimentGroupResponse>,
) -> Result<ExperimentGroups> {
debug!("Converting experiment groups response");
let mut trimmed_group_list: ExperimentGroups = Vec::new();
for exp_group in response {
let condition_map = conversions::hashmap_to_map(exp_group.context);
let context = Exp::<Condition>::try_from(condition_map)
.map_err(|e| {
SuperpositionError::SerializationError(format!(
"Invalid condition: {}",
e
))
})?
.into_inner();
let group_type = match exp_group.group_type {
SdkGroupType::SystemGenerated => GroupType::SystemGenerated,
SdkGroupType::UserCreated => GroupType::UserCreated,
_ => {
return Err(SuperpositionError::SerializationError(
"Unknown group type".to_string(),
))
}
};
let experiment_group = FfiExperimentGroup {
id: exp_group.id,
context,
traffic_percentage: exp_group.traffic_percentage as u8,
member_experiment_ids: exp_group.member_experiment_ids,
group_type,
buckets: Buckets::try_from(
exp_group
.buckets
.into_iter()
.map(|b| {
b.map(|bucket| Bucket {
variant_id: bucket.variant_id,
experiment_id: bucket.experiment_id,
})
})
.collect::<Vec<_>>(),
)
.map_err(SuperpositionError::SerializationError)?,
};
trimmed_group_list.push(experiment_group);
}
Ok(trimmed_group_list)
}
pub fn convert_experiment_config_response(
response: superposition_sdk::operation::get_experiment_config::GetExperimentConfigOutput,
) -> Result<ExperimentConfig> {
debug!("Converting experiment config response");
let experiments = Self::convert_experiments_response(response.experiments)?;
let experiment_groups =
Self::convert_experiment_groups_response(response.experiment_groups)?;
Ok(ExperimentConfig {
experiments,
experiment_groups,
})
}
pub fn config_to_legacy_format(config: &Config) -> HashMap<String, Value> {
let mut result = HashMap::new();
result.insert(
"default_configs".to_string(),
Value::Object((*config.default_configs).clone()),
);
let mut overrides_map = Map::new();
for (key, overrides) in &config.overrides {
let override_value: Map<String, Value> = overrides.clone().into();
overrides_map.insert(key.clone(), Value::Object(override_value));
}
result.insert("overrides".to_string(), Value::Object(overrides_map));
let contexts_array: Vec<Value> = config
.contexts
.iter()
.map(|context| {
let condition_map: Map<String, Value> = context.condition.clone().into();
serde_json::json!({
"id": context.id,
"priority": context.priority,
"weight": context.weight,
"override_with_keys": context.override_with_keys,
"condition": condition_map
})
})
.collect();
result.insert("contexts".to_string(), Value::Array(contexts_array));
debug!(
"Converted Config to legacy format with {} sections",
result.len()
);
result
}
pub fn evaluate_config(
config: Config,
dimension_data: &Map<String, Value>,
prefix_filter: Option<&[String]>,
) -> Result<HashMap<String, Value>> {
debug!(
"Evaluating config with dimension data: {:?}",
dimension_data.keys().collect::<Vec<_>>()
);
let filtered_config = config.filter_by_dimensions(dimension_data);
debug!(
"Filtered config has {} contexts after dimension filtering",
filtered_config.contexts.len()
);
let final_config = if let Some(prefixes) = prefix_filter {
let prefix_set: std::collections::HashSet<String> =
prefixes.iter().cloned().collect();
filtered_config.filter_by_prefix(&prefix_set)
} else {
filtered_config
};
debug!(
"Final config has {} contexts after prefix filtering",
final_config.contexts.len()
);
let mut result = final_config.default_configs.into_inner();
let mut sorted_contexts = final_config.contexts.clone();
sorted_contexts.sort_by_key(|c| std::cmp::Reverse(c.priority));
for context in sorted_contexts {
if let Some(override_key) = context.override_with_keys.first() {
if let Some(overrides) = final_config.overrides.get(override_key) {
let override_map: Map<String, Value> = overrides.clone().into();
for (override_key, value) in override_map {
result.insert(override_key, value);
debug!("Applied override for key");
}
}
}
}
debug!(
"Config evaluation completed with {} final keys",
result.len()
);
let final_result: HashMap<String, Value> = result.into_iter().collect();
Ok(final_result)
}
}