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