Skip to main content

native/kameleoon_elixir_bridge/src/client_ex.rs

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);