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