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