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