use std::{
collections::HashMap,
panic::RefUnwindSafe,
sync::{Arc, Mutex},
time::Duration,
};
use kameleoon_core::{
config::KameleoonClientConfig,
core_client::CoreKameleoonClient,
core_factory::{CoreKameleoonClientFactory, ForgetParams},
error::{ErrorCode, KameleoonError},
types::RemoteVisitorDataFilter,
};
use rustler::{types::LocalPid, Atom, Encoder, Env, NifResult, Reference, Resource, ResourceArc, Term};
use crate::cookies_ex::{CookieEx, GetVisitorCodeResultEx, MapCookieAccessor};
use crate::data_ex::{CustomDataEx, KameleoonDataEx};
use crate::datafile_ex::DataFileEx;
use crate::error_ex::to_nif_error;
use crate::remote_visitor_data_filter_ex::RemoteVisitorDataFilterInput;
use crate::utils_ex::AsyncReply;
use crate::variation_ex::VariationEx;
use crate::{config_ex::KameloonClientConfigEx, utils_ex::ArcStr};
const SDK_NAME: &str = "ELIXIR";
rustler::atoms! {
ok,
error,
nil,
kameleoon_native,
datafile_updated,
struct_key = "__struct__",
client_id,
client_secret,
refresh_interval_minutes,
session_duration_minutes,
default_timeout_millis,
tracking_interval_millis,
proxy_host,
environment,
top_level_domain,
network_domain,
version,
type_key = "type",
goal_id,
revenue,
negative,
metadata,
cookies,
index,
name,
values,
overwrite,
country,
region,
city,
postal_code,
latitude,
longitude,
url,
title,
referrers,
value,
visitor_code,
previous_visit_amount,
current_visit,
custom_data,
conversions,
experiments,
page_views,
geolocation,
device,
browser,
operating_system,
kcs,
personalizations,
cbs,
}
pub(crate) struct ClientResource {
pub(crate) client: Arc<CoreKameleoonClient>,
site_code: String,
environment: Option<String>,
}
impl Resource for ClientResource {}
impl RefUnwindSafe for ClientResource {}
#[rustler::nif]
fn create_client(
site_code: String,
config: Option<KameloonClientConfigEx>,
config_path: Option<String>,
event_owner: LocalPid,
sdk_version: String,
) -> NifResult<(ResourceArc<ClientResource>, String, Option<String>)> {
let config = config_path
.as_deref()
.and_then(KameleoonClientConfig::read_from_file)
.or_else(|| config.map(KameleoonClientConfig::from));
let environment = config.as_ref().and_then(|c| c.environment.clone());
let client = CoreKameleoonClientFactory::create(&site_code, config_path.as_deref(), config, SDK_NAME, &sdk_version)
.map_err(to_nif_error)?;
let resource = ResourceArc::new(ClientResource {
client,
site_code: site_code.clone(),
environment: environment.clone(),
});
set_datafile_callback(&resource, event_owner);
Ok((resource, site_code, environment))
}
#[rustler::nif]
fn forget_client(site_code: String, environment: Option<String>) -> NifResult<Atom> {
environment
.as_deref()
.map_or_else(
|| CoreKameleoonClientFactory::forget(&site_code),
|env| CoreKameleoonClientFactory::forget_with_params(&site_code, ForgetParams::Environment(env)),
)
.map_err(to_nif_error)?;
Ok(ok())
}
#[rustler::nif]
fn initialize(
resource: ResourceArc<ClientResource>,
timeout: Option<u64>,
owner: LocalPid,
reply_ref: Reference,
) -> Atom {
let reply = Mutex::new(Some(AsyncReply::new(owner, reply_ref, resource.client.runtime_handle())));
resource.client.initialize(
timeout.map(Duration::from_millis),
Box::new(move |result| {
if let Some(reply) = reply.lock().ok().and_then(|mut reply| reply.take()) {
reply.send(result.clone().map(|_| ok()));
}
}),
);
ok()
}
#[rustler::nif]
fn is_ready(resource: ResourceArc<ClientResource>) -> bool {
resource.client.is_ready()
}
#[rustler::nif]
fn get_visitor_code(
resource: ResourceArc<ClientResource>,
cookies: HashMap<String, String>,
default_visitor_code: Option<String>,
) -> NifResult<GetVisitorCodeResultEx> {
let mut cookies = MapCookieAccessor::new(cookies);
let visitor_code =
resource.client.get_visitor_code(&mut cookies, default_visitor_code.as_deref()).map_err(to_nif_error)?;
Ok(GetVisitorCodeResultEx {
visitor_code,
cookies: cookies.into_ex(),
})
}
#[rustler::nif]
fn set_legal_consent(
resource: ResourceArc<ClientResource>,
visitor_code: String,
consent: bool,
cookies: Option<HashMap<String, String>>,
) -> NifResult<Option<CookieEx>> {
let mut cookies = cookies.map(MapCookieAccessor::new);
resource.client.set_legal_consent(&visitor_code, consent, cookies.as_mut()).map_err(to_nif_error)?;
match cookies {
Some(cookies) => Ok(Some(cookies.into_ex())),
None => Ok(None),
}
}
#[rustler::nif]
fn add_data(
resource: ResourceArc<ClientResource>,
visitor_code: String,
data: Vec<KameleoonDataEx>,
track: bool,
) -> NifResult<Atom> {
let data = data.into_iter().map(KameleoonDataEx::into_inner).collect();
resource.client.add_data(&visitor_code, data, track).map(|_| ok()).map_err(to_nif_error)
}
#[rustler::nif]
fn flush(resource: ResourceArc<ClientResource>, visitor_code: String) -> NifResult<Atom> {
resource.client.flush(&visitor_code).map(|_| ok()).map_err(to_nif_error)
}
#[rustler::nif]
fn flush_instant(
resource: ResourceArc<ClientResource>,
visitor_code: String,
owner: LocalPid,
reply_ref: Reference,
) -> Atom {
let reply = AsyncReply::new(owner, reply_ref, resource.client.runtime_handle());
resource.client.flush_instant(&visitor_code, move |result| {
reply.send(result.map(|_| ok()));
});
ok()
}
#[rustler::nif]
fn track_conversion(
resource: ResourceArc<ClientResource>,
visitor_code: String,
goal_id: u32,
revenue: Option<f32>,
negative: bool,
metadata: Option<Vec<CustomDataEx>>,
) -> NifResult<Atom> {
let metadata = metadata.unwrap_or_default().into_iter().map(CustomDataEx::into_inner).collect();
resource
.client
.track_conversion(&visitor_code, goal_id, revenue.unwrap_or_default(), negative, metadata)
.map(|_| ok())
.map_err(to_nif_error)
}
#[rustler::nif]
fn is_feature_active(
resource: ResourceArc<ClientResource>,
visitor_code: String,
feature_key: String,
track: bool,
) -> NifResult<bool> {
resource.client.is_feature_active(&visitor_code, &feature_key, track).map_err(to_nif_error)
}
#[rustler::nif]
fn get_variation(
resource: ResourceArc<ClientResource>,
visitor_code: String,
feature_key: String,
track: bool,
) -> NifResult<VariationEx> {
resource.client.get_variation(&visitor_code, &feature_key, track).map(VariationEx::from).map_err(to_nif_error)
}
#[rustler::nif]
fn get_variations(
resource: ResourceArc<ClientResource>,
visitor_code: String,
only_active: bool,
track: bool,
) -> NifResult<HashMap<ArcStr, VariationEx>> {
resource.client.get_variations(&visitor_code, only_active, track).map_err(to_nif_error).map(|variations| {
variations.into_iter().map(|(key, variation)| (ArcStr::from(key), VariationEx::from(variation))).collect()
})
}
#[rustler::nif]
fn set_forced_variation(
resource: ResourceArc<ClientResource>,
visitor_code: String,
experiment_id: u32,
variation_key: Option<String>,
force_targeting: bool,
) -> NifResult<Atom> {
resource
.client
.set_forced_variation(&visitor_code, experiment_id, variation_key.as_deref(), force_targeting)
.map(|_| ok())
.map_err(to_nif_error)
}
#[rustler::nif]
fn evaluate_audiences(resource: ResourceArc<ClientResource>, visitor_code: String) -> NifResult<Atom> {
resource.client.evaluate_audiences(&visitor_code).map(|_| ok()).map_err(to_nif_error)
}
#[rustler::nif]
fn get_engine_tracking_code(resource: ResourceArc<ClientResource>, visitor_code: String) -> NifResult<String> {
resource.client.get_engine_tracking_code(&visitor_code).map_err(to_nif_error)
}
#[rustler::nif]
fn get_remote_data(resource: ResourceArc<ClientResource>, key: String, owner: LocalPid, reply_ref: Reference) -> Atom {
let reply = AsyncReply::new(owner, reply_ref, resource.client.runtime_handle());
resource.client.get_remote_data(&key, move |result| {
reply.send(result.and_then(|bytes| {
String::from_utf8(bytes).map_err(|err| KameleoonError::new(ErrorCode::Internal, err.to_string()))
}));
});
ok()
}
#[rustler::nif]
fn get_remote_visitor_data(
resource: ResourceArc<ClientResource>,
visitor_code: String,
filter: Option<RemoteVisitorDataFilterInput>,
owner: LocalPid,
reply_ref: Reference,
) -> Atom {
let reply = AsyncReply::new(owner, reply_ref, resource.client.runtime_handle());
resource.client.get_remote_visitor_data(&visitor_code, filter.map(RemoteVisitorDataFilter::from), move |result| {
reply.send(result.map(|_| ok()));
});
ok()
}
#[rustler::nif]
fn get_visitor_warehouse_audience(
resource: ResourceArc<ClientResource>,
visitor_code: String,
custom_data_index: u32,
warehouse_key: Option<String>,
owner: LocalPid,
reply_ref: Reference,
) -> Atom {
let reply = AsyncReply::new(owner, reply_ref, resource.client.runtime_handle());
resource.client.get_visitor_warehouse_audience(
&visitor_code,
warehouse_key.as_deref(),
custom_data_index,
move |result| {
reply.send(result.map(|_| ok()));
},
);
ok()
}
#[rustler::nif]
fn get_datafile(resource: ResourceArc<ClientResource>) -> NifResult<DataFileEx> {
let datafile = resource.client.get_types_datafile();
Ok(DataFileEx::from(datafile.as_ref()))
}
fn load(env: Env, _term: Term) -> bool {
env.register::<ClientResource>().is_ok()
}
fn set_datafile_callback(resource: &ResourceArc<ClientResource>, event_owner: LocalPid) {
let site_code = resource.site_code.clone();
let environment = resource.environment.clone();
resource.client.on_datafile_update(Some(Box::new(move || {
let mut env = rustler::OwnedEnv::new();
let site_code = site_code.clone();
let environment = environment.clone();
let _ = env.send_and_clear(&event_owner, |env| (datafile_updated(), site_code, environment).encode(env));
})));
}
rustler::init!("Elixir.Kameleoon.Native.Nif", load = load);