oai-statsig-rust 0.27.0

Statsig Rust SDK for usage in multi-user server environments.
Documentation
use crate::statsig_metadata;
use crate::{evaluation::dynamic_value::DynamicValue, hashing};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::{collections::HashMap, sync::Arc};

use super::{
    into_optional::IntoOptional,
    unit_id::UnitID,
    user_data::{UserData, UserDataMap, UserDataMapOf},
    user_value::{UserValue, UserValueMap},
};

pub type FastUserCustomMap = UserValueMap;
pub type FastUserUnitIDMap = UserDataMapOf<UserValue>;

#[skip_serializing_none]
#[derive(Clone, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct FastUserData {
    #[serde(rename = "userID")]
    pub user_id: Option<UserValue>,
    #[serde(rename = "customIDs")]
    pub custom_ids: Option<FastUserUnitIDMap>,

    pub email: Option<UserValue>,
    pub ip: Option<UserValue>,
    pub user_agent: Option<UserValue>,
    pub country: Option<UserValue>,
    pub locale: Option<UserValue>,
    pub app_version: Option<UserValue>,
    pub statsig_environment: Option<UserDataMap>,

    #[serde(skip_serializing)]
    pub private_attributes: Option<UserDataMap>,
    pub custom: Option<FastUserCustomMap>,
}

impl FastUserData {
    pub(crate) fn create_exposure_dedupe_user_hash(&self, unit_id_type: Option<&str>) -> u64 {
        let user_id_hash = self.user_id.as_ref().map_or(0, UserValue::hash_value);
        let stable_id_hash = self.get_unit_id_hash("stableID");
        let unit_type_hash = unit_id_type.map_or(0, |id_type| self.get_unit_id_hash(id_type));
        let custom_ids_hash_sum = self.sum_custom_id_hashes();

        hashing::hash_one(vec![
            user_id_hash,
            stable_id_hash,
            unit_type_hash,
            custom_ids_hash_sum,
        ])
    }

    pub(crate) fn sum_custom_id_hashes(&self) -> u64 {
        self.custom_ids.as_ref().map_or(0, |custom_ids| {
            custom_ids
                .values()
                .fold(0u64, |acc, value| acc.wrapping_add(value.hash_value()))
        })
    }

    pub(crate) fn get_unit_id_hash(&self, id_type: &str) -> u64 {
        if id_type.eq_ignore_ascii_case("userid") {
            return self.user_id.as_ref().map_or(0, UserValue::hash_value);
        }

        if let Some(custom_ids) = &self.custom_ids {
            if let Some(id) = custom_ids.get(id_type) {
                return id.hash_value();
            }

            if let Some(id) = custom_ids.get(&id_type.to_lowercase()) {
                return id.hash_value();
            }
        }

        0
    }

    pub(crate) fn to_bytes(&self) -> Option<Vec<u8>> {
        serde_json::to_vec(self).ok()
    }
}

#[derive(Clone)]
pub struct FastStatsigUser {
    pub data: Arc<FastUserData>,
    pub(crate) sdk_version: &'static str,
}

impl FastStatsigUser {
    pub fn new(inner: FastUserData) -> Self {
        Self {
            data: Arc::new(inner),
            sdk_version: statsig_metadata::SDK_VERSION,
        }
    }

    pub fn get_user_id(&self) -> Option<&str> {
        self.data.user_id.as_ref().and_then(UserValue::string_value)
    }

    pub fn set_user_id(&mut self, user_id: impl Into<UnitID>) {
        let unit_id = user_id.into();
        let mut_data = Arc::make_mut(&mut self.data);
        mut_data.user_id = Some(UserValue::from(unit_id));
    }

    pub fn get_custom_ids(&self) -> Option<HashMap<&str, &str>> {
        let mapped = self
            .data
            .custom_ids
            .as_ref()?
            .iter()
            .map(|(key, value)| (key.as_str(), value.string_value().unwrap_or("")))
            .collect();

        Some(mapped)
    }

    pub fn set_custom_ids<K, U>(&mut self, custom_ids: HashMap<K, U>)
    where
        K: Into<String>,
        U: Into<UnitID>,
    {
        let custom_ids = custom_ids
            .into_iter()
            .map(|(key, value)| (key.into(), UserValue::from(value.into())))
            .collect();

        let mut_data = Arc::make_mut(&mut self.data);
        mut_data.custom_ids = Some(custom_ids);
    }

    pub(crate) fn get_unit_id_by_name(
        &self,
        id_type: &str,
        lowercased_id_type: &str,
    ) -> Option<&UserValue> {
        if lowercased_id_type == "userid" {
            return self.data.user_id.as_ref();
        }

        let custom_ids = self.data.custom_ids.as_ref()?;

        if let Some(custom_id) = custom_ids.get(id_type) {
            return Some(custom_id);
        }

        custom_ids.get(lowercased_id_type)
    }

    pub fn get_statsig_environment(&self) -> Option<HashMap<&str, &str>> {
        let mapped = self
            .data
            .statsig_environment
            .as_ref()?
            .iter()
            .map(dynamic_entry_to_key_value_refs)
            .collect();

        Some(mapped)
    }

    pub fn set_statsig_environment<K, U>(&mut self, statsig_environment: Option<HashMap<K, U>>)
    where
        K: Into<String>,
        U: Into<String>,
    {
        let mut_data = Arc::make_mut(&mut self.data);
        let statsig_environment = match statsig_environment {
            Some(value) => value,
            None => {
                mut_data.statsig_environment = None;
                return;
            }
        };

        let statsig_environment: UserDataMap = statsig_environment
            .into_iter()
            .map(|(key, value)| (key.into(), value.into().into()))
            .collect();

        mut_data.statsig_environment = Some(statsig_environment);
    }

    pub fn get_custom(&self) -> Option<&FastUserCustomMap> {
        self.data.custom.as_ref()
    }

    pub fn set_custom<K, V>(&mut self, value: impl IntoOptional<HashMap<K, V>>)
    where
        K: Into<String>,
        V: Into<UserValue>,
    {
        let mut_data = Arc::make_mut(&mut self.data);
        let value = match value.into_optional() {
            Some(value) => value,
            None => {
                mut_data.custom = None;
                return;
            }
        };

        mut_data.custom = Some(
            value
                .into_iter()
                .map(|(key, value)| (key.into(), value.into()))
                .collect(),
        );
    }

    pub fn get_private_attributes(&self) -> Option<&UserDataMap> {
        self.data.private_attributes.as_ref()
    }

    pub fn set_private_attributes<K, V>(&mut self, value: impl IntoOptional<HashMap<K, V>>)
    where
        K: Into<String>,
        V: Into<DynamicValue>,
    {
        let mut_data = Arc::make_mut(&mut self.data);
        let value = match value.into_optional() {
            Some(value) => value,
            None => {
                mut_data.private_attributes = None;
                return;
            }
        };

        mut_data.private_attributes = Some(
            value
                .into_iter()
                .map(|(key, value)| (key.into(), value.into()))
                .collect(),
        );
    }

    pub fn to_public_user(&self) -> crate::StatsigUser {
        crate::StatsigUser::new(self.to_public_user_data())
    }

    pub(crate) fn to_public_user_data(&self) -> UserData {
        UserData {
            user_id: self.data.user_id.as_ref().map(user_value_to_dynamic_value),
            custom_ids: self
                .data
                .custom_ids
                .as_ref()
                .map(user_value_map_to_dynamic_value_map),
            email: self.data.email.as_ref().map(user_value_to_dynamic_value),
            ip: self.data.ip.as_ref().map(user_value_to_dynamic_value),
            user_agent: self
                .data
                .user_agent
                .as_ref()
                .map(user_value_to_dynamic_value),
            country: self.data.country.as_ref().map(user_value_to_dynamic_value),
            locale: self.data.locale.as_ref().map(user_value_to_dynamic_value),
            app_version: self
                .data
                .app_version
                .as_ref()
                .map(user_value_to_dynamic_value),
            statsig_environment: self.data.statsig_environment.clone(),
            private_attributes: self.data.private_attributes.clone(),
            custom: self.data.custom.as_ref().map(|values| {
                values
                    .iter()
                    .map(|(key, value)| (key.clone(), user_value_to_dynamic_value(value)))
                    .collect()
            }),
        }
    }
}

macro_rules! string_field_accessor {
    ($getter_name:ident, $setter_name:ident, $field:ident) => {
        pub fn $getter_name(&self) -> Option<&str> {
            self.data.$field.as_ref().and_then(UserValue::string_value)
        }

        pub fn $setter_name(&mut self, value: impl IntoOptional<String>) {
            let value = value.into_optional();
            let mut_data = Arc::make_mut(&mut self.data);
            match value {
                Some(value) => {
                    mut_data.$field = Some(UserValue::from(value));
                }
                None => mut_data.$field = None,
            }
        }
    };
}

impl FastStatsigUser {
    string_field_accessor!(get_email, set_email, email);
    string_field_accessor!(get_ip, set_ip, ip);
    string_field_accessor!(get_user_agent, set_user_agent, user_agent);
    string_field_accessor!(get_country, set_country, country);
    string_field_accessor!(get_locale, set_locale, locale);
    string_field_accessor!(get_app_version, set_app_version, app_version);
}

impl FastStatsigUser {
    pub fn to_bytes(&self) -> Option<Vec<u8>> {
        self.data.to_bytes()
    }
}

fn dynamic_entry_to_key_value_refs<'a>(
    entry: (&'a String, &'a DynamicValue),
) -> (&'a str, &'a str) {
    let (key, value) = entry;

    (
        key.as_str(),
        value
            .string_value
            .as_ref()
            .map(|value| value.value.as_str())
            .unwrap_or(""),
    )
}

fn user_value_to_dynamic_value(value: &UserValue) -> DynamicValue {
    DynamicValue::from_json_value(value)
}

fn user_value_map_to_dynamic_value_map(values: &UserDataMapOf<UserValue>) -> UserDataMap {
    values
        .iter()
        .map(|(key, value)| (key.clone(), user_value_to_dynamic_value(value)))
        .collect()
}