statsig_rust/specs_adapter/
statsig_local_file_specs_adapter.rs

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