1use apimock_routing::RuleSet;
16use constant::*;
17use listener_config::ListenerConfig;
18use log_config::LogConfig;
19use serde::Deserialize;
20use service_config::ServiceConfig;
21
22use std::{fs, path::Path};
23
24use crate::{
25 error::{ConfigError, ConfigResult},
26 path_util::current_dir_to_file_parent_dir_relative_path,
27};
28
29pub mod constant;
30pub mod file_tree_config;
31pub mod listener_config;
32pub mod log_config;
33pub mod service_config;
34
35#[derive(Clone, Deserialize)]
38pub struct Config {
39 #[serde(skip)]
43 pub file_path: Option<String>,
44
45 pub listener: Option<ListenerConfig>,
46 pub log: Option<LogConfig>,
47 pub service: ServiceConfig,
48 pub file_tree_view: Option<file_tree_config::FileTreeViewConfig>,
52}
53
54impl Config {
55 #[allow(clippy::result_large_err)]
65 pub fn new(
66 config_file_path: Option<&String>,
67 fallback_respond_dir_path: Option<&String>,
68 ) -> ConfigResult<Self> {
69 let mut ret = Self::init(config_file_path)?;
70
71 ret.set_rule_sets()?;
72
73 ret.compute_fallback_respond_dir(fallback_respond_dir_path)?;
74
75 if !ret.validate() {
76 return Err(ConfigError::Validation);
77 }
78
79 log::info!("{}", ret);
80
81 Ok(ret)
82 }
83
84 #[allow(clippy::result_large_err)]
91 fn init(config_file_path: Option<&String>) -> ConfigResult<Self> {
92 let Some(config_file_path) = config_file_path else {
93 return Ok(Config::default());
94 };
95
96 log::info!("[config] {}\n", config_file_path);
97
98 let path = Path::new(config_file_path);
99 let toml_string =
100 fs::read_to_string(config_file_path).map_err(|e| ConfigError::ConfigRead {
101 path: path.to_path_buf(),
102 source: e,
103 })?;
104
105 let mut config: Config =
106 toml::from_str(&toml_string).map_err(|e| ConfigError::ConfigParse {
107 path: path.to_path_buf(),
108 canonical: path.canonicalize().ok(),
109 source: e,
110 })?;
111 config.file_path = Some(config_file_path.to_owned());
112
113 Ok(config)
114 }
115
116 #[allow(clippy::result_large_err)]
121 fn set_rule_sets(&mut self) -> ConfigResult<()> {
122 let relative_dir_path = self.current_dir_to_parent_dir_relative_path()?;
123
124 let Some(rule_sets_file_paths) = self.service.rule_sets_file_paths.as_ref() else {
125 return Ok(());
126 };
127
128 let mut rule_sets = Vec::with_capacity(rule_sets_file_paths.len());
129 for (rule_set_idx, rule_set_file_path) in rule_sets_file_paths.iter().enumerate() {
130 let joined = Path::new(relative_dir_path.as_str()).join(rule_set_file_path);
131 let path_str = joined.to_str().ok_or_else(|| ConfigError::ConfigRead {
132 path: joined.clone(),
133 source: std::io::Error::new(
134 std::io::ErrorKind::InvalidData,
135 format!(
136 "rule set #{} path contains non-UTF-8 bytes: {}",
137 rule_set_idx + 1,
138 joined.to_string_lossy(),
139 ),
140 ),
141 })?;
142
143 rule_sets.push(RuleSet::new(
146 path_str,
147 relative_dir_path.as_str(),
148 rule_set_idx,
149 )?);
150 }
151
152 self.service.rule_sets = rule_sets;
153 Ok(())
154 }
155
156 #[allow(clippy::result_large_err)]
162 pub fn compute_fallback_respond_dir(
163 &mut self,
164 fallback_respond_dir_path: Option<&String>,
165 ) -> ConfigResult<()> {
166 if let Some(fallback_respond_dir_path) = fallback_respond_dir_path {
167 self.service.fallback_respond_dir = fallback_respond_dir_path.to_owned();
168 return Ok(());
169 }
170
171 if self.service.fallback_respond_dir.as_str() == SERVICE_DEFAULT_FALLBACK_RESPOND_DIR {
172 return Ok(());
173 }
174
175 let relative_path = self.current_dir_to_parent_dir_relative_path()?;
176 let joined =
177 Path::new(relative_path.as_str()).join(self.service.fallback_respond_dir.as_str());
178 let resolved = joined.to_str().ok_or_else(|| ConfigError::PathResolve {
179 path: joined.clone(),
180 source: std::io::Error::new(
181 std::io::ErrorKind::InvalidData,
182 format!(
183 "fallback_respond_dir path contains non-UTF-8 bytes: {}",
184 joined.to_string_lossy(),
185 ),
186 ),
187 })?;
188 self.service.fallback_respond_dir = resolved.to_owned();
189 Ok(())
190 }
191
192 pub fn listener_http_addr(&self) -> Option<String> {
194 let https_is_active = self.listener_https_addr().is_some();
195 if https_is_active {
196 let port_is_single = self
197 .listener
198 .as_ref()
199 .and_then(|l| l.tls.as_ref())
200 .map(|t| t.port.is_none())
201 .unwrap_or(false);
202 if port_is_single {
203 return None;
204 }
205 }
206
207 let listener_default;
208 let listener = match self.listener.as_ref() {
209 Some(l) => l,
210 None => {
211 listener_default = ListenerConfig::default();
212 &listener_default
213 }
214 };
215
216 Some(format!("{}:{}", listener.ip_address, listener.port))
217 }
218
219 pub fn listener_https_addr(&self) -> Option<String> {
221 let listener = self.listener.as_ref()?;
222 let tls = listener.tls.as_ref()?;
223 let port = tls.port.unwrap_or(listener.port);
224 Some(format!("{}:{}", listener.ip_address, port))
225 }
226
227 fn validate(&self) -> bool {
231 if let Some(listener) = self.listener.as_ref() {
232 if !listener.validate() {
233 return false;
234 }
235
236 if self.listener_http_addr().is_none() && self.listener_https_addr().is_none() {
237 log::error!("at least one listener (http or https) is required");
238 return false;
239 }
240 }
241 self.service.validate()
242 }
243
244 #[allow(clippy::result_large_err)]
249 pub fn current_dir_to_parent_dir_relative_path(&self) -> ConfigResult<String> {
250 let Some(file_path) = self.file_path.as_ref() else {
251 return Ok(String::from("."));
252 };
253
254 let relative_dir_path = current_dir_to_file_parent_dir_relative_path(file_path.as_str())
255 .map_err(|e| ConfigError::PathResolve {
256 path: Path::new(file_path).to_path_buf(),
257 source: e,
258 })?;
259
260 let as_str = relative_dir_path
261 .to_str()
262 .ok_or_else(|| ConfigError::PathResolve {
263 path: relative_dir_path.clone(),
264 source: std::io::Error::new(
265 std::io::ErrorKind::InvalidData,
266 format!(
267 "relative path contains non-UTF-8 bytes: {}",
268 relative_dir_path.to_string_lossy()
269 ),
270 ),
271 })?;
272
273 Ok(as_str.to_owned())
274 }
275}
276
277impl Default for Config {
278 fn default() -> Self {
279 Config {
280 file_path: None,
281 listener: Some(ListenerConfig {
282 ip_address: LISTENER_DEFAULT_IP_ADDRESS.to_owned(),
283 port: LISTENER_DEFAULT_PORT,
284 tls: None,
285 }),
286 log: Some(LogConfig::default()),
287 service: ServiceConfig::default(),
288 file_tree_view: None,
289 }
290 }
291}
292
293impl std::fmt::Display for Config {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 let log = self.log.clone().unwrap_or_default();
296 let _ = write!(f, "{}", log);
297 let _ = writeln!(f, "{}", PRINT_DELIMITER);
298 let _ = write!(f, "{}", self.service);
299 Ok(())
300 }
301}