etopay_sdk/core/
config.rs1use super::Sdk;
6use crate::error::{Error, Result};
7use crate::user::UserRepo;
8use crate::user::repository::UserRepoImpl;
9use log::info;
10use std::path::Path;
11use std::str::FromStr;
12
13const ETOPAY_LOGFILE: &str = "etopay_sdk.log";
15
16#[derive(Debug)]
18pub struct Config {
19 pub path_prefix: Box<Path>,
22
23 pub auth_provider: String,
25
26 pub backend_url: reqwest::Url,
28
29 pub log_level: log::LevelFilter,
31}
32
33#[derive(Debug, serde::Deserialize)]
35#[cfg_attr(test, derive(PartialEq))]
36struct DeserializedConfig {
37 backend_url: String,
38
39 #[serde(default = "default_log_level")]
40 log_level: String,
41
42 #[serde(default = "default_storage_path")]
43 storage_path: String,
44
45 auth_provider: String,
46}
47
48#[cfg(test)]
49impl Default for DeserializedConfig {
50 fn default() -> Self {
52 Self {
53 backend_url: "http://example.com".to_string(),
54 auth_provider: "standalone".to_string(),
55 log_level: default_log_level(),
56 storage_path: default_storage_path(),
57 }
58 }
59}
60
61fn default_log_level() -> String {
62 "INFO".to_string()
63}
64fn default_storage_path() -> String {
65 ".".to_string()
66}
67
68impl FromStr for DeserializedConfig {
70 type Err = crate::Error;
71
72 fn from_str(s: &str) -> Result<Self> {
73 serde_json::from_str(s)
74 .map_err(|e| crate::Error::SetConfig(format!("Could not deserialize JSON config: {e:#?}")))
75 }
76}
77
78impl TryFrom<DeserializedConfig> for Config {
79 type Error = Error;
80
81 fn try_from(value: DeserializedConfig) -> Result<Self> {
82 let path_prefix = Path::new(&value.storage_path);
83 #[cfg(not(target_arch = "wasm32"))]
84 if !path_prefix.is_dir() {
85 return Err(crate::Error::SetConfig(
86 "storage_path must be a valid existing directory".to_string(),
87 ));
88 }
89
90 if value.auth_provider.is_empty() {
91 return Err(crate::Error::SetConfig("auth_provider must not be empty".to_string()));
92 }
93
94 Ok(Self {
95 backend_url: reqwest::Url::parse(&value.backend_url).map_err(|e| crate::Error::SetConfig(e.to_string()))?,
96 log_level: log::LevelFilter::from_str(&value.log_level)
97 .map_err(|e| crate::Error::SetConfig(format!("Could not parse log level: {e:#?}")))?,
98 auth_provider: value.auth_provider,
99 path_prefix: path_prefix.into(),
100 })
101 }
102}
103
104impl Config {
105 pub fn from_json(json: impl AsRef<str>) -> Result<Self> {
107 let json = json.as_ref();
108 let deserialized_config: DeserializedConfig = json.parse()?;
109 Self::try_from(deserialized_config)
110 }
111}
112
113impl Sdk {
114 pub fn set_config(&mut self, config: Config) -> Result<()> {
116 info!("Setting config: {config:?}");
117 #[cfg(not(target_arch = "wasm32"))]
121 {
122 let log_file = config.path_prefix.join(ETOPAY_LOGFILE);
123 let log_file_str = log_file
124 .to_str()
125 .ok_or_else(|| crate::Error::SetConfig(format!("config file path is not valid utf-8: {log_file:?}")))?;
126 crate::logger::init_logger(config.log_level, log_file_str)?;
127 }
128
129 self.config = Some(config);
130
131 self.initialize_user_repository()?;
133
134 Ok(())
135 }
136
137 fn initialize_user_repository(&mut self) -> Result<()> {
139 #[cfg(feature = "jammdb_repo")]
141 let repo: Box<dyn UserRepo + Send + Sync> = {
142 let config = self.config.as_ref().ok_or(crate::Error::MissingConfig)?;
143 Box::new(UserRepoImpl::new(crate::user::file_storage::FileUserStorage::new(
144 &config.path_prefix,
145 )?))
146 };
147
148 #[cfg(target_arch = "wasm32")]
150 let repo: Box<dyn UserRepo + Send + Sync> = {
151 let browser_storage = crate::user::web_storage::BrowserLocalStorage::new();
153 if browser_storage.is_available() {
154 Box::new(UserRepoImpl::new(browser_storage))
155 } else {
156 log::warn!("Browser Local Storage is not available, falling back to in-memory user storage!");
157 Box::new(UserRepoImpl::new(crate::user::memory_storage::MemoryUserStorage::new()))
158 }
159 };
160
161 #[cfg(all(not(target_arch = "wasm32"), not(feature = "jammdb_repo")))]
164 let repo: Box<dyn UserRepo + Send + Sync> =
165 Box::new(UserRepoImpl::new(crate::user::memory_storage::MemoryUserStorage::new()));
166
167 self.repo = Some(repo);
168 Ok(())
169 }
170}
171
172#[cfg(test)]
173#[allow(clippy::expect_used)] impl Config {
175 pub fn new_test_with_cleanup() -> (Self, testing::CleanUp) {
177 let cleanup = testing::CleanUp::default();
178 (
179 Self {
180 backend_url: reqwest::Url::parse("http://example.com").expect("should be a valid url"),
181 path_prefix: Path::new(&cleanup.path_prefix).into(),
182 auth_provider: "standalone".to_string(),
183 log_level: log::LevelFilter::Debug,
184 },
185 cleanup,
186 )
187 }
188
189 pub fn new_test_with_cleanup_url(url: &str) -> (Self, testing::CleanUp) {
191 let cleanup = testing::CleanUp::default();
192 (
193 Self {
194 backend_url: url.parse().expect("should be a valid url"),
195 path_prefix: Path::new(&cleanup.path_prefix).into(),
196 auth_provider: "standalone".to_string(),
197 log_level: log::LevelFilter::Debug,
198 },
199 cleanup,
200 )
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use rstest::rstest;
208
209 fn valid_deserialized_config() -> DeserializedConfig {
210 DeserializedConfig {
211 backend_url: "http://example.com".to_string(),
212 log_level: "INFO".to_string(),
213 storage_path: ".".to_string(),
214 auth_provider: "nonempty".to_string(),
215 }
216 }
217
218 #[test]
219 fn test_valid_config() {
220 Config::try_from(valid_deserialized_config()).unwrap();
221 }
222
223 #[test]
224 fn test_path_prefix_error() {
225 let mut config = valid_deserialized_config();
226 config.storage_path = "nonexistent_file.txt".to_string();
227
228 Config::try_from(config).unwrap_err();
229 }
230
231 #[test]
232 fn test_empty_auth_provider_error() {
233 let mut config = valid_deserialized_config();
234 config.auth_provider = "".to_string();
235
236 Config::try_from(config).unwrap_err();
237 }
238
239 #[test]
240 fn test_invalid_backend_url_error() {
241 let mut config = valid_deserialized_config();
242 config.backend_url = "invalid url".to_string();
243
244 Config::try_from(config).unwrap_err();
245 }
246
247 #[test]
248 fn test_default_log_level() {
249 let log_level = default_log_level();
250 assert_eq!(log_level, "INFO".to_string())
251 }
252
253 #[test]
254 fn test_default_storage_path() {
255 let storage_path = default_storage_path();
256 assert_eq!(storage_path, ".".to_string())
257 }
258
259 #[rstest]
260 #[case(
261 r#"{
262 "backend_url": "http://example.com",
263 "storage_path": ".",
264 "log_level": "INFO",
265 "auth_provider": "standalone"
266 }"#,
267 Ok(DeserializedConfig::default())
268 )]
269 #[case(
270 r#"{
271 "backend_url": "http://example.com
272 }"#,
273 Err(crate::Error::SetConfig("Could not deserialize JSON config: ...".to_string())),
274 )]
275 fn test_deserialized_config_from_str(#[case] input: &str, #[case] expected: Result<DeserializedConfig>) {
276 let result = DeserializedConfig::from_str(input);
277 match expected {
278 Ok(resp) => {
279 assert_eq!(result.unwrap(), resp);
280 }
281 Err(_expected_err) => {
282 result.unwrap_err();
283 }
284 }
285 }
286}