Skip to main content

native/kameleoon_elixir_bridge/src/data_ex.rs

use std::collections::HashMap;

use kameleoon_core::{
    data::{
        ApplicationVersion, Browser, BrowserKind, Conversion, ConversionOpts, Cookie, CustomData, CustomDataOpts,
        Device, DeviceKind, Geolocation, KameleoonData, OperatingSystem, OperatingSystemKind, PageView,
        UniqueIdentifier, UserAgent,
    },
    error::KameleoonError,
};
use rustler::{types::atom::Atom, Decoder, Error as NifError, NifResult, Term};

use crate::client_ex::{
    city, cookies, country, goal_id, index, latitude, longitude, metadata, name, negative, overwrite, postal_code,
    referrers, region, revenue, struct_key, title, type_key, url, value, values, version,
};
use crate::error_ex::to_nif_error;
use crate::utils_ex::{ensure_map, optional_field, optional_field_or, required_field, string_or_atom};

rustler::atoms! {
    application_version = "Elixir.Kameleoon.Data.ApplicationVersion",
    browser = "Elixir.Kameleoon.Data.Browser",
    conversion = "Elixir.Kameleoon.Data.Conversion",
    cookie = "Elixir.Kameleoon.Data.Cookie",
    custom_data = "Elixir.Kameleoon.Data.CustomData",
    device = "Elixir.Kameleoon.Data.Device",
    geolocation = "Elixir.Kameleoon.Data.Geolocation",
    operating_system = "Elixir.Kameleoon.Data.OperatingSystem",
    page_view = "Elixir.Kameleoon.Data.PageView",
    unique_identifier = "Elixir.Kameleoon.Data.UniqueIdentifier",
    user_agent = "Elixir.Kameleoon.Data.UserAgent",
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DataKind {
    ApplicationVersion,
    Browser,
    Conversion,
    Cookie,
    CustomData,
    Device,
    Geolocation,
    OperatingSystem,
    PageView,
    UniqueIdentifier,
    UserAgent,
}

fn data_kind(term: Term) -> NifResult<DataKind> {
    if let Ok(module) = term.map_get(struct_key()) {
        return data_kind_from_struct_atom(Atom::decode(module)?);
    }

    Err(NifError::BadArg)
}

fn data_kind_from_struct_atom(module: Atom) -> NifResult<DataKind> {
    match module {
        m if m == application_version() => Ok(DataKind::ApplicationVersion),
        m if m == browser() => Ok(DataKind::Browser),
        m if m == conversion() => Ok(DataKind::Conversion),
        m if m == cookie() => Ok(DataKind::Cookie),
        m if m == custom_data() => Ok(DataKind::CustomData),
        m if m == device() => Ok(DataKind::Device),
        m if m == geolocation() => Ok(DataKind::Geolocation),
        m if m == operating_system() => Ok(DataKind::OperatingSystem),
        m if m == page_view() => Ok(DataKind::PageView),
        m if m == unique_identifier() => Ok(DataKind::UniqueIdentifier),
        m if m == user_agent() => Ok(DataKind::UserAgent),
        _ => Err(NifError::BadArg),
    }
}

#[derive(Debug)]
pub(crate) struct KameleoonDataEx(KameleoonData);

impl KameleoonDataEx {
    pub(crate) fn into_inner(self) -> KameleoonData {
        self.0
    }
}

impl<'a> Decoder<'a> for KameleoonDataEx {
    fn decode(term: Term<'a>) -> NifResult<Self> {
        ensure_map(term)?;

        let data_type = data_kind(term)?;

        let data = match data_type {
            DataKind::ApplicationVersion => ApplicationVersion::new(required_field::<String>(term, version())?).into(),
            DataKind::Browser => Browser::new(
                parse_browser_kind(&required_type_field(term)?).map_err(to_nif_error)?,
                optional_field(term, version())?,
            )
            .into(),
            DataKind::Conversion => {
                let metadata: Vec<CustomData> = optional_field(term, metadata())?
                    .map(|metadata: Vec<CustomDataEx>| metadata.into_iter().map(CustomDataEx::into_inner).collect())
                    .unwrap_or_default();

                let mut opts = ConversionOpts::new().negative(optional_field(term, negative())?.unwrap_or(false));
                if let Some(revenue) = optional_field(term, revenue())? {
                    opts = opts.revenue(revenue);
                }
                if !metadata.is_empty() {
                    opts = opts.metadata(metadata);
                }

                Conversion::new_with_opts(required_field(term, goal_id())?, opts).into()
            }
            DataKind::Cookie => Cookie::new(required_field::<HashMap<String, String>>(term, cookies())?).into(),
            DataKind::CustomData => custom_data_from_parts(
                optional_field(term, index())?,
                optional_field(term, name())?,
                optional_field_or(term, values(), Vec::<String>::new())?,
                optional_field(term, overwrite())?,
            )
            .map_err(to_nif_error)?
            .into(),
            DataKind::Device => {
                Device::new(parse_device_kind(&required_type_field(term)?).map_err(to_nif_error)?).into()
            }
            DataKind::Geolocation => Geolocation::new(
                required_field::<String>(term, country())?,
                optional_field::<String>(term, region())?,
                optional_field::<String>(term, city())?,
                optional_field::<String>(term, postal_code())?,
                optional_field(term, latitude())?,
                optional_field(term, longitude())?,
            )
            .into(),
            DataKind::OperatingSystem => {
                OperatingSystem::new(parse_operating_system_kind(&required_type_field(term)?).map_err(to_nif_error)?)
                    .into()
            }
            DataKind::PageView => PageView::new(
                required_field::<String>(term, url())?,
                optional_field::<String>(term, title())?,
                optional_field::<Vec<i32>>(term, referrers())?.unwrap_or_default(),
            )
            .into(),
            DataKind::UniqueIdentifier => UniqueIdentifier::new(required_field(term, value())?).into(),
            DataKind::UserAgent => UserAgent::new(required_field::<String>(term, value())?).into(),
        };

        Ok(Self(data))
    }
}

fn required_type_field(term: Term) -> NifResult<String> {
    string_or_atom(required_field(term, type_key())?)
}

#[derive(Debug)]
pub(crate) struct CustomDataEx(CustomData);

impl CustomDataEx {
    pub(crate) fn into_inner(self) -> CustomData {
        self.0
    }
}

impl<'a> Decoder<'a> for CustomDataEx {
    fn decode(term: Term<'a>) -> NifResult<Self> {
        ensure_map(term)?;
        if data_kind(term)? != DataKind::CustomData {
            return Err(NifError::BadArg);
        }

        let custom_data = custom_data_from_parts(
            optional_field(term, index())?,
            optional_field(term, name())?,
            optional_field_or(term, values(), Vec::new())?,
            optional_field(term, overwrite())?,
        )
        .map_err(to_nif_error)?;

        Ok(Self(custom_data))
    }
}

fn custom_data_from_parts(
    index: Option<u32>,
    name: Option<String>,
    values: Vec<String>,
    overwrite: Option<bool>,
) -> Result<CustomData, KameleoonError> {
    let opts = CustomDataOpts::new().overwrite(overwrite.unwrap_or(true));
    match (index, name) {
        (Some(index), _) => Ok(CustomData::new_with_index_opts(index, values, opts)),
        (None, Some(name)) => Ok(CustomData::new_with_name_opts(name, values, opts)),
        (None, None) => Err(KameleoonError::from("custom_data requires either `index` or `name`".to_owned())),
    }
}

fn parse_browser_kind(value: &str) -> Result<BrowserKind, KameleoonError> {
    Ok(match value.to_ascii_uppercase().as_str() {
        "CHROME" => BrowserKind::Chrome,
        "IE" | "INTERNET_EXPLORER" => BrowserKind::InternetExplorer,
        "FIREFOX" => BrowserKind::Firefox,
        "SAFARI" => BrowserKind::Safari,
        "OPERA" => BrowserKind::Opera,
        "OTHER" => BrowserKind::Other,
        _ => return Err(KameleoonError::from(format!("unsupported browser kind `{value}`"))),
    })
}

fn parse_device_kind(value: &str) -> Result<DeviceKind, KameleoonError> {
    match value.to_ascii_uppercase().as_str() {
        "PHONE" => Ok(DeviceKind::Phone),
        "TABLET" => Ok(DeviceKind::Tablet),
        "DESKTOP" => Ok(DeviceKind::Desktop),
        _ => Err(KameleoonError::from(format!("unsupported device kind `{value}`"))),
    }
}

fn parse_operating_system_kind(value: &str) -> Result<OperatingSystemKind, KameleoonError> {
    match value.to_ascii_uppercase().as_str() {
        "WINDOWS" => Ok(OperatingSystemKind::Windows),
        "MAC" => Ok(OperatingSystemKind::Mac),
        "IOS" => Ok(OperatingSystemKind::IOS),
        "LINUX" => Ok(OperatingSystemKind::Linux),
        "ANDROID" => Ok(OperatingSystemKind::Android),
        "WINDOWS_PHONE" => Ok(OperatingSystemKind::WindowsPhone),
        _ => Err(KameleoonError::from(format!("unsupported operating_system kind `{value}`"))),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn custom_data_requires_index_or_name() {
        let result = custom_data_from_parts(None, None, vec!["test".to_owned()], None);

        assert!(result.is_err());
    }

    #[test]
    fn custom_data_with_name_round_trips() {
        let custom_data = custom_data_from_parts(None, Some("plan".to_owned()), vec!["pro".to_owned()], Some(false))
            .expect("custom_data should be created");

        assert_eq!(custom_data.name.as_deref(), Some("plan"));
        assert_eq!(custom_data.values, vec!["pro".to_owned()]);
        assert!(!custom_data.overwrite);
    }
}