proc_heim/process/model/
builders.rs

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/// Builder for [`Cmd`].
9#[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    /// Create empty builder. Same as [`CmdBuilder::default()`].
18    pub fn builder() -> Self {
19        CmdBuilder::default()
20    }
21
22    /// Set command name.
23    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    /// Set command arguments.
32    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    /// Set command options.
42    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    /// Create a [`Cmd`] from this builder.
51    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/// Enum returned from [`CmdBuilder::build`] method.
72#[derive(thiserror::Error, Debug)]
73pub enum CmdBuilderError {
74    /// The command name was not initialized.
75    #[error("The command name was not initialized")]
76    UninitializedCmdName,
77}
78
79// CmdOptions ---------------------------------
80
81/// Builder for [`CmdOptions`].
82#[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    /// Create empty builder. Same as [`CmdOptionsBuilder::default()`].
96    pub fn builder() -> Self {
97        CmdOptionsBuilder::default()
98    }
99
100    /// Set process current directory.
101    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    /// Clear or retain environment variables inherited from a parent process.
110    pub fn clear_inherited_envs(&mut self, clear_envs: bool) -> &mut Self {
111        self.clear_envs = clear_envs;
112        self
113    }
114
115    /// Set environment variables.
116    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    /// Remove specific environment variables inherited from a parent process.
131    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    /// Set message output buffer capacity.
141    pub fn output_buffer_capacity(&mut self, capacity: BufferCapacity) -> &mut Self {
142        self.output_buffer_capacity = capacity;
143        self
144    }
145
146    /// Set message input type.
147    pub fn message_input(&mut self, messaging_type: MessagingType) -> &mut Self {
148        self.message_input = Some(messaging_type);
149        self
150    }
151
152    /// Set message output type.
153    pub fn message_output(&mut self, messaging_type: MessagingType) -> &mut Self {
154        self.message_output = Some(messaging_type);
155        self
156    }
157
158    /// Set logging type.
159    pub fn logging_type(&mut self, logging_type: LoggingType) -> &mut Self {
160        self.logging_type = Some(logging_type);
161        self
162    }
163
164    /// Create a [`CmdOptions`] from this builder.
165    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// Script -------------------------------------
185
186/// Builder for [`Script`].
187#[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    /// Create empty builder. Same as [`ScriptBuilder::default()`].
197    pub fn builder() -> Self {
198        ScriptBuilder::default()
199    }
200
201    /// Set scripting language.
202    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    /// Set script content.
211    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    /// Set script arguments.
220    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    /// Set script options.
230    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    /// Create a [`Script`] from this builder.
239    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/// Enum returned from [`ScriptBuilder::build`] method.
266#[derive(thiserror::Error, Debug)]
267pub enum ScriptBuilderError {
268    /// The scripting language was not initialized.
269    #[error("The scripting language was not initialized")]
270    UninitializedScriptingLanguage,
271    /// The script content was not initialized.
272    #[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}