statsig_rust/specs_adapter/
statsig_local_file_specs_adapter.rs

1use crate::hashing::djb2;
2use crate::spec_types::{SpecsResponse, SpecsResponseFull};
3use crate::specs_adapter::{SpecsAdapter, SpecsSource, SpecsUpdate, SpecsUpdateListener};
4use crate::statsig_err::StatsigErr;
5use crate::{log_w, StatsigRuntime};
6use async_trait::async_trait;
7use chrono::Utc;
8
9use std::sync::{Arc, RwLock};
10use std::time::Duration;
11
12use super::{SpecsInfo, StatsigHttpSpecsAdapter};
13
14const TAG: &str = stringify!(StatsigLocalFileSpecsAdapter);
15
16pub struct StatsigLocalFileSpecsAdapter {
17    file_path: String,
18    listener: RwLock<Option<Arc<dyn SpecsUpdateListener>>>,
19    http_adapter: StatsigHttpSpecsAdapter,
20}
21
22impl StatsigLocalFileSpecsAdapter {
23    #[must_use]
24    pub fn new(
25        sdk_key: &str,
26        output_directory: &str,
27        specs_url: Option<String>,
28        fallback_to_statsig_api: bool,
29    ) -> Self {
30        let hashed_key = djb2(sdk_key);
31        let file_path = format!("{output_directory}/{hashed_key}_specs.json");
32
33        Self {
34            file_path,
35            listener: RwLock::new(None),
36            http_adapter: StatsigHttpSpecsAdapter::new(
37                sdk_key,
38                specs_url.as_ref(),
39                fallback_to_statsig_api,
40                None,
41            ),
42        }
43    }
44
45    pub async fn fetch_and_write_to_file(&self) -> Result<(), StatsigErr> {
46        let specs_info = match self.read_specs_from_file() {
47            Ok(Some(specs)) => SpecsInfo {
48                lcut: Some(specs.time),
49                checksum: specs.checksum,
50                source: SpecsSource::Adapter("FileBased".to_owned()),
51            },
52            _ => SpecsInfo::empty(),
53        };
54
55        let data = match self.http_adapter.fetch_specs_from_network(specs_info).await {
56            Some(data) => data,
57            None => {
58                return Err(StatsigErr::NetworkError("No data received".to_string()));
59            }
60        };
61
62        if let Some(response) = self.parse_specs_data_to_full_response(&data) {
63            if response.has_updates {
64                self.write_specs_to_file(&data);
65            }
66        }
67
68        Ok(())
69    }
70
71    pub fn resync_from_file(&self) -> Result<(), StatsigErr> {
72        let data = match std::fs::read_to_string(&self.file_path) {
73            Ok(data) => data,
74            Err(e) => {
75                return Err(StatsigErr::FileError(e.to_string()));
76            }
77        };
78
79        match &self.listener.read() {
80            Ok(lock) => match lock.as_ref() {
81                Some(listener) => listener.did_receive_specs_update(SpecsUpdate {
82                    data,
83                    source: SpecsSource::Adapter("FileBased".to_owned()),
84                    received_at: Utc::now().timestamp_millis() as u64,
85                }),
86                None => Err(StatsigErr::UnstartedAdapter("Listener not set".to_string())),
87            },
88            Err(e) => Err(StatsigErr::LockFailure(e.to_string())),
89        }
90    }
91
92    fn read_specs_from_file(&self) -> Result<Option<Box<SpecsResponseFull>>, StatsigErr> {
93        if !std::path::Path::new(&self.file_path).exists() {
94            return Ok(None);
95        }
96
97        let data = match std::fs::read_to_string(&self.file_path) {
98            Ok(data) => data,
99            Err(e) => {
100                return Err(StatsigErr::FileError(e.to_string()));
101            }
102        };
103
104        Ok(self.parse_specs_data_to_full_response(&data))
105    }
106
107    fn parse_specs_data_to_full_response(&self, data: &str) -> Option<Box<SpecsResponseFull>> {
108        match serde_json::from_str::<SpecsResponse>(data) {
109            Ok(SpecsResponse::Full(full)) => Some(full),
110            Ok(SpecsResponse::NoUpdates(_)) => None,
111            Err(e) => {
112                log_w!(TAG, "Failed to parse specs data: {}", e);
113                None
114            }
115        }
116    }
117
118    fn write_specs_to_file(&self, data: &str) {
119        match std::fs::write(&self.file_path, data) {
120            Ok(()) => (),
121            Err(e) => log_w!(TAG, "Failed to write specs to file: {}", e),
122        }
123    }
124}
125
126#[async_trait]
127impl SpecsAdapter for StatsigLocalFileSpecsAdapter {
128    async fn start(
129        self: Arc<Self>,
130        _statsig_runtime: &Arc<StatsigRuntime>,
131    ) -> Result<(), StatsigErr> {
132        self.resync_from_file()
133    }
134
135    fn initialize(&self, listener: Arc<dyn SpecsUpdateListener>) {
136        if let Ok(mut mut_listener) = self.listener.write() {
137            *mut_listener = Some(listener);
138        }
139    }
140
141    async fn shutdown(
142        &self,
143        _timeout: Duration,
144        _statsig_runtime: &Arc<StatsigRuntime>,
145    ) -> Result<(), StatsigErr> {
146        Ok(())
147    }
148
149    async fn schedule_background_sync(
150        self: Arc<Self>,
151        _statsig_runtime: &Arc<StatsigRuntime>,
152    ) -> Result<(), StatsigErr> {
153        Ok(())
154    }
155
156    fn get_type_name(&self) -> String {
157        stringify!(StatsigLocalFileSpecsAdapter).to_string()
158    }
159}