mdbook_validator/
config.rs1use std::collections::HashMap;
6use std::path::PathBuf;
7
8use anyhow::Result;
9use tracing::debug;
10
11use crate::error::ValidatorError;
12use serde::Deserialize;
13
14#[derive(Debug, Clone, Deserialize)]
16pub struct ValidatorConfig {
17 pub container: String,
19 pub script: PathBuf,
21 #[serde(default)]
24 pub exec_command: Option<String>,
25}
26
27#[derive(Debug, Clone, Deserialize, Default)]
29pub struct Config {
30 #[serde(default)]
32 pub validators: HashMap<String, ValidatorConfig>,
33 #[serde(default = "default_fail_fast")]
35 pub fail_fast: bool,
36 #[serde(default)]
39 pub fixtures_dir: Option<PathBuf>,
40}
41
42const fn default_fail_fast() -> bool {
43 true
44}
45
46impl Config {
47 pub fn from_context(ctx: &mdbook_preprocessor::PreprocessorContext) -> Result<Self> {
53 let config: Option<Config> = ctx.config.get("preprocessor.validator")?;
55 let config = config.ok_or_else(|| ValidatorError::Config {
56 message: "No [preprocessor.validator] section in book.toml".into(),
57 })?;
58
59 debug!(
60 validators = config.validators.len(),
61 fail_fast = config.fail_fast,
62 fixtures_dir = ?config.fixtures_dir,
63 "Loaded config"
64 );
65
66 for name in config.validators.keys() {
67 debug!(validator = %name, "Registered validator");
68 }
69
70 Ok(config)
71 }
72
73 pub fn get_validator(&self, name: &str) -> Result<&ValidatorConfig> {
79 self.validators.get(name).ok_or_else(|| {
80 ValidatorError::UnknownValidator {
81 name: name.to_owned(),
82 }
83 .into()
84 })
85 }
86}
87
88impl ValidatorConfig {
89 pub fn validate(&self, name: &str) -> Result<()> {
95 if self.container.is_empty() {
96 return Err(ValidatorError::InvalidConfig {
97 name: name.to_owned(),
98 reason: "container cannot be empty".into(),
99 }
100 .into());
101 }
102 if self.script.as_os_str().is_empty() {
103 return Err(ValidatorError::InvalidConfig {
104 name: name.to_owned(),
105 reason: "script path cannot be empty".into(),
106 }
107 .into());
108 }
109 Ok(())
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::error::ValidatorError;
117
118 #[test]
121 fn validator_config_valid() {
122 let config = ValidatorConfig {
123 container: "ubuntu:22.04".to_owned(),
124 script: PathBuf::from("validators/validate.sh"),
125 exec_command: None,
126 };
127 assert!(config.validate("test").is_ok());
128 }
129
130 #[test]
131 fn validator_config_empty_container() {
132 let config = ValidatorConfig {
133 container: String::new(),
134 script: PathBuf::from("validators/validate.sh"),
135 exec_command: None,
136 };
137 let err = config
138 .validate("test")
139 .unwrap_err()
140 .downcast::<ValidatorError>()
141 .expect("should be ValidatorError");
142 assert!(matches!(
143 err,
144 ValidatorError::InvalidConfig { reason, .. } if reason.contains("container cannot be empty")
145 ));
146 }
147
148 #[test]
149 fn validator_config_empty_script() {
150 let config = ValidatorConfig {
151 container: "ubuntu:22.04".to_owned(),
152 script: PathBuf::new(),
153 exec_command: None,
154 };
155 let err = config
156 .validate("test")
157 .unwrap_err()
158 .downcast::<ValidatorError>()
159 .expect("should be ValidatorError");
160 assert!(matches!(
161 err,
162 ValidatorError::InvalidConfig { reason, .. } if reason.contains("script path cannot be empty")
163 ));
164 }
165
166 #[test]
167 fn validator_config_with_exec_command() {
168 let config = ValidatorConfig {
169 container: "ubuntu:22.04".to_owned(),
170 script: PathBuf::from("validators/validate.sh"),
171 exec_command: Some("sqlite3 -json /tmp/test.db".to_owned()),
172 };
173 assert!(config.validate("test").is_ok());
174 assert_eq!(
175 config.exec_command,
176 Some("sqlite3 -json /tmp/test.db".to_owned())
177 );
178 }
179
180 #[test]
183 fn config_get_validator_exists() {
184 let mut validators = HashMap::new();
185 validators.insert(
186 "sqlite".to_owned(),
187 ValidatorConfig {
188 container: "keinos/sqlite3:3.47.2".to_owned(),
189 script: PathBuf::from("validators/validate-sqlite.sh"),
190 exec_command: None,
191 },
192 );
193 let config = Config {
194 validators,
195 fail_fast: true,
196 fixtures_dir: None,
197 };
198
199 let result = config.get_validator("sqlite");
200 assert!(result.is_ok());
201 assert_eq!(result.unwrap().container, "keinos/sqlite3:3.47.2");
202 }
203
204 #[test]
205 fn config_get_validator_not_found() {
206 let config = Config::default();
207 let result = config.get_validator("nonexistent");
208 assert!(result.is_err());
209 let err = result
210 .unwrap_err()
211 .downcast::<ValidatorError>()
212 .expect("should be ValidatorError");
213 assert!(matches!(
214 err,
215 ValidatorError::UnknownValidator { name } if name == "nonexistent"
216 ));
217 }
218
219 #[test]
220 fn config_default_fail_fast_true() {
221 assert!(default_fail_fast());
223 }
224
225 #[test]
228 fn config_parse_from_toml() {
229 let toml_str = r#"
230 fail_fast = false
231 [validators.sqlite]
232 container = "keinos/sqlite3:3.47.2"
233 script = "validators/validate-sqlite.sh"
234 "#;
235 let config: Config = toml::from_str(toml_str).unwrap();
236 assert!(!config.fail_fast);
237 assert!(config.validators.contains_key("sqlite"));
238 }
239
240 #[test]
241 fn config_parse_multiple_validators() {
242 let toml_str = r#"
243 [validators.sqlite]
244 container = "keinos/sqlite3:3.47.2"
245 script = "validators/validate-sqlite.sh"
246
247 [validators.osquery]
248 container = "osquery/osquery:5.17.0-ubuntu22.04"
249 script = "validators/validate-osquery.sh"
250 "#;
251 let config: Config = toml::from_str(toml_str).unwrap();
252 assert_eq!(config.validators.len(), 2);
253 assert!(config.validators.contains_key("sqlite"));
254 assert!(config.validators.contains_key("osquery"));
255 }
256
257 #[test]
258 fn config_parse_with_exec_command() {
259 let toml_str = r#"
260 [validators.custom]
261 container = "ubuntu:22.04"
262 script = "validators/validate-custom.sh"
263 exec_command = "python3 -c"
264 "#;
265 let config: Config = toml::from_str(toml_str).unwrap();
266 let custom = config.validators.get("custom").unwrap();
267 assert_eq!(custom.exec_command, Some("python3 -c".to_owned()));
268 }
269
270 #[test]
271 fn config_parse_with_fixtures_dir() {
272 let toml_str = r#"
273 fixtures_dir = "test-fixtures"
274 [validators.sqlite]
275 container = "keinos/sqlite3:3.47.2"
276 script = "validators/validate-sqlite.sh"
277 "#;
278 let config: Config = toml::from_str(toml_str).unwrap();
279 assert_eq!(config.fixtures_dir, Some(PathBuf::from("test-fixtures")));
280 }
281
282 #[test]
283 fn config_parse_empty_validators() {
284 let toml_str = r"
285 fail_fast = true
286 ";
287 let config: Config = toml::from_str(toml_str).unwrap();
288 assert!(config.validators.is_empty());
289 assert!(config.fail_fast);
290 }
291}