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