proc_heim/process/model/
serde.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use serde::{de::Error, Deserialize, Deserializer};
4
5use crate::process::{validate_stdout_config, LoggingType, MessagingType};
6
7use super::{BufferCapacity, CmdOptions};
8
9impl<'de> Deserialize<'de> for BufferCapacity {
10    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
11    where
12        D: Deserializer<'de>,
13    {
14        #[derive(Deserialize)]
15        struct BufferCapacityHelper(usize);
16
17        let helper = BufferCapacityHelper::deserialize(deserializer)?;
18        BufferCapacity::try_from(helper.0).map_err(Error::custom)
19    }
20}
21
22impl serde::Serialize for BufferCapacity {
23    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
24    where
25        S: serde::Serializer,
26    {
27        serializer.serialize_i64(self.inner as i64)
28    }
29}
30
31impl<'de> Deserialize<'de> for CmdOptions {
32    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
33    where
34        D: Deserializer<'de>,
35    {
36        #[derive(Deserialize, Default)]
37        #[serde(default)]
38        struct CmdOptionsHelper {
39            current_dir: Option<PathBuf>,
40            clear_envs: bool,
41            envs: HashMap<String, String>,
42            envs_to_remove: Vec<String>,
43            output_buffer_capacity: BufferCapacity,
44            message_input: Option<MessagingType>,
45            message_output: Option<MessagingType>,
46            logging_type: Option<LoggingType>,
47        }
48
49        let mut helper = CmdOptionsHelper::deserialize(deserializer)?;
50
51        validate_stdout_config(helper.message_output.as_ref(), helper.logging_type.as_ref())
52            .map_err(Error::custom)?;
53
54        if !helper.envs.is_empty() && !helper.envs_to_remove.is_empty() {
55            for env in &helper.envs_to_remove {
56                helper.envs.remove(env);
57            }
58        }
59
60        Ok(CmdOptions {
61            current_dir: helper.current_dir,
62            clear_envs: helper.clear_envs,
63            envs: helper.envs,
64            envs_to_remove: helper.envs_to_remove,
65            output_buffer_capacity: helper.output_buffer_capacity,
66            message_input: helper.message_input,
67            message_output: helper.message_output,
68            logging_type: helper.logging_type,
69        })
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use std::{collections::HashMap, path::PathBuf};
76
77    use crate::process::{
78        BufferCapacity, Cmd, CmdOptions, LoggingType, MessagingType, Script, ScriptRunConfig,
79        ScriptingLanguage,
80    };
81
82    // BufferCapacity ----------------------------------------------
83
84    #[test]
85    fn should_serialize_buffer_capacity() {
86        let capacity = BufferCapacity::try_from(123).unwrap();
87
88        let serialized = serde_json::to_string(&capacity).unwrap();
89        assert_eq!("123", serialized);
90    }
91
92    #[test]
93    fn should_deserialize_buffer_capacity() {
94        let expected = BufferCapacity::try_from(9876).unwrap();
95
96        let deserialized = serde_json::from_str("9876").unwrap();
97        assert_eq!(expected, deserialized);
98    }
99
100    #[test]
101    fn should_return_err_when_deserializing_invalid_buffer_capacity() {
102        let value = usize::MAX / 2 + 1;
103        let result = serde_json::from_str::<'_, BufferCapacity>(&value.to_string());
104        assert!(result.is_err());
105        let expected_msg =
106            "Buffer capacity must be greater than 0 and less or equal usize::MAX / 2";
107        assert!(result.err().unwrap().to_string().contains(expected_msg));
108
109        let value = 0;
110        let result = serde_json::from_str::<'_, BufferCapacity>(&value.to_string());
111        assert!(result.is_err());
112        assert!(result.err().unwrap().to_string().contains(expected_msg));
113
114        let value = -1;
115        let result = serde_json::from_str::<'_, BufferCapacity>(&value.to_string());
116        assert!(result.is_err());
117
118        let value = 3.4;
119        let result = serde_json::from_str::<'_, BufferCapacity>(&value.to_string());
120        assert!(result.is_err());
121    }
122
123    // CmdOptions ----------------------------------------------
124
125    #[test]
126    fn should_serialize_and_deserialize_options() {
127        let options = create_options();
128
129        let serialized = serde_json::to_string(&options).unwrap();
130        assert_eq!(options_json(), serialized);
131
132        let deserialized = serde_json::from_str(&serialized).unwrap();
133        assert_eq!(options, deserialized);
134    }
135
136    #[test]
137    fn should_deserialize_options_with_defaults() {
138        // without fields which is not Option<T>
139        let deserialized: CmdOptions =
140            serde_json::from_str(options_without_option_type_fields()).unwrap();
141        assert!(!deserialized.clear_envs);
142        assert_eq!(
143            BufferCapacity::default(),
144            deserialized.output_buffer_capacity
145        );
146
147        // with some fields
148        let serialized = r#"{"current_dir": "/bin", "clear_envs": true}"#;
149        let deserialized: CmdOptions = serde_json::from_str(serialized).unwrap();
150        let expected = CmdOptions {
151            current_dir: PathBuf::from("/bin").into(),
152            clear_envs: true,
153            ..Default::default()
154        };
155        assert_eq!(expected, deserialized);
156
157        // with none field
158        let deserialized: CmdOptions = serde_json::from_str("{}").unwrap();
159        assert_eq!(CmdOptions::default(), deserialized);
160    }
161
162    #[test]
163    fn should_return_err_when_deserializing_invalid_options() {
164        // Invalid buffer capacity = 0
165        let serialized = options_json_with_invalid_buffer_capacity();
166        let result = serde_json::from_str::<'_, CmdOptions>(serialized);
167        let expected_msg =
168            "Buffer capacity must be greater than 0 and less or equal usize::MAX / 2";
169        result_contains_err_msg(&result, expected_msg);
170
171        // Message output and logging type conflict
172        let serialized = options_json_with_invalid_message_output();
173        let result = serde_json::from_str::<'_, CmdOptions>(serialized);
174        let expected_msg = format!(
175            "Cannot use {:?} together with {:?} for stdout configuration",
176            MessagingType::StandardIo,
177            LoggingType::StdoutOnly
178        );
179        result_contains_err_msg(&result, &expected_msg);
180    }
181
182    #[test]
183    fn should_properly_set_envs_during_deserialization() {
184        let serialized = options_json_with_wrong_defined_envs();
185
186        let options = serde_json::from_str::<'_, CmdOptions>(serialized).unwrap();
187
188        let envs = options.envs;
189        assert_eq!(1, envs.len()); // PATH should be removed, because it appears in envs_to_remove
190        let env = envs.get("ENV1");
191        assert!(env.is_some());
192        assert_eq!("value1", env.unwrap());
193    }
194
195    fn create_options() -> CmdOptions {
196        let mut envs = HashMap::new();
197        envs.insert("ENV1", "value1");
198        envs.insert("PATH", "/bin");
199
200        let mut options = CmdOptions::default();
201        options.set_current_dir("/some/path".into());
202        options.clear_inherited_envs(true);
203        options.set_envs(envs);
204        options.remove_env("PATH");
205        options.set_message_output_buffer_capacity(BufferCapacity::try_from(24).unwrap());
206        options.set_message_input(MessagingType::StandardIo);
207        options
208            .set_message_output(MessagingType::NamedPipe)
209            .unwrap();
210        options.set_logging_type(LoggingType::StderrOnly).unwrap();
211        options
212    }
213
214    fn options_json() -> &'static str {
215        r#"{"current_dir":"/some/path","clear_envs":true,"envs":{"ENV1":"value1"},"envs_to_remove":["PATH"],"output_buffer_capacity":24,"message_input":"StandardIo","message_output":"NamedPipe","logging_type":"StderrOnly"}"#
216    }
217
218    /// No clear_envs and output_buffer_capacity fields are set
219    fn options_without_option_type_fields() -> &'static str {
220        r#"{"current_dir":"/some/path","envs":{"ENV1":"value1"},"envs_to_remove":["PATH"],"message_input":"StandardIo","message_output":"NamedPipe","logging_type":"StderrOnly"}"#
221    }
222
223    fn options_json_with_invalid_buffer_capacity() -> &'static str {
224        r#"{"current_dir":"/some/path","clear_envs":true,"envs":{"ENV1":"value1"},"envs_to_remove":["PATH"],"output_buffer_capacity":0,"message_input":"StandardIo","message_output":"NamedPipe","logging_type":"StderrOnly"}"#
225    }
226
227    fn options_json_with_invalid_message_output() -> &'static str {
228        r#"{"current_dir":"/some/path","clear_envs":true,"envs":{"ENV1":"value1"},"envs_to_remove":["PATH"],"output_buffer_capacity":7,"message_input":"StandardIo","message_output":"StandardIo","logging_type":"StdoutOnly"}"#
229    }
230
231    fn options_json_with_wrong_defined_envs() -> &'static str {
232        r#"{"current_dir":"/some/path","clear_envs":true,"envs":{"ENV1":"value1","PATH":"/bin"},"envs_to_remove":["PATH"],"output_buffer_capacity":24,"message_input":"StandardIo","message_output":"NamedPipe","logging_type":"StderrOnly"}"#
233    }
234
235    fn result_contains_err_msg<T, E: ToString>(result: &Result<T, E>, expected_msg: &str) {
236        assert!(result.is_err());
237        assert!(result
238            .as_ref()
239            .err()
240            .unwrap()
241            .to_string()
242            .contains(expected_msg));
243    }
244
245    // Cmd -------------------------------------------------
246
247    #[test]
248    fn should_serialize_and_deserialize_cmd() {
249        let options = create_options();
250        let cmd = Cmd::with_args_and_options("ls", ["-l", "/bin"], options);
251
252        let serialized = serde_json::to_string(&cmd).unwrap();
253        let deserialized = serde_json::from_str(&serialized).unwrap();
254
255        assert_eq!(cmd, deserialized);
256    }
257
258    #[test]
259    fn should_serialize_and_deserialize_cmd_with_defaults() {
260        // with no options
261        let expected = Cmd::with_args("ls", ["-l", "/bin"]);
262        let serialized = r#"{"cmd":"ls","args":["-l","/bin"]}"#;
263        let deserialized = serde_json::from_str(serialized).unwrap();
264        assert_eq!(expected, deserialized);
265
266        // with no options and args
267        let expected = Cmd::new("ls");
268        let serialized = r#"{"cmd":"ls"}"#;
269        let deserialized = serde_json::from_str(serialized).unwrap();
270        assert_eq!(expected, deserialized);
271    }
272
273    // Script ----------------------------------------------
274
275    #[test]
276    fn should_serialize_and_deserialize_script() {
277        let options = create_options();
278        let run_config = ScriptRunConfig::new("bash", ["-C"], "sh");
279        let lang = ScriptingLanguage::Other(run_config);
280        let content = r#"
281            echo Hello
282            echo World
283        "#;
284        let script = Script::with_args_and_options(lang, content, ["-l", "/bin"], options);
285
286        let serialized = serde_json::to_string(&script).unwrap();
287        let deserialized = serde_json::from_str(&serialized).unwrap();
288
289        assert_eq!(script, deserialized);
290    }
291
292    #[test]
293    fn should_deserialize_script_with_defaults() {
294        let lang = ScriptingLanguage::Bash;
295        let content = "echo";
296
297        // no options and no language
298        let script = Script::with_args(lang.clone(), content, ["Hello", "World"]);
299        let serialized = r#"{"content":"echo","args":["Hello","World"]}"#;
300        let deserialized = serde_json::from_str(serialized).unwrap();
301        assert_eq!(script, deserialized);
302
303        // no options, language and args
304        let script = Script::new(lang, content);
305        let serialized = r#"{"content":"echo"}"#;
306        let deserialized = serde_json::from_str(serialized).unwrap();
307        assert_eq!(script, deserialized);
308    }
309}