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
13#[derive(Debug)]
15pub struct Config {
16 pub path_prefix: Box<Path>,
19
20 pub auth_provider: String,
22
23 pub backend_url: reqwest::Url,
25
26 pub log_level: log::LevelFilter,
28}
29
30#[derive(Debug, serde::Deserialize)]
32#[cfg_attr(test, derive(PartialEq))]
33struct DeserializedConfig {
34 backend_url: String,
35
36 #[serde(default = "default_log_level")]
37 log_level: String,
38
39 #[serde(default = "default_storage_path")]
40 storage_path: String,
41
42 auth_provider: String,
43}
44
45#[cfg(test)]
46impl Default for DeserializedConfig {
47 fn default() -> Self {
49 Self {
50 backend_url: "http://example.com".to_string(),
51 auth_provider: "standalone".to_string(),
52 log_level: default_log_level(),
53 storage_path: default_storage_path(),
54 }
55 }
56}
57
58fn default_log_level() -> String {
59 "INFO".to_string()
60}
61fn default_storage_path() -> String {
62 ".".to_string()
63}
64
65impl FromStr for DeserializedConfig {
67 type Err = crate::Error;
68
69 fn from_str(s: &str) -> Result<Self> {
70 serde_json::from_str(s)
71 .map_err(|e| crate::Error::SetConfig(format!("Could not deserialize JSON config: {e:#?}")))
72 }
73}
74
75impl TryFrom<DeserializedConfig> for Config {
76 type Error = Error;
77
78 fn try_from(value: DeserializedConfig) -> Result<Self> {
79 let path_prefix = Path::new(&value.storage_path);
80 #[cfg(not(target_arch = "wasm32"))]
81 if !path_prefix.is_dir() {
82 return Err(crate::Error::SetConfig(
83 "storage_path must be a valid existing directory".to_string(),
84 ));
85 }
86
87 if value.auth_provider.is_empty() {
88 return Err(crate::Error::SetConfig("auth_provider must not be empty".to_string()));
89 }
90
91 Ok(Self {
92 backend_url: reqwest::Url::parse(&value.backend_url).map_err(|e| crate::Error::SetConfig(e.to_string()))?,
93 log_level: log::LevelFilter::from_str(&value.log_level)
94 .map_err(|e| crate::Error::SetConfig(format!("Could not parse log level: {e:#?}")))?,
95 auth_provider: value.auth_provider,
96 path_prefix: path_prefix.into(),
97 })
98 }
99}
100
101impl Config {
102 #[allow(clippy::result_large_err)]
104 pub fn from_json(json: impl AsRef<str>) -> Result<Self> {
105 let json = json.as_ref();
106 let deserialized_config: DeserializedConfig = json.parse()?;
107 Self::try_from(deserialized_config)
108 }
109}
110
111impl Sdk {
112 #[allow(clippy::result_large_err)]
114 pub fn set_config(&mut self, config: Config) -> Result<()> {
115 info!("Setting config: {config:?}");
116 #[cfg(not(target_arch = "wasm32"))]
120 {
121 let log_file = config.path_prefix.join("etopay_sdk.log");
122 let log_file_str = log_file
123 .to_str()
124 .ok_or_else(|| crate::Error::SetConfig(format!("config file path is not valid utf-8: {log_file:?}")))?;
125 crate::logger::init_logger(config.log_level, log_file_str)?;
126 }
127
128 self.config = Some(config);
129
130 self.initialize_user_repository()?;
132
133 Ok(())
134 }
135
136 #[allow(clippy::result_large_err)]
138 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}