statsig_rust/specs_adapter/
statsig_local_file_specs_adapter.rs1use 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)),
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, source: SpecsSource::Adapter("FileBased".to_owned()),
56 },
57 _ => SpecsInfo::empty(),
58 };
59
60 let data = match self.http_adapter.fetch_specs_from_network(specs_info).await {
61 Ok(data) => String::from_utf8(data)
62 .map_err(|e| StatsigErr::SerializationError(e.to_string()))?,
63 Err(e) => {
64 return Err(StatsigErr::NetworkError(
65 e,
66 Some("No data received".to_string()),
67 ));
68 }
69 };
70
71 if let Some(response) = self.parse_specs_data_to_full_response(&data) {
72 if response.has_updates {
73 self.write_specs_to_file(&data);
74 }
75 }
76
77 Ok(())
78 }
79
80 pub fn resync_from_file(&self) -> Result<(), StatsigErr> {
81 let data = match std::fs::read_to_string(&self.file_path) {
82 Ok(data) => data,
83 Err(e) => {
84 return Err(StatsigErr::FileError(e.to_string()));
85 }
86 };
87
88 match &self.listener.read() {
89 Ok(lock) => match lock.as_ref() {
90 Some(listener) => listener.did_receive_specs_update(SpecsUpdate {
91 data: data.into_bytes(),
92 source: SpecsSource::Adapter("FileBased".to_owned()),
93 received_at: Utc::now().timestamp_millis() as u64,
94 }),
95 None => Err(StatsigErr::UnstartedAdapter("Listener not set".to_string())),
96 },
97 Err(e) => Err(StatsigErr::LockFailure(e.to_string())),
98 }
99 }
100
101 fn read_specs_from_file(&self) -> Result<Option<SpecsResponseFull>, StatsigErr> {
102 if !std::path::Path::new(&self.file_path).exists() {
103 return Ok(None);
104 }
105
106 let data = match std::fs::read_to_string(&self.file_path) {
107 Ok(data) => data,
108 Err(e) => {
109 return Err(StatsigErr::FileError(e.to_string()));
110 }
111 };
112
113 Ok(self.parse_specs_data_to_full_response(&data))
114 }
115
116 fn parse_specs_data_to_full_response(&self, data: &str) -> Option<SpecsResponseFull> {
117 let mut place = SpecsResponseFull::default();
118 match DecodedSpecsResponse::from_slice(data.as_bytes(), &mut place, None) {
119 Ok(_) => Some(place),
120 Err(e) => {
121 log_w!(TAG, "Failed to parse specs data: {}", e);
122 None
123 }
124 }
125 }
126
127 fn write_specs_to_file(&self, data: &str) {
128 match std::fs::write(&self.file_path, data) {
129 Ok(()) => (),
130 Err(e) => log_w!(TAG, "Failed to write specs to file: {}", e),
131 }
132 }
133}
134
135#[async_trait]
136impl SpecsAdapter for StatsigLocalFileSpecsAdapter {
137 async fn start(
138 self: Arc<Self>,
139 _statsig_runtime: &Arc<StatsigRuntime>,
140 ) -> Result<(), StatsigErr> {
141 self.resync_from_file()
142 }
143
144 fn initialize(&self, listener: Arc<dyn SpecsUpdateListener>) {
145 if let Ok(mut mut_listener) = self.listener.write() {
146 *mut_listener = Some(listener);
147 }
148 }
149
150 async fn shutdown(
151 &self,
152 _timeout: Duration,
153 _statsig_runtime: &Arc<StatsigRuntime>,
154 ) -> Result<(), StatsigErr> {
155 Ok(())
156 }
157
158 async fn schedule_background_sync(
159 self: Arc<Self>,
160 _statsig_runtime: &Arc<StatsigRuntime>,
161 ) -> Result<(), StatsigErr> {
162 Ok(())
163 }
164
165 fn get_type_name(&self) -> String {
166 stringify!(StatsigLocalFileSpecsAdapter).to_string()
167 }
168}