use rowan::NodeOrToken;
use wdl_grammar::SyntaxTokenExt;
use super::BoundDecl;
use super::Decl;
use super::Expr;
use super::LiteralBoolean;
use super::LiteralFloat;
use super::LiteralInteger;
use super::LiteralString;
use super::OpenHeredoc;
use super::Placeholder;
use super::StructDefinition;
use super::TaskKeyword;
use super::WorkflowDefinition;
use crate::AstNode;
use crate::AstToken;
use crate::Comment;
use crate::Documented;
use crate::Ident;
use crate::SyntaxKind;
use crate::SyntaxNode;
use crate::SyntaxToken;
use crate::TreeNode;
use crate::TreeToken;
use crate::v1::CommandKeyword;
use crate::v1::MetaKeyword;
use crate::v1::ParameterMetaKeyword;
use crate::v1::RequirementsKeyword;
pub mod common;
pub mod requirements;
pub mod runtime;
pub const TASK_FIELDS: &[(&str, &str)] = &[
(TASK_FIELD_NAME, "The task name."),
(
TASK_FIELD_ID,
"A String with the unique ID of the task. The execution engine may choose the format for \
this ID, but it is suggested to include at least the following information:\nThe task \
name\nThe task alias, if it differs from the task name\nThe index of the task instance, \
if it is within a scatter statement",
),
(
TASK_FIELD_CONTAINER,
"The URI String of the container in which the task is executing, or None if the task is \
being executed in the host environment.",
),
(
TASK_FIELD_CPU,
"The allocated number of cpus as a Float. Must be greater than 0.",
),
(
TASK_FIELD_MEMORY,
"The allocated memory in bytes as an Int. Must be greater than 0.",
),
(
TASK_FIELD_GPU,
"An Array[String] with one specification per allocated GPU. The specification is \
execution engine-specific. If no GPUs were allocated, then the value must be an empty \
array.",
),
(
TASK_FIELD_FPGA,
"An Array[String] with one specification per allocated FPGA. The specification is \
execution engine-specific. If no FPGAs were allocated, then the value must be an empty \
array.",
),
(
TASK_FIELD_DISKS,
"A Map[String, Int] with one entry for each disk mount point. The key is the mount point \
and the value is the initial amount of disk space allocated, in bytes. The execution \
engine must, at a minimum, provide one entry for each disk mount point requested, but \
may provide more. The amount of disk space available for a given mount point may \
increase during the lifetime of the task (e.g., autoscaling volumes provided by some \
cloud services).",
),
(
TASK_FIELD_ATTEMPT,
"The current task attempt. The value must be 0 the first time the task is executed, and \
incremented by 1 each time the task is retried (if any).",
),
(
TASK_FIELD_PREVIOUS,
"An Object containing the resource requirements from the previous task attempt. Available \
in requirements, hints, runtime, output, and command sections. All constituent members \
are optional and `None` on the first attempt.",
),
(
TASK_FIELD_END_TIME,
"An Int? whose value is the time by which the task must be completed, as a Unix time \
stamp. A value of 0 means that the execution engine does not impose a time limit. A \
value of None means that the execution engine cannot determine whether the runtime of \
the task is limited. A positive value is a guarantee that the task will be preempted at \
the specified time, but is not a guarantee that the task won't be preempted earlier.",
),
(
TASK_FIELD_RETURN_CODE,
"An Int? whose value is initially None and is set to the value of the command's return \
code. The value is only guaranteed to be defined in the output section.",
),
(
TASK_FIELD_META,
"An Object containing a copy of the task's meta section, or the empty Object if there is \
no meta section or if it is empty.",
),
(
TASK_FIELD_PARAMETER_META,
"An Object containing a copy of the task's parameter_meta section, or the empty Object if \
there is no parameter_meta section or if it is empty.",
),
(
TASK_FIELD_EXT,
"An Object containing execution engine-specific attributes, or the empty Object if there \
aren't any. Members of ext should be considered optional. It is recommended to only \
access a member of ext using string interpolation to avoid an error if it is not defined.",
),
];
pub const RUNTIME_KEYS: &[(&str, &str)] = &[
(
TASK_REQUIREMENT_CONTAINER,
"Specifies the container image (e.g., Docker, Singularity) to use for the task.",
),
(
TASK_REQUIREMENT_CPU,
"The number of CPU cores required for the task.",
),
(
TASK_REQUIREMENT_MEMORY,
"The amount of memory required, specified as a string with units (e.g., '2 GiB').",
),
(
TASK_REQUIREMENT_DISKS,
"Specifies the disk requirements for the task.",
),
(TASK_REQUIREMENT_GPU, "Specifies GPU requirements."),
];
pub const REQUIREMENTS_KEY: &[(&str, &str)] = &[
(
TASK_REQUIREMENT_CONTAINER,
"Specifies a list of allowed container images. Use `*` to allow any POSIX environment.",
),
(
TASK_REQUIREMENT_CPU,
"The minimum number of CPU cores required.",
),
(
TASK_REQUIREMENT_MEMORY,
"The minimum amount of memory required.",
),
(TASK_REQUIREMENT_GPU, "The minimum GPU requirements."),
(TASK_REQUIREMENT_FPGA, "The minimum FPGA requirements."),
(TASK_REQUIREMENT_DISKS, "The minimum disk requirements."),
(
TASK_REQUIREMENT_MAX_RETRIES,
"The maximum number of times the task can be retried.",
),
(
TASK_REQUIREMENT_RETURN_CODES,
"A list of acceptable return codes from the command.",
),
];
pub const TASK_HINT_KEYS: &[(&str, &str)] = &[
(
TASK_HINT_DISKS,
"A hint to the execution engine to mount disks with specific attributes. The value of \
this hint can be a String with a specification that applies to all mount points, or a \
Map with the key being the mount point and the value being a String with the \
specification for that mount point.",
),
(
TASK_HINT_GPU,
"A hint to the execution engine to provision hardware accelerators with specific \
attributes. Accelerator specifications are left intentionally vague as they are \
primarily intended to be used in the context of a specific compute environment.",
),
(
TASK_HINT_FPGA,
"A hint to the execution engine to provision hardware accelerators with specific \
attributes. Accelerator specifications are left intentionally vague as they are \
primarily intended to be used in the context of a specific compute environment.",
),
(
TASK_HINT_INPUTS,
"Provides input-specific hints. Each key must refer to a parameter defined in the task's \
input section. A key may also used dotted notation to refer to a specific member of a \
struct input.",
),
(
TASK_HINT_LOCALIZATION_OPTIONAL,
"A hint to the execution engine about whether the File inputs for this task need to be \
localized prior to executing the task. The value of this hint is a Boolean for which \
true indicates that the contents of the File inputs may be streamed on demand.",
),
(
TASK_HINT_MAX_CPU,
"A hint to the execution engine that the task expects to use no more than the specified \
number of CPUs. The value of this hint has the same specification as requirements.cpu.",
),
(
TASK_HINT_MAX_MEMORY,
"A hint to the execution engine that the task expects to use no more than the specified \
amount of memory. The value of this hint has the same specification as \
requirements.memory.",
),
(
TASK_HINT_OUTPUTS,
"Provides output-specific hints. Each key must refer to a parameter defined in the task's \
output section. A key may also use dotted notation to refer to a specific member of a \
struct output.",
),
(
TASK_HINT_SHORT_TASK,
"A hint to the execution engine about the expected duration of this task. The value of \
this hint is a Boolean for which true indicates that that this task is not expected to \
take long to execute, which the execution engine can interpret as permission to optimize \
the execution of the task.",
),
(
TASK_HINT_CACHEABLE,
"A hint to the execution engine that the task's execution result is cacheable. The value \
of this hint is a Boolean for which true indicates that the execution result is \
cacheable and false indicates it is not. The default value of the hint depends on the \
engine's configuration.",
),
];
pub const TASK_FIELD_NAME: &str = "name";
pub const TASK_FIELD_ID: &str = "id";
pub const TASK_FIELD_CONTAINER: &str = "container";
pub const TASK_FIELD_CPU: &str = "cpu";
pub const TASK_FIELD_MEMORY: &str = "memory";
pub const TASK_FIELD_ATTEMPT: &str = "attempt";
pub const TASK_FIELD_PREVIOUS: &str = "previous";
pub const TASK_FIELD_GPU: &str = "gpu";
pub const TASK_FIELD_FPGA: &str = "fpga";
pub const TASK_FIELD_DISKS: &str = "disks";
pub const TASK_FIELD_END_TIME: &str = "end_time";
pub const TASK_FIELD_RETURN_CODE: &str = "return_code";
pub const TASK_FIELD_META: &str = "meta";
pub const TASK_FIELD_PARAMETER_META: &str = "parameter_meta";
pub const TASK_FIELD_EXT: &str = "ext";
pub const TASK_FIELD_MAX_RETRIES: &str = "max_retries";
pub const TASK_REQUIREMENT_CONTAINER: &str = "container";
pub const TASK_REQUIREMENT_CONTAINER_ALIAS: &str = "docker";
pub const TASK_REQUIREMENT_CPU: &str = "cpu";
pub const TASK_REQUIREMENT_DISKS: &str = "disks";
pub const TASK_REQUIREMENT_GPU: &str = "gpu";
pub const TASK_REQUIREMENT_FPGA: &str = "fpga";
pub const TASK_REQUIREMENT_MAX_RETRIES: &str = "max_retries";
pub const TASK_REQUIREMENT_MAX_RETRIES_ALIAS: &str = "maxRetries";
pub const TASK_REQUIREMENT_MEMORY: &str = "memory";
pub const TASK_REQUIREMENT_RETURN_CODES: &str = "return_codes";
pub const TASK_REQUIREMENT_RETURN_CODES_ALIAS: &str = "returnCodes";
pub const TASK_HINT_DISKS: &str = "disks";
pub const TASK_HINT_GPU: &str = "gpu";
pub const TASK_HINT_FPGA: &str = "fpga";
pub const TASK_HINT_INPUTS: &str = "inputs";
pub const TASK_HINT_LOCALIZATION_OPTIONAL: &str = "localization_optional";
pub const TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS: &str = "localizationOptional";
pub const TASK_HINT_MAX_CPU: &str = "max_cpu";
pub const TASK_HINT_MAX_CPU_ALIAS: &str = "maxCpu";
pub const TASK_HINT_MAX_MEMORY: &str = "max_memory";
pub const TASK_HINT_MAX_MEMORY_ALIAS: &str = "maxMemory";
pub const TASK_HINT_OUTPUTS: &str = "outputs";
pub const TASK_HINT_SHORT_TASK: &str = "short_task";
pub const TASK_HINT_SHORT_TASK_ALIAS: &str = "shortTask";
pub const TASK_HINT_CACHEABLE: &str = "cacheable";
fn unescape_command_text(s: &str, heredoc: bool, buffer: &mut String) {
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' => match chars.peek() {
Some('\\') | Some('~') => {
buffer.push(chars.next().unwrap());
}
Some('>') if heredoc => {
buffer.push(chars.next().unwrap());
}
Some('$') | Some('}') if !heredoc => {
buffer.push(chars.next().unwrap());
}
_ => {
buffer.push('\\');
}
},
_ => {
buffer.push(c);
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TaskDefinition<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> TaskDefinition<N> {
pub fn name(&self) -> Ident<N::Token> {
self.token().expect("task should have a name")
}
pub fn keyword(&self) -> TaskKeyword<N::Token> {
self.token().expect("task should have a keyword")
}
pub fn items(&self) -> impl Iterator<Item = TaskItem<N>> + use<'_, N> {
TaskItem::children(&self.0)
}
pub fn input(&self) -> Option<InputSection<N>> {
self.child()
}
pub fn output(&self) -> Option<OutputSection<N>> {
self.child()
}
pub fn command(&self) -> Option<CommandSection<N>> {
self.child()
}
pub fn requirements(&self) -> Option<RequirementsSection<N>> {
self.child()
}
pub fn hints(&self) -> Option<TaskHintsSection<N>> {
self.child()
}
pub fn runtime(&self) -> Option<RuntimeSection<N>> {
self.child()
}
pub fn metadata(&self) -> Option<MetadataSection<N>> {
self.child()
}
pub fn parameter_metadata(&self) -> Option<ParameterMetadataSection<N>> {
self.child()
}
pub fn declarations(&self) -> impl Iterator<Item = BoundDecl<N>> + use<'_, N> {
self.children()
}
}
impl<N: TreeNode> AstNode<N> for TaskDefinition<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::TaskDefinitionNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::TaskDefinitionNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
impl Documented<SyntaxNode> for TaskDefinition<SyntaxNode> {
fn doc_comments(&self) -> Option<Vec<Comment<<SyntaxNode as TreeNode>::Token>>> {
Some(crate::doc_comments::<SyntaxNode>(self.keyword().inner().preceding_trivia()).collect())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TaskItem<N: TreeNode = SyntaxNode> {
Input(InputSection<N>),
Output(OutputSection<N>),
Command(CommandSection<N>),
Requirements(RequirementsSection<N>),
Hints(TaskHintsSection<N>),
Runtime(RuntimeSection<N>),
Metadata(MetadataSection<N>),
ParameterMetadata(ParameterMetadataSection<N>),
Declaration(BoundDecl<N>),
}
impl<N: TreeNode> TaskItem<N> {
pub fn can_cast(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::InputSectionNode
| SyntaxKind::OutputSectionNode
| SyntaxKind::CommandSectionNode
| SyntaxKind::RequirementsSectionNode
| SyntaxKind::TaskHintsSectionNode
| SyntaxKind::RuntimeSectionNode
| SyntaxKind::MetadataSectionNode
| SyntaxKind::ParameterMetadataSectionNode
| SyntaxKind::BoundDeclNode
)
}
pub fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::InputSectionNode => Some(Self::Input(
InputSection::cast(inner).expect("input section to cast"),
)),
SyntaxKind::OutputSectionNode => Some(Self::Output(
OutputSection::cast(inner).expect("output section to cast"),
)),
SyntaxKind::CommandSectionNode => Some(Self::Command(
CommandSection::cast(inner).expect("command section to cast"),
)),
SyntaxKind::RequirementsSectionNode => Some(Self::Requirements(
RequirementsSection::cast(inner).expect("requirements section to cast"),
)),
SyntaxKind::RuntimeSectionNode => Some(Self::Runtime(
RuntimeSection::cast(inner).expect("runtime section to cast"),
)),
SyntaxKind::MetadataSectionNode => Some(Self::Metadata(
MetadataSection::cast(inner).expect("metadata section to cast"),
)),
SyntaxKind::ParameterMetadataSectionNode => Some(Self::ParameterMetadata(
ParameterMetadataSection::cast(inner).expect("parameter metadata section to cast"),
)),
SyntaxKind::TaskHintsSectionNode => Some(Self::Hints(
TaskHintsSection::cast(inner).expect("task hints section to cast"),
)),
SyntaxKind::BoundDeclNode => Some(Self::Declaration(
BoundDecl::cast(inner).expect("bound decl to cast"),
)),
_ => None,
}
}
pub fn inner(&self) -> &N {
match self {
Self::Input(element) => element.inner(),
Self::Output(element) => element.inner(),
Self::Command(element) => element.inner(),
Self::Requirements(element) => element.inner(),
Self::Hints(element) => element.inner(),
Self::Runtime(element) => element.inner(),
Self::Metadata(element) => element.inner(),
Self::ParameterMetadata(element) => element.inner(),
Self::Declaration(element) => element.inner(),
}
}
pub fn as_input_section(&self) -> Option<&InputSection<N>> {
match self {
Self::Input(s) => Some(s),
_ => None,
}
}
pub fn into_input_section(self) -> Option<InputSection<N>> {
match self {
Self::Input(s) => Some(s),
_ => None,
}
}
pub fn as_output_section(&self) -> Option<&OutputSection<N>> {
match self {
Self::Output(s) => Some(s),
_ => None,
}
}
pub fn into_output_section(self) -> Option<OutputSection<N>> {
match self {
Self::Output(s) => Some(s),
_ => None,
}
}
pub fn as_command_section(&self) -> Option<&CommandSection<N>> {
match self {
Self::Command(s) => Some(s),
_ => None,
}
}
pub fn into_command_section(self) -> Option<CommandSection<N>> {
match self {
Self::Command(s) => Some(s),
_ => None,
}
}
pub fn as_requirements_section(&self) -> Option<&RequirementsSection<N>> {
match self {
Self::Requirements(s) => Some(s),
_ => None,
}
}
pub fn into_requirements_section(self) -> Option<RequirementsSection<N>> {
match self {
Self::Requirements(s) => Some(s),
_ => None,
}
}
pub fn as_hints_section(&self) -> Option<&TaskHintsSection<N>> {
match self {
Self::Hints(s) => Some(s),
_ => None,
}
}
pub fn into_hints_section(self) -> Option<TaskHintsSection<N>> {
match self {
Self::Hints(s) => Some(s),
_ => None,
}
}
pub fn as_runtime_section(&self) -> Option<&RuntimeSection<N>> {
match self {
Self::Runtime(s) => Some(s),
_ => None,
}
}
pub fn into_runtime_section(self) -> Option<RuntimeSection<N>> {
match self {
Self::Runtime(s) => Some(s),
_ => None,
}
}
pub fn as_metadata_section(&self) -> Option<&MetadataSection<N>> {
match self {
Self::Metadata(s) => Some(s),
_ => None,
}
}
pub fn into_metadata_section(self) -> Option<MetadataSection<N>> {
match self {
Self::Metadata(s) => Some(s),
_ => None,
}
}
pub fn as_parameter_metadata_section(&self) -> Option<&ParameterMetadataSection<N>> {
match self {
Self::ParameterMetadata(s) => Some(s),
_ => None,
}
}
pub fn into_parameter_metadata_section(self) -> Option<ParameterMetadataSection<N>> {
match self {
Self::ParameterMetadata(s) => Some(s),
_ => None,
}
}
pub fn as_declaration(&self) -> Option<&BoundDecl<N>> {
match self {
Self::Declaration(d) => Some(d),
_ => None,
}
}
pub fn into_declaration(self) -> Option<BoundDecl<N>> {
match self {
Self::Declaration(d) => Some(d),
_ => None,
}
}
pub fn child(node: &N) -> Option<Self> {
node.children().find_map(Self::cast)
}
pub fn children(node: &N) -> impl Iterator<Item = Self> + use<'_, N> {
node.children().filter_map(Self::cast)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SectionParent<N: TreeNode = SyntaxNode> {
Task(TaskDefinition<N>),
Workflow(WorkflowDefinition<N>),
Struct(StructDefinition<N>),
}
impl<N: TreeNode> SectionParent<N> {
pub fn can_cast(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::TaskDefinitionNode
| SyntaxKind::WorkflowDefinitionNode
| SyntaxKind::StructDefinitionNode
)
}
pub fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::TaskDefinitionNode => Some(Self::Task(
TaskDefinition::cast(inner).expect("task definition to cast"),
)),
SyntaxKind::WorkflowDefinitionNode => Some(Self::Workflow(
WorkflowDefinition::cast(inner).expect("workflow definition to cast"),
)),
SyntaxKind::StructDefinitionNode => Some(Self::Struct(
StructDefinition::cast(inner).expect("struct definition to cast"),
)),
_ => None,
}
}
pub fn inner(&self) -> &N {
match self {
Self::Task(element) => element.inner(),
Self::Workflow(element) => element.inner(),
Self::Struct(element) => element.inner(),
}
}
pub fn name(&self) -> Ident<N::Token> {
match self {
Self::Task(t) => t.name(),
Self::Workflow(w) => w.name(),
Self::Struct(s) => s.name(),
}
}
pub fn as_task(&self) -> Option<&TaskDefinition<N>> {
match self {
Self::Task(task) => Some(task),
_ => None,
}
}
pub fn into_task(self) -> Option<TaskDefinition<N>> {
match self {
Self::Task(task) => Some(task),
_ => None,
}
}
pub fn unwrap_task(self) -> TaskDefinition<N> {
match self {
Self::Task(task) => task,
_ => panic!("not a task definition"),
}
}
pub fn as_workflow(&self) -> Option<&WorkflowDefinition<N>> {
match self {
Self::Workflow(workflow) => Some(workflow),
_ => None,
}
}
pub fn into_workflow(self) -> Option<WorkflowDefinition<N>> {
match self {
Self::Workflow(workflow) => Some(workflow),
_ => None,
}
}
pub fn unwrap_workflow(self) -> WorkflowDefinition<N> {
match self {
Self::Workflow(workflow) => workflow,
_ => panic!("not a workflow definition"),
}
}
pub fn as_struct(&self) -> Option<&StructDefinition<N>> {
match self {
Self::Struct(r#struct) => Some(r#struct),
_ => None,
}
}
pub fn into_struct(self) -> Option<StructDefinition<N>> {
match self {
Self::Struct(r#struct) => Some(r#struct),
_ => None,
}
}
pub fn unwrap_struct(self) -> StructDefinition<N> {
match self {
Self::Struct(def) => def,
_ => panic!("not a struct definition"),
}
}
pub fn child(node: &N) -> Option<Self> {
node.children().find_map(Self::cast)
}
pub fn children(node: &N) -> impl Iterator<Item = Self> + use<'_, N> {
node.children().filter_map(Self::cast)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InputSection<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> InputSection<N> {
pub fn declarations(&self) -> impl Iterator<Item = Decl<N>> + use<'_, N> {
Decl::children(&self.0)
}
pub fn parent(&self) -> SectionParent<N> {
SectionParent::cast(self.0.parent().expect("should have a parent"))
.expect("parent should cast")
}
}
impl<N: TreeNode> AstNode<N> for InputSection<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::InputSectionNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::InputSectionNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OutputSection<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> OutputSection<N> {
pub fn declarations(&self) -> impl Iterator<Item = BoundDecl<N>> + use<'_, N> {
self.children()
}
pub fn parent(&self) -> SectionParent<N> {
SectionParent::cast(self.0.parent().expect("should have a parent"))
.expect("parent should cast")
}
}
impl<N: TreeNode> AstNode<N> for OutputSection<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::OutputSectionNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::OutputSectionNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StrippedCommandPart<N: TreeNode = SyntaxNode> {
Text(String),
Placeholder(Placeholder<N>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CommandSection<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> CommandSection<N> {
pub fn is_heredoc(&self) -> bool {
self.token::<OpenHeredoc<N::Token>>().is_some()
}
pub fn parts(&self) -> impl Iterator<Item = CommandPart<N>> + use<'_, N> {
self.0.children_with_tokens().filter_map(CommandPart::cast)
}
pub fn count_whitespace(&self) -> Option<usize> {
let mut min_leading_spaces = usize::MAX;
let mut min_leading_tabs = usize::MAX;
let mut parsing_leading_whitespace = false;
let mut leading_spaces = 0;
let mut leading_tabs = 0;
for part in self.parts() {
match part {
CommandPart::Text(text) => {
for c in text.text().chars() {
match c {
' ' if parsing_leading_whitespace => {
leading_spaces += 1;
}
'\t' if parsing_leading_whitespace => {
leading_tabs += 1;
}
'\n' => {
parsing_leading_whitespace = true;
leading_spaces = 0;
leading_tabs = 0;
}
'\r' => {}
_ => {
if parsing_leading_whitespace {
parsing_leading_whitespace = false;
if leading_spaces == 0 && leading_tabs == 0 {
min_leading_spaces = 0;
min_leading_tabs = 0;
continue;
}
if leading_spaces < min_leading_spaces && leading_spaces > 0 {
min_leading_spaces = leading_spaces;
}
if leading_tabs < min_leading_tabs && leading_tabs > 0 {
min_leading_tabs = leading_tabs;
}
}
}
}
}
}
CommandPart::Placeholder(_) => {
if parsing_leading_whitespace {
parsing_leading_whitespace = false;
if leading_spaces == 0 && leading_tabs == 0 {
min_leading_spaces = 0;
min_leading_tabs = 0;
continue;
}
if leading_spaces < min_leading_spaces && leading_spaces > 0 {
min_leading_spaces = leading_spaces;
}
if leading_tabs < min_leading_tabs && leading_tabs > 0 {
min_leading_tabs = leading_tabs;
}
}
}
}
}
if (min_leading_spaces == 0 && min_leading_tabs == 0)
|| (min_leading_spaces == usize::MAX && min_leading_tabs == usize::MAX)
{
return Some(0);
}
if (min_leading_spaces > 0 && min_leading_spaces != usize::MAX)
&& (min_leading_tabs > 0 && min_leading_tabs != usize::MAX)
{
return None;
}
let final_leading_whitespace = if min_leading_spaces < min_leading_tabs {
min_leading_spaces
} else {
min_leading_tabs
};
Some(final_leading_whitespace)
}
pub fn strip_whitespace(&self) -> Option<Vec<StrippedCommandPart<N>>> {
let mut result = Vec::new();
let heredoc = self.is_heredoc();
for part in self.parts() {
match part {
CommandPart::Text(text) => {
let mut s = String::new();
unescape_command_text(text.text(), heredoc, &mut s);
result.push(StrippedCommandPart::Text(s));
}
CommandPart::Placeholder(p) => {
result.push(StrippedCommandPart::Placeholder(p));
}
}
}
let mut whole_first_line_trimmed = false;
if let Some(StrippedCommandPart::Text(text)) = result.first_mut() {
let end_of_first_line = text.find('\n').map(|p| p + 1).unwrap_or(text.len());
let line = &text[..end_of_first_line];
let len = line.len() - line.trim_start().len();
whole_first_line_trimmed = len == line.len();
text.replace_range(..len, "");
}
if let Some(StrippedCommandPart::Text(text)) = result.last_mut() {
if let Some(index) = text.rfind(|c| !matches!(c, ' ' | '\t')) {
text.truncate(index + 1);
} else {
text.clear();
}
if text.ends_with('\n') {
text.pop();
}
if text.ends_with('\r') {
text.pop();
}
}
let num_stripped_chars = self.count_whitespace()?;
if num_stripped_chars == 0 {
return Some(result);
}
let mut strip_leading_whitespace = whole_first_line_trimmed;
for part in &mut result {
match part {
StrippedCommandPart::Text(text) => {
let mut offset = 0;
while let Some(next) = text[offset..].find('\n') {
let next = next + offset;
if offset > 0 {
strip_leading_whitespace = true;
}
if !strip_leading_whitespace {
offset = next + 1;
continue;
}
let line = &text[offset..next];
let line = line.strip_suffix('\r').unwrap_or(line);
let len = line.len().min(num_stripped_chars);
text.replace_range(offset..offset + len, "");
offset = next + 1 - len;
}
if strip_leading_whitespace || offset > 0 {
let line = &text[offset..];
let line = line.strip_suffix('\r').unwrap_or(line);
let len = line.len().min(num_stripped_chars);
text.replace_range(offset..offset + len, "");
}
}
StrippedCommandPart::Placeholder(_) => {
strip_leading_whitespace = false;
}
}
}
Some(result)
}
pub fn parent(&self) -> SectionParent<N> {
SectionParent::cast(self.0.parent().expect("should have a parent"))
.expect("parent should cast")
}
pub fn keyword(&self) -> CommandKeyword<N::Token> {
self.token()
.expect("CommandSection must have CommandKeyword")
}
}
impl<N: TreeNode> AstNode<N> for CommandSection<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::CommandSectionNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::CommandSectionNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CommandText<T: TreeToken = SyntaxToken>(T);
impl<T: TreeToken> CommandText<T> {
pub fn unescape_to(&self, heredoc: bool, buffer: &mut String) {
unescape_command_text(self.text(), heredoc, buffer);
}
}
impl<T: TreeToken> AstToken<T> for CommandText<T> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::LiteralCommandText
}
fn cast(inner: T) -> Option<Self> {
match inner.kind() {
SyntaxKind::LiteralCommandText => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &T {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CommandPart<N: TreeNode = SyntaxNode> {
Text(CommandText<N::Token>),
Placeholder(Placeholder<N>),
}
impl<N: TreeNode> CommandPart<N> {
pub fn unwrap_text(self) -> CommandText<N::Token> {
match self {
Self::Text(text) => text,
_ => panic!("not string text"),
}
}
pub fn unwrap_placeholder(self) -> Placeholder<N> {
match self {
Self::Placeholder(p) => p,
_ => panic!("not a placeholder"),
}
}
fn cast(element: NodeOrToken<N, N::Token>) -> Option<Self> {
match element {
NodeOrToken::Node(n) => Some(Self::Placeholder(Placeholder::cast(n)?)),
NodeOrToken::Token(t) => Some(Self::Text(CommandText::cast(t)?)),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RequirementsSection<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> RequirementsSection<N> {
pub fn items(&self) -> impl Iterator<Item = RequirementsItem<N>> + use<'_, N> {
self.children()
}
pub fn parent(&self) -> SectionParent<N> {
SectionParent::cast(self.0.parent().expect("should have a parent"))
.expect("parent should cast")
}
pub fn container(&self) -> Option<requirements::item::Container<N>> {
self.child()
}
pub fn keyword(&self) -> RequirementsKeyword<N::Token> {
self.token()
.expect("RequirementsSection must have RequirementsKeyword")
}
}
impl<N: TreeNode> AstNode<N> for RequirementsSection<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::RequirementsSectionNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::RequirementsSectionNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RequirementsItem<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> RequirementsItem<N> {
pub fn name(&self) -> Ident<N::Token> {
self.token().expect("expected an item name")
}
pub fn expr(&self) -> Expr<N> {
Expr::child(&self.0).expect("expected an item expression")
}
pub fn into_container(self) -> Option<requirements::item::Container<N>> {
requirements::item::Container::try_from(self).ok()
}
}
impl<N: TreeNode> AstNode<N> for RequirementsItem<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::RequirementsItemNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::RequirementsItemNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TaskHintsSection<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> TaskHintsSection<N> {
pub fn items(&self) -> impl Iterator<Item = TaskHintsItem<N>> + use<'_, N> {
self.children()
}
pub fn parent(&self) -> TaskDefinition<N> {
TaskDefinition::cast(self.0.parent().expect("should have a parent"))
.expect("parent should cast")
}
}
impl<N: TreeNode> AstNode<N> for TaskHintsSection<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::TaskHintsSectionNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::TaskHintsSectionNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TaskHintsItem<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> TaskHintsItem<N> {
pub fn name(&self) -> Ident<N::Token> {
self.token().expect("expected an item name")
}
pub fn expr(&self) -> Expr<N> {
Expr::child(&self.0).expect("expected an item expression")
}
}
impl<N: TreeNode> AstNode<N> for TaskHintsItem<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::TaskHintsItemNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::TaskHintsItemNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RuntimeSection<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> RuntimeSection<N> {
pub fn items(&self) -> impl Iterator<Item = RuntimeItem<N>> + use<'_, N> {
self.children()
}
pub fn parent(&self) -> SectionParent<N> {
SectionParent::cast(self.0.parent().expect("should have a parent"))
.expect("parent should cast")
}
pub fn container(&self) -> Option<runtime::item::Container<N>> {
self.child()
}
}
impl<N: TreeNode> AstNode<N> for RuntimeSection<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::RuntimeSectionNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::RuntimeSectionNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RuntimeItem<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> RuntimeItem<N> {
pub fn name(&self) -> Ident<N::Token> {
self.token().expect("expected an item name")
}
pub fn expr(&self) -> Expr<N> {
Expr::child(&self.0).expect("expected an item expression")
}
pub fn into_container(self) -> Option<runtime::item::Container<N>> {
runtime::item::Container::try_from(self).ok()
}
}
impl<N: TreeNode> AstNode<N> for RuntimeItem<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::RuntimeItemNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::RuntimeItemNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MetadataSection<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> MetadataSection<N> {
pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
self.children()
}
pub fn parent(&self) -> SectionParent<N> {
SectionParent::cast(self.0.parent().expect("should have a parent"))
.expect("parent should cast")
}
pub fn keyword(&self) -> MetaKeyword<N::Token> {
self.token().expect("MetadataSection must have MetaKeyword")
}
}
impl<N: TreeNode> AstNode<N> for MetadataSection<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::MetadataSectionNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::MetadataSectionNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MetadataObjectItem<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> MetadataObjectItem<N> {
pub fn name(&self) -> Ident<N::Token> {
self.token().expect("expected a name")
}
pub fn value(&self) -> MetadataValue<N> {
self.child().expect("expected a value")
}
}
impl<N: TreeNode> AstNode<N> for MetadataObjectItem<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::MetadataObjectItemNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::MetadataObjectItemNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MetadataValue<N: TreeNode = SyntaxNode> {
Boolean(LiteralBoolean<N>),
Integer(LiteralInteger<N>),
Float(LiteralFloat<N>),
String(LiteralString<N>),
Null(LiteralNull<N>),
Object(MetadataObject<N>),
Array(MetadataArray<N>),
}
impl<N: TreeNode> MetadataValue<N> {
pub fn unwrap_boolean(self) -> LiteralBoolean<N> {
match self {
Self::Boolean(b) => b,
_ => panic!("not a boolean"),
}
}
pub fn unwrap_integer(self) -> LiteralInteger<N> {
match self {
Self::Integer(i) => i,
_ => panic!("not an integer"),
}
}
pub fn unwrap_float(self) -> LiteralFloat<N> {
match self {
Self::Float(f) => f,
_ => panic!("not a float"),
}
}
pub fn unwrap_string(self) -> LiteralString<N> {
match self {
Self::String(s) => s,
_ => panic!("not a string"),
}
}
pub fn unwrap_null(self) -> LiteralNull<N> {
match self {
Self::Null(n) => n,
_ => panic!("not a null"),
}
}
pub fn unwrap_object(self) -> MetadataObject<N> {
match self {
Self::Object(o) => o,
_ => panic!("not an object"),
}
}
pub fn unwrap_array(self) -> MetadataArray<N> {
match self {
Self::Array(a) => a,
_ => panic!("not an array"),
}
}
}
impl<N: TreeNode> AstNode<N> for MetadataValue<N> {
fn can_cast(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::LiteralBooleanNode
| SyntaxKind::LiteralIntegerNode
| SyntaxKind::LiteralFloatNode
| SyntaxKind::LiteralStringNode
| SyntaxKind::LiteralNullNode
| SyntaxKind::MetadataObjectNode
| SyntaxKind::MetadataArrayNode
)
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::LiteralBooleanNode => Some(Self::Boolean(LiteralBoolean(inner))),
SyntaxKind::LiteralIntegerNode => Some(Self::Integer(LiteralInteger(inner))),
SyntaxKind::LiteralFloatNode => Some(Self::Float(LiteralFloat(inner))),
SyntaxKind::LiteralStringNode => Some(Self::String(LiteralString(inner))),
SyntaxKind::LiteralNullNode => Some(Self::Null(LiteralNull(inner))),
SyntaxKind::MetadataObjectNode => Some(Self::Object(MetadataObject(inner))),
SyntaxKind::MetadataArrayNode => Some(Self::Array(MetadataArray(inner))),
_ => None,
}
}
fn inner(&self) -> &N {
match self {
Self::Boolean(b) => &b.0,
Self::Integer(i) => &i.0,
Self::Float(f) => &f.0,
Self::String(s) => &s.0,
Self::Null(n) => &n.0,
Self::Object(o) => &o.0,
Self::Array(a) => &a.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LiteralNull<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> AstNode<N> for LiteralNull<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::LiteralNullNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::LiteralNullNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MetadataObject<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> MetadataObject<N> {
pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
self.children()
}
}
impl<N: TreeNode> AstNode<N> for MetadataObject<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::MetadataObjectNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::MetadataObjectNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MetadataArray<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> MetadataArray<N> {
pub fn elements(&self) -> impl Iterator<Item = MetadataValue<N>> + use<'_, N> {
self.children()
}
}
impl<N: TreeNode> AstNode<N> for MetadataArray<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::MetadataArrayNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::MetadataArrayNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParameterMetadataSection<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> ParameterMetadataSection<N> {
pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
self.children()
}
pub fn parent(&self) -> SectionParent<N> {
SectionParent::cast(self.0.parent().expect("should have a parent"))
.expect("parent should cast")
}
pub fn keyword(&self) -> ParameterMetaKeyword<N::Token> {
self.token()
.expect("ParameterMetadataSection must have ParameterMetaKeyword")
}
}
impl<N: TreeNode> AstNode<N> for ParameterMetadataSection<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::ParameterMetadataSectionNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::ParameterMetadataSectionNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
use crate::Document;
#[test]
fn tasks() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
input {
String name
}
output {
String greeting = stdout()
}
command <<<
printf "hello, ~{name}!
>>>
requirements {
container: "baz/qux"
}
hints {
foo: "bar"
}
runtime {
container: "foo/bar"
}
meta {
description: "a test"
foo: null
}
parameter_meta {
name: {
help: "a name to greet"
}
}
String x = "private"
}
"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].name().text(), "test");
let input = tasks[0].input().expect("should have an input section");
assert_eq!(input.parent().unwrap_task().name().text(), "test");
let decls: Vec<_> = input.declarations().collect();
assert_eq!(decls.len(), 1);
assert_eq!(
decls[0].clone().unwrap_unbound_decl().ty().to_string(),
"String"
);
assert_eq!(decls[0].clone().unwrap_unbound_decl().name().text(), "name");
let output = tasks[0].output().expect("should have an output section");
assert_eq!(output.parent().unwrap_task().name().text(), "test");
let decls: Vec<_> = output.declarations().collect();
assert_eq!(decls.len(), 1);
assert_eq!(decls[0].ty().to_string(), "String");
assert_eq!(decls[0].name().text(), "greeting");
assert_eq!(decls[0].expr().unwrap_call().target().text(), "stdout");
let command = tasks[0].command().expect("should have a command section");
assert_eq!(command.parent().name().text(), "test");
assert!(command.is_heredoc());
let parts: Vec<_> = command.parts().collect();
assert_eq!(parts.len(), 3);
assert_eq!(
parts[0].clone().unwrap_text().text(),
"\n printf \"hello, "
);
assert_eq!(
parts[1]
.clone()
.unwrap_placeholder()
.expr()
.unwrap_name_ref()
.name()
.text(),
"name"
);
assert_eq!(parts[2].clone().unwrap_text().text(), "!\n ");
let requirements = tasks[0]
.requirements()
.expect("should have a requirements section");
assert_eq!(requirements.parent().name().text(), "test");
let items: Vec<_> = requirements.items().collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0].name().text(), TASK_REQUIREMENT_CONTAINER);
assert_eq!(
items[0]
.expr()
.unwrap_literal()
.unwrap_string()
.text()
.unwrap()
.text(),
"baz/qux"
);
let hints = tasks[0].hints().expect("should have a hints section");
assert_eq!(hints.parent().name().text(), "test");
let items: Vec<_> = hints.items().collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0].name().text(), "foo");
assert_eq!(
items[0]
.expr()
.unwrap_literal()
.unwrap_string()
.text()
.unwrap()
.text(),
"bar"
);
let runtime = tasks[0].runtime().expect("should have a runtime section");
assert_eq!(runtime.parent().name().text(), "test");
let items: Vec<_> = runtime.items().collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0].name().text(), TASK_REQUIREMENT_CONTAINER);
assert_eq!(
items[0]
.expr()
.unwrap_literal()
.unwrap_string()
.text()
.unwrap()
.text(),
"foo/bar"
);
let metadata = tasks[0].metadata().expect("should have a metadata section");
assert_eq!(metadata.parent().unwrap_task().name().text(), "test");
let items: Vec<_> = metadata.items().collect();
assert_eq!(items.len(), 2);
assert_eq!(items[0].name().text(), "description");
assert_eq!(
items[0].value().unwrap_string().text().unwrap().text(),
"a test"
);
assert_eq!(items[1].name().text(), "foo");
items[1].value().unwrap_null();
let param_meta = tasks[0]
.parameter_metadata()
.expect("should have a parameter metadata section");
assert_eq!(param_meta.parent().unwrap_task().name().text(), "test");
let items: Vec<_> = param_meta.items().collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0].name().text(), "name");
let items: Vec<_> = items[0].value().unwrap_object().items().collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0].name().text(), "help");
assert_eq!(
items[0].value().unwrap_string().text().unwrap().text(),
"a name to greet"
);
let decls: Vec<_> = tasks[0].declarations().collect();
assert_eq!(decls.len(), 1);
assert_eq!(decls[0].ty().to_string(), "String");
assert_eq!(decls[0].name().text(), "x");
assert_eq!(
decls[0]
.expr()
.unwrap_literal()
.unwrap_string()
.text()
.unwrap()
.text(),
"private"
);
}
#[test]
fn whitespace_stripping_without_interpolation() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
command <<<
echo "hello"
echo "world"
echo \
"goodbye"
>>>
}
"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace().unwrap();
assert_eq!(stripped.len(), 1);
let text = match &stripped[0] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(
text,
"echo \"hello\"\necho \"world\"\necho \\\n \"goodbye\""
);
}
#[test]
fn whitespace_stripping_with_interpolation() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
input {
String name
Boolean flag
}
command <<<
echo "hello, ~{
if flag
then name
else "Jerry"
}!"
>>>
}
"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace().unwrap();
assert_eq!(stripped.len(), 3);
let text = match &stripped[0] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, "echo \"hello, ");
let _placeholder = match &stripped[1] {
StrippedCommandPart::Placeholder(p) => p,
_ => panic!("expected placeholder"),
};
let text = match &stripped[2] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, "!\"");
}
#[test]
fn whitespace_stripping_when_interpolation_starts_line() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
input {
Int placeholder
}
command <<<
# other weird whitespace
~{placeholder} "$trailing_pholder" ~{placeholder}
~{placeholder} somecommand.py "$leading_pholder"
>>>
}
"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace().unwrap();
assert_eq!(stripped.len(), 7);
let text = match &stripped[0] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, " # other weird whitespace\n");
let _placeholder = match &stripped[1] {
StrippedCommandPart::Placeholder(p) => p,
_ => panic!("expected placeholder"),
};
let text = match &stripped[2] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, " \"$trailing_pholder\" ");
let _placeholder = match &stripped[3] {
StrippedCommandPart::Placeholder(p) => p,
_ => panic!("expected placeholder"),
};
let text = match &stripped[4] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, "\n");
let _placeholder = match &stripped[5] {
StrippedCommandPart::Placeholder(p) => p,
_ => panic!("expected placeholder"),
};
let text = match &stripped[6] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, " somecommand.py \"$leading_pholder\"");
}
#[test]
fn whitespace_stripping_when_command_is_empty() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
command <<<>>>
}
"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace().unwrap();
assert_eq!(stripped.len(), 0);
}
#[test]
fn whitespace_stripping_when_command_is_one_line_of_whitespace() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
command <<< >>>
}
"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace().unwrap();
assert_eq!(stripped.len(), 1);
let text = match &stripped[0] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, "");
}
#[test]
fn whitespace_stripping_when_command_is_one_newline() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
command <<<
>>>
}
"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace().unwrap();
assert_eq!(stripped.len(), 1);
let text = match &stripped[0] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, "");
}
#[test]
fn whitespace_stripping_when_command_is_a_blank_line() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
command <<<
>>>
}
"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace().unwrap();
assert_eq!(stripped.len(), 1);
let text = match &stripped[0] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, "");
}
#[test]
fn whitespace_stripping_when_command_is_a_blank_line_with_spaces() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
command <<<
>>>
}
"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace().unwrap();
assert_eq!(stripped.len(), 1);
let text = match &stripped[0] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, " ");
}
#[test]
fn whitespace_stripping_with_mixed_indentation() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
command <<<
echo "hello"
echo "world"
echo \
"goodbye"
>>>
}"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace();
assert!(stripped.is_none());
}
#[test]
fn whitespace_stripping_with_funky_indentation() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
command <<<
echo "hello"
echo "world"
echo \
"goodbye"
>>>
}"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace().unwrap();
assert_eq!(stripped.len(), 1);
let text = match &stripped[0] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(
text,
"echo \"hello\"\n echo \"world\"\necho \\\n \"goodbye\""
);
}
#[test]
fn whitespace_stripping_with_content_on_first_line() {
let (document, diagnostics) = Document::parse(
r#"
version 1.2
task test {
command <<< weird stuff $firstlinelint
# other weird whitespace
somecommand.py $line120 ~{placeholder}
>>>
}"#,
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace().unwrap();
assert_eq!(stripped.len(), 3);
let text = match &stripped[0] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(
text,
"weird stuff $firstlinelint\n # other weird whitespace\nsomecommand.py $line120 "
);
let _placeholder = match &stripped[1] {
StrippedCommandPart::Placeholder(p) => p,
_ => panic!("expected placeholder"),
};
let text = match &stripped[2] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, "");
}
#[test]
fn whitespace_stripping_on_windows() {
let (document, diagnostics) = Document::parse(
"version 1.2\r\ntask test {\r\n command <<<\r\n echo \"hello\"\r\n \
>>>\r\n}\r\n",
None,
);
assert!(diagnostics.is_empty());
let ast = document.ast();
let ast = ast.as_v1().expect("should be a V1 AST");
let tasks: Vec<_> = ast.tasks().collect();
assert_eq!(tasks.len(), 1);
let command = tasks[0].command().expect("should have a command section");
let stripped = command.strip_whitespace().unwrap();
assert_eq!(stripped.len(), 1);
let text = match &stripped[0] {
StrippedCommandPart::Text(text) => text,
_ => panic!("expected text"),
};
assert_eq!(text, "echo \"hello\"");
}
}