1use std::{collections::HashMap, path::PathBuf};
2
3use super::{
4 validate_stdout_config, BufferCapacity, Cmd, CmdOptions, CmdOptionsError, LoggingType,
5 MessagingType, Script, ScriptingLanguage,
6};
7
8#[derive(Clone, Default, Debug)]
10pub struct CmdBuilder {
11 cmd: Option<String>,
12 args: Option<Vec<String>>,
13 options: Option<CmdOptions>,
14}
15
16impl CmdBuilder {
17 pub fn builder() -> Self {
19 CmdBuilder::default()
20 }
21
22 pub fn cmd<T>(&mut self, cmd: T) -> &mut Self
24 where
25 T: Into<String>,
26 {
27 self.cmd = Some(cmd.into());
28 self
29 }
30
31 pub fn args<I, T>(&mut self, args: I) -> &mut Self
33 where
34 I: IntoIterator<Item = T>,
35 T: Into<String>,
36 {
37 self.args = Some(args.into_iter().map(Into::into).collect());
38 self
39 }
40
41 pub fn options<T>(&mut self, options: T) -> &mut Self
43 where
44 T: Into<CmdOptions>,
45 {
46 self.options = Some(options.into());
47 self
48 }
49
50 pub fn build(&mut self) -> Result<Cmd, CmdBuilderError> {
52 Ok(Cmd {
53 cmd: match self.cmd.take() {
54 Some(value) => value,
55 None => {
56 return Err(CmdBuilderError::UninitializedCmdName);
57 }
58 },
59 args: match self.args.take() {
60 Some(value) => value,
61 None => Default::default(),
62 },
63 options: match self.options.take() {
64 Some(value) => value,
65 None => Default::default(),
66 },
67 })
68 }
69}
70
71#[derive(thiserror::Error, Debug)]
73pub enum CmdBuilderError {
74 #[error("The command name was not initialized")]
76 UninitializedCmdName,
77}
78
79#[derive(Debug, Clone, Default)]
83pub struct CmdOptionsBuilder {
84 current_dir: Option<PathBuf>,
85 clear_envs: bool,
86 envs: Option<HashMap<String, String>>,
87 envs_to_remove: Option<Vec<String>>,
88 output_buffer_capacity: BufferCapacity,
89 message_input: Option<MessagingType>,
90 message_output: Option<MessagingType>,
91 logging_type: Option<LoggingType>,
92}
93
94impl CmdOptionsBuilder {
95 pub fn builder() -> Self {
97 CmdOptionsBuilder::default()
98 }
99
100 pub fn current_dir<T>(&mut self, current_dir: T) -> &mut Self
102 where
103 T: Into<PathBuf>,
104 {
105 self.current_dir = Some(current_dir.into());
106 self
107 }
108
109 pub fn clear_inherited_envs(&mut self, clear_envs: bool) -> &mut Self {
111 self.clear_envs = clear_envs;
112 self
113 }
114
115 pub fn envs<K, V, I>(&mut self, envs: I) -> &mut Self
117 where
118 K: Into<String>,
119 V: Into<String>,
120 I: IntoIterator<Item = (K, V)>,
121 {
122 self.envs = Some(
123 envs.into_iter()
124 .map(|(k, v)| (k.into(), v.into()))
125 .collect(),
126 );
127 self
128 }
129
130 pub fn inherited_envs_to_remove<I, T>(&mut self, envs: I) -> &mut Self
132 where
133 I: IntoIterator<Item = T>,
134 T: Into<String>,
135 {
136 self.envs_to_remove = Some(envs.into_iter().map(Into::into).collect());
137 self
138 }
139
140 pub fn output_buffer_capacity(&mut self, capacity: BufferCapacity) -> &mut Self {
142 self.output_buffer_capacity = capacity;
143 self
144 }
145
146 pub fn message_input(&mut self, messaging_type: MessagingType) -> &mut Self {
148 self.message_input = Some(messaging_type);
149 self
150 }
151
152 pub fn message_output(&mut self, messaging_type: MessagingType) -> &mut Self {
154 self.message_output = Some(messaging_type);
155 self
156 }
157
158 pub fn logging_type(&mut self, logging_type: LoggingType) -> &mut Self {
160 self.logging_type = Some(logging_type);
161 self
162 }
163
164 pub fn build(&mut self) -> Result<CmdOptions, CmdOptionsError> {
166 self.validate()?;
167 Ok(CmdOptions {
168 current_dir: self.current_dir.take(),
169 clear_envs: self.clear_envs,
170 envs: self.envs.take().unwrap_or_default(),
171 envs_to_remove: self.envs_to_remove.take().unwrap_or_default(),
172 output_buffer_capacity: self.output_buffer_capacity,
173 message_input: self.message_input.take(),
174 message_output: self.message_output.take(),
175 logging_type: self.logging_type.take(),
176 })
177 }
178
179 fn validate(&self) -> Result<(), CmdOptionsError> {
180 validate_stdout_config(self.message_output.as_ref(), self.logging_type.as_ref())
181 }
182}
183
184#[derive(Clone, Default, Debug)]
188pub struct ScriptBuilder {
189 language: Option<ScriptingLanguage>,
190 content: Option<String>,
191 args: Option<Vec<String>>,
192 options: Option<CmdOptions>,
193}
194
195impl ScriptBuilder {
196 pub fn builder() -> Self {
198 ScriptBuilder::default()
199 }
200
201 pub fn language<T>(&mut self, language: T) -> &mut Self
203 where
204 T: Into<ScriptingLanguage>,
205 {
206 self.language = Some(language.into());
207 self
208 }
209
210 pub fn content<S>(&mut self, content: S) -> &mut Self
212 where
213 S: Into<String>,
214 {
215 self.content = Some(content.into());
216 self
217 }
218
219 pub fn args<I, T>(&mut self, args: I) -> &mut Self
221 where
222 I: IntoIterator<Item = T>,
223 T: Into<String>,
224 {
225 self.args = Some(args.into_iter().map(Into::into).collect());
226 self
227 }
228
229 pub fn options<T>(&mut self, options: T) -> &mut Self
231 where
232 T: Into<CmdOptions>,
233 {
234 self.options = Some(options.into());
235 self
236 }
237
238 pub fn build(&mut self) -> Result<Script, ScriptBuilderError> {
240 Ok(Script {
241 lang: match self.language.take() {
242 Some(value) => value,
243 None => {
244 return Err(ScriptBuilderError::UninitializedScriptingLanguage);
245 }
246 },
247 content: match self.content.take() {
248 Some(value) => value,
249 None => {
250 return Err(ScriptBuilderError::UninitializedScriptContent);
251 }
252 },
253 args: match self.args.take() {
254 Some(value) => value,
255 None => Default::default(),
256 },
257 options: match self.options.take() {
258 Some(value) => value,
259 None => Default::default(),
260 },
261 })
262 }
263}
264
265#[derive(thiserror::Error, Debug)]
267pub enum ScriptBuilderError {
268 #[error("The scripting language was not initialized")]
270 UninitializedScriptingLanguage,
271 #[error("The script content was not initialized")]
273 UninitializedScriptContent,
274}
275
276#[cfg(test)]
277mod tests {
278 use std::{collections::HashMap, path::PathBuf, str::FromStr};
279
280 use crate::process::{
281 model::builders::{ScriptBuilder, ScriptBuilderError},
282 BufferCapacity, Cmd, CmdOptions, LoggingType, MessagingType, Script, ScriptingLanguage,
283 };
284
285 use super::{CmdBuilder, CmdOptionsBuilder};
286
287 #[test]
288 fn should_build_cmd() {
289 let options = CmdOptions::with_standard_io_messaging();
290 let expected = Cmd::with_args_and_options("ls", ["-l"], options.clone());
291 let actual = CmdBuilder::builder()
292 .cmd("ls")
293 .args(["-l"])
294 .options(options)
295 .build()
296 .unwrap();
297 assert_eq!(expected, actual);
298 }
299
300 #[test]
301 fn should_build_cmd_without_optional_fields() {
302 let expected = Cmd::new("echo");
303 let actual = CmdBuilder::builder().cmd("echo").build().unwrap();
304 assert_eq!(expected, actual);
305 }
306
307 #[test]
308 fn should_return_err_when_cmd_name_was_not_provided() {
309 let cmd = CmdBuilder::builder().build();
310 assert!(cmd.is_err());
311 }
312
313 #[test]
314 fn should_build_script() {
315 let options = CmdOptions::with_logging(LoggingType::StdoutAndStderrMerged);
316 let expected =
317 Script::with_args_and_options(ScriptingLanguage::Bash, "ls", ["-l"], options.clone());
318 let actual = ScriptBuilder::builder()
319 .language(ScriptingLanguage::Bash)
320 .content("ls")
321 .args(["-l"])
322 .options(options)
323 .build()
324 .unwrap();
325 assert_eq!(expected, actual);
326 }
327
328 #[test]
329 fn should_build_script_without_optional_fields() {
330 let expected = Script::new(ScriptingLanguage::Bash, "ls");
331 let actual = ScriptBuilder::builder()
332 .language(ScriptingLanguage::Bash)
333 .content("ls")
334 .build()
335 .unwrap();
336 assert_eq!(expected, actual);
337 }
338
339 #[test]
340 fn should_return_err_when_script_lang_was_not_provided() {
341 let result = ScriptBuilder::builder().content("ls").build();
342 assert!(matches!(
343 result,
344 Err(ScriptBuilderError::UninitializedScriptingLanguage)
345 ));
346 }
347
348 #[test]
349 fn should_return_err_when_script_content_was_not_provided() {
350 let result = ScriptBuilder::builder()
351 .language(ScriptingLanguage::Perl)
352 .build();
353 assert!(matches!(
354 result,
355 Err(ScriptBuilderError::UninitializedScriptContent)
356 ));
357 }
358
359 #[test]
360 fn should_build_options() {
361 let current_dir = PathBuf::from_str("/some/path").unwrap();
362 let clear_envs = true;
363
364 let mut envs = HashMap::new();
365 envs.insert("ENV1", "value1");
366
367 let env_to_remove = "PATH";
368
369 let capacity = BufferCapacity::try_from(24).unwrap();
370 let message_input = MessagingType::StandardIo;
371 let message_output = MessagingType::NamedPipe;
372 let logging_type = LoggingType::StderrOnly;
373
374 let mut expected = CmdOptions::default();
375 expected.set_current_dir(current_dir.clone());
376 expected.clear_inherited_envs(clear_envs);
377 expected.set_envs(envs.clone());
378 expected.remove_env(env_to_remove);
379 expected.set_message_output_buffer_capacity(capacity);
380 expected.set_message_input(message_input.clone());
381 expected.set_message_output(message_output.clone()).unwrap();
382 expected.set_logging_type(logging_type.clone()).unwrap();
383
384 let actual = CmdOptionsBuilder::builder()
385 .current_dir(current_dir)
386 .clear_inherited_envs(clear_envs)
387 .envs(envs)
388 .inherited_envs_to_remove([env_to_remove])
389 .output_buffer_capacity(capacity)
390 .message_input(message_input)
391 .message_output(message_output)
392 .logging_type(logging_type)
393 .build()
394 .unwrap();
395 assert_eq!(expected, actual);
396 }
397
398 #[test]
399 fn should_build_options_with_default_values() {
400 let options = CmdOptionsBuilder::builder().build().unwrap();
401 assert_eq!(BufferCapacity::default(), options.output_buffer_capacity);
402 assert!(!options.clear_envs);
403 }
404
405 #[test]
406 fn should_return_err_when_options_are_invalid() {
407 let mut builder = CmdOptionsBuilder::builder();
408 builder.message_output(MessagingType::StandardIo);
409
410 assert!(builder
411 .clone()
412 .logging_type(LoggingType::StderrOnly)
413 .build()
414 .is_ok());
415
416 assert!(builder
417 .clone()
418 .logging_type(LoggingType::StdoutAndStderr)
419 .build()
420 .is_err());
421
422 assert!(builder
423 .clone()
424 .logging_type(LoggingType::StdoutAndStderrMerged)
425 .build()
426 .is_err());
427
428 assert!(builder
429 .clone()
430 .logging_type(LoggingType::StdoutOnly)
431 .build()
432 .is_err());
433 }
434}