cdk_ansible_core/core/
mod.rs

1use dyn_clone::{DynClone, clone_trait_object};
2use erased_serde::serialize_trait_object;
3use indexmap::IndexMap;
4use serde::Serialize;
5#[derive(Default, Debug, Clone, PartialEq)]
6pub struct Inventory {
7    pub name: String,
8    pub root: InventoryRoot,
9}
10
11#[derive(Default, Debug, Clone, PartialEq, Serialize)]
12pub struct InventoryRoot {
13    pub all: Child,
14}
15
16#[derive(Default, Debug, Clone, PartialEq, Serialize)]
17pub struct Child {
18    #[serde(skip_serializing_if = "OptU::is_unset")]
19    pub hosts: OptU<IndexMap<String, Option<IndexMap<String, serde_json::Value>>>>,
20    #[serde(skip_serializing_if = "OptU::is_unset")]
21    pub children: OptU<IndexMap<String, Child>>,
22    #[serde(skip_serializing_if = "OptU::is_unset")]
23    pub vars: OptU<IndexMap<String, serde_json::Value>>,
24}
25
26#[derive(Clone, Debug)]
27pub struct Playbook {
28    /// Name of the playbook
29    /// The output file name will be `<name>.yaml`
30    pub name: String,
31    pub plays: Vec<Play>,
32}
33
34/// Option for an unset value
35///
36/// This differs from `Option<T>` in that it has a [`OptU::Unset`], not [`None`]
37/// In serializing, [`OptU::Unset`] is skipped, while [`None`] is serialized as `null`.
38///
39/// ```rust
40/// use cdk_ansible_core::core::OptU;
41///
42/// let x: OptU<i32> = OptU::Unset;
43/// ```
44#[derive(Clone, Debug, PartialEq, Default, Serialize)]
45#[serde(untagged)]
46pub enum OptU<T: Serialize> {
47    Some(T),
48    #[default]
49    Unset,
50}
51
52/// For skip_serializing_if of serde
53impl<T: Serialize> OptU<T> {
54    pub fn is_unset(&self) -> bool {
55        matches!(self, OptU::Unset)
56    }
57}
58
59/// Play
60/// Optional Values are defined in [`PlayOptions`]
61#[derive(Serialize, Clone, Debug)]
62pub struct Play {
63    /// Identifier. Can be used for documentation, or in tasks/handlers.
64    pub name: String,
65    /// A list of groups, hosts or host pattern that translates into a list of hosts that are the play's target.
66    pub hosts: Vec<String>,
67    #[serde(flatten)]
68    pub options: PlayOptions,
69    /// Main list of tasks to execute in the play, they run after roles and before post_tasks.
70    pub tasks: Vec<Task>,
71}
72
73/// [playbook keywords (play)](https://docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html#play)
74#[derive(Serialize, Default, Clone, Debug)]
75pub struct PlayOptions {
76    /// Force any un-handled task errors on any host to propagate to all hosts and end the play.
77    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
78    pub any_errors_fatal: OptU<bool>,
79    /// Boolean that controls if privilege escalation is used or not on Task execution.
80    /// Implemented by the become plugin. See Become plugins.
81    #[serde(
82        rename = "become",
83        default = "OptU::default",
84        skip_serializing_if = "OptU::is_unset"
85    )]
86    pub become_: OptU<bool>,
87    /// Path to the executable used to elevate privileges. Implemented by the become plugin. See Become plugins.
88    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
89    pub become_exe: OptU<String>,
90    /// A string of flag(s) to pass to the privilege escalation program when become is True.
91    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
92    pub become_flags: OptU<String>,
93    /// Which method of privilege escalation to use (such as sudo or su).
94    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
95    pub become_method: OptU<String>,
96    /// User that you 'become' after using privilege escalation. The remote/login user must have permissions to become this user.
97    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
98    pub become_user: OptU<String>,
99    /// A boolean that controls if a task is executed in 'check' mode. See Validating tasks: check mode and diff mode.
100    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
101    pub check_mode: OptU<bool>,
102    /// List of collection namespaces to search for modules, plugins, and roles. See Using collections in a playbook
103    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
104    pub collections: OptU<Vec<String>>,
105    /// Allows you to change the connection plugin used for tasks to execute on the target. See Using connection plugins.
106    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
107    pub connection: OptU<String>,
108    /// Enable debugging tasks based on the state of the task result. See Debugging tasks.
109    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
110    pub debugger: OptU<bool>,
111    /// Toggle to make tasks return 'diff' information or not.
112    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
113    pub diff: OptU<bool>,
114    /// A dictionary that gets converted into environment vars to be provided for the task upon execution.
115    /// This can ONLY be used with modules. This is not supported for any other type of plugins nor Ansible itself nor its configuration,
116    /// it just sets the variables for the code responsible for executing the task.
117    /// This is not a recommended way to pass in confidential data.
118    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
119    pub environment: OptU<IndexMap<String, String>>,
120    /// Set the fact path option for the fact gathering plugin controlled by gather_facts.
121    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
122    pub fact_path: OptU<String>,
123    /// Will force notified handler execution for hosts even if they failed during the play.
124    /// Will not trigger if the play itself fails.
125    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
126    pub force_handlers: OptU<bool>,
127    /// A boolean that controls if the play will automatically run the 'setup' task to gather facts for the hosts.
128    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
129    pub gather_facts: OptU<bool>,
130    /// Allows you to pass subset options to the fact gathering plugin controlled by gather_facts.
131    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
132    pub gather_subset: OptU<Vec<String>>,
133    /// Allows you to set the timeout for the fact gathering plugin controlled by gather_facts.
134    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
135    pub gather_timeout: OptU<i64>,
136    /// A section with tasks that are treated as handlers, these won't get executed normally,
137    /// only when notified after each section of tasks is complete.
138    /// A handler's listen field is not templatable.
139    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
140    pub handlers: OptU<Vec<Task>>,
141    /// Boolean that allows you to ignore task failures and continue with play. It does not affect connection errors.
142    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
143    pub ignore_errors: OptU<bool>,
144    /// Boolean that allows you to ignore task failures due to an unreachable host and continue with the play.
145    /// This does not affect other task errors (see ignore_errors) but is useful for groups of volatile/ephemeral hosts.
146    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
147    pub ignore_unreachable: OptU<bool>,
148    /// Can be used to abort the run after a given percentage of hosts in the current batch has failed.
149    /// This only works on linear or linear-derived strategies.
150    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
151    pub max_fail_percentage: OptU<i64>,
152    /// Specifies default parameter values for modules.
153    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
154    pub module_defaults: OptU<IndexMap<String, serde_json::Value>>,
155    /// Boolean that controls information disclosure.
156    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
157    pub no_log: OptU<bool>,
158    /// Controls the sorting of hosts as they are used for executing the play.
159    /// Possible values are inventory (default), sorted, reverse_sorted, reverse_inventory and shuffle.
160    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
161    pub order: OptU<String>,
162    /// Used to override the default port used in a connection.
163    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
164    pub port: OptU<i64>,
165    /// A list of tasks to execute after the tasks section.
166    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
167    pub post_tasks: OptU<Vec<Task>>,
168    /// A list of tasks to execute before roles.
169    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
170    pub pre_tasks: OptU<Vec<Task>>,
171    /// User used to log into the target via the connection plugin.
172    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
173    pub remote_user: OptU<String>,
174    /// List of roles to be imported into the play
175    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
176    pub roles: OptU<Vec<String>>,
177    /// Boolean that will bypass the host loop, forcing the task to attempt to execute on the first host available
178    /// and afterward apply any results and facts to all active hosts in the same batch.
179    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
180    pub run_once: OptU<bool>,
181    /// Explicitly define how Ansible batches the execution of the current play on the play's target. See Setting the batch size with serial.
182    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
183    pub serial: OptU<i64>,
184    /// Allows you to choose the strategy plugin to use for the play. See Strategy plugins.
185    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
186    pub strategy: OptU<String>,
187    /// Tags applied to the task or included tasks, this allows selecting subsets of tasks from the command line.
188    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
189    pub tags: OptU<Vec<String>>,
190    /// Limit the number of concurrent task runs on task, block and playbook level. This is independent of the forks and serial settings, but cannot be set higher than those limits. For example, if forks is set to 10 and the throttle is set to 15, at most 10 hosts will be operated on in parallel.
191    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
192    pub throttle: OptU<i64>,
193    /// Time limit for the task action to execute in, if exceeded, Ansible will interrupt the process. Timeout does not include templating or looping.
194    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
195    pub timeout: OptU<i64>,
196    /// Dictionary/map of variables
197    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
198    pub vars: OptU<IndexMap<String, serde_json::Value>>,
199    /// List of files that contain vars to include in the play.
200    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
201    pub vars_files: OptU<Vec<String>>,
202    /// List of variables to prompt for.
203    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
204    pub vars_prompt: OptU<Vec<String>>,
205}
206
207#[derive(Serialize, Clone, Debug)]
208pub struct Task {
209    /// Identifier. Can be used for documentation, or in tasks/handlers.
210    pub name: String,
211    #[serde(flatten)]
212    pub options: TaskOptions,
213    #[serde(flatten)]
214    pub command: Box<dyn TaskModule>,
215}
216
217/// Task module trait
218///
219/// If you want to add a new task module, you need to implement this trait
220/// https://crates.io/crates/erased-serde
221///
222/// ```rust
223/// use cdk_ansible_core::core::TaskModule;
224/// use serde::Serialize;
225///
226/// #[derive(Serialize, Clone, Debug)]
227/// struct SampleTaskModule {
228///     x1: String,
229/// }
230/// impl TaskModule for SampleTaskModule {}
231/// ```
232pub trait TaskModule: erased_serde::Serialize + DynClone + std::fmt::Debug {}
233
234serialize_trait_object!(TaskModule);
235clone_trait_object!(TaskModule);
236
237/// [playbook keyword (task)](https://docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html#task)
238#[derive(Serialize, Default, Clone, Debug, PartialEq)]
239pub struct TaskOptions {
240    /// The 'action' to execute for a task, it normally translates into a C(module) or action plugin.
241    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
242    pub action: OptU<String>,
243    /// Force any un-handled task errors on any host to propagate to all hosts and end the play.
244    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
245    pub any_errors_fatal: OptU<bool>,
246    /// A secondary way to add arguments into a task. Takes a dictionary in which keys map to options and values.
247    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
248    pub args: OptU<IndexMap<String, serde_json::Value>>,
249    /// Run a task asynchronously if the C(action) supports this; the value is the maximum runtime in seconds.
250    #[serde(
251        rename = "async",
252        default = "OptU::default",
253        skip_serializing_if = "OptU::is_unset"
254    )]
255    pub async_: OptU<i64>,
256    /// Boolean that controls if privilege escalation is used or not on Task execution.
257    /// Implemented by the become plugin. See Become plugins.
258    #[serde(
259        rename = "become",
260        default = "OptU::default",
261        skip_serializing_if = "OptU::is_unset"
262    )]
263    pub become_: OptU<bool>,
264    /// Path to the executable used to elevate privileges. Implemented by the become plugin. See Become plugins.
265    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
266    pub become_exe: OptU<String>,
267    /// A string of flag(s) to pass to the privilege escalation program when become is True.
268    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
269    pub become_flags: OptU<String>,
270    /// Which method of privilege escalation to use (such as sudo or su).
271    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
272    pub become_method: OptU<String>,
273    /// User that you 'become' after using privilege escalation. The remote/login user must have permissions to become this user.
274    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
275    pub become_user: OptU<String>,
276    /// Conditional expression that overrides the task's normal 'changed' status.
277    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
278    pub changed_when: OptU<String>,
279    /// A boolean that controls if a task is executed in 'check' mode. See Validating tasks: check mode and diff mode.
280    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
281    pub check_mode: OptU<bool>,
282    /// List of collection namespaces to search for modules, plugins, and roles. See Using collections in a playbook
283    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
284    pub collections: OptU<Vec<String>>,
285    /// Allows you to change the connection plugin used for tasks to execute on the target. See Using connection plugins.
286    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
287    pub connection: OptU<String>,
288    /// Enable debugging tasks based on the state of the task result. See Debugging tasks.
289    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
290    pub debugger: OptU<bool>,
291    ///
292    /// delay
293    /// Number of seconds to delay between retries. This setting is only used in combination with until.
294    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
295    pub delay: OptU<i64>,
296    //
297    /// delegate_facts
298    /// Boolean that allows you to apply facts to a delegated host instead of inventory_hostname.
299    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
300    pub delegate_facts: OptU<bool>,
301    /// Host to execute task instead of the target (inventory_hostname).
302    /// Connection vars from the delegated host will also be used for the task.
303    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
304    pub delegate_to: OptU<String>,
305
306    /// Toggle to make tasks return 'diff' information or not.
307    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
308    pub diff: OptU<bool>,
309
310    /// A dictionary that gets converted into environment vars to be provided for the task upon execution.
311    /// This can ONLY be used with modules. This is not supported for any other type of plugins nor Ansible itself nor its configuration,
312    /// it just sets the variables for the code responsible for executing the task.
313    /// This is not a recommended way to pass in confidential data.
314    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
315    pub environment: OptU<IndexMap<String, String>>,
316    /// Conditional expression that overrides the task's normal 'failed' status.
317    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
318    pub failed_when: OptU<String>,
319
320    /// Boolean that allows you to ignore task failures and continue with play. It does not affect connection errors.
321    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
322    pub ignore_errors: OptU<bool>,
323
324    /// Boolean that allows you to ignore task failures due to an unreachable host and continue with the play.
325    /// This does not affect other task errors (see ignore_errors) but is useful for groups of volatile/ephemeral hosts.
326    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
327    pub ignore_unreachable: OptU<bool>,
328
329    /// Same as action but also implies delegate_to: localhost
330    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
331    pub local_action: OptU<String>,
332    /// Takes a list for the task to iterate over, saving each list element into the item variable (configurable via loop_control)
333    #[serde(
334        rename = "loop",
335        default = "OptU::default",
336        skip_serializing_if = "OptU::is_unset"
337    )]
338    pub loop_: OptU<Vec<serde_json::Value>>,
339
340    /// Several keys here allow you to modify/set loop behavior in a task. See Adding controls to loops.
341    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
342    pub loop_control: OptU<IndexMap<String, serde_json::Value>>,
343
344    /// Specifies default parameter values for modules.
345    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
346    pub module_defaults: OptU<IndexMap<String, serde_json::Value>>,
347
348    /// Boolean that controls information disclosure.
349    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
350    pub no_log: OptU<bool>,
351    /// List of handlers to notify when the task returns a 'changed=True' status.
352    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
353    pub notify: OptU<Vec<String>>,
354
355    /// Sets the polling interval in seconds for async tasks (default 10s).
356    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
357    pub poll: OptU<i64>,
358
359    /// Used to override the default port used in a connection.
360    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
361    pub port: OptU<i64>,
362
363    /// Name of variable that will contain task status and module return data.
364    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
365    pub register: OptU<String>,
366
367    /// User used to log into the target via the connection plugin.
368    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
369    pub remote_user: OptU<String>,
370
371    /// Number of retries before giving up in a until loop. This setting is only used in combination with until.
372    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
373    pub retries: OptU<i64>,
374    /// Boolean that will bypass the host loop, forcing the task to attempt to execute on the first host available
375    /// and afterward apply any results and facts to all active hosts in the same batch.
376    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
377    pub run_once: OptU<bool>,
378
379    /// Tags applied to the task or included tasks, this allows selecting subsets of tasks from the command line.
380    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
381    pub tags: OptU<Vec<String>>,
382
383    /// Limit the number of concurrent task runs on task, block and playbook level.
384    /// This is independent of the forks and serial settings, but cannot be set higher than those limits.
385    /// For example, if forks is set to 10 and the throttle is set to 15, at most 10 hosts will be operated on in parallel.
386    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
387    pub throttle: OptU<i64>,
388
389    /// Time limit for the task action to execute in, if exceeded, Ansible will interrupt the process.
390    /// Timeout does not include templating or looping.
391    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
392    pub timeout: OptU<i64>,
393
394    /// This keyword implies a 'retries loop' that will go on until the condition supplied here is met or we hit the retries limit.
395    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
396    pub until: OptU<String>,
397
398    /// Dictionary/map of variables
399    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
400    pub vars: OptU<IndexMap<String, serde_json::Value>>,
401
402    /// Conditional expression, determines if an iteration of a task is run or not.
403    #[serde(default = "OptU::default", skip_serializing_if = "OptU::is_unset")]
404    pub when: OptU<String>,
405    // FIXME: not supported yet!
406    // with_<lookup_plugin>
407    // The same as loop but magically adds the output of any lookup plugin to generate the item list.
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    #[derive(Serialize, Clone, Debug, PartialEq)]
415    struct SampleTaskModule {
416        x1: String,
417    }
418
419    impl TaskModule for SampleTaskModule {}
420
421    #[test]
422    fn test_play_minimum() {
423        assert_eq!(
424            serde_json::to_string(&Play {
425                name: "play1".to_string(),
426                hosts: vec!["host1".to_string()],
427                tasks: vec![Task {
428                    name: "task1".to_string(),
429                    options: TaskOptions::default(),
430                    command: Box::new(SampleTaskModule {
431                        x1: "x1".to_string(),
432                    }),
433                }],
434                options: PlayOptions::default(),
435            })
436            .expect("failed to serialize"),
437            r#"{"name":"play1","hosts":["host1"],"tasks":[{"name":"task1","x1":"x1"}]}"#
438        );
439    }
440
441    #[test]
442    fn test_play_with_all_fields() {
443        assert_eq!(
444            serde_json::to_string(&Play {
445                name: "play1".to_string(),
446                hosts: vec!["host1".to_string()],
447                tasks: vec![Task {
448                    name: "task1".to_string(),
449                    options: TaskOptions::default(),
450                    command: Box::new(SampleTaskModule {
451                        x1: "x1".to_string(),
452                    }),
453                }],
454                options: PlayOptions {
455                    any_errors_fatal: OptU::Some(true),
456                    become_: OptU::Some(true),
457                    become_exe: OptU::Some("become_exe".to_string()),
458                    become_flags: OptU::Some("become_flags".to_string()),
459                    become_method: OptU::Some("become_method".to_string()),
460                    become_user: OptU::Some("become_user".to_string()),
461                    check_mode: OptU::Some(true),
462                    collections: OptU::Some(vec!["collection1".to_string()]),
463                    connection: OptU::Some("connection1".to_string()),
464                    debugger: OptU::Some(true),
465                    diff: OptU::Some(true),
466                    environment: OptU::Some(IndexMap::from([(
467                        "env1".to_string(),
468                        "value1".to_string()
469                    )])),
470                    fact_path: OptU::Some("fact_path".to_string()),
471                    force_handlers: OptU::Some(true),
472                    gather_facts: OptU::Some(true),
473                    gather_subset: OptU::Some(vec!["gather_subset1".to_string()]),
474                    gather_timeout: OptU::Some(10),
475                    handlers: OptU::Some(vec![Task {
476                        name: "handler1".to_string(),
477                        options: TaskOptions::default(),
478                        command: Box::new(SampleTaskModule {
479                            x1: "x1".to_string(),
480                        }),
481                    }]),
482                    ignore_errors: OptU::Some(true),
483                    ignore_unreachable: OptU::Some(true),
484                    max_fail_percentage: OptU::Some(10),
485                    module_defaults: OptU::Some(IndexMap::from([(
486                        "module1".to_string(),
487                        serde_json::Value::String("value1".to_string())
488                    )])),
489                    no_log: OptU::Some(true),
490                    order: OptU::Some("order".to_string()),
491                    port: OptU::Some(10),
492                    post_tasks: OptU::Some(vec![Task {
493                        name: "post_task1".to_string(),
494                        options: TaskOptions::default(),
495                        command: Box::new(SampleTaskModule {
496                            x1: "x1".to_string(),
497                        }),
498                    }]),
499                    pre_tasks: OptU::Some(vec![Task {
500                        name: "pre_task1".to_string(),
501                        options: TaskOptions::default(),
502                        command: Box::new(SampleTaskModule {
503                            x1: "x1".to_string(),
504                        }),
505                    }]),
506                    remote_user: OptU::Some("remote_user".to_string()),
507                    roles: OptU::Some(vec!["role1".to_string()]),
508                    run_once: OptU::Some(true),
509                    serial: OptU::Some(10),
510                    strategy: OptU::Some("strategy".to_string()),
511                    tags: OptU::Some(vec!["tag1".to_string()]),
512                    throttle: OptU::Some(10),
513                    timeout: OptU::Some(10),
514                    vars: OptU::Some(IndexMap::from([(
515                        "var1".to_string(),
516                        serde_json::Value::String("value1".to_string())
517                    )])),
518                    vars_files: OptU::Some(vec!["vars_file1".to_string()]),
519                    vars_prompt: OptU::Some(vec!["vars_prompt1".to_string()]),
520                }
521            })
522            .expect("failed to serialize"),
523            String::new()
524                + "{"
525                + r#""name":"play1","#
526                + r#""hosts":["host1"],"#
527                + r#""any_errors_fatal":true,"#
528                + r#""become":true,"#
529                + r#""become_exe":"become_exe","#
530                + r#""become_flags":"become_flags","#
531                + r#""become_method":"become_method","#
532                + r#""become_user":"become_user","#
533                + r#""check_mode":true,"#
534                + r#""collections":["collection1"],"#
535                + r#""connection":"connection1","#
536                + r#""debugger":true,"#
537                + r#""diff":true,"#
538                + r#""environment":{"env1":"value1"},"#
539                + r#""fact_path":"fact_path","#
540                + r#""force_handlers":true,"#
541                + r#""gather_facts":true,"#
542                + r#""gather_subset":["gather_subset1"],"#
543                + r#""gather_timeout":10,"#
544                + r#""handlers":[{"name":"handler1","x1":"x1"}],"#
545                + r#""ignore_errors":true,"#
546                + r#""ignore_unreachable":true,"#
547                + r#""max_fail_percentage":10,"#
548                + r#""module_defaults":{"module1":"value1"},"#
549                + r#""no_log":true,"#
550                + r#""order":"order","#
551                + r#""port":10,"#
552                + r#""post_tasks":[{"name":"post_task1","x1":"x1"}],"#
553                + r#""pre_tasks":[{"name":"pre_task1","x1":"x1"}],"#
554                + r#""remote_user":"remote_user","#
555                + r#""roles":["role1"],"#
556                + r#""run_once":true,"#
557                + r#""serial":10,"#
558                + r#""strategy":"strategy","#
559                + r#""tags":["tag1"],"#
560                + r#""throttle":10,"#
561                + r#""timeout":10,"#
562                + r#""vars":{"var1":"value1"},"#
563                + r#""vars_files":["vars_file1"],"#
564                + r#""vars_prompt":["vars_prompt1"],"#
565                + r#""tasks":[{"name":"task1","x1":"x1"}]"#
566                + r#"}"#
567        );
568    }
569
570    #[test]
571    /// Test all fields
572    fn test_task_options_with_all_fields() {
573        assert_eq!(
574            serde_json::to_string(&TaskOptions {
575                action: OptU::Some("action1".to_string()),
576                any_errors_fatal: OptU::Some(true),
577                args: OptU::Some(IndexMap::from([(
578                    "arg1".to_string(),
579                    serde_json::Value::String("value1".to_string())
580                )])),
581                async_: OptU::Some(10),
582                become_: OptU::Some(true),
583                become_exe: OptU::Some("become_exe".to_string()),
584                become_flags: OptU::Some("become_flags".to_string()),
585                become_method: OptU::Some("become_method".to_string()),
586                become_user: OptU::Some("become_user".to_string()),
587                changed_when: OptU::Some("changed_when".to_string()),
588                check_mode: OptU::Some(true),
589                collections: OptU::Some(vec!["collection1".to_string()]),
590                connection: OptU::Some("connection1".to_string()),
591                debugger: OptU::Some(true),
592                delay: OptU::Some(10),
593                delegate_facts: OptU::Some(true),
594                delegate_to: OptU::Some("delegate_to".to_string()),
595                diff: OptU::Some(true),
596                environment: OptU::Some(IndexMap::from([(
597                    "env1".to_string(),
598                    "value1".to_string()
599                )])),
600                failed_when: OptU::Some("failed_when".to_string()),
601                ignore_errors: OptU::Some(true),
602                ignore_unreachable: OptU::Some(true),
603                local_action: OptU::Some("local_action".to_string()),
604                loop_: OptU::Some(vec![serde_json::Value::String("loop1".to_string())]),
605                loop_control: OptU::Some(IndexMap::from([(
606                    "loop_control1".to_string(),
607                    serde_json::Value::String("value1".to_string())
608                )])),
609                module_defaults: OptU::Some(IndexMap::from([(
610                    "module1".to_string(),
611                    serde_json::Value::String("value1".to_string())
612                )])),
613                no_log: OptU::Some(true),
614                notify: OptU::Some(vec!["notify1".to_string()]),
615                poll: OptU::Some(10),
616                port: OptU::Some(10),
617                register: OptU::Some("register".to_string()),
618                remote_user: OptU::Some("remote_user".to_string()),
619                retries: OptU::Some(10),
620                run_once: OptU::Some(true),
621                tags: OptU::Some(vec!["tag1".to_string()]),
622                throttle: OptU::Some(10),
623                timeout: OptU::Some(10),
624                until: OptU::Some("until".to_string()),
625                vars: OptU::Some(IndexMap::from([(
626                    "var1".to_string(),
627                    serde_json::Value::String("value1".to_string())
628                )])),
629                when: OptU::Some("when".to_string()),
630            })
631            .expect("failed to serialize"),
632            String::new()
633                + r#"{""#
634                + r#"action":"action1","#
635                + r#""any_errors_fatal":true,"#
636                + r#""args":{"arg1":"value1"},"#
637                + r#""async":10,"#
638                + r#""become":true,"#
639                + r#""become_exe":"become_exe","#
640                + r#""become_flags":"become_flags","#
641                + r#""become_method":"become_method","#
642                + r#""become_user":"become_user","#
643                + r#""changed_when":"changed_when","#
644                + r#""check_mode":true,"#
645                + r#""collections":["collection1"],"#
646                + r#""connection":"connection1","#
647                + r#""debugger":true,"#
648                + r#""delay":10,"#
649                + r#""delegate_facts":true,"#
650                + r#""delegate_to":"delegate_to","#
651                + r#""diff":true,"#
652                + r#""environment":{"env1":"value1"},"#
653                + r#""failed_when":"failed_when","#
654                + r#""ignore_errors":true,"#
655                + r#""ignore_unreachable":true,"#
656                + r#""local_action":"local_action","#
657                + r#""loop":["loop1"],"#
658                + r#""loop_control":{"loop_control1":"value1"},"#
659                + r#""module_defaults":{"module1":"value1"},"#
660                + r#""no_log":true,"#
661                + r#""notify":["notify1"],"#
662                + r#""poll":10,"#
663                + r#""port":10,"#
664                + r#""register":"register","#
665                + r#""remote_user":"remote_user","#
666                + r#""retries":10,"#
667                + r#""run_once":true,"#
668                + r#""tags":["tag1"],"#
669                + r#""throttle":10,"#
670                + r#""timeout":10,"#
671                + r#""until":"until","#
672                + r#""vars":{"var1":"value1"},"#
673                + r#""when":"when""#
674                + r#"}"#
675        );
676    }
677}