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 pub fn new(
62 config_file_path: Option<&String>,
63 fallback_respond_dir_path: Option<&String>,
64 ) -> ConfigResult<Self> {
65 let mut ret = Self::init(config_file_path)?;
66
67 ret.set_rule_sets()?;
68
69 ret.compute_fallback_respond_dir(fallback_respond_dir_path)?;
70
71 if !ret.validate() {
72 return Err(ConfigError::Validation);
73 }
74
75 log::info!("{}", ret);
76
77 Ok(ret)
78 }
79
80 fn init(config_file_path: Option<&String>) -> ConfigResult<Self> {
84 let Some(config_file_path) = config_file_path else {
85 return Ok(Config::default());
86 };
87
88 log::info!("[config] {}\n", config_file_path);
89
90 let path = Path::new(config_file_path);
91 let toml_string =
92 fs::read_to_string(config_file_path).map_err(|e| ConfigError::ConfigRead {
93 path: path.to_path_buf(),
94 source: e,
95 })?;
96
97 let mut config: Config =
98 toml::from_str(&toml_string).map_err(|e| ConfigError::ConfigParse {
99 path: path.to_path_buf(),
100 canonical: path.canonicalize().ok(),
101 source: e,
102 })?;
103 config.file_path = Some(config_file_path.to_owned());
104
105 Ok(config)
106 }
107
108 fn set_rule_sets(&mut self) -> ConfigResult<()> {
110 let relative_dir_path = self.current_dir_to_parent_dir_relative_path()?;
111
112 let Some(rule_sets_file_paths) = self.service.rule_sets_file_paths.as_ref() else {
113 return Ok(());
114 };
115
116 let mut rule_sets = Vec::with_capacity(rule_sets_file_paths.len());
117 for (rule_set_idx, rule_set_file_path) in rule_sets_file_paths.iter().enumerate() {
118 let joined = Path::new(relative_dir_path.as_str()).join(rule_set_file_path);
119 let path_str = joined.to_str().ok_or_else(|| ConfigError::ConfigRead {
120 path: joined.clone(),
121 source: std::io::Error::new(
122 std::io::ErrorKind::InvalidData,
123 format!(
124 "rule set #{} path contains non-UTF-8 bytes: {}",
125 rule_set_idx + 1,
126 joined.to_string_lossy(),
127 ),
128 ),
129 })?;
130
131 rule_sets.push(RuleSet::new(
134 path_str,
135 relative_dir_path.as_str(),
136 rule_set_idx,
137 )?);
138 }
139
140 self.service.rule_sets = rule_sets;
141 Ok(())
142 }
143
144 pub fn compute_fallback_respond_dir(
147 &mut self,
148 fallback_respond_dir_path: Option<&String>,
149 ) -> ConfigResult<()> {
150 if let Some(fallback_respond_dir_path) = fallback_respond_dir_path {
151 self.service.fallback_respond_dir = fallback_respond_dir_path.to_owned();
152 return Ok(());
153 }
154
155 if self.service.fallback_respond_dir.as_str() == SERVICE_DEFAULT_FALLBACK_RESPOND_DIR {
156 return Ok(());
157 }
158
159 let relative_path = self.current_dir_to_parent_dir_relative_path()?;
160 let joined =
161 Path::new(relative_path.as_str()).join(self.service.fallback_respond_dir.as_str());
162 let resolved = joined.to_str().ok_or_else(|| ConfigError::PathResolve {
163 path: joined.clone(),
164 source: std::io::Error::new(
165 std::io::ErrorKind::InvalidData,
166 format!(
167 "fallback_respond_dir path contains non-UTF-8 bytes: {}",
168 joined.to_string_lossy(),
169 ),
170 ),
171 })?;
172 self.service.fallback_respond_dir = resolved.to_owned();
173 Ok(())
174 }
175
176 pub fn listener_http_addr(&self) -> Option<String> {
178 let https_is_active = self.listener_https_addr().is_some();
179 if https_is_active {
180 let port_is_single = self
181 .listener
182 .as_ref()
183 .and_then(|l| l.tls.as_ref())
184 .map(|t| t.port.is_none())
185 .unwrap_or(false);
186 if port_is_single {
187 return None;
188 }
189 }
190
191 let listener_default;
192 let listener = match self.listener.as_ref() {
193 Some(l) => l,
194 None => {
195 listener_default = ListenerConfig::default();
196 &listener_default
197 }
198 };
199
200 Some(format!("{}:{}", listener.ip_address, listener.port))
201 }
202
203 pub fn listener_https_addr(&self) -> Option<String> {
205 let listener = self.listener.as_ref()?;
206 let tls = listener.tls.as_ref()?;
207 let port = tls.port.unwrap_or(listener.port);
208 Some(format!("{}:{}", listener.ip_address, port))
209 }
210
211 fn validate(&self) -> bool {
215 if let Some(listener) = self.listener.as_ref() {
216 if !listener.validate() {
217 return false;
218 }
219
220 if self.listener_http_addr().is_none() && self.listener_https_addr().is_none() {
221 log::error!("at least one listener (http or https) is required");
222 return false;
223 }
224 }
225 self.service.validate()
226 }
227
228 pub fn current_dir_to_parent_dir_relative_path(&self) -> ConfigResult<String> {
230 let Some(file_path) = self.file_path.as_ref() else {
231 return Ok(String::from("."));
232 };
233
234 let relative_dir_path =
235 current_dir_to_file_parent_dir_relative_path(file_path.as_str()).map_err(|e| {
236 ConfigError::PathResolve {
237 path: Path::new(file_path).to_path_buf(),
238 source: e,
239 }
240 })?;
241
242 let as_str = relative_dir_path
243 .to_str()
244 .ok_or_else(|| ConfigError::PathResolve {
245 path: relative_dir_path.clone(),
246 source: std::io::Error::new(
247 std::io::ErrorKind::InvalidData,
248 format!(
249 "relative path contains non-UTF-8 bytes: {}",
250 relative_dir_path.to_string_lossy()
251 ),
252 ),
253 })?;
254
255 Ok(as_str.to_owned())
256 }
257}
258
259impl Default for Config {
260 fn default() -> Self {
261 Config {
262 file_path: None,
263 listener: Some(ListenerConfig {
264 ip_address: LISTENER_DEFAULT_IP_ADDRESS.to_owned(),
265 port: LISTENER_DEFAULT_PORT,
266 tls: None,
267 }),
268 log: Some(LogConfig::default()),
269 service: ServiceConfig::default(),
270 file_tree_view: None,
271 }
272 }
273}
274
275impl std::fmt::Display for Config {
276 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277 let log = self.log.clone().unwrap_or_default();
278 let _ = write!(f, "{}", log);
279 let _ = writeln!(f, "{}", PRINT_DELIMITER);
280 let _ = write!(f, "{}", self.service);
281 Ok(())
282 }
283}