sigstat 0.0.4

Statsig Rust SDK for usage in multi-user server environments.
Documentation
use async_trait::async_trait;
use chrono::Utc;
use sigstat::{
    SpecsAdapter, SpecsSource, SpecsUpdate, SpecsUpdateListener, StatsigErr, StatsigRuntime,
};
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::Duration;

pub struct MockSpecsAdapter {
    json_data_path: String,
    should_throw: bool,
    delay_ms: Option<u64>,
    listener: RwLock<Option<Arc<dyn SpecsUpdateListener>>>,
}

impl MockSpecsAdapter {
    pub fn with_data(path: &str) -> Self {
        Self {
            json_data_path: path.to_string(),
            should_throw: false,
            delay_ms: None,
            listener: RwLock::new(None),
        }
    }

    pub fn throwing() -> Self {
        Self {
            json_data_path: "".to_string(),
            should_throw: true,
            delay_ms: None,
            listener: RwLock::new(None),
        }
    }

    pub fn delayed(path: &str, delay_ms: u64) -> Self {
        Self {
            json_data_path: path.to_string(),
            should_throw: false,
            delay_ms: Some(delay_ms),
            listener: RwLock::new(None),
        }
    }

    pub async fn resync(&self) {
        self.manually_sync_specs(None).await.unwrap();
    }

    async fn manually_sync_specs(&self, _current_lcut: Option<u64>) -> Result<(), StatsigErr> {
        if self.should_throw {
            return Err(StatsigErr::CustomError("Not today".to_string()));
        }

        if let Some(delay) = self.delay_ms {
            tokio::time::sleep(Duration::from_millis(delay)).await;
        }

        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        path.push(self.json_data_path.as_str());

        let data = fs::read_to_string(path).expect("Unable to read file");
        let update = SpecsUpdate {
            data,
            source: SpecsSource::Bootstrap,
            received_at: Utc::now().timestamp_millis() as u64,
        };

        self.listener
            .write()
            .as_ref()
            .unwrap()
            .as_ref()
            .unwrap()
            .did_receive_specs_update(update)?;

        Ok(())
    }
}

#[async_trait]
impl SpecsAdapter for MockSpecsAdapter {
    fn initialize(&self, listener: Arc<dyn SpecsUpdateListener>) {
        if let Ok(mut mut_listener) = self.listener.write() {
            *mut_listener = Some(listener);
        }
    }

    async fn start(
        self: Arc<Self>,
        _statsig_runtime: &Arc<StatsigRuntime>,
    ) -> Result<(), StatsigErr> {
        let lcut = match self.listener.read() {
            Ok(lock) => match lock.as_ref() {
                Some(listener) => listener.get_current_specs_info().lcut,
                None => None,
            },
            Err(_) => None,
        };
        self.manually_sync_specs(lcut).await
    }

    async fn schedule_background_sync(
        self: Arc<Self>,
        _statsig_runtime: &Arc<StatsigRuntime>,
    ) -> Result<(), StatsigErr> {
        Ok(())
    }

    async fn shutdown(
        &self,
        _timeout: Duration,
        _statsig_runtime: &Arc<StatsigRuntime>,
    ) -> Result<(), StatsigErr> {
        Ok(())
    }

    fn get_type_name(&self) -> String {
        stringify!(MockSpecsAdapter).to_string()
    }
}