use std::{
collections::HashMap,
ffi::{c_char, CStr, CString},
ptr,
};
use serde_json::{Map, Value};
use superposition_types::{Context, DimensionInfo, ExtendedMap, Overrides};
use crate::config::{self, MergeStrategy};
use crate::experiment::{ExperimentConfig, ExperimentGroups, ExperimentationArgs};
use crate::ffi::ProviderCache;
use crate::{
get_applicable_variants, ConfigFormat, Experiments, FfiExperiment, JsonFormat,
TomlFormat,
};
#[no_mangle]
pub extern "C" fn core_provider_cache_new() -> *mut ProviderCache {
Box::into_raw(Box::new(ProviderCache::default()))
}
#[no_mangle]
pub unsafe extern "C" fn core_provider_cache_free(handle: *mut ProviderCache) {
if !handle.is_null() {
drop(Box::from_raw(handle));
}
}
#[no_mangle]
pub unsafe extern "C" fn core_provider_cache_init_config(
handle: *mut ProviderCache,
default_config_json: *const c_char,
contexts_json: *const c_char,
overrides_json: *const c_char,
dimensions_json: *const c_char,
ebuf: *mut c_char,
) {
if handle.is_null() {
copy_string(ebuf, "handle is null");
return;
}
let default_config = match parse_json::<Map<String, Value>>(default_config_json) {
Ok(v) => v,
Err(e) => {
copy_string(ebuf, format!("Failed to parse default_config: {}", e));
return;
}
};
let contexts = match parse_json::<Vec<Context>>(contexts_json) {
Ok(v) => v,
Err(e) => {
copy_string(ebuf, format!("Failed to parse contexts: {}", e));
return;
}
};
let overrides = match parse_json::<HashMap<String, Overrides>>(overrides_json) {
Ok(v) => v,
Err(e) => {
copy_string(ebuf, format!("Failed to parse overrides: {}", e));
return;
}
};
let dimensions = match parse_json::<HashMap<String, DimensionInfo>>(dimensions_json) {
Ok(v) => v,
Err(e) => {
copy_string(ebuf, format!("Failed to parse dimensions: {}", e));
return;
}
};
let cache = &*handle;
match cache.data.lock() {
Ok(mut d) => {
d.config.default_configs = default_config.into();
d.config.contexts = contexts;
d.config.overrides = overrides;
d.config.dimensions = dimensions;
}
Err(e) => copy_string(ebuf, format!("Failed to acquire cache lock: {}", e)),
}
}
#[no_mangle]
pub unsafe extern "C" fn core_provider_cache_init_experiments(
handle: *mut ProviderCache,
experiments_json: *const c_char,
experiment_groups_json: *const c_char,
ebuf: *mut c_char,
) {
if handle.is_null() {
copy_string(ebuf, "handle is null");
return;
}
let experiments = match parse_json::<Vec<FfiExperiment>>(experiments_json) {
Ok(v) => v,
Err(e) => {
copy_string(ebuf, format!("Failed to parse experiments: {}", e));
return;
}
};
let experiment_groups = match parse_json::<ExperimentGroups>(experiment_groups_json) {
Ok(v) => v,
Err(e) => {
copy_string(ebuf, format!("Failed to parse experiment_groups: {}", e));
return;
}
};
let cache = &*handle;
match cache.data.lock() {
Ok(mut d) => {
d.experiment = Some(ExperimentConfig {
experiments,
experiment_groups,
});
}
Err(e) => copy_string(ebuf, format!("Failed to acquire cache lock: {}", e)),
}
}
#[no_mangle]
pub unsafe extern "C" fn core_provider_cache_eval_config(
handle: *mut ProviderCache,
query_data_json: *const c_char,
merge_strategy_str: *const c_char,
filter_prefixes_json: *const c_char,
filter_exclude_prefixes_json: *const c_char,
targeting_key: *const c_char,
ebuf: *mut c_char,
) -> *mut c_char {
if handle.is_null() {
copy_string(ebuf, "handle is null");
return ptr::null_mut();
}
let mut query_data = match parse_json::<Map<String, Value>>(query_data_json) {
Ok(v) => v,
Err(e) => {
copy_string(ebuf, format!("Failed to parse query_data: {}", e));
return ptr::null_mut();
}
};
let merge_strategy = match c_str_to_string(merge_strategy_str) {
Ok(s) => match s.to_lowercase().as_str() {
"replace" => MergeStrategy::REPLACE,
_ => MergeStrategy::MERGE,
},
Err(e) => {
copy_string(ebuf, format!("Failed to parse merge_strategy: {}", e));
return ptr::null_mut();
}
};
let filter_prefixes: Option<Vec<String>> = if filter_prefixes_json.is_null() {
None
} else {
match parse_json::<Vec<String>>(filter_prefixes_json) {
Ok(v) => Some(v),
Err(e) => {
copy_string(ebuf, format!("Failed to parse filter_prefixes: {}", e));
return ptr::null_mut();
}
}
};
let filter_exclude_prefixes: Option<Vec<String>> =
if filter_exclude_prefixes_json.is_null() {
None
} else {
match parse_json::<Vec<String>>(filter_exclude_prefixes_json) {
Ok(v) => Some(v),
Err(e) => {
copy_string(
ebuf,
format!("Failed to parse filter_exclude_prefixes: {}", e),
);
return ptr::null_mut();
}
}
};
let tkey: Option<String> = if targeting_key.is_null() {
None
} else {
match c_str_to_string(targeting_key) {
Ok(s) if !s.is_empty() => Some(s),
_ => None,
}
};
let cache = &*handle;
let data = match cache.data.lock() {
Ok(d) => d,
Err(e) => {
copy_string(ebuf, format!("Failed to acquire cache lock: {}", e));
return ptr::null_mut();
}
};
if let Some(ref experiment_config) = data.experiment {
if (!experiment_config.experiments.is_empty()
|| !experiment_config.experiment_groups.is_empty())
&& tkey.as_ref().is_some_and(|key| !key.is_empty())
{
let variants = get_applicable_variants(
&data.config.dimensions,
experiment_config.experiments.clone(),
&experiment_config.experiment_groups,
query_data.clone(),
tkey.as_deref().unwrap_or(""),
filter_prefixes.clone(),
filter_exclude_prefixes.clone(),
);
query_data.insert("variantIds".to_string(), variants.into());
}
}
let config = data.config.clone();
let result = config::eval(
config.default_configs,
&config.contexts,
&config.overrides,
&config.dimensions,
query_data,
merge_strategy,
filter_prefixes,
filter_exclude_prefixes,
);
match serde_json::to_string(&result) {
Ok(json_str) => string_to_c_str(json_str),
Err(e) => {
copy_string(ebuf, format!("Failed to serialize result: {}", e));
ptr::null_mut()
}
}
}
fn c_str_to_string(s: *const c_char) -> Result<String, String> {
if s.is_null() {
return Err("Null pointer encountered while converting".into());
}
unsafe {
CStr::from_ptr(s)
.to_str()
.map(String::from)
.map_err(|e| format!("Invalid UTF-8: {}", e))
}
}
fn parse_json<T: serde::de::DeserializeOwned>(s: *const c_char) -> Result<T, String> {
let json_str = c_str_to_string(s)?;
serde_json::from_str(&json_str).map_err(|e| format!("Invalid JSON: {}", e))
}
fn string_to_c_str(s: String) -> *mut c_char {
CString::new(s).unwrap().into_raw()
}
unsafe fn copy_string(to: *mut c_char, from: impl AsRef<str>) {
let from = from.as_ref();
let cstr = CString::new(from).unwrap();
let src = cstr.as_ptr();
ptr::copy_nonoverlapping(src, to, from.len() + 1 );
}
#[no_mangle]
pub unsafe extern "C" fn core_get_resolved_config(
default_config_json: *const c_char,
contexts_json: *const c_char,
overrides_json: *const c_char,
dimensions: *const c_char,
query_data_json: *const c_char,
merge_strategy_str: *const c_char,
filter_prefixes_json: *const c_char,
filter_exclude_prefixes_json: *const c_char,
experimentation_json: *const c_char,
ebuf: *mut c_char,
) -> *mut c_char {
if default_config_json.is_null()
|| contexts_json.is_null()
|| overrides_json.is_null()
|| dimensions.is_null()
|| query_data_json.is_null()
|| merge_strategy_str.is_null()
{
copy_string(ebuf, "Null pointer provided in required value");
return ptr::null_mut();
}
let default_config = match parse_json::<Map<String, Value>>(default_config_json) {
Ok(config) => ExtendedMap::from(config),
Err(e) => {
copy_string(ebuf, format!("Failed to parse default_config: {}", e));
return ptr::null_mut();
}
};
let contexts = match parse_json::<Vec<Context>>(contexts_json) {
Ok(contexts) => contexts,
Err(e) => {
copy_string(ebuf, format!("Failed to parse contexts: {}", e));
return ptr::null_mut();
}
};
let overrides = match parse_json::<HashMap<String, Overrides>>(overrides_json) {
Ok(overrides) => overrides,
Err(e) => {
copy_string(ebuf, format!("Failed to parse overrides: {}", e));
return ptr::null_mut();
}
};
let mut query_data = match parse_json::<Map<String, Value>>(query_data_json) {
Ok(data) => data,
Err(e) => {
copy_string(ebuf, format!("Failed to parse query_data: {}", e));
return ptr::null_mut();
}
};
let merge_strategy = match c_str_to_string(merge_strategy_str) {
Ok(strategy) => match strategy.to_lowercase().as_str() {
"merge" => MergeStrategy::MERGE,
"replace" => MergeStrategy::REPLACE,
_ => MergeStrategy::default(),
},
Err(e) => {
copy_string(ebuf, format!("Failed to parse merge_strategy: {}", e));
return ptr::null_mut();
}
};
let filter_prefixes: Option<Vec<String>> = if filter_prefixes_json.is_null() {
None
} else {
match parse_json::<Vec<String>>(filter_prefixes_json) {
Ok(prefixes) => Some(prefixes),
Err(e) => {
copy_string(ebuf, format!("Failed to parse filter_prefixes: {}", e));
return ptr::null_mut();
}
}
};
let filter_exclude_prefixes: Option<Vec<String>> =
if filter_exclude_prefixes_json.is_null() {
None
} else {
match parse_json::<Vec<String>>(filter_exclude_prefixes_json) {
Ok(v) => Some(v),
Err(e) => {
copy_string(
ebuf,
format!("Failed to parse filter_exclude_prefixes: {}", e),
);
return ptr::null_mut();
}
}
};
let experimentation: Option<ExperimentationArgs> = if experimentation_json.is_null() {
None
} else {
match parse_json::<ExperimentationArgs>(experimentation_json) {
Ok(exp_args) => Some(exp_args),
Err(e) => {
copy_string(ebuf, format!("Failed to parse experimentation: {}", e));
return ptr::null_mut();
}
}
};
let dimensions = match parse_json::<HashMap<String, DimensionInfo>>(dimensions) {
Ok(dimensions) => dimensions,
Err(e) => {
copy_string(ebuf, format!("Failed to parse dimensions: {}", e));
return ptr::null_mut();
}
};
if let Some(e_args) = experimentation {
let identifier = e_args.targeting_key;
let variants = get_applicable_variants(
&dimensions,
e_args.experiments,
&e_args.experiment_groups,
query_data.clone(),
&identifier,
filter_prefixes.clone(),
filter_exclude_prefixes.clone(),
);
query_data.insert("variantIds".to_string(), variants.into());
}
let result = config::eval(
default_config,
&contexts,
&overrides,
&dimensions,
query_data,
merge_strategy,
filter_prefixes,
filter_exclude_prefixes,
);
match serde_json::to_string(&result) {
Ok(json_str) => string_to_c_str(json_str),
Err(e) => {
copy_string(ebuf, format!("Failed to serialize result: {}", e));
ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn core_test_connection() -> i32 {
1 }
#[no_mangle]
pub unsafe extern "C" fn core_free_string(s: *mut c_char) {
if !s.is_null() {
drop(CString::from_raw(s));
}
}
#[no_mangle]
pub unsafe extern "C" fn core_get_applicable_variants(
experiments_json: *const c_char,
experiment_groups_json: *const c_char,
dimensions: *const c_char,
query_data_json: *const c_char,
identifier: *const c_char,
filter_prefixes_json: *const c_char,
filter_exclude_prefixes_json: *const c_char,
ebuf: *mut c_char,
) -> *mut c_char {
if experiments_json.is_null() || query_data_json.is_null() || dimensions.is_null() {
copy_string(ebuf, "Null pointer provided");
return ptr::null_mut();
}
let experiments = match parse_json::<Experiments>(experiments_json) {
Ok(experiments) => experiments,
Err(e) => {
copy_string(ebuf, format!("Failed to parse experiments: {}", e));
return ptr::null_mut();
}
};
let experiment_groups = match parse_json::<ExperimentGroups>(experiment_groups_json) {
Ok(groups) => groups,
Err(e) => {
copy_string(ebuf, format!("Failed to parse experiment_groups: {}", e));
return ptr::null_mut();
}
};
let query_data = match parse_json::<Map<String, Value>>(query_data_json) {
Ok(data) => data,
Err(e) => {
copy_string(ebuf, format!("Failed to parse query_data: {}", e));
return ptr::null_mut();
}
};
let dimensions = match parse_json::<HashMap<String, DimensionInfo>>(dimensions) {
Ok(dimensions) => dimensions,
Err(e) => {
copy_string(ebuf, format!("Failed to parse dimensions: {}", e));
return ptr::null_mut();
}
};
let filter_prefixes: Option<Vec<String>> = if filter_prefixes_json.is_null() {
None
} else {
match parse_json::<Vec<String>>(filter_prefixes_json) {
Ok(prefixes) => Some(prefixes),
Err(e) => {
copy_string(ebuf, format!("Failed to parse filter_prefixes: {}", e));
return ptr::null_mut();
}
}
};
let filter_exclude_prefixes: Option<Vec<String>> =
if filter_exclude_prefixes_json.is_null() {
None
} else {
match parse_json::<Vec<String>>(filter_exclude_prefixes_json) {
Ok(v) => Some(v),
Err(e) => {
copy_string(
ebuf,
format!("Failed to parse filter_exclude_prefixes: {}", e),
);
return ptr::null_mut();
}
}
};
let identifier = match c_str_to_string(identifier) {
Ok(id) => id,
Err(e) => {
copy_string(ebuf, format!("Failed to parse identifier: {}", e));
return ptr::null_mut();
}
};
let result = get_applicable_variants(
&dimensions,
experiments,
&experiment_groups,
query_data,
&identifier,
filter_prefixes,
filter_exclude_prefixes,
);
match serde_json::to_string(&result) {
Ok(json_str) => string_to_c_str(json_str),
Err(e) => {
copy_string(ebuf, format!("Failed to serialize result: {}", e));
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn core_parse_toml_config(
toml_content: *const c_char,
ebuf: *mut c_char,
) -> *mut c_char {
if toml_content.is_null() {
copy_string(ebuf, "toml_content is null");
return ptr::null_mut();
}
let toml_str = match c_str_to_string(toml_content) {
Ok(s) => s,
Err(e) => {
copy_string(ebuf, format!("Invalid UTF-8 in toml_content: {}", e));
return ptr::null_mut();
}
};
let parsed = match TomlFormat::parse_config(&toml_str) {
Ok(p) => p,
Err(e) => {
copy_string(ebuf, e.to_string());
return ptr::null_mut();
}
};
match serde_json::to_string(&parsed) {
Ok(json_str) => string_to_c_str(json_str),
Err(e) => {
copy_string(ebuf, format!("JSON serialization error: {}", e));
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn core_parse_json_config(
json_content: *const c_char,
ebuf: *mut c_char,
) -> *mut c_char {
if json_content.is_null() {
copy_string(ebuf, "json_content is null");
return ptr::null_mut();
}
let json_str = match c_str_to_string(json_content) {
Ok(s) => s,
Err(e) => {
copy_string(ebuf, format!("Invalid UTF-8 in json_content: {}", e));
return ptr::null_mut();
}
};
let parsed = match JsonFormat::parse_config(&json_str) {
Ok(p) => p,
Err(e) => {
copy_string(ebuf, e.to_string());
return ptr::null_mut();
}
};
match serde_json::to_string(&parsed) {
Ok(json_str) => string_to_c_str(json_str),
Err(e) => {
copy_string(ebuf, format!("JSON serialization error: {}", e));
ptr::null_mut()
}
}
}