Skip to main content

wdl_ast/v1/
task.rs

1//! V1 AST representation for task definitions.
2
3use std::fmt;
4
5use rowan::NodeOrToken;
6
7use super::BoundDecl;
8use super::Decl;
9use super::Expr;
10use super::LiteralBoolean;
11use super::LiteralFloat;
12use super::LiteralInteger;
13use super::LiteralString;
14use super::OpenHeredoc;
15use super::Placeholder;
16use super::StructDefinition;
17use super::WorkflowDefinition;
18use crate::AstNode;
19use crate::AstToken;
20use crate::Ident;
21use crate::SyntaxKind;
22use crate::SyntaxNode;
23use crate::SyntaxToken;
24use crate::TreeNode;
25use crate::TreeToken;
26use crate::v1::display::write_input_section;
27use crate::v1::display::write_output_section;
28
29pub mod common;
30pub mod requirements;
31pub mod runtime;
32
33/// The set of all valid task fields and their descriptions for the implicit
34/// `task` variable.
35pub const TASK_FIELDS: &[(&str, &str)] = &[
36    (TASK_FIELD_NAME, "The task name."),
37    (
38        TASK_FIELD_ID,
39        "A String with the unique ID of the task. The execution engine may choose the format for \
40         this ID, but it is suggested to include at least the following information:\nThe task \
41         name\nThe task alias, if it differs from the task name\nThe index of the task instance, \
42         if it is within a scatter statement",
43    ),
44    (
45        TASK_FIELD_CONTAINER,
46        "The URI String of the container in which the task is executing, or None if the task is \
47         being executed in the host environment.",
48    ),
49    (
50        TASK_FIELD_CPU,
51        "The allocated number of cpus as a Float. Must be greater than 0.",
52    ),
53    (
54        TASK_FIELD_MEMORY,
55        "The allocated memory in bytes as an Int. Must be greater than 0.",
56    ),
57    (
58        TASK_FIELD_GPU,
59        "An Array[String] with one specification per allocated GPU. The specification is \
60         execution engine-specific. If no GPUs were allocated, then the value must be an empty \
61         array.",
62    ),
63    (
64        TASK_FIELD_FPGA,
65        "An Array[String] with one specification per allocated FPGA. The specification is \
66         execution engine-specific. If no FPGAs were allocated, then the value must be an empty \
67         array.",
68    ),
69    (
70        TASK_FIELD_DISKS,
71        "A Map[String, Int] with one entry for each disk mount point. The key is the mount point \
72         and the value is the initial amount of disk space allocated, in bytes. The execution \
73         engine must, at a minimum, provide one entry for each disk mount point requested, but \
74         may provide more. The amount of disk space available for a given mount point may \
75         increase during the lifetime of the task (e.g., autoscaling volumes provided by some \
76         cloud services).",
77    ),
78    (
79        TASK_FIELD_ATTEMPT,
80        "The current task attempt. The value must be 0 the first time the task is executed, and \
81         incremented by 1 each time the task is retried (if any).",
82    ),
83    (
84        TASK_FIELD_PREVIOUS,
85        "An Object containing the resource requirements from the previous task attempt. Available \
86         in requirements, hints, runtime, output, and command sections. All constituent members \
87         are optional and `None` on the first attempt.",
88    ),
89    (
90        TASK_FIELD_END_TIME,
91        "An Int? whose value is the time by which the task must be completed, as a Unix time \
92         stamp. A value of 0 means that the execution engine does not impose a time limit. A \
93         value of None means that the execution engine cannot determine whether the runtime of \
94         the task is limited. A positive value is a guarantee that the task will be preempted at \
95         the specified time, but is not a guarantee that the task won't be preempted earlier.",
96    ),
97    (
98        TASK_FIELD_RETURN_CODE,
99        "An Int? whose value is initially None and is set to the value of the command's return \
100         code. The value is only guaranteed to be defined in the output section.",
101    ),
102    (
103        TASK_FIELD_META,
104        "An Object containing a copy of the task's meta section, or the empty Object if there is \
105         no meta section or if it is empty.",
106    ),
107    (
108        TASK_FIELD_PARAMETER_META,
109        "An Object containing a copy of the task's parameter_meta section, or the empty Object if \
110         there is no parameter_meta section or if it is empty.",
111    ),
112    (
113        TASK_FIELD_EXT,
114        "An Object containing execution engine-specific attributes, or the empty Object if there \
115         aren't any. Members of ext should be considered optional. It is recommended to only \
116         access a member of ext using string interpolation to avoid an error if it is not defined.",
117    ),
118];
119
120/// The set of all valid runtime section keys and their descriptions.
121pub const RUNTIME_KEYS: &[(&str, &str)] = &[
122    (
123        TASK_REQUIREMENT_CONTAINER,
124        "Specifies the container image (e.g., Docker, Singularity) to use for the task.",
125    ),
126    (
127        TASK_REQUIREMENT_CPU,
128        "The number of CPU cores required for the task.",
129    ),
130    (
131        TASK_REQUIREMENT_MEMORY,
132        "The amount of memory required, specified as a string with units (e.g., '2 GiB').",
133    ),
134    (
135        TASK_REQUIREMENT_DISKS,
136        "Specifies the disk requirements for the task.",
137    ),
138    (TASK_REQUIREMENT_GPU, "Specifies GPU requirements."),
139];
140
141/// The set of all valid requirements section keys and their descriptions.
142pub const REQUIREMENTS_KEY: &[(&str, &str)] = &[
143    (
144        TASK_REQUIREMENT_CONTAINER,
145        "Specifies a list of allowed container images. Use `*` to allow any POSIX environment.",
146    ),
147    (
148        TASK_REQUIREMENT_CPU,
149        "The minimum number of CPU cores required.",
150    ),
151    (
152        TASK_REQUIREMENT_MEMORY,
153        "The minimum amount of memory required.",
154    ),
155    (TASK_REQUIREMENT_GPU, "The minimum GPU requirements."),
156    (TASK_REQUIREMENT_FPGA, "The minimum FPGA requirements."),
157    (TASK_REQUIREMENT_DISKS, "The minimum disk requirements."),
158    (
159        TASK_REQUIREMENT_MAX_RETRIES,
160        "The maximum number of times the task can be retried.",
161    ),
162    (
163        TASK_REQUIREMENT_RETURN_CODES,
164        "A list of acceptable return codes from the command.",
165    ),
166];
167
168/// The set of all valid task hints section keys and their descriptions.
169pub const TASK_HINT_KEYS: &[(&str, &str)] = &[
170    (
171        TASK_HINT_DISKS,
172        "A hint to the execution engine to mount disks with specific attributes. The value of \
173         this hint can be a String with a specification that applies to all mount points, or a \
174         Map with the key being the mount point and the value being a String with the \
175         specification for that mount point.",
176    ),
177    (
178        TASK_HINT_GPU,
179        "A hint to the execution engine to provision hardware accelerators with specific \
180         attributes. Accelerator specifications are left intentionally vague as they are \
181         primarily intended to be used in the context of a specific compute environment.",
182    ),
183    (
184        TASK_HINT_FPGA,
185        "A hint to the execution engine to provision hardware accelerators with specific \
186         attributes. Accelerator specifications are left intentionally vague as they are \
187         primarily intended to be used in the context of a specific compute environment.",
188    ),
189    (
190        TASK_HINT_INPUTS,
191        "Provides input-specific hints. Each key must refer to a parameter defined in the task's \
192         input section. A key may also used dotted notation to refer to a specific member of a \
193         struct input.",
194    ),
195    (
196        TASK_HINT_LOCALIZATION_OPTIONAL,
197        "A hint to the execution engine about whether the File inputs for this task need to be \
198         localized prior to executing the task. The value of this hint is a Boolean for which \
199         true indicates that the contents of the File inputs may be streamed on demand.",
200    ),
201    (
202        TASK_HINT_MAX_CPU,
203        "A hint to the execution engine that the task expects to use no more than the specified \
204         number of CPUs. The value of this hint has the same specification as requirements.cpu.",
205    ),
206    (
207        TASK_HINT_MAX_MEMORY,
208        "A hint to the execution engine that the task expects to use no more than the specified \
209         amount of memory. The value of this hint has the same specification as \
210         requirements.memory.",
211    ),
212    (
213        TASK_HINT_OUTPUTS,
214        "Provides output-specific hints. Each key must refer to a parameter defined in the task's \
215         output section. A key may also use dotted notation to refer to a specific member of a \
216         struct output.",
217    ),
218    (
219        TASK_HINT_SHORT_TASK,
220        "A hint to the execution engine about the expected duration of this task. The value of \
221         this hint is a Boolean for which true indicates that that this task is not expected to \
222         take long to execute, which the execution engine can interpret as permission to optimize \
223         the execution of the task.",
224    ),
225    (
226        TASK_HINT_CACHEABLE,
227        "A hint to the execution engine that the task's execution result is cacheable. The value \
228         of this hint is a Boolean for which true indicates that the execution result is \
229         cacheable and false indicates it is not. The default value of the hint depends on the \
230         engine's configuration.",
231    ),
232];
233
234/// The name of the `name` task variable field.
235pub const TASK_FIELD_NAME: &str = "name";
236/// The name of the `id` task variable field.
237pub const TASK_FIELD_ID: &str = "id";
238/// The name of the `container` task variable field.
239pub const TASK_FIELD_CONTAINER: &str = "container";
240/// The name of the `cpu` task variable field.
241pub const TASK_FIELD_CPU: &str = "cpu";
242/// The name of the `memory` task variable field.
243pub const TASK_FIELD_MEMORY: &str = "memory";
244/// The name of the `attempt` task variable field.
245pub const TASK_FIELD_ATTEMPT: &str = "attempt";
246/// The name of the `previous` task variable field.
247pub const TASK_FIELD_PREVIOUS: &str = "previous";
248/// The name of the `gpu` task variable field.
249pub const TASK_FIELD_GPU: &str = "gpu";
250/// The name of the `fpga` task variable field.
251pub const TASK_FIELD_FPGA: &str = "fpga";
252/// The name of the `disks` task variable field.
253pub const TASK_FIELD_DISKS: &str = "disks";
254/// The name of the `end_time` task variable field.
255pub const TASK_FIELD_END_TIME: &str = "end_time";
256/// The name of the `return_code` task variable field.
257pub const TASK_FIELD_RETURN_CODE: &str = "return_code";
258/// The name of the `meta` task variable field.
259pub const TASK_FIELD_META: &str = "meta";
260/// The name of the `parameter_meta` task variable field.
261pub const TASK_FIELD_PARAMETER_META: &str = "parameter_meta";
262/// The name of the `ext` task variable field.
263pub const TASK_FIELD_EXT: &str = "ext";
264/// The name of the `max_retries` task variable field.
265pub const TASK_FIELD_MAX_RETRIES: &str = "max_retries";
266
267/// The name of the `container` task requirement.
268pub const TASK_REQUIREMENT_CONTAINER: &str = "container";
269/// The alias of the `container` task requirement (i.e. `docker`).
270pub const TASK_REQUIREMENT_CONTAINER_ALIAS: &str = "docker";
271/// The name of the `cpu` task requirement.
272pub const TASK_REQUIREMENT_CPU: &str = "cpu";
273/// The name of the `disks` task requirement.
274pub const TASK_REQUIREMENT_DISKS: &str = "disks";
275/// The name of the `gpu` task requirement.
276pub const TASK_REQUIREMENT_GPU: &str = "gpu";
277/// The name of the `fpga` task requirement.
278pub const TASK_REQUIREMENT_FPGA: &str = "fpga";
279/// The name of the `max_retries` task requirement.
280pub const TASK_REQUIREMENT_MAX_RETRIES: &str = "max_retries";
281/// The alias of the `max_retries` task requirement (i.e. `maxRetries``).
282pub const TASK_REQUIREMENT_MAX_RETRIES_ALIAS: &str = "maxRetries";
283/// The name of the `memory` task requirement.
284pub const TASK_REQUIREMENT_MEMORY: &str = "memory";
285/// The name of the `return_codes` task requirement.
286pub const TASK_REQUIREMENT_RETURN_CODES: &str = "return_codes";
287/// The alias of the `return_codes` task requirement (i.e. `returnCodes`).
288pub const TASK_REQUIREMENT_RETURN_CODES_ALIAS: &str = "returnCodes";
289
290/// The name of the `disks` task hint.
291pub const TASK_HINT_DISKS: &str = "disks";
292/// The name of the `gpu` task hint.
293pub const TASK_HINT_GPU: &str = "gpu";
294/// The name of the `fpga` task hint.
295pub const TASK_HINT_FPGA: &str = "fpga";
296/// The name of the `inputs` task hint.
297pub const TASK_HINT_INPUTS: &str = "inputs";
298/// The name of the `localization_optional` task hint.
299pub const TASK_HINT_LOCALIZATION_OPTIONAL: &str = "localization_optional";
300/// The alias of the `localization_optional` task hint (i.e.
301/// `localizationOptional`).
302pub const TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS: &str = "localizationOptional";
303/// The name of the `max_cpu` task hint.
304pub const TASK_HINT_MAX_CPU: &str = "max_cpu";
305/// The alias of the `max_cpu` task hint (i.e. `maxCpu`).
306pub const TASK_HINT_MAX_CPU_ALIAS: &str = "maxCpu";
307/// The name of the `max_memory` task hint.
308pub const TASK_HINT_MAX_MEMORY: &str = "max_memory";
309/// The alias of the `max_memory` task hin (e.g. `maxMemory`).
310pub const TASK_HINT_MAX_MEMORY_ALIAS: &str = "maxMemory";
311/// The name of the `outputs` task hint.
312pub const TASK_HINT_OUTPUTS: &str = "outputs";
313/// The name of the `short_task` task hint.
314pub const TASK_HINT_SHORT_TASK: &str = "short_task";
315/// The alias of the `short_task` task hint (e.g. `shortTask`).
316pub const TASK_HINT_SHORT_TASK_ALIAS: &str = "shortTask";
317/// The name of the `cacheable` task hint.
318pub const TASK_HINT_CACHEABLE: &str = "cacheable";
319
320/// Unescapes command text.
321fn unescape_command_text(s: &str, heredoc: bool, buffer: &mut String) {
322    let mut chars = s.chars().peekable();
323    while let Some(c) = chars.next() {
324        match c {
325            '\\' => match chars.peek() {
326                Some('\\') | Some('~') => {
327                    buffer.push(chars.next().unwrap());
328                }
329                Some('>') if heredoc => {
330                    buffer.push(chars.next().unwrap());
331                }
332                Some('$') | Some('}') if !heredoc => {
333                    buffer.push(chars.next().unwrap());
334                }
335                _ => {
336                    buffer.push('\\');
337                }
338            },
339            _ => {
340                buffer.push(c);
341            }
342        }
343    }
344}
345
346/// Represents a task definition.
347#[derive(Clone, Debug, PartialEq, Eq)]
348pub struct TaskDefinition<N: TreeNode = SyntaxNode>(N);
349
350impl<N: TreeNode> TaskDefinition<N> {
351    /// Gets the name of the task.
352    pub fn name(&self) -> Ident<N::Token> {
353        self.token().expect("task should have a name")
354    }
355
356    /// Gets the items of the task.
357    pub fn items(&self) -> impl Iterator<Item = TaskItem<N>> + use<'_, N> {
358        TaskItem::children(&self.0)
359    }
360
361    /// Gets the input section of the task.
362    pub fn input(&self) -> Option<InputSection<N>> {
363        self.child()
364    }
365
366    /// Gets the output section of the task.
367    pub fn output(&self) -> Option<OutputSection<N>> {
368        self.child()
369    }
370
371    /// Gets the command section of the task.
372    pub fn command(&self) -> Option<CommandSection<N>> {
373        self.child()
374    }
375
376    /// Gets the requirements sections of the task.
377    pub fn requirements(&self) -> Option<RequirementsSection<N>> {
378        self.child()
379    }
380
381    /// Gets the hints section of the task.
382    pub fn hints(&self) -> Option<TaskHintsSection<N>> {
383        self.child()
384    }
385
386    /// Gets the runtime section of the task.
387    pub fn runtime(&self) -> Option<RuntimeSection<N>> {
388        self.child()
389    }
390
391    /// Gets the metadata section of the task.
392    pub fn metadata(&self) -> Option<MetadataSection<N>> {
393        self.child()
394    }
395
396    /// Gets the parameter section of the task.
397    pub fn parameter_metadata(&self) -> Option<ParameterMetadataSection<N>> {
398        self.child()
399    }
400
401    /// Gets the private declarations of the task.
402    pub fn declarations(&self) -> impl Iterator<Item = BoundDecl<N>> + use<'_, N> {
403        self.children()
404    }
405
406    /// Writes a Markdown formatted description of the task.
407    pub fn markdown_description(&self, f: &mut impl fmt::Write) -> fmt::Result {
408        writeln!(f, "```wdl\ntask {}\n```\n---", self.name().text())?;
409
410        if let Some(meta) = self.metadata()
411            && let Some(desc) = meta.items().find(|i| i.name().text() == "description")
412            && let MetadataValue::String(s) = desc.value()
413            && let Some(text) = s.text()
414        {
415            writeln!(f, "{}\n", text.text())?;
416        }
417
418        write_input_section(f, self.input().as_ref(), self.parameter_metadata().as_ref())?;
419        write_output_section(
420            f,
421            self.output().as_ref(),
422            self.parameter_metadata().as_ref(),
423        )?;
424
425        Ok(())
426    }
427}
428
429impl<N: TreeNode> AstNode<N> for TaskDefinition<N> {
430    fn can_cast(kind: SyntaxKind) -> bool {
431        kind == SyntaxKind::TaskDefinitionNode
432    }
433
434    fn cast(inner: N) -> Option<Self> {
435        match inner.kind() {
436            SyntaxKind::TaskDefinitionNode => Some(Self(inner)),
437            _ => None,
438        }
439    }
440
441    fn inner(&self) -> &N {
442        &self.0
443    }
444}
445
446/// Represents an item in a task definition.
447#[derive(Clone, Debug, PartialEq, Eq)]
448pub enum TaskItem<N: TreeNode = SyntaxNode> {
449    /// The item is an input section.
450    Input(InputSection<N>),
451    /// The item is an output section.
452    Output(OutputSection<N>),
453    /// The item is a command section.
454    Command(CommandSection<N>),
455    /// The item is a requirements section.
456    Requirements(RequirementsSection<N>),
457    /// The item is a task hints section.
458    Hints(TaskHintsSection<N>),
459    /// The item is a runtime section.
460    Runtime(RuntimeSection<N>),
461    /// The item is a metadata section.
462    Metadata(MetadataSection<N>),
463    /// The item is a parameter meta section.
464    ParameterMetadata(ParameterMetadataSection<N>),
465    /// The item is a private bound declaration.
466    Declaration(BoundDecl<N>),
467}
468
469impl<N: TreeNode> TaskItem<N> {
470    /// Returns whether or not the given syntax kind can be cast to
471    /// [`TaskItem`].
472    pub fn can_cast(kind: SyntaxKind) -> bool {
473        matches!(
474            kind,
475            SyntaxKind::InputSectionNode
476                | SyntaxKind::OutputSectionNode
477                | SyntaxKind::CommandSectionNode
478                | SyntaxKind::RequirementsSectionNode
479                | SyntaxKind::TaskHintsSectionNode
480                | SyntaxKind::RuntimeSectionNode
481                | SyntaxKind::MetadataSectionNode
482                | SyntaxKind::ParameterMetadataSectionNode
483                | SyntaxKind::BoundDeclNode
484        )
485    }
486
487    /// Casts the given node to [`TaskItem`].
488    ///
489    /// Returns `None` if the node cannot be cast.
490    pub fn cast(inner: N) -> Option<Self> {
491        match inner.kind() {
492            SyntaxKind::InputSectionNode => Some(Self::Input(
493                InputSection::cast(inner).expect("input section to cast"),
494            )),
495            SyntaxKind::OutputSectionNode => Some(Self::Output(
496                OutputSection::cast(inner).expect("output section to cast"),
497            )),
498            SyntaxKind::CommandSectionNode => Some(Self::Command(
499                CommandSection::cast(inner).expect("command section to cast"),
500            )),
501            SyntaxKind::RequirementsSectionNode => Some(Self::Requirements(
502                RequirementsSection::cast(inner).expect("requirements section to cast"),
503            )),
504            SyntaxKind::RuntimeSectionNode => Some(Self::Runtime(
505                RuntimeSection::cast(inner).expect("runtime section to cast"),
506            )),
507            SyntaxKind::MetadataSectionNode => Some(Self::Metadata(
508                MetadataSection::cast(inner).expect("metadata section to cast"),
509            )),
510            SyntaxKind::ParameterMetadataSectionNode => Some(Self::ParameterMetadata(
511                ParameterMetadataSection::cast(inner).expect("parameter metadata section to cast"),
512            )),
513            SyntaxKind::TaskHintsSectionNode => Some(Self::Hints(
514                TaskHintsSection::cast(inner).expect("task hints section to cast"),
515            )),
516            SyntaxKind::BoundDeclNode => Some(Self::Declaration(
517                BoundDecl::cast(inner).expect("bound decl to cast"),
518            )),
519            _ => None,
520        }
521    }
522
523    /// Gets a reference to the inner node.
524    pub fn inner(&self) -> &N {
525        match self {
526            Self::Input(element) => element.inner(),
527            Self::Output(element) => element.inner(),
528            Self::Command(element) => element.inner(),
529            Self::Requirements(element) => element.inner(),
530            Self::Hints(element) => element.inner(),
531            Self::Runtime(element) => element.inner(),
532            Self::Metadata(element) => element.inner(),
533            Self::ParameterMetadata(element) => element.inner(),
534            Self::Declaration(element) => element.inner(),
535        }
536    }
537
538    /// Attempts to get a reference to the inner [`InputSection`].
539    ///
540    /// * If `self` is a [`TaskItem::Input`], then a reference to the inner
541    ///   [`InputSection`] is returned wrapped in [`Some`].
542    /// * Else, [`None`] is returned.
543    pub fn as_input_section(&self) -> Option<&InputSection<N>> {
544        match self {
545            Self::Input(s) => Some(s),
546            _ => None,
547        }
548    }
549
550    /// Consumes `self` and attempts to return the inner [`InputSection`].
551    ///
552    /// * If `self` is a [`TaskItem::Input`], then the inner [`InputSection`] is
553    ///   returned wrapped in [`Some`].
554    /// * Else, [`None`] is returned.
555    pub fn into_input_section(self) -> Option<InputSection<N>> {
556        match self {
557            Self::Input(s) => Some(s),
558            _ => None,
559        }
560    }
561
562    /// Attempts to get a reference to the inner [`OutputSection`].
563    ///
564    /// * If `self` is a [`TaskItem::Output`], then a reference to the inner
565    ///   [`OutputSection`] is returned wrapped in [`Some`].
566    /// * Else, [`None`] is returned.
567    pub fn as_output_section(&self) -> Option<&OutputSection<N>> {
568        match self {
569            Self::Output(s) => Some(s),
570            _ => None,
571        }
572    }
573
574    /// Consumes `self` and attempts to return the inner [`OutputSection`].
575    ///
576    /// * If `self` is a [`TaskItem::Output`], then the inner [`OutputSection`]
577    ///   is returned wrapped in [`Some`].
578    /// * Else, [`None`] is returned.
579    pub fn into_output_section(self) -> Option<OutputSection<N>> {
580        match self {
581            Self::Output(s) => Some(s),
582            _ => None,
583        }
584    }
585
586    /// Attempts to get a reference to the inner [`CommandSection`].
587    ///
588    /// * If `self` is a [`TaskItem::Command`], then a reference to the inner
589    ///   [`CommandSection`] is returned wrapped in [`Some`].
590    /// * Else, [`None`] is returned.
591    pub fn as_command_section(&self) -> Option<&CommandSection<N>> {
592        match self {
593            Self::Command(s) => Some(s),
594            _ => None,
595        }
596    }
597
598    /// Consumes `self` and attempts to return the inner [`CommandSection`].
599    ///
600    /// * If `self` is a [`TaskItem::Command`], then the inner
601    ///   [`CommandSection`] is returned wrapped in [`Some`].
602    /// * Else, [`None`] is returned.
603    pub fn into_command_section(self) -> Option<CommandSection<N>> {
604        match self {
605            Self::Command(s) => Some(s),
606            _ => None,
607        }
608    }
609
610    /// Attempts to get a reference to the inner [`RequirementsSection`].
611    ///
612    /// * If `self` is a [`TaskItem::Requirements`], then a reference to the
613    ///   inner [`RequirementsSection`] is returned wrapped in [`Some`].
614    /// * Else, [`None`] is returned.
615    pub fn as_requirements_section(&self) -> Option<&RequirementsSection<N>> {
616        match self {
617            Self::Requirements(s) => Some(s),
618            _ => None,
619        }
620    }
621
622    /// Consumes `self` and attempts to return the inner
623    /// [`RequirementsSection`].
624    ///
625    /// * If `self` is a [`TaskItem::Requirements`], then the inner
626    ///   [`RequirementsSection`] is returned wrapped in [`Some`].
627    /// * Else, [`None`] is returned.
628    pub fn into_requirements_section(self) -> Option<RequirementsSection<N>> {
629        match self {
630            Self::Requirements(s) => Some(s),
631            _ => None,
632        }
633    }
634
635    /// Attempts to get a reference to the inner [`TaskHintsSection`].
636    ///
637    /// * If `self` is a [`TaskItem::Hints`], then a reference to the inner
638    ///   [`TaskHintsSection`] is returned wrapped in [`Some`].
639    /// * Else, [`None`] is returned.
640    pub fn as_hints_section(&self) -> Option<&TaskHintsSection<N>> {
641        match self {
642            Self::Hints(s) => Some(s),
643            _ => None,
644        }
645    }
646
647    /// Consumes `self` and attempts to return the inner [`TaskHintsSection`].
648    ///
649    /// * If `self` is a [`TaskItem::Hints`], then the inner
650    ///   [`TaskHintsSection`] is returned wrapped in [`Some`].
651    /// * Else, [`None`] is returned.
652    pub fn into_hints_section(self) -> Option<TaskHintsSection<N>> {
653        match self {
654            Self::Hints(s) => Some(s),
655            _ => None,
656        }
657    }
658
659    /// Attempts to get a reference to the inner [`RuntimeSection`].
660    ///
661    /// * If `self` is a [`TaskItem::Runtime`], then a reference to the inner
662    ///   [`RuntimeSection`] is returned wrapped in [`Some`].
663    /// * Else, [`None`] is returned.
664    pub fn as_runtime_section(&self) -> Option<&RuntimeSection<N>> {
665        match self {
666            Self::Runtime(s) => Some(s),
667            _ => None,
668        }
669    }
670
671    /// Consumes `self` and attempts to return the inner [`RuntimeSection`].
672    ///
673    /// * If `self` is a [`TaskItem::Runtime`], then the inner
674    ///   [`RuntimeSection`] is returned wrapped in [`Some`].
675    /// * Else, [`None`] is returned.
676    pub fn into_runtime_section(self) -> Option<RuntimeSection<N>> {
677        match self {
678            Self::Runtime(s) => Some(s),
679            _ => None,
680        }
681    }
682
683    /// Attempts to get a reference to the inner [`MetadataSection`].
684    ///
685    /// * If `self` is a [`TaskItem::Metadata`], then a reference to the inner
686    ///   [`MetadataSection`] is returned wrapped in [`Some`].
687    /// * Else, [`None`] is returned.
688    pub fn as_metadata_section(&self) -> Option<&MetadataSection<N>> {
689        match self {
690            Self::Metadata(s) => Some(s),
691            _ => None,
692        }
693    }
694
695    /// Consumes `self` and attempts to return the inner [`MetadataSection`].
696    ///
697    /// * If `self` is a [`TaskItem::Metadata`], then the inner
698    ///   [`MetadataSection`] is returned wrapped in [`Some`].
699    /// * Else, [`None`] is returned.
700    pub fn into_metadata_section(self) -> Option<MetadataSection<N>> {
701        match self {
702            Self::Metadata(s) => Some(s),
703            _ => None,
704        }
705    }
706
707    /// Attempts to get a reference to the inner [`ParameterMetadataSection`].
708    ///
709    /// * If `self` is a [`TaskItem::ParameterMetadata`], then a reference to
710    ///   the inner [`ParameterMetadataSection`] is returned wrapped in
711    ///   [`Some`].
712    /// * Else, [`None`] is returned.
713    pub fn as_parameter_metadata_section(&self) -> Option<&ParameterMetadataSection<N>> {
714        match self {
715            Self::ParameterMetadata(s) => Some(s),
716            _ => None,
717        }
718    }
719
720    /// Consumes `self` and attempts to return the inner
721    /// [`ParameterMetadataSection`].
722    ///
723    /// * If `self` is a [`TaskItem::ParameterMetadata`], then the inner
724    ///   [`ParameterMetadataSection`] is returned wrapped in [`Some`].
725    /// * Else, [`None`] is returned.
726    pub fn into_parameter_metadata_section(self) -> Option<ParameterMetadataSection<N>> {
727        match self {
728            Self::ParameterMetadata(s) => Some(s),
729            _ => None,
730        }
731    }
732
733    /// Attempts to get a reference to the inner [`BoundDecl`].
734    ///
735    /// * If `self` is a [`TaskItem::Declaration`], then a reference to the
736    ///   inner [`BoundDecl`] is returned wrapped in [`Some`].
737    /// * Else, [`None`] is returned.
738    pub fn as_declaration(&self) -> Option<&BoundDecl<N>> {
739        match self {
740            Self::Declaration(d) => Some(d),
741            _ => None,
742        }
743    }
744
745    /// Consumes `self` and attempts to return the inner [`BoundDecl`].
746    ///
747    /// * If `self` is a [`TaskItem::Declaration`], then the inner [`BoundDecl`]
748    ///   is returned wrapped in [`Some`].
749    /// * Else, [`None`] is returned.
750    pub fn into_declaration(self) -> Option<BoundDecl<N>> {
751        match self {
752            Self::Declaration(d) => Some(d),
753            _ => None,
754        }
755    }
756
757    /// Finds the first child that can be cast to a [`TaskItem`].
758    pub fn child(node: &N) -> Option<Self> {
759        node.children().find_map(Self::cast)
760    }
761
762    /// Finds all children that can be cast to a [`TaskItem`].
763    pub fn children(node: &N) -> impl Iterator<Item = Self> + use<'_, N> {
764        node.children().filter_map(Self::cast)
765    }
766}
767
768/// Represents the parent of a section.
769#[derive(Clone, Debug, PartialEq, Eq)]
770pub enum SectionParent<N: TreeNode = SyntaxNode> {
771    /// The parent is a task.
772    Task(TaskDefinition<N>),
773    /// The parent is a workflow.
774    Workflow(WorkflowDefinition<N>),
775    /// The parent is a struct.
776    Struct(StructDefinition<N>),
777}
778
779impl<N: TreeNode> SectionParent<N> {
780    /// Returns whether or not the given syntax kind can be cast to
781    /// [`SectionParent`].
782    pub fn can_cast(kind: SyntaxKind) -> bool {
783        matches!(
784            kind,
785            SyntaxKind::TaskDefinitionNode
786                | SyntaxKind::WorkflowDefinitionNode
787                | SyntaxKind::StructDefinitionNode
788        )
789    }
790
791    /// Casts the given node to [`SectionParent`].
792    ///
793    /// Returns `None` if the node cannot be cast.
794    pub fn cast(inner: N) -> Option<Self> {
795        match inner.kind() {
796            SyntaxKind::TaskDefinitionNode => Some(Self::Task(
797                TaskDefinition::cast(inner).expect("task definition to cast"),
798            )),
799            SyntaxKind::WorkflowDefinitionNode => Some(Self::Workflow(
800                WorkflowDefinition::cast(inner).expect("workflow definition to cast"),
801            )),
802            SyntaxKind::StructDefinitionNode => Some(Self::Struct(
803                StructDefinition::cast(inner).expect("struct definition to cast"),
804            )),
805            _ => None,
806        }
807    }
808
809    /// Gets a reference to the inner node.
810    pub fn inner(&self) -> &N {
811        match self {
812            Self::Task(element) => element.inner(),
813            Self::Workflow(element) => element.inner(),
814            Self::Struct(element) => element.inner(),
815        }
816    }
817
818    /// Gets the name of the section parent.
819    pub fn name(&self) -> Ident<N::Token> {
820        match self {
821            Self::Task(t) => t.name(),
822            Self::Workflow(w) => w.name(),
823            Self::Struct(s) => s.name(),
824        }
825    }
826
827    /// Attempts to get a reference to the inner [`TaskDefinition`].
828    ///
829    /// * If `self` is a [`SectionParent::Task`], then a reference to the inner
830    ///   [`TaskDefinition`] is returned wrapped in [`Some`].
831    /// * Else, [`None`] is returned.
832    pub fn as_task(&self) -> Option<&TaskDefinition<N>> {
833        match self {
834            Self::Task(task) => Some(task),
835            _ => None,
836        }
837    }
838
839    /// Consumes `self` and attempts to return the inner [`TaskDefinition`].
840    ///
841    /// * If `self` is a [`SectionParent::Task`], then the inner
842    ///   [`TaskDefinition`] is returned wrapped in [`Some`].
843    /// * Else, [`None`] is returned.
844    pub fn into_task(self) -> Option<TaskDefinition<N>> {
845        match self {
846            Self::Task(task) => Some(task),
847            _ => None,
848        }
849    }
850
851    /// Unwraps to a task definition.
852    ///
853    /// # Panics
854    ///
855    /// Panics if it is not a task definition.
856    pub fn unwrap_task(self) -> TaskDefinition<N> {
857        match self {
858            Self::Task(task) => task,
859            _ => panic!("not a task definition"),
860        }
861    }
862
863    /// Attempts to get a reference to the inner [`WorkflowDefinition`].
864    ///
865    /// * If `self` is a [`SectionParent::Workflow`], then a reference to the
866    ///   inner [`WorkflowDefinition`] is returned wrapped in [`Some`].
867    /// * Else, [`None`] is returned.
868    pub fn as_workflow(&self) -> Option<&WorkflowDefinition<N>> {
869        match self {
870            Self::Workflow(workflow) => Some(workflow),
871            _ => None,
872        }
873    }
874
875    /// Consumes `self` and attempts to return the inner [`WorkflowDefinition`].
876    ///
877    /// * If `self` is a [`SectionParent::Workflow`], then the inner
878    ///   [`WorkflowDefinition`] is returned wrapped in [`Some`].
879    /// * Else, [`None`] is returned.
880    pub fn into_workflow(self) -> Option<WorkflowDefinition<N>> {
881        match self {
882            Self::Workflow(workflow) => Some(workflow),
883            _ => None,
884        }
885    }
886
887    /// Unwraps to a workflow definition.
888    ///
889    /// # Panics
890    ///
891    /// Panics if it is not a workflow definition.
892    pub fn unwrap_workflow(self) -> WorkflowDefinition<N> {
893        match self {
894            Self::Workflow(workflow) => workflow,
895            _ => panic!("not a workflow definition"),
896        }
897    }
898
899    /// Attempts to get a reference to the inner [`StructDefinition`].
900    ///
901    /// * If `self` is a [`SectionParent::Struct`], then a reference to the
902    ///   inner [`StructDefinition`] is returned wrapped in [`Some`].
903    /// * Else, [`None`] is returned.
904    pub fn as_struct(&self) -> Option<&StructDefinition<N>> {
905        match self {
906            Self::Struct(r#struct) => Some(r#struct),
907            _ => None,
908        }
909    }
910
911    /// Consumes `self` and attempts to return the inner [`StructDefinition`].
912    ///
913    /// * If `self` is a [`SectionParent::Struct`], then the inner
914    ///   [`StructDefinition`] is returned wrapped in [`Some`].
915    /// * Else, [`None`] is returned.
916    pub fn into_struct(self) -> Option<StructDefinition<N>> {
917        match self {
918            Self::Struct(r#struct) => Some(r#struct),
919            _ => None,
920        }
921    }
922
923    /// Unwraps to a struct definition.
924    ///
925    /// # Panics
926    ///
927    /// Panics if it is not a struct definition.
928    pub fn unwrap_struct(self) -> StructDefinition<N> {
929        match self {
930            Self::Struct(def) => def,
931            _ => panic!("not a struct definition"),
932        }
933    }
934
935    /// Finds the first child that can be cast to a [`SectionParent`].
936    pub fn child(node: &N) -> Option<Self> {
937        node.children().find_map(Self::cast)
938    }
939
940    /// Finds all children that can be cast to a [`SectionParent`].
941    pub fn children(node: &N) -> impl Iterator<Item = Self> + use<'_, N> {
942        node.children().filter_map(Self::cast)
943    }
944}
945
946/// Represents an input section in a task or workflow definition.
947#[derive(Clone, Debug, PartialEq, Eq)]
948pub struct InputSection<N: TreeNode = SyntaxNode>(N);
949
950impl<N: TreeNode> InputSection<N> {
951    /// Gets the declarations of the input section.
952    pub fn declarations(&self) -> impl Iterator<Item = Decl<N>> + use<'_, N> {
953        Decl::children(&self.0)
954    }
955
956    /// Gets the parent of the input section.
957    pub fn parent(&self) -> SectionParent<N> {
958        SectionParent::cast(self.0.parent().expect("should have a parent"))
959            .expect("parent should cast")
960    }
961}
962
963impl<N: TreeNode> AstNode<N> for InputSection<N> {
964    fn can_cast(kind: SyntaxKind) -> bool {
965        kind == SyntaxKind::InputSectionNode
966    }
967
968    fn cast(inner: N) -> Option<Self> {
969        match inner.kind() {
970            SyntaxKind::InputSectionNode => Some(Self(inner)),
971            _ => None,
972        }
973    }
974
975    fn inner(&self) -> &N {
976        &self.0
977    }
978}
979
980/// Represents an output section in a task or workflow definition.
981#[derive(Clone, Debug, PartialEq, Eq)]
982pub struct OutputSection<N: TreeNode = SyntaxNode>(N);
983
984impl<N: TreeNode> OutputSection<N> {
985    /// Gets the declarations of the output section.
986    pub fn declarations(&self) -> impl Iterator<Item = BoundDecl<N>> + use<'_, N> {
987        self.children()
988    }
989
990    /// Gets the parent of the output section.
991    pub fn parent(&self) -> SectionParent<N> {
992        SectionParent::cast(self.0.parent().expect("should have a parent"))
993            .expect("parent should cast")
994    }
995}
996
997impl<N: TreeNode> AstNode<N> for OutputSection<N> {
998    fn can_cast(kind: SyntaxKind) -> bool {
999        kind == SyntaxKind::OutputSectionNode
1000    }
1001
1002    fn cast(inner: N) -> Option<Self> {
1003        match inner.kind() {
1004            SyntaxKind::OutputSectionNode => Some(Self(inner)),
1005            _ => None,
1006        }
1007    }
1008
1009    fn inner(&self) -> &N {
1010        &self.0
1011    }
1012}
1013
1014/// A command part stripped of leading whitespace.
1015///
1016/// Placeholders are not changed and are copied as is.
1017#[derive(Clone, Debug, PartialEq, Eq)]
1018pub enum StrippedCommandPart<N: TreeNode = SyntaxNode> {
1019    /// A text part.
1020    Text(String),
1021    /// A placeholder part.
1022    Placeholder(Placeholder<N>),
1023}
1024
1025/// Represents a command section in a task definition.
1026#[derive(Clone, Debug, PartialEq, Eq)]
1027pub struct CommandSection<N: TreeNode = SyntaxNode>(N);
1028
1029impl<N: TreeNode> CommandSection<N> {
1030    /// Gets whether or not the command section is a heredoc command.
1031    pub fn is_heredoc(&self) -> bool {
1032        self.token::<OpenHeredoc<N::Token>>().is_some()
1033    }
1034
1035    /// Gets the parts of the command.
1036    pub fn parts(&self) -> impl Iterator<Item = CommandPart<N>> + use<'_, N> {
1037        self.0.children_with_tokens().filter_map(CommandPart::cast)
1038    }
1039
1040    /// Counts the leading whitespace of the command.
1041    ///
1042    /// If the command has mixed indentation, this will return None.
1043    pub fn count_whitespace(&self) -> Option<usize> {
1044        let mut min_leading_spaces = usize::MAX;
1045        let mut min_leading_tabs = usize::MAX;
1046        let mut parsing_leading_whitespace = false; // init to false so that the first line is skipped
1047
1048        let mut leading_spaces = 0;
1049        let mut leading_tabs = 0;
1050        for part in self.parts() {
1051            match part {
1052                CommandPart::Text(text) => {
1053                    for c in text.text().chars() {
1054                        match c {
1055                            ' ' if parsing_leading_whitespace => {
1056                                leading_spaces += 1;
1057                            }
1058                            '\t' if parsing_leading_whitespace => {
1059                                leading_tabs += 1;
1060                            }
1061                            '\n' => {
1062                                parsing_leading_whitespace = true;
1063                                leading_spaces = 0;
1064                                leading_tabs = 0;
1065                            }
1066                            '\r' => {}
1067                            _ => {
1068                                if parsing_leading_whitespace {
1069                                    parsing_leading_whitespace = false;
1070                                    if leading_spaces == 0 && leading_tabs == 0 {
1071                                        min_leading_spaces = 0;
1072                                        min_leading_tabs = 0;
1073                                        continue;
1074                                    }
1075                                    if leading_spaces < min_leading_spaces && leading_spaces > 0 {
1076                                        min_leading_spaces = leading_spaces;
1077                                    }
1078                                    if leading_tabs < min_leading_tabs && leading_tabs > 0 {
1079                                        min_leading_tabs = leading_tabs;
1080                                    }
1081                                }
1082                            }
1083                        }
1084                    }
1085                    // The last line is intentionally skipped.
1086                }
1087                CommandPart::Placeholder(_) => {
1088                    if parsing_leading_whitespace {
1089                        parsing_leading_whitespace = false;
1090                        if leading_spaces == 0 && leading_tabs == 0 {
1091                            min_leading_spaces = 0;
1092                            min_leading_tabs = 0;
1093                            continue;
1094                        }
1095                        if leading_spaces < min_leading_spaces && leading_spaces > 0 {
1096                            min_leading_spaces = leading_spaces;
1097                        }
1098                        if leading_tabs < min_leading_tabs && leading_tabs > 0 {
1099                            min_leading_tabs = leading_tabs;
1100                        }
1101                    }
1102                }
1103            }
1104        }
1105
1106        // Check for no indentation or all whitespace, in which case we're done
1107        if (min_leading_spaces == 0 && min_leading_tabs == 0)
1108            || (min_leading_spaces == usize::MAX && min_leading_tabs == usize::MAX)
1109        {
1110            return Some(0);
1111        }
1112
1113        // Check for mixed indentation
1114        if (min_leading_spaces > 0 && min_leading_spaces != usize::MAX)
1115            && (min_leading_tabs > 0 && min_leading_tabs != usize::MAX)
1116        {
1117            return None;
1118        }
1119
1120        // Exactly one of the two will be equal to usize::MAX because it never appeared.
1121        // The other will be the number of leading spaces or tabs to strip.
1122        let final_leading_whitespace = if min_leading_spaces < min_leading_tabs {
1123            min_leading_spaces
1124        } else {
1125            min_leading_tabs
1126        };
1127
1128        Some(final_leading_whitespace)
1129    }
1130
1131    /// Strips leading whitespace from the command.
1132    ///
1133    /// If the command has mixed indentation, this will return `None`.
1134    pub fn strip_whitespace(&self) -> Option<Vec<StrippedCommandPart<N>>> {
1135        let mut result = Vec::new();
1136        let heredoc = self.is_heredoc();
1137        for part in self.parts() {
1138            match part {
1139                CommandPart::Text(text) => {
1140                    let mut s = String::new();
1141                    unescape_command_text(text.text(), heredoc, &mut s);
1142                    result.push(StrippedCommandPart::Text(s));
1143                }
1144                CommandPart::Placeholder(p) => {
1145                    result.push(StrippedCommandPart::Placeholder(p));
1146                }
1147            }
1148        }
1149
1150        // Trim the first line
1151        let mut whole_first_line_trimmed = false;
1152        if let Some(StrippedCommandPart::Text(text)) = result.first_mut() {
1153            let end_of_first_line = text.find('\n').map(|p| p + 1).unwrap_or(text.len());
1154            let line = &text[..end_of_first_line];
1155            let len = line.len() - line.trim_start().len();
1156            whole_first_line_trimmed = len == line.len();
1157            text.replace_range(..len, "");
1158        }
1159
1160        // Trim the last line
1161        if let Some(StrippedCommandPart::Text(text)) = result.last_mut() {
1162            if let Some(index) = text.rfind(|c| !matches!(c, ' ' | '\t')) {
1163                text.truncate(index + 1);
1164            } else {
1165                text.clear();
1166            }
1167
1168            if text.ends_with('\n') {
1169                text.pop();
1170            }
1171
1172            if text.ends_with('\r') {
1173                text.pop();
1174            }
1175        }
1176
1177        // Return immediately if command contains mixed indentation
1178        let num_stripped_chars = self.count_whitespace()?;
1179
1180        // If there is no leading whitespace, we're done
1181        if num_stripped_chars == 0 {
1182            return Some(result);
1183        }
1184
1185        // Finally, strip the leading whitespace on each line
1186        // This is done in place using the `replace_range` method; the method will
1187        // internally do moves without allocations
1188        let mut strip_leading_whitespace = whole_first_line_trimmed;
1189        for part in &mut result {
1190            match part {
1191                StrippedCommandPart::Text(text) => {
1192                    let mut offset = 0;
1193                    while let Some(next) = text[offset..].find('\n') {
1194                        let next = next + offset;
1195                        if offset > 0 {
1196                            strip_leading_whitespace = true;
1197                        }
1198
1199                        if !strip_leading_whitespace {
1200                            offset = next + 1;
1201                            continue;
1202                        }
1203
1204                        let line = &text[offset..next];
1205                        let line = line.strip_suffix('\r').unwrap_or(line);
1206                        let len = line.len().min(num_stripped_chars);
1207                        text.replace_range(offset..offset + len, "");
1208                        offset = next + 1 - len;
1209                    }
1210
1211                    // Replace any remaining text
1212                    if strip_leading_whitespace || offset > 0 {
1213                        let line = &text[offset..];
1214                        let line = line.strip_suffix('\r').unwrap_or(line);
1215                        let len = line.len().min(num_stripped_chars);
1216                        text.replace_range(offset..offset + len, "");
1217                    }
1218                }
1219                StrippedCommandPart::Placeholder(_) => {
1220                    strip_leading_whitespace = false;
1221                }
1222            }
1223        }
1224
1225        Some(result)
1226    }
1227
1228    /// Gets the parent of the command section.
1229    pub fn parent(&self) -> SectionParent<N> {
1230        SectionParent::cast(self.0.parent().expect("should have a parent"))
1231            .expect("parent should cast")
1232    }
1233}
1234
1235impl<N: TreeNode> AstNode<N> for CommandSection<N> {
1236    fn can_cast(kind: SyntaxKind) -> bool {
1237        kind == SyntaxKind::CommandSectionNode
1238    }
1239
1240    fn cast(inner: N) -> Option<Self> {
1241        match inner.kind() {
1242            SyntaxKind::CommandSectionNode => Some(Self(inner)),
1243            _ => None,
1244        }
1245    }
1246
1247    fn inner(&self) -> &N {
1248        &self.0
1249    }
1250}
1251
1252/// Represents a textual part of a command.
1253#[derive(Clone, Debug, PartialEq, Eq)]
1254pub struct CommandText<T: TreeToken = SyntaxToken>(T);
1255
1256impl<T: TreeToken> CommandText<T> {
1257    /// Unescapes the command text to the given buffer.
1258    ///
1259    /// When `heredoc` is true, only heredoc escape sequences are allowed.
1260    ///
1261    /// Otherwise, brace command sequences are accepted.
1262    pub fn unescape_to(&self, heredoc: bool, buffer: &mut String) {
1263        unescape_command_text(self.text(), heredoc, buffer);
1264    }
1265}
1266
1267impl<T: TreeToken> AstToken<T> for CommandText<T> {
1268    fn can_cast(kind: SyntaxKind) -> bool {
1269        kind == SyntaxKind::LiteralCommandText
1270    }
1271
1272    fn cast(inner: T) -> Option<Self> {
1273        match inner.kind() {
1274            SyntaxKind::LiteralCommandText => Some(Self(inner)),
1275            _ => None,
1276        }
1277    }
1278
1279    fn inner(&self) -> &T {
1280        &self.0
1281    }
1282}
1283
1284/// Represents a part of a command.
1285#[derive(Clone, Debug, PartialEq, Eq)]
1286pub enum CommandPart<N: TreeNode = SyntaxNode> {
1287    /// A textual part of the command.
1288    Text(CommandText<N::Token>),
1289    /// A placeholder encountered in the command.
1290    Placeholder(Placeholder<N>),
1291}
1292
1293impl<N: TreeNode> CommandPart<N> {
1294    /// Unwraps the command part into text.
1295    ///
1296    /// # Panics
1297    ///
1298    /// Panics if the command part is not text.
1299    pub fn unwrap_text(self) -> CommandText<N::Token> {
1300        match self {
1301            Self::Text(text) => text,
1302            _ => panic!("not string text"),
1303        }
1304    }
1305
1306    /// Unwraps the command part into a placeholder.
1307    ///
1308    /// # Panics
1309    ///
1310    /// Panics if the command part is not a placeholder.
1311    pub fn unwrap_placeholder(self) -> Placeholder<N> {
1312        match self {
1313            Self::Placeholder(p) => p,
1314            _ => panic!("not a placeholder"),
1315        }
1316    }
1317
1318    /// Casts the given [`NodeOrToken`] to [`CommandPart`].
1319    ///
1320    /// Returns `None` if it cannot case cannot be cast.
1321    fn cast(element: NodeOrToken<N, N::Token>) -> Option<Self> {
1322        match element {
1323            NodeOrToken::Node(n) => Some(Self::Placeholder(Placeholder::cast(n)?)),
1324            NodeOrToken::Token(t) => Some(Self::Text(CommandText::cast(t)?)),
1325        }
1326    }
1327}
1328
1329/// Represents a requirements section in a task definition.
1330#[derive(Clone, Debug, PartialEq, Eq)]
1331pub struct RequirementsSection<N: TreeNode = SyntaxNode>(N);
1332
1333impl<N: TreeNode> RequirementsSection<N> {
1334    /// Gets the items in the requirements section.
1335    pub fn items(&self) -> impl Iterator<Item = RequirementsItem<N>> + use<'_, N> {
1336        self.children()
1337    }
1338
1339    /// Gets the parent of the requirements section.
1340    pub fn parent(&self) -> SectionParent<N> {
1341        SectionParent::cast(self.0.parent().expect("should have a parent"))
1342            .expect("parent should cast")
1343    }
1344
1345    /// Gets the `container` item as a
1346    /// [`Container`](requirements::item::Container) (if it exists).
1347    pub fn container(&self) -> Option<requirements::item::Container<N>> {
1348        // NOTE: validation should ensure that, at most, one `container` item exists in
1349        // the `requirements` section.
1350        self.child()
1351    }
1352}
1353
1354impl<N: TreeNode> AstNode<N> for RequirementsSection<N> {
1355    fn can_cast(kind: SyntaxKind) -> bool {
1356        kind == SyntaxKind::RequirementsSectionNode
1357    }
1358
1359    fn cast(inner: N) -> Option<Self> {
1360        match inner.kind() {
1361            SyntaxKind::RequirementsSectionNode => Some(Self(inner)),
1362            _ => None,
1363        }
1364    }
1365
1366    fn inner(&self) -> &N {
1367        &self.0
1368    }
1369}
1370
1371/// Represents an item in a requirements section.
1372#[derive(Clone, Debug, PartialEq, Eq)]
1373pub struct RequirementsItem<N: TreeNode = SyntaxNode>(N);
1374
1375impl<N: TreeNode> RequirementsItem<N> {
1376    /// Gets the name of the requirements item.
1377    pub fn name(&self) -> Ident<N::Token> {
1378        self.token().expect("expected an item name")
1379    }
1380
1381    /// Gets the expression of the requirements item.
1382    pub fn expr(&self) -> Expr<N> {
1383        Expr::child(&self.0).expect("expected an item expression")
1384    }
1385
1386    /// Consumes `self` and attempts to cast the requirements item to a
1387    /// [`Container`](requirements::item::Container).
1388    pub fn into_container(self) -> Option<requirements::item::Container<N>> {
1389        requirements::item::Container::try_from(self).ok()
1390    }
1391}
1392
1393impl<N: TreeNode> AstNode<N> for RequirementsItem<N> {
1394    fn can_cast(kind: SyntaxKind) -> bool {
1395        kind == SyntaxKind::RequirementsItemNode
1396    }
1397
1398    fn cast(inner: N) -> Option<Self> {
1399        match inner.kind() {
1400            SyntaxKind::RequirementsItemNode => Some(Self(inner)),
1401            _ => None,
1402        }
1403    }
1404
1405    fn inner(&self) -> &N {
1406        &self.0
1407    }
1408}
1409
1410/// Represents a hints section in a task definition.
1411#[derive(Clone, Debug, PartialEq, Eq)]
1412pub struct TaskHintsSection<N: TreeNode = SyntaxNode>(N);
1413
1414impl<N: TreeNode> TaskHintsSection<N> {
1415    /// Gets the items in the hints section.
1416    pub fn items(&self) -> impl Iterator<Item = TaskHintsItem<N>> + use<'_, N> {
1417        self.children()
1418    }
1419
1420    /// Gets the parent of the hints section.
1421    pub fn parent(&self) -> TaskDefinition<N> {
1422        TaskDefinition::cast(self.0.parent().expect("should have a parent"))
1423            .expect("parent should cast")
1424    }
1425}
1426
1427impl<N: TreeNode> AstNode<N> for TaskHintsSection<N> {
1428    fn can_cast(kind: SyntaxKind) -> bool {
1429        kind == SyntaxKind::TaskHintsSectionNode
1430    }
1431
1432    fn cast(inner: N) -> Option<Self> {
1433        match inner.kind() {
1434            SyntaxKind::TaskHintsSectionNode => Some(Self(inner)),
1435            _ => None,
1436        }
1437    }
1438
1439    fn inner(&self) -> &N {
1440        &self.0
1441    }
1442}
1443
1444/// Represents an item in a task hints section.
1445#[derive(Clone, Debug, PartialEq, Eq)]
1446pub struct TaskHintsItem<N: TreeNode = SyntaxNode>(N);
1447
1448impl<N: TreeNode> TaskHintsItem<N> {
1449    /// Gets the name of the hints item.
1450    pub fn name(&self) -> Ident<N::Token> {
1451        self.token().expect("expected an item name")
1452    }
1453
1454    /// Gets the expression of the hints item.
1455    pub fn expr(&self) -> Expr<N> {
1456        Expr::child(&self.0).expect("expected an item expression")
1457    }
1458}
1459
1460impl<N: TreeNode> AstNode<N> for TaskHintsItem<N> {
1461    fn can_cast(kind: SyntaxKind) -> bool {
1462        kind == SyntaxKind::TaskHintsItemNode
1463    }
1464
1465    fn cast(inner: N) -> Option<Self> {
1466        match inner.kind() {
1467            SyntaxKind::TaskHintsItemNode => Some(Self(inner)),
1468            _ => None,
1469        }
1470    }
1471
1472    fn inner(&self) -> &N {
1473        &self.0
1474    }
1475}
1476
1477/// Represents a runtime section in a task definition.
1478#[derive(Clone, Debug, PartialEq, Eq)]
1479pub struct RuntimeSection<N: TreeNode = SyntaxNode>(N);
1480
1481impl<N: TreeNode> RuntimeSection<N> {
1482    /// Gets the items in the runtime section.
1483    pub fn items(&self) -> impl Iterator<Item = RuntimeItem<N>> + use<'_, N> {
1484        self.children()
1485    }
1486
1487    /// Gets the parent of the runtime section.
1488    pub fn parent(&self) -> SectionParent<N> {
1489        SectionParent::cast(self.0.parent().expect("should have a parent"))
1490            .expect("parent should cast")
1491    }
1492
1493    /// Gets the `container` item as a [`Container`](runtime::item::Container)
1494    /// (if it exists).
1495    pub fn container(&self) -> Option<runtime::item::Container<N>> {
1496        // NOTE: validation should ensure that, at most, one `container`/`docker` item
1497        // exists in the `runtime` section.
1498        self.child()
1499    }
1500}
1501
1502impl<N: TreeNode> AstNode<N> for RuntimeSection<N> {
1503    fn can_cast(kind: SyntaxKind) -> bool {
1504        kind == SyntaxKind::RuntimeSectionNode
1505    }
1506
1507    fn cast(inner: N) -> Option<Self> {
1508        match inner.kind() {
1509            SyntaxKind::RuntimeSectionNode => Some(Self(inner)),
1510            _ => None,
1511        }
1512    }
1513
1514    fn inner(&self) -> &N {
1515        &self.0
1516    }
1517}
1518
1519/// Represents an item in a runtime section.
1520#[derive(Clone, Debug, PartialEq, Eq)]
1521pub struct RuntimeItem<N: TreeNode = SyntaxNode>(N);
1522
1523impl<N: TreeNode> RuntimeItem<N> {
1524    /// Gets the name of the runtime item.
1525    pub fn name(&self) -> Ident<N::Token> {
1526        self.token().expect("expected an item name")
1527    }
1528
1529    /// Gets the expression of the runtime item.
1530    pub fn expr(&self) -> Expr<N> {
1531        Expr::child(&self.0).expect("expected an item expression")
1532    }
1533
1534    /// Consumes `self` and attempts to cast the runtime item to a
1535    /// [`Container`](runtime::item::Container).
1536    pub fn into_container(self) -> Option<runtime::item::Container<N>> {
1537        runtime::item::Container::try_from(self).ok()
1538    }
1539}
1540
1541impl<N: TreeNode> AstNode<N> for RuntimeItem<N> {
1542    fn can_cast(kind: SyntaxKind) -> bool {
1543        kind == SyntaxKind::RuntimeItemNode
1544    }
1545
1546    fn cast(inner: N) -> Option<Self> {
1547        match inner.kind() {
1548            SyntaxKind::RuntimeItemNode => Some(Self(inner)),
1549            _ => None,
1550        }
1551    }
1552
1553    fn inner(&self) -> &N {
1554        &self.0
1555    }
1556}
1557
1558/// Represents a metadata section in a task or workflow definition.
1559#[derive(Clone, Debug, PartialEq, Eq)]
1560pub struct MetadataSection<N: TreeNode = SyntaxNode>(N);
1561
1562impl<N: TreeNode> MetadataSection<N> {
1563    /// Gets the items of the metadata section.
1564    pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
1565        self.children()
1566    }
1567
1568    /// Gets the parent of the metadata section.
1569    pub fn parent(&self) -> SectionParent<N> {
1570        SectionParent::cast(self.0.parent().expect("should have a parent"))
1571            .expect("parent should cast")
1572    }
1573}
1574
1575impl<N: TreeNode> AstNode<N> for MetadataSection<N> {
1576    fn can_cast(kind: SyntaxKind) -> bool {
1577        kind == SyntaxKind::MetadataSectionNode
1578    }
1579
1580    fn cast(inner: N) -> Option<Self> {
1581        match inner.kind() {
1582            SyntaxKind::MetadataSectionNode => Some(Self(inner)),
1583            _ => None,
1584        }
1585    }
1586
1587    fn inner(&self) -> &N {
1588        &self.0
1589    }
1590}
1591
1592/// Represents a metadata object item.
1593#[derive(Clone, Debug, PartialEq, Eq)]
1594pub struct MetadataObjectItem<N: TreeNode = SyntaxNode>(N);
1595
1596impl<N: TreeNode> MetadataObjectItem<N> {
1597    /// Gets the name of the item.
1598    pub fn name(&self) -> Ident<N::Token> {
1599        self.token().expect("expected a name")
1600    }
1601
1602    /// Gets the value of the item.
1603    pub fn value(&self) -> MetadataValue<N> {
1604        self.child().expect("expected a value")
1605    }
1606}
1607
1608impl<N: TreeNode> AstNode<N> for MetadataObjectItem<N> {
1609    fn can_cast(kind: SyntaxKind) -> bool {
1610        kind == SyntaxKind::MetadataObjectItemNode
1611    }
1612
1613    fn cast(inner: N) -> Option<Self> {
1614        match inner.kind() {
1615            SyntaxKind::MetadataObjectItemNode => Some(Self(inner)),
1616            _ => None,
1617        }
1618    }
1619
1620    fn inner(&self) -> &N {
1621        &self.0
1622    }
1623}
1624
1625/// Represents a metadata value.
1626#[derive(Clone, Debug, PartialEq, Eq)]
1627pub enum MetadataValue<N: TreeNode = SyntaxNode> {
1628    /// The value is a literal boolean.
1629    Boolean(LiteralBoolean<N>),
1630    /// The value is a literal integer.
1631    Integer(LiteralInteger<N>),
1632    /// The value is a literal float.
1633    Float(LiteralFloat<N>),
1634    /// The value is a literal string.
1635    String(LiteralString<N>),
1636    /// The value is a literal null.
1637    Null(LiteralNull<N>),
1638    /// The value is a metadata object.
1639    Object(MetadataObject<N>),
1640    /// The value is a metadata array.
1641    Array(MetadataArray<N>),
1642}
1643
1644impl<N: TreeNode> MetadataValue<N> {
1645    /// Unwraps the metadata value into a boolean.
1646    ///
1647    /// # Panics
1648    ///
1649    /// Panics if the metadata value is not a boolean.
1650    pub fn unwrap_boolean(self) -> LiteralBoolean<N> {
1651        match self {
1652            Self::Boolean(b) => b,
1653            _ => panic!("not a boolean"),
1654        }
1655    }
1656
1657    /// Unwraps the metadata value into an integer.
1658    ///
1659    /// # Panics
1660    ///
1661    /// Panics if the metadata value is not an integer.
1662    pub fn unwrap_integer(self) -> LiteralInteger<N> {
1663        match self {
1664            Self::Integer(i) => i,
1665            _ => panic!("not an integer"),
1666        }
1667    }
1668
1669    /// Unwraps the metadata value into a float.
1670    ///
1671    /// # Panics
1672    ///
1673    /// Panics if the metadata value is not a float.
1674    pub fn unwrap_float(self) -> LiteralFloat<N> {
1675        match self {
1676            Self::Float(f) => f,
1677            _ => panic!("not a float"),
1678        }
1679    }
1680
1681    /// Unwraps the metadata value into a string.
1682    ///
1683    /// # Panics
1684    ///
1685    /// Panics if the metadata value is not a string.
1686    pub fn unwrap_string(self) -> LiteralString<N> {
1687        match self {
1688            Self::String(s) => s,
1689            _ => panic!("not a string"),
1690        }
1691    }
1692
1693    /// Unwraps the metadata value into a null.
1694    ///
1695    /// # Panics
1696    ///
1697    /// Panics if the metadata value is not a null.
1698    pub fn unwrap_null(self) -> LiteralNull<N> {
1699        match self {
1700            Self::Null(n) => n,
1701            _ => panic!("not a null"),
1702        }
1703    }
1704
1705    /// Unwraps the metadata value into an object.
1706    ///
1707    /// # Panics
1708    ///
1709    /// Panics if the metadata value is not an object.
1710    pub fn unwrap_object(self) -> MetadataObject<N> {
1711        match self {
1712            Self::Object(o) => o,
1713            _ => panic!("not an object"),
1714        }
1715    }
1716
1717    /// Unwraps the metadata value into an array.
1718    ///
1719    /// # Panics
1720    ///
1721    /// Panics if the metadata value is not an array.
1722    pub fn unwrap_array(self) -> MetadataArray<N> {
1723        match self {
1724            Self::Array(a) => a,
1725            _ => panic!("not an array"),
1726        }
1727    }
1728}
1729
1730impl<N: TreeNode> AstNode<N> for MetadataValue<N> {
1731    fn can_cast(kind: SyntaxKind) -> bool {
1732        matches!(
1733            kind,
1734            SyntaxKind::LiteralBooleanNode
1735                | SyntaxKind::LiteralIntegerNode
1736                | SyntaxKind::LiteralFloatNode
1737                | SyntaxKind::LiteralStringNode
1738                | SyntaxKind::LiteralNullNode
1739                | SyntaxKind::MetadataObjectNode
1740                | SyntaxKind::MetadataArrayNode
1741        )
1742    }
1743
1744    fn cast(inner: N) -> Option<Self> {
1745        match inner.kind() {
1746            SyntaxKind::LiteralBooleanNode => Some(Self::Boolean(LiteralBoolean(inner))),
1747            SyntaxKind::LiteralIntegerNode => Some(Self::Integer(LiteralInteger(inner))),
1748            SyntaxKind::LiteralFloatNode => Some(Self::Float(LiteralFloat(inner))),
1749            SyntaxKind::LiteralStringNode => Some(Self::String(LiteralString(inner))),
1750            SyntaxKind::LiteralNullNode => Some(Self::Null(LiteralNull(inner))),
1751            SyntaxKind::MetadataObjectNode => Some(Self::Object(MetadataObject(inner))),
1752            SyntaxKind::MetadataArrayNode => Some(Self::Array(MetadataArray(inner))),
1753            _ => None,
1754        }
1755    }
1756
1757    fn inner(&self) -> &N {
1758        match self {
1759            Self::Boolean(b) => &b.0,
1760            Self::Integer(i) => &i.0,
1761            Self::Float(f) => &f.0,
1762            Self::String(s) => &s.0,
1763            Self::Null(n) => &n.0,
1764            Self::Object(o) => &o.0,
1765            Self::Array(a) => &a.0,
1766        }
1767    }
1768}
1769
1770/// Represents a literal null.
1771#[derive(Clone, Debug, PartialEq, Eq)]
1772pub struct LiteralNull<N: TreeNode = SyntaxNode>(N);
1773
1774impl<N: TreeNode> AstNode<N> for LiteralNull<N> {
1775    fn can_cast(kind: SyntaxKind) -> bool {
1776        kind == SyntaxKind::LiteralNullNode
1777    }
1778
1779    fn cast(inner: N) -> Option<Self> {
1780        match inner.kind() {
1781            SyntaxKind::LiteralNullNode => Some(Self(inner)),
1782            _ => None,
1783        }
1784    }
1785
1786    fn inner(&self) -> &N {
1787        &self.0
1788    }
1789}
1790
1791/// Represents a metadata object.
1792#[derive(Clone, Debug, PartialEq, Eq)]
1793pub struct MetadataObject<N: TreeNode = SyntaxNode>(N);
1794
1795impl<N: TreeNode> MetadataObject<N> {
1796    /// Gets the items of the metadata object.
1797    pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
1798        self.children()
1799    }
1800}
1801
1802impl<N: TreeNode> AstNode<N> for MetadataObject<N> {
1803    fn can_cast(kind: SyntaxKind) -> bool {
1804        kind == SyntaxKind::MetadataObjectNode
1805    }
1806
1807    fn cast(inner: N) -> Option<Self> {
1808        match inner.kind() {
1809            SyntaxKind::MetadataObjectNode => Some(Self(inner)),
1810            _ => None,
1811        }
1812    }
1813
1814    fn inner(&self) -> &N {
1815        &self.0
1816    }
1817}
1818
1819/// Represents a metadata array.
1820#[derive(Clone, Debug, PartialEq, Eq)]
1821pub struct MetadataArray<N: TreeNode = SyntaxNode>(N);
1822
1823impl<N: TreeNode> MetadataArray<N> {
1824    /// Gets the elements of the metadata array.
1825    pub fn elements(&self) -> impl Iterator<Item = MetadataValue<N>> + use<'_, N> {
1826        self.children()
1827    }
1828}
1829
1830impl<N: TreeNode> AstNode<N> for MetadataArray<N> {
1831    fn can_cast(kind: SyntaxKind) -> bool {
1832        kind == SyntaxKind::MetadataArrayNode
1833    }
1834
1835    fn cast(inner: N) -> Option<Self> {
1836        match inner.kind() {
1837            SyntaxKind::MetadataArrayNode => Some(Self(inner)),
1838            _ => None,
1839        }
1840    }
1841
1842    fn inner(&self) -> &N {
1843        &self.0
1844    }
1845}
1846
1847/// Represents a parameter metadata section in a task or workflow definition.
1848#[derive(Clone, Debug, PartialEq, Eq)]
1849pub struct ParameterMetadataSection<N: TreeNode = SyntaxNode>(N);
1850
1851impl<N: TreeNode> ParameterMetadataSection<N> {
1852    /// Gets the items of the parameter metadata section.
1853    pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
1854        self.children()
1855    }
1856
1857    /// Gets the parent of the parameter metadata section.
1858    pub fn parent(&self) -> SectionParent<N> {
1859        SectionParent::cast(self.0.parent().expect("should have a parent"))
1860            .expect("parent should cast")
1861    }
1862}
1863
1864impl<N: TreeNode> AstNode<N> for ParameterMetadataSection<N> {
1865    fn can_cast(kind: SyntaxKind) -> bool {
1866        kind == SyntaxKind::ParameterMetadataSectionNode
1867    }
1868
1869    fn cast(inner: N) -> Option<Self> {
1870        match inner.kind() {
1871            SyntaxKind::ParameterMetadataSectionNode => Some(Self(inner)),
1872            _ => None,
1873        }
1874    }
1875
1876    fn inner(&self) -> &N {
1877        &self.0
1878    }
1879}
1880
1881#[cfg(test)]
1882mod test {
1883    use pretty_assertions::assert_eq;
1884
1885    use super::*;
1886    use crate::Document;
1887
1888    #[test]
1889    fn tasks() {
1890        let (document, diagnostics) = Document::parse(
1891            r#"
1892version 1.2
1893
1894task test {
1895    input {
1896        String name
1897    }
1898
1899    output {
1900        String output = stdout()
1901    }
1902
1903    command <<<
1904        printf "hello, ~{name}!
1905    >>>
1906
1907    requirements {
1908        container: "baz/qux"
1909    }
1910
1911    hints {
1912        foo: "bar"
1913    }
1914
1915    runtime {
1916        container: "foo/bar"
1917    }
1918
1919    meta {
1920        description: "a test"
1921        foo: null
1922    }
1923
1924    parameter_meta {
1925        name: {
1926            help: "a name to greet"
1927        }
1928    }
1929
1930    String x = "private"
1931}
1932"#,
1933        );
1934
1935        assert!(diagnostics.is_empty());
1936        let ast = document.ast();
1937        let ast = ast.as_v1().expect("should be a V1 AST");
1938        let tasks: Vec<_> = ast.tasks().collect();
1939        assert_eq!(tasks.len(), 1);
1940        assert_eq!(tasks[0].name().text(), "test");
1941
1942        // Task input
1943        let input = tasks[0].input().expect("should have an input section");
1944        assert_eq!(input.parent().unwrap_task().name().text(), "test");
1945        let decls: Vec<_> = input.declarations().collect();
1946        assert_eq!(decls.len(), 1);
1947        assert_eq!(
1948            decls[0].clone().unwrap_unbound_decl().ty().to_string(),
1949            "String"
1950        );
1951        assert_eq!(decls[0].clone().unwrap_unbound_decl().name().text(), "name");
1952
1953        // Task output
1954        let output = tasks[0].output().expect("should have an output section");
1955        assert_eq!(output.parent().unwrap_task().name().text(), "test");
1956        let decls: Vec<_> = output.declarations().collect();
1957        assert_eq!(decls.len(), 1);
1958        assert_eq!(decls[0].ty().to_string(), "String");
1959        assert_eq!(decls[0].name().text(), "output");
1960        assert_eq!(decls[0].expr().unwrap_call().target().text(), "stdout");
1961
1962        // Task command
1963        let command = tasks[0].command().expect("should have a command section");
1964        assert_eq!(command.parent().name().text(), "test");
1965        assert!(command.is_heredoc());
1966        let parts: Vec<_> = command.parts().collect();
1967        assert_eq!(parts.len(), 3);
1968        assert_eq!(
1969            parts[0].clone().unwrap_text().text(),
1970            "\n        printf \"hello, "
1971        );
1972        assert_eq!(
1973            parts[1]
1974                .clone()
1975                .unwrap_placeholder()
1976                .expr()
1977                .unwrap_name_ref()
1978                .name()
1979                .text(),
1980            "name"
1981        );
1982        assert_eq!(parts[2].clone().unwrap_text().text(), "!\n    ");
1983
1984        // Task requirements
1985        let requirements = tasks[0]
1986            .requirements()
1987            .expect("should have a requirements section");
1988        assert_eq!(requirements.parent().name().text(), "test");
1989        let items: Vec<_> = requirements.items().collect();
1990        assert_eq!(items.len(), 1);
1991        assert_eq!(items[0].name().text(), TASK_REQUIREMENT_CONTAINER);
1992        assert_eq!(
1993            items[0]
1994                .expr()
1995                .unwrap_literal()
1996                .unwrap_string()
1997                .text()
1998                .unwrap()
1999                .text(),
2000            "baz/qux"
2001        );
2002
2003        // Task hints
2004        let hints = tasks[0].hints().expect("should have a hints section");
2005        assert_eq!(hints.parent().name().text(), "test");
2006        let items: Vec<_> = hints.items().collect();
2007        assert_eq!(items.len(), 1);
2008        assert_eq!(items[0].name().text(), "foo");
2009        assert_eq!(
2010            items[0]
2011                .expr()
2012                .unwrap_literal()
2013                .unwrap_string()
2014                .text()
2015                .unwrap()
2016                .text(),
2017            "bar"
2018        );
2019
2020        // Task runtimes
2021        let runtime = tasks[0].runtime().expect("should have a runtime section");
2022        assert_eq!(runtime.parent().name().text(), "test");
2023        let items: Vec<_> = runtime.items().collect();
2024        assert_eq!(items.len(), 1);
2025        assert_eq!(items[0].name().text(), TASK_REQUIREMENT_CONTAINER);
2026        assert_eq!(
2027            items[0]
2028                .expr()
2029                .unwrap_literal()
2030                .unwrap_string()
2031                .text()
2032                .unwrap()
2033                .text(),
2034            "foo/bar"
2035        );
2036
2037        // Task metadata
2038        let metadata = tasks[0].metadata().expect("should have a metadata section");
2039        assert_eq!(metadata.parent().unwrap_task().name().text(), "test");
2040        let items: Vec<_> = metadata.items().collect();
2041        assert_eq!(items.len(), 2);
2042        assert_eq!(items[0].name().text(), "description");
2043        assert_eq!(
2044            items[0].value().unwrap_string().text().unwrap().text(),
2045            "a test"
2046        );
2047
2048        // Second metadata
2049        assert_eq!(items[1].name().text(), "foo");
2050        items[1].value().unwrap_null();
2051
2052        // Task parameter metadata
2053        let param_meta = tasks[0]
2054            .parameter_metadata()
2055            .expect("should have a parameter metadata section");
2056        assert_eq!(param_meta.parent().unwrap_task().name().text(), "test");
2057        let items: Vec<_> = param_meta.items().collect();
2058        assert_eq!(items.len(), 1);
2059        assert_eq!(items[0].name().text(), "name");
2060        let items: Vec<_> = items[0].value().unwrap_object().items().collect();
2061        assert_eq!(items.len(), 1);
2062        assert_eq!(items[0].name().text(), "help");
2063        assert_eq!(
2064            items[0].value().unwrap_string().text().unwrap().text(),
2065            "a name to greet"
2066        );
2067
2068        // Task declarations
2069        let decls: Vec<_> = tasks[0].declarations().collect();
2070        assert_eq!(decls.len(), 1);
2071
2072        // First task declaration
2073        assert_eq!(decls[0].ty().to_string(), "String");
2074        assert_eq!(decls[0].name().text(), "x");
2075        assert_eq!(
2076            decls[0]
2077                .expr()
2078                .unwrap_literal()
2079                .unwrap_string()
2080                .text()
2081                .unwrap()
2082                .text(),
2083            "private"
2084        );
2085    }
2086
2087    #[test]
2088    fn whitespace_stripping_without_interpolation() {
2089        let (document, diagnostics) = Document::parse(
2090            r#"
2091version 1.2
2092
2093task test {
2094    command <<<
2095        echo "hello"
2096        echo "world"
2097        echo \
2098            "goodbye"
2099    >>>
2100}
2101"#,
2102        );
2103
2104        assert!(diagnostics.is_empty());
2105        let ast = document.ast();
2106        let ast = ast.as_v1().expect("should be a V1 AST");
2107        let tasks: Vec<_> = ast.tasks().collect();
2108        assert_eq!(tasks.len(), 1);
2109
2110        let command = tasks[0].command().expect("should have a command section");
2111
2112        let stripped = command.strip_whitespace().unwrap();
2113
2114        assert_eq!(stripped.len(), 1);
2115        let text = match &stripped[0] {
2116            StrippedCommandPart::Text(text) => text,
2117            _ => panic!("expected text"),
2118        };
2119        assert_eq!(
2120            text,
2121            "echo \"hello\"\necho \"world\"\necho \\\n    \"goodbye\""
2122        );
2123    }
2124
2125    #[test]
2126    fn whitespace_stripping_with_interpolation() {
2127        let (document, diagnostics) = Document::parse(
2128            r#"
2129version 1.2
2130
2131task test {
2132    input {
2133        String name
2134        Boolean flag
2135    }
2136
2137    command <<<
2138        echo "hello, ~{
2139if flag
2140then name
2141               else "Jerry"
2142    }!"
2143    >>>
2144}
2145    "#,
2146        );
2147
2148        assert!(diagnostics.is_empty());
2149        let ast = document.ast();
2150        let ast = ast.as_v1().expect("should be a V1 AST");
2151        let tasks: Vec<_> = ast.tasks().collect();
2152        assert_eq!(tasks.len(), 1);
2153
2154        let command = tasks[0].command().expect("should have a command section");
2155
2156        let stripped = command.strip_whitespace().unwrap();
2157        assert_eq!(stripped.len(), 3);
2158        let text = match &stripped[0] {
2159            StrippedCommandPart::Text(text) => text,
2160            _ => panic!("expected text"),
2161        };
2162        assert_eq!(text, "echo \"hello, ");
2163
2164        let _placeholder = match &stripped[1] {
2165            StrippedCommandPart::Placeholder(p) => p,
2166            _ => panic!("expected placeholder"),
2167        };
2168        // not testing anything with the placeholder, just making sure it's there
2169
2170        let text = match &stripped[2] {
2171            StrippedCommandPart::Text(text) => text,
2172            _ => panic!("expected text"),
2173        };
2174        assert_eq!(text, "!\"");
2175    }
2176
2177    #[test]
2178    fn whitespace_stripping_when_interpolation_starts_line() {
2179        let (document, diagnostics) = Document::parse(
2180            r#"
2181version 1.2
2182
2183task test {
2184    input {
2185      Int placeholder
2186    }
2187
2188    command <<<
2189            # other weird whitspace
2190      ~{placeholder} "$trailing_pholder" ~{placeholder}
2191      ~{placeholder} somecommand.py "$leading_pholder"
2192    >>>
2193}
2194"#,
2195        );
2196
2197        assert!(diagnostics.is_empty());
2198        let ast = document.ast();
2199        let ast = ast.as_v1().expect("should be a V1 AST");
2200        let tasks: Vec<_> = ast.tasks().collect();
2201        assert_eq!(tasks.len(), 1);
2202
2203        let command = tasks[0].command().expect("should have a command section");
2204
2205        let stripped = command.strip_whitespace().unwrap();
2206        assert_eq!(stripped.len(), 7);
2207        let text = match &stripped[0] {
2208            StrippedCommandPart::Text(text) => text,
2209            _ => panic!("expected text"),
2210        };
2211        assert_eq!(text, "      # other weird whitspace\n");
2212
2213        let _placeholder = match &stripped[1] {
2214            StrippedCommandPart::Placeholder(p) => p,
2215            _ => panic!("expected placeholder"),
2216        };
2217        // not testing anything with the placeholder, just making sure it's there
2218
2219        let text = match &stripped[2] {
2220            StrippedCommandPart::Text(text) => text,
2221            _ => panic!("expected text"),
2222        };
2223        assert_eq!(text, " \"$trailing_pholder\" ");
2224
2225        let _placeholder = match &stripped[3] {
2226            StrippedCommandPart::Placeholder(p) => p,
2227            _ => panic!("expected placeholder"),
2228        };
2229        // not testing anything with the placeholder, just making sure it's there
2230
2231        let text = match &stripped[4] {
2232            StrippedCommandPart::Text(text) => text,
2233            _ => panic!("expected text"),
2234        };
2235        assert_eq!(text, "\n");
2236
2237        let _placeholder = match &stripped[5] {
2238            StrippedCommandPart::Placeholder(p) => p,
2239            _ => panic!("expected placeholder"),
2240        };
2241        // not testing anything with the placeholder, just making sure it's there
2242
2243        let text = match &stripped[6] {
2244            StrippedCommandPart::Text(text) => text,
2245            _ => panic!("expected text"),
2246        };
2247        assert_eq!(text, " somecommand.py \"$leading_pholder\"");
2248    }
2249
2250    #[test]
2251    fn whitespace_stripping_when_command_is_empty() {
2252        let (document, diagnostics) = Document::parse(
2253            r#"
2254version 1.2
2255
2256task test {
2257    command <<<>>>
2258}
2259    "#,
2260        );
2261
2262        assert!(diagnostics.is_empty());
2263        let ast = document.ast();
2264        let ast = ast.as_v1().expect("should be a V1 AST");
2265        let tasks: Vec<_> = ast.tasks().collect();
2266        assert_eq!(tasks.len(), 1);
2267
2268        let command = tasks[0].command().expect("should have a command section");
2269
2270        let stripped = command.strip_whitespace().unwrap();
2271        assert_eq!(stripped.len(), 0);
2272    }
2273
2274    #[test]
2275    fn whitespace_stripping_when_command_is_one_line_of_whitespace() {
2276        let (document, diagnostics) = Document::parse(
2277            r#"
2278version 1.2
2279
2280task test {
2281    command <<<     >>>
2282}
2283    "#,
2284        );
2285
2286        assert!(diagnostics.is_empty());
2287        let ast = document.ast();
2288        let ast = ast.as_v1().expect("should be a V1 AST");
2289        let tasks: Vec<_> = ast.tasks().collect();
2290        assert_eq!(tasks.len(), 1);
2291
2292        let command = tasks[0].command().expect("should have a command section");
2293
2294        let stripped = command.strip_whitespace().unwrap();
2295        assert_eq!(stripped.len(), 1);
2296        let text = match &stripped[0] {
2297            StrippedCommandPart::Text(text) => text,
2298            _ => panic!("expected text"),
2299        };
2300        assert_eq!(text, "");
2301    }
2302
2303    #[test]
2304    fn whitespace_stripping_when_command_is_one_newline() {
2305        let (document, diagnostics) = Document::parse(
2306            r#"
2307version 1.2
2308
2309task test {
2310    command <<<
2311    >>>
2312}
2313    "#,
2314        );
2315
2316        assert!(diagnostics.is_empty());
2317        let ast = document.ast();
2318        let ast = ast.as_v1().expect("should be a V1 AST");
2319        let tasks: Vec<_> = ast.tasks().collect();
2320        assert_eq!(tasks.len(), 1);
2321
2322        let command = tasks[0].command().expect("should have a command section");
2323
2324        let stripped = command.strip_whitespace().unwrap();
2325        assert_eq!(stripped.len(), 1);
2326        let text = match &stripped[0] {
2327            StrippedCommandPart::Text(text) => text,
2328            _ => panic!("expected text"),
2329        };
2330        assert_eq!(text, "");
2331    }
2332
2333    #[test]
2334    fn whitespace_stripping_when_command_is_a_blank_line() {
2335        let (document, diagnostics) = Document::parse(
2336            r#"
2337version 1.2
2338
2339task test {
2340    command <<<
2341
2342    >>>
2343}
2344    "#,
2345        );
2346
2347        assert!(diagnostics.is_empty());
2348        let ast = document.ast();
2349        let ast = ast.as_v1().expect("should be a V1 AST");
2350        let tasks: Vec<_> = ast.tasks().collect();
2351        assert_eq!(tasks.len(), 1);
2352
2353        let command = tasks[0].command().expect("should have a command section");
2354
2355        let stripped = command.strip_whitespace().unwrap();
2356        assert_eq!(stripped.len(), 1);
2357        let text = match &stripped[0] {
2358            StrippedCommandPart::Text(text) => text,
2359            _ => panic!("expected text"),
2360        };
2361        assert_eq!(text, "");
2362    }
2363
2364    #[test]
2365    fn whitespace_stripping_when_command_is_a_blank_line_with_spaces() {
2366        let (document, diagnostics) = Document::parse(
2367            r#"
2368version 1.2
2369
2370task test {
2371    command <<<
2372    
2373    >>>
2374}
2375    "#,
2376        );
2377
2378        assert!(diagnostics.is_empty());
2379        let ast = document.ast();
2380        let ast = ast.as_v1().expect("should be a V1 AST");
2381        let tasks: Vec<_> = ast.tasks().collect();
2382        assert_eq!(tasks.len(), 1);
2383
2384        let command = tasks[0].command().expect("should have a command section");
2385
2386        let stripped = command.strip_whitespace().unwrap();
2387        assert_eq!(stripped.len(), 1);
2388        let text = match &stripped[0] {
2389            StrippedCommandPart::Text(text) => text,
2390            _ => panic!("expected text"),
2391        };
2392        assert_eq!(text, "    ");
2393    }
2394
2395    #[test]
2396    fn whitespace_stripping_with_mixed_indentation() {
2397        let (document, diagnostics) = Document::parse(
2398            r#"
2399version 1.2
2400
2401task test {
2402    command <<<
2403        echo "hello"
2404			echo "world"
2405        echo \
2406            "goodbye"
2407    >>>
2408        }"#,
2409        );
2410
2411        assert!(diagnostics.is_empty());
2412        let ast = document.ast();
2413        let ast = ast.as_v1().expect("should be a V1 AST");
2414        let tasks: Vec<_> = ast.tasks().collect();
2415        assert_eq!(tasks.len(), 1);
2416
2417        let command = tasks[0].command().expect("should have a command section");
2418
2419        let stripped = command.strip_whitespace();
2420        assert!(stripped.is_none());
2421    }
2422
2423    #[test]
2424    fn whitespace_stripping_with_funky_indentation() {
2425        let (document, diagnostics) = Document::parse(
2426            r#"
2427version 1.2
2428
2429task test {
2430    command <<<
2431    echo "hello"
2432        echo "world"
2433    echo \
2434            "goodbye"
2435                >>>
2436        }"#,
2437        );
2438
2439        assert!(diagnostics.is_empty());
2440        let ast = document.ast();
2441        let ast = ast.as_v1().expect("should be a V1 AST");
2442        let tasks: Vec<_> = ast.tasks().collect();
2443        assert_eq!(tasks.len(), 1);
2444
2445        let command = tasks[0].command().expect("should have a command section");
2446
2447        let stripped = command.strip_whitespace().unwrap();
2448        assert_eq!(stripped.len(), 1);
2449        let text = match &stripped[0] {
2450            StrippedCommandPart::Text(text) => text,
2451            _ => panic!("expected text"),
2452        };
2453        assert_eq!(
2454            text,
2455            "echo \"hello\"\n    echo \"world\"\necho \\\n        \"goodbye\""
2456        );
2457    }
2458
2459    /// Regression test for issue [#268](https://github.com/stjude-rust-labs/wdl/issues/268).
2460    #[test]
2461    fn whitespace_stripping_with_content_on_first_line() {
2462        let (document, diagnostics) = Document::parse(
2463            r#"
2464version 1.2
2465
2466task test {
2467    command <<<      weird stuff $firstlinelint
2468            # other weird whitespace
2469      somecommand.py $line120 ~{placeholder}
2470    >>>
2471        }"#,
2472        );
2473
2474        assert!(diagnostics.is_empty());
2475        let ast = document.ast();
2476        let ast = ast.as_v1().expect("should be a V1 AST");
2477        let tasks: Vec<_> = ast.tasks().collect();
2478        assert_eq!(tasks.len(), 1);
2479
2480        let command = tasks[0].command().expect("should have a command section");
2481
2482        let stripped = command.strip_whitespace().unwrap();
2483        assert_eq!(stripped.len(), 3);
2484        let text = match &stripped[0] {
2485            StrippedCommandPart::Text(text) => text,
2486            _ => panic!("expected text"),
2487        };
2488        assert_eq!(
2489            text,
2490            "weird stuff $firstlinelint\n      # other weird whitespace\nsomecommand.py $line120 "
2491        );
2492
2493        let _placeholder = match &stripped[1] {
2494            StrippedCommandPart::Placeholder(p) => p,
2495            _ => panic!("expected placeholder"),
2496        };
2497        // not testing anything with the placeholder, just making sure it's there
2498
2499        let text = match &stripped[2] {
2500            StrippedCommandPart::Text(text) => text,
2501            _ => panic!("expected text"),
2502        };
2503        assert_eq!(text, "");
2504    }
2505
2506    #[test]
2507    fn whitespace_stripping_on_windows() {
2508        let (document, diagnostics) = Document::parse(
2509            "version 1.2\r\ntask test {\r\n    command <<<\r\n        echo \"hello\"\r\n    \
2510             >>>\r\n}\r\n",
2511        );
2512
2513        assert!(diagnostics.is_empty());
2514        let ast = document.ast();
2515        let ast = ast.as_v1().expect("should be a V1 AST");
2516        let tasks: Vec<_> = ast.tasks().collect();
2517        assert_eq!(tasks.len(), 1);
2518
2519        let command = tasks[0].command().expect("should have a command section");
2520        let stripped = command.strip_whitespace().unwrap();
2521        assert_eq!(stripped.len(), 1);
2522        let text = match &stripped[0] {
2523            StrippedCommandPart::Text(text) => text,
2524            _ => panic!("expected text"),
2525        };
2526        assert_eq!(text, "echo \"hello\"");
2527    }
2528}