statsig_rust/specs_adapter/
statsig_local_file_specs_adapter.rs1use 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}