diff --git a/.gitignore b/.gitignore index a221ac104..2aaf99d6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ target/ .vscode/ + +rustc-ice* diff --git a/Cargo.lock b/Cargo.lock index fef20e59a..b8cabb4dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -949,6 +949,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nonempty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995defdca0a589acfdd1bd2e8e3b896b4d4f7675a31fd14c32611440c7f608e6" + [[package]] name = "num-bigint" version = "0.4.4" @@ -1091,6 +1097,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +dependencies = [ + "num-traits", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -2147,7 +2162,35 @@ dependencies = [ ] [[package]] -name = "wdl-grammar" +name = "wdl-ast" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "indexmap 2.1.0", + "lazy_static", + "log", + "nonempty", + "ordered-float", + "pest", + "regex", + "tokio", + "wdl-core", + "wdl-grammar", +] + +[[package]] +name = "wdl-core" +version = "0.1.0" +dependencies = [ + "clap", + "pest", + "serde", + "to_snake_case", +] + +[[package]] +name = "wdl-gauntlet" version = "0.1.0" dependencies = [ "async-recursion", @@ -2159,14 +2202,33 @@ dependencies = [ "indexmap 2.1.0", "log", "octocrab", + "reqwest", + "serde", + "serde_with", + "tokio", + "toml", + "wdl-ast", + "wdl-core", + "wdl-grammar", +] + +[[package]] +name = "wdl-grammar" +version = "0.1.0" +dependencies = [ + "clap", + "colored", + "env_logger", + "indexmap 2.1.0", + "lazy_static", + "log", "pest", "pest_derive", - "reqwest", "serde", "serde_with", - "to_snake_case", "tokio", "toml", + "wdl-core", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4489b4cef..58fab5582 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["wdl", "wdl-grammar"] +members = ["wdl", "wdl-ast", "wdl-core", "wdl-gauntlet", "wdl-grammar"] resolver = "2" [workspace.package] @@ -9,11 +9,17 @@ version = "0.1.0" [workspace.dependencies] clap = { version = "4.4.7", features = ["derive"] } +colored = "2.0.4" env_logger = "0.10.0" indexmap = { version = "2.1.0", features = ["serde"] } +lazy_static = "1.4.0" log = "0.4.20" +nonempty = "0.9.0" +ordered-float = "4.2.0" pest = { version = "2.7.5", features = ["pretty-print"] } pest_derive = "2.7.5" +regex = "1.10.2" serde = { version = "1", features = ["derive"] } -serde_with = { version = "3.4.0" } +serde_with = "3.4.0" +tokio = { version = "1.33.0", features = ["full"] } toml = "0.8.8" diff --git a/Gauntlet.toml b/Gauntlet.toml index 46d9ca21d..e12a1c8de 100644 --- a/Gauntlet.toml +++ b/Gauntlet.toml @@ -1,20 +1,20 @@ version = "v1" -[[repositories]] -organization = "stjudecloud" -name = "workflows" - [[repositories]] organization = "PacificBiosciences" name = "HiFi-human-WGS-WDL" +[[repositories]] +organization = "biowdl" +name = "tasks" + [[repositories]] organization = "chanzuckerberg" name = "czid-workflows" [[repositories]] -organization = "biowdl" -name = "tasks" +organization = "stjudecloud" +name = "workflows" [[ignored_errors]] document = "biowdl/tasks:bcftools.wdl" @@ -86,16 +86,16 @@ parse error: [[ignored_errors]] document = "stjudecloud/workflows:tools/bwa.wdl" -error = '''validation error: [v1::001] invalid escape character '\.' in string at line 34:17''' +error = '''validation error: [v1::001] invalid escape character '\.' in string at line 41:17''' [[ignored_errors]] document = "stjudecloud/workflows:tools/fq.wdl" -error = '''validation error: [v1::001] invalid escape character '\.' in string at line 123:17''' +error = '''validation error: [v1::001] invalid escape character '\.' in string at line 128:17''' [[ignored_errors]] document = "stjudecloud/workflows:tools/kraken2.wdl" -error = '''validation error: [v1::001] invalid escape character '\.' in string at line 335:17''' +error = '''validation error: [v1::001] invalid escape character '\.' in string at line 336:17''' [[ignored_errors]] document = "stjudecloud/workflows:workflows/rnaseq/rnaseq-standard-fastq.wdl" -error = '''validation error: [v1::001] invalid escape character '\*' in string at line 55:241''' +error = '''validation error: [v1::001] invalid escape character '\*' in string at line 56:241''' diff --git a/wdl-ast/Cargo.toml b/wdl-ast/Cargo.toml new file mode 100644 index 000000000..73b07e2a6 --- /dev/null +++ b/wdl-ast/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "wdl-ast" +version = "0.1.0" +authors = ["Clay McLeod "] +edition.workspace = true +license.workspace = true +description = "Abstract syntax tree for Workflow Description Language (WDL) documents" +homepage = "https://github.com/stjude-rust-labs/wdl" +repository = "https://github.com/stjude-rust-labs/wdl" +documentation = "https://docs.rs/wdl-ast" + +[dependencies] +clap = { workspace = true, optional = true } +env_logger = { workspace = true, optional = true } +indexmap.workspace = true +lazy_static.workspace = true +log = { workspace = true, optional = true } +nonempty.workspace = true +ordered-float.workspace = true +pest.workspace = true +regex.workspace = true +tokio = { workspace = true, optional = true } +wdl-core = { path = "../wdl-core", version = "0.1.0" } +wdl-grammar = { path = "../wdl-grammar", version = "0.1.0" } + +[features] +binaries = ["dep:clap", "dep:env_logger", "dep:log", "dep:tokio"] + +[[bin]] +name = "wdl-ast" +path = "src/main.rs" +required-features = ["binaries"] diff --git a/wdl-ast/src/commands.rs b/wdl-ast/src/commands.rs new file mode 100644 index 000000000..ace47859b --- /dev/null +++ b/wdl-ast/src/commands.rs @@ -0,0 +1,3 @@ +//! Subcommands for the `wdl-ast` command-line tool. + +pub mod parse; diff --git a/wdl-ast/src/commands/parse.rs b/wdl-ast/src/commands/parse.rs new file mode 100644 index 000000000..e4631f43f --- /dev/null +++ b/wdl-ast/src/commands/parse.rs @@ -0,0 +1,79 @@ +//! `wdl-ast parse` + +use core::lint::warning::display; +use std::path::PathBuf; + +use clap::Parser; + +use log::warn; +use wdl_ast as ast; +use wdl_core as core; +use wdl_grammar as grammar; + +/// An error related to the `wdl-ast parse` subcommand. +#[derive(Debug)] +pub enum Error { + /// An abstract syntax tree error. + Ast(ast::Error), + + /// An input/output error. + InputOutput(std::io::Error), + + /// A grammar error. + Grammar(grammar::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Ast(err) => write!(f, "ast error: {err}"), + Error::InputOutput(err) => write!(f, "i/o error: {err}"), + Error::Grammar(err) => write!(f, "grammar error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// Arguments for the `wdl-ast parse` subcommand. +#[derive(Debug, Parser)] +pub struct Args { + /// Path to the WDL document. + #[clap(value_name = "PATH")] + path: PathBuf, + + /// The Workflow Description Language (WDL) specification version to use. + #[arg(value_name = "VERSION", short = 's', long, default_value_t, value_enum)] + specification_version: core::Version, +} + +/// Main function for this subcommand. +pub fn parse(args: Args) -> Result<()> { + let contents = std::fs::read_to_string(args.path).map_err(Error::InputOutput)?; + + let document = match args.specification_version { + core::Version::V1 => { + let pt = grammar::v1::parse(grammar::v1::Rule::document, &contents) + .map_err(Error::Grammar)?; + + ast::v1::parse(pt).map_err(Error::Ast)? + } + }; + + if let Some(warnings) = document.warnings() { + for warning in warnings { + let mut buffer = String::new(); + warning.display(&mut buffer, display::Mode::OneLine).unwrap(); + warn!("{}", buffer); + } + } + + for (_, task) in document.tasks() { + dbg!(task.runtime()); + } + + Ok(()) +} diff --git a/wdl-ast/src/common.rs b/wdl-ast/src/common.rs new file mode 100644 index 000000000..4642d2898 --- /dev/null +++ b/wdl-ast/src/common.rs @@ -0,0 +1,9 @@ +//! Common functionality used across all WDL abstract syntax trees. + +mod linter; +mod tree; +mod validator; + +pub use linter::Linter; +pub use tree::Tree; +pub use validator::Validator; diff --git a/wdl-ast/src/common/linter.rs b/wdl-ast/src/common/linter.rs new file mode 100644 index 000000000..97456dbaa --- /dev/null +++ b/wdl-ast/src/common/linter.rs @@ -0,0 +1,35 @@ +//! An abstract syntax tree linter. + +use wdl_core as core; + +use core::lint; +use core::lint::Warning; + +use crate::v1::Document; + +/// A [`Result`](std::result::Result) for [`Linter::lint()`]. +pub type Result = std::result::Result>, Box>; + +/// An abstract syntax tree linter. +#[derive(Debug)] +pub struct Linter; + +impl Linter { + /// Lints a WDL abstract syntax tree according to a set of lint rules and + /// returns a set of lint warnings (if any are detected). + pub fn lint(tree: &Document) -> Result { + let warnings = crate::v1::lint::RULES + .iter() + .map(|rule| rule.check(tree)) + .collect::>>, Box>>()? + .into_iter() + .flatten() + .flatten() + .collect::>(); + + match warnings.is_empty() { + true => Ok(None), + false => Ok(Some(warnings)), + } + } +} diff --git a/wdl-ast/src/common/tree.rs b/wdl-ast/src/common/tree.rs new file mode 100644 index 000000000..081a368ee --- /dev/null +++ b/wdl-ast/src/common/tree.rs @@ -0,0 +1,51 @@ +//! An abstract syntax tree. + +use wdl_core as core; + +use core::lint; + +use crate::v1::Document; + +/// An abstract syntax tree with a set of lint [`Warning`](lint::Warning)s. +/// +/// **Note:** this struct implements [`std::ops::Deref`] for a parsed WDL +/// [`Document`], so you can treat this exactly as if you were workings with a +/// [`Document`] directly. +#[derive(Debug)] +pub struct Tree { + /// The inner document. + inner: Document, + + /// The lint warnings associated with the parse tree. + warnings: Option>, +} + +impl Tree { + /// Creates a new [`Tree`]. + pub fn new(inner: Document, warnings: Option>) -> Self { + Self { inner, warnings } + } + + /// Gets the inner [`Document`] for the [`Tree`] by reference. + pub fn inner(&self) -> &Document { + &self.inner + } + + /// Consumes `self` to return the inner [`Document`] from the [`Tree`]. + pub fn into_inner(self) -> Document { + self.inner + } + + /// Gets the [`Warning`](lint::Warning)s from the [`Tree`] by reference. + pub fn warnings(&self) -> Option<&Vec> { + self.warnings.as_ref() + } +} + +impl std::ops::Deref for Tree { + type Target = Document; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} \ No newline at end of file diff --git a/wdl-ast/src/common/validator.rs b/wdl-ast/src/common/validator.rs new file mode 100644 index 000000000..fbcf175f7 --- /dev/null +++ b/wdl-ast/src/common/validator.rs @@ -0,0 +1,19 @@ +//! An abstract syntax tree validator. + +use wdl_core as core; + +use core::validation; + +use crate::v1::Document; + +/// An abstract syntax tree validator. +#[derive(Debug)] +pub struct Validator; + +impl Validator { + /// Validates an abstract syntax tree according to a set of validation + /// rules. + pub fn validate(tree: &Document) -> validation::Result { + crate::v1::validation::RULES.iter().try_for_each(|rule| rule.validate(tree)) + } +} \ No newline at end of file diff --git a/wdl-ast/src/lib.rs b/wdl-ast/src/lib.rs new file mode 100644 index 000000000..d9218a928 --- /dev/null +++ b/wdl-ast/src/lib.rs @@ -0,0 +1,47 @@ +//! An abstract syntax tree for Workflow Description Language documents. + +#![feature(decl_macro)] +#![warn(missing_docs)] +#![warn(rust_2018_idioms)] +#![warn(rust_2021_compatibility)] +#![warn(missing_debug_implementations)] +#![warn(clippy::missing_docs_in_private_items)] +#![warn(rustdoc::broken_intra_doc_links)] + +use wdl_core as core; + +pub mod common; +pub mod v1; + +/// An error related to an abstract syntax tree (AST). +#[derive(Debug)] +pub enum Error { + /// An error occurred while linting a parse tree. + /// + /// **Note:** this is not a lint _warning_! A lint error is an unrecoverable + /// error that occurs during the process of linting. + Lint(Box), + + /// An error occurred while parsing a WDL v1.x abstract syntax tree. + ParseV1(Box), + + /// An error occurred while validating an abstract syntax tree. + Validation(Box), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Lint(err) => { + write!(f, "lint error: {err}") + } + Error::ParseV1(err) => write!(f, "parse error:\n\n{err}"), + Error::Validation(err) => write!(f, "validation error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +pub type Result = std::result::Result; diff --git a/wdl-ast/src/main.rs b/wdl-ast/src/main.rs new file mode 100644 index 000000000..646e27e8e --- /dev/null +++ b/wdl-ast/src/main.rs @@ -0,0 +1,109 @@ +//! A command-line tool for exploring an abstract syntax tree constructed from a +//! Workflow Description Language (WDL) grammar. +//! +//! **Note:** this tool is intended to be used as a utility to test and develop +//! the [`wdl-ast`](https://crates.io/crates/wdl-ast) crate. It is not intended +//! to be used by a general audience. + +#![feature(let_chains)] +#![warn(missing_docs)] +#![warn(rust_2018_idioms)] +#![warn(rust_2021_compatibility)] +#![warn(missing_debug_implementations)] +#![warn(clippy::missing_docs_in_private_items)] +#![warn(rustdoc::broken_intra_doc_links)] + +use clap::Parser; +use clap::Subcommand; +use log::LevelFilter; + +mod commands; + +use crate::commands::parse; + +/// Subcommands for the `wdl-grammar` command-line tool. +#[derive(Debug, Subcommand)] +pub enum Command { + /// Parses and displays an abstract syntax tree. + Parse(parse::Args), +} + +/// Parse and testing Workflow Description Language (WDL) grammar. +#[derive(Parser, Debug)] +#[command(author, version, about, long_about)] +struct Args { + /// The subcommand to execute. + #[command(subcommand)] + command: Command, + + /// Detailed information, including debug information, is logged in the + /// console. + #[arg(short, long, global = true)] + debug: bool, + + /// Enables logging for all modules (not just `wdl-grammar`). + #[arg(short, long, global = true)] + log_all_modules: bool, + + /// Only errors are logged to the console. + #[arg(short, long, global = true)] + quiet: bool, + + /// All available information, including trace information, is logged in the + /// console. + #[arg(short, long, global = true)] + trace: bool, + + /// Additional information is logged in the console. + #[arg(short, long, global = true)] + verbose: bool, +} + +/// The inner function for the binary. +async fn inner() -> Result<(), Box> { + let args = Args::parse(); + + let level = if args.trace { + LevelFilter::max() + } else if args.debug { + LevelFilter::Debug + } else if args.verbose { + LevelFilter::Info + } else if args.quiet { + LevelFilter::Error + } else { + LevelFilter::Warn + }; + + let module = match args.log_all_modules { + true => None, + false => Some("wdl_ast"), + }; + + env_logger::builder().filter(module, level).init(); + + match args.command { + Command::Parse(args) => parse::parse(args)?, + }; + + Ok(()) +} + +#[tokio::main] +async fn main() { + match inner().await { + Ok(_) => {} + Err(err) => eprintln!("error: {}", err), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_arguments() { + use clap::CommandFactory; + Args::command().debug_assert() + } +} diff --git a/wdl-ast/src/v1.rs b/wdl-ast/src/v1.rs new file mode 100644 index 000000000..3971986c2 --- /dev/null +++ b/wdl-ast/src/v1.rs @@ -0,0 +1,90 @@ +//! WDL 1.x. + +use wdl_grammar as grammar; + +pub mod document; +pub mod lint; +pub mod macros; +pub mod validation; + +pub use document::Document; + +use crate::common::Linter; +use crate::common::Tree; +use crate::common::Validator; + +/// An error related to building an abstract syntax tree. +#[derive(Debug)] +pub enum Error { + /// A document error. + Document(Box), + + /// Attempted to create an AST element from a node that was incompatible. + InvalidNode(String), + + /// Missing a node that was expected to exist. + MissingNode(String), + + /// The [parse tree](grammar::common::Tree) had no root nodes. + MissingRootNode, + + /// Multiple nodes were found when only one was expected. + MultipleNodes(String), + + /// The [parse tree](grammar::common::Tree) had multiple root nodes. + MultipleRootNodes, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidNode(explanation) => write!(f, "invalid node: {explanation}"), + Error::Document(err) => write!(f, "document error: {err}"), + Error::MissingNode(explanation) => write!(f, "missing node: {explanation}"), + Error::MissingRootNode => write!(f, "parse tree had no root nodes"), + Error::MultipleNodes(explanation) => write!(f, "multiple nodes: {explanation}"), + Error::MultipleRootNodes => write!(f, "parse tree had multiple root nodes"), + } + } +} + +impl std::error::Error for Error {} + +/// Parses an abstract syntax tree (in the form of a [`Document`]) from a +/// [`Tree`]. +/// +/// # Examples +/// +/// ``` +/// use wdl_ast as ast; +/// use wdl_grammar as grammar; +/// +/// use grammar::v1::Rule; +/// +/// let pt = grammar::v1::parse(Rule::document, "version 1.1").unwrap(); +/// let ast = ast::v1::parse(pt).unwrap(); +/// +/// assert_eq!(ast.version(), &ast::v1::document::Version::OneDotOne); +/// ``` +pub fn parse(tree: grammar::common::Tree<'_, grammar::v1::Rule>) -> Result { + let root = match tree.len() { + 0 => return Err(super::Error::ParseV1(Box::new(Error::MissingRootNode))), + 1 => { + // SAFETY: we just ensured there is exactly one element in the parse + // tree. Thus, this will always unwrap. + tree.into_inner().next().unwrap() + } + _ => return Err(super::Error::ParseV1(Box::new(Error::MultipleRootNodes))), + }; + + let document = Document::try_from(root) + .map_err(|err| super::Error::ParseV1(Box::new(Error::Document(Box::new(err)))))?; + + Validator::validate(&document) + .map_err(Box::new) + .map_err(super::Error::Validation)?; + + let warnings = Linter::lint(&document).unwrap(); + + Ok(Tree::new(document, warnings)) +} diff --git a/wdl-ast/src/v1/document.rs b/wdl-ast/src/v1/document.rs new file mode 100644 index 000000000..0d94b1173 --- /dev/null +++ b/wdl-ast/src/v1/document.rs @@ -0,0 +1,321 @@ +//! Documents. + +use indexmap::IndexMap; +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +mod builder; +pub mod declaration; +pub mod expression; +pub mod identifier; +pub mod import; +pub mod input; +pub mod metadata; +pub mod output; +pub mod private_declarations; +pub mod r#struct; +pub mod task; +mod version; +pub mod workflow; + +pub use builder::Builder; +pub use declaration::Declaration; +pub use expression::Expression; +pub use identifier::Identifier; +pub use import::Import; +pub use input::Input; +pub use metadata::Metadata; +pub use output::Output; +pub use private_declarations::PrivateDeclarations; +pub use r#struct::Struct; +pub use task::Task; +pub use version::Version; +pub use workflow::Workflow; + +use crate::v1::macros::check_node; + +/// An error related to a [`Document`]. +#[derive(Debug)] +pub enum Error { + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An import error. + Import(import::Error), + + /// A struct error. + Struct(r#struct::Error), + + /// A task error. + Task(task::Error), + + /// A version error. + Version(version::Error), + + /// A workflow error. + Workflow(workflow::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Import(err) => write!(f, "import error: {err}"), + Error::Struct(err) => write!(f, "struct error: {err}"), + Error::Task(err) => write!(f, "task error: {err}"), + Error::Version(err) => write!(f, "version error: {err}"), + Error::Workflow(err) => write!(f, "workflow error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A document. +#[derive(Clone, Debug)] +pub struct Document { + /// Document version. + version: Version, + + /// Document imports. + imports: Vec, + + /// Document structs. + structs: Vec, + + /// Document tasks. + tasks: IndexMap, + + /// Document workflow. + workflow: Option, +} + +impl Document { + /// Gets the imports for this [`Document`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use grammar::v1::Rule; + /// + /// let pt = grammar::v1::parse( + /// Rule::document, + /// r#"version 1.1 + /// import "../hello.wdl" as hello alias foo as bar"#, + /// ) + /// .unwrap(); + /// let ast = ast::v1::parse(pt).unwrap(); + /// + /// assert_eq!(ast.imports().len(), 1); + /// + /// let import = ast.imports().first().unwrap(); + /// assert_eq!(import.uri(), "../hello.wdl"); + /// assert_eq!(import.r#as().unwrap().as_str(), "hello"); + /// assert_eq!( + /// import.aliases().unwrap().get("foo"), + /// Some(&Identifier::try_from("bar").unwrap()) + /// ); + /// ``` + pub fn imports(&self) -> &[Import] { + self.imports.as_ref() + } + + /// Gets the structs for this [`Document`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::identifier::singular::Identifier; + /// use grammar::v1::Rule; + /// + /// let pt = grammar::v1::parse( + /// Rule::document, + /// r#"version 1.1 + /// struct Hello { + /// String world + /// }"#, + /// ) + /// .unwrap(); + /// let ast = ast::v1::parse(pt).unwrap(); + /// + /// assert_eq!(ast.structs().len(), 1); + /// + /// let r#struct = ast.structs().first().unwrap(); + /// assert_eq!(r#struct.name().as_str(), "Hello"); + /// + /// let declaration = r#struct.declarations().unwrap().into_iter().next().unwrap(); + /// assert_eq!(declaration.name().as_str(), "world"); + /// assert_eq!(declaration.r#type().kind(), &Kind::String); + /// assert_eq!(declaration.r#type().optional(), false); + /// ``` + pub fn structs(&self) -> &[Struct] { + self.structs.as_ref() + } + + /// Gets the tasks for this [`Document`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use grammar::v1::Rule; + /// + /// let pt = grammar::v1::parse( + /// Rule::document, + /// "version 1.1 + /// task say_hello { + /// command <<< + /// echo 'Hello, world!' + /// >>> + /// }", + /// ) + /// .unwrap(); + /// let ast = ast::v1::parse(pt).unwrap(); + /// + /// assert_eq!(ast.tasks().len(), 1); + /// let (name, task) = ast.tasks().first().unwrap(); + /// assert_eq!(name.as_str(), "say_hello"); + /// assert_eq!(task.name().as_str(), "say_hello"); + /// assert_eq!(task.command().to_string(), "echo 'Hello, world!'"); + /// ``` + pub fn tasks(&self) -> &IndexMap { + &self.tasks + } + + /// Gets the workflow for this [`Document`] by reference (if it exists). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document::workflow::execution::Statement; + /// use grammar::v1::Rule; + /// + /// let pt = grammar::v1::parse( + /// Rule::document, + /// r#"version 1.1 + /// workflow hello_world { + /// call test + /// }"#, + /// ) + /// .unwrap(); + /// let ast = ast::v1::parse(pt).unwrap(); + /// + /// let workflow = ast.workflow().unwrap(); + /// assert_eq!(workflow.name().as_str(), "hello_world"); + /// + /// let statements = workflow.statements().unwrap(); + /// assert_eq!(statements.len(), 1); + /// + /// let call = match statements.into_iter().next().unwrap() { + /// Statement::Call(call) => call, + /// _ => unreachable!(), + /// }; + /// assert_eq!(call.name().to_string(), "test"); + /// ``` + pub fn workflow(&self) -> Option<&Workflow> { + self.workflow.as_ref() + } + + /// Gets the document version of this [`Document`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document::Version; + /// use grammar::v1::Rule; + /// + /// let pt = grammar::v1::parse(Rule::document, "version 1.1").unwrap(); + /// let ast = ast::v1::parse(pt).unwrap(); + /// + /// let mut version = ast.version(); + /// assert_eq!(version, &Version::OneDotOne); + /// ``` + pub fn version(&self) -> &Version { + &self.version + } +} + +impl TryFrom> for Document { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, document); + let mut builder = builder::Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::version => { + let version = Version::try_from(node).map_err(Error::Version)?; + builder = builder.version(version).map_err(Error::Builder)?; + } + Rule::import => { + let import = Import::try_from(node).map_err(Error::Import)?; + builder = builder.push_import(import); + } + Rule::r#struct => { + let r#struct = Struct::try_from(node).map_err(Error::Struct)?; + builder = builder.push_struct(r#struct); + } + Rule::task => { + let task = Task::try_from(node).map_err(Error::Task)?; + builder = builder.insert_task(task.name().clone(), task); + } + Rule::workflow => { + let workflow = Workflow::try_from(node).map_err(Error::Workflow)?; + builder = builder.workflow(workflow).map_err(Error::Builder)?; + } + Rule::EOI => {} + Rule::COMMENT => {} + Rule::WHITESPACE => {} + rule => unreachable!("workflow should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::v1::macros; + + #[test] + fn it_parses_from_a_supported_node_type() { + let version = macros::test::valid_node!("version 1.1", version, Version); + assert_eq!(version, Version::OneDotOne); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + macros::test::invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + version, + Version + ); + } +} diff --git a/wdl-ast/src/v1/document/builder.rs b/wdl-ast/src/v1/document/builder.rs new file mode 100644 index 000000000..ac946c51c --- /dev/null +++ b/wdl-ast/src/v1/document/builder.rs @@ -0,0 +1,293 @@ +//! A builder for a [`Document`]. + +use indexmap::IndexMap; + +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::task::Task; +use crate::v1::document::Document; +use crate::v1::document::Import; +use crate::v1::document::Struct; +use crate::v1::document::Version; +use crate::v1::document::Workflow; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A version was not provided to the [`Builder`]. + Version, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Version => write!(f, "version"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error that occurs when a multiple values were provded for a field that +/// only accepts a single value. +#[derive(Debug)] +pub enum MultipleError { + /// Attempted to set multiple values for the version field within the + /// [`Builder`]. + Version, + + /// Attempted to set multiple values for the workflow field within the + /// [`Builder`]. + Workflow, +} + +impl std::fmt::Display for MultipleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MultipleError::Version => write!(f, "version"), + MultipleError::Workflow => write!(f, "workflow"), + } + } +} + +impl std::error::Error for MultipleError {} + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), + + /// Multiple values were provided for a field that accepts a single value. + Multiple(MultipleError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + Error::Multiple(err) => { + write!(f, "multiple values provided for single value field: {err}") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for a [`Document`]. +#[derive(Default, Debug)] +pub struct Builder { + /// Document imports. + imports: Vec, + + /// Document structs. + structs: Vec, + + /// Document tasks. + tasks: IndexMap, + + /// Document workflow. + workflow: Option, + + /// Document version. + version: Option, +} + +impl Builder { + /// Pushes an [`Import`] into the document [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document; + /// use ast::v1::document::import::Builder; + /// + /// let import = Builder::default() + /// .uri(String::from("../mapping.wdl"))? + /// .try_build()?; + /// + /// let document = document::Builder::default() + /// .version(document::Version::OneDotOne)? + /// .push_import(import) + /// .try_build()?; + /// + /// let import = document.imports().into_iter().next().unwrap(); + /// assert_eq!(import.uri(), "../mapping.wdl"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn push_import(mut self, import: Import) -> Self { + self.imports.push(import); + self + } + + /// Pushes a [`Struct`] into the document [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::r#struct; + /// use ast::v1::document::r#struct::Builder; + /// + /// let r#struct = Builder::default() + /// .name(Identifier::try_from("a_struct").unwrap())? + /// .try_build()?; + /// + /// let document = document::Builder::default() + /// .version(document::Version::OneDotOne)? + /// .push_struct(r#struct) + /// .try_build()?; + /// + /// let r#struct = document.structs().first().unwrap(); + /// assert_eq!(r#struct.name().as_str(), "a_struct"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn push_struct(mut self, r#struct: Struct) -> Self { + self.structs.push(r#struct); + self + } + + /// Inserts a task into the document [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::task::command::Contents; + /// + /// let name = Identifier::try_from(String::from("name"))?; + /// let contents = "echo 'Hello, world!'".parse::().unwrap(); + /// let command = document::task::Command::HereDoc(contents); + /// let task = document::task::Builder::default() + /// .name(name.clone())? + /// .command(command)? + /// .try_build()?; + /// + /// let document = document::Builder::default() + /// .version(document::Version::OneDotOne)? + /// .insert_task(name, task) + /// .try_build()?; + /// assert_eq!(document.tasks().len(), 1); + /// + /// let (name, task) = document.tasks().first().unwrap(); + /// assert_eq!(name.as_str(), "name"); + /// assert_eq!(task.name().as_str(), "name"); + /// assert_eq!(task.command().to_string(), "echo 'Hello, world!'"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn insert_task(mut self, name: Identifier, task: Task) -> Self { + self.tasks.insert(name, task); + self + } + + /// Adds a document version to the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document; + /// + /// let document = document::Builder::default() + /// .version(document::Version::OneDotOne)? + /// .try_build()?; + /// + /// assert_eq!(document.version(), &document::Version::OneDotOne); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn version(mut self, version: Version) -> Result { + if self.version.is_some() { + return Err(Error::Multiple(MultipleError::Version)); + } + + self.version = Some(version); + Ok(self) + } + + /// Adds a workflow to the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::workflow; + /// + /// let workflow = workflow::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .try_build()?; + /// + /// let document = document::Builder::default() + /// .version(document::Version::OneDotOne)? + /// .workflow(workflow.clone())? + /// .try_build()?; + /// + /// assert_eq!(document.workflow(), Some(&workflow)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn workflow(mut self, workflow: Workflow) -> Result { + if self.workflow.is_some() { + return Err(Error::Multiple(MultipleError::Workflow)); + } + + self.workflow = Some(workflow); + Ok(self) + } + + /// Consumes `self` to attempt to build a [`Document`] from the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document; + /// + /// let document = document::Builder::default() + /// .version(document::Version::OneDotOne)? + /// .try_build()?; + /// + /// assert_eq!(document.version(), &document::Version::OneDotOne); + /// + /// # Ok::<(), Box>(()) + pub fn try_build(self) -> Result { + let version = self + .version + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Version)))?; + + Ok(Document { + imports: self.imports, + structs: self.structs, + tasks: self.tasks, + workflow: self.workflow, + version, + }) + } +} diff --git a/wdl-ast/src/v1/document/declaration.rs b/wdl-ast/src/v1/document/declaration.rs new file mode 100644 index 000000000..c28f5b58c --- /dev/null +++ b/wdl-ast/src/v1/document/declaration.rs @@ -0,0 +1,389 @@ +//! Declarations. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +pub mod r#type; + +pub mod bound; +pub mod unbound; + +pub use r#type::Type; + +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::Expression; + +/// An error related to a [`Declaration`]. +#[derive(Debug)] +pub enum Error { + /// A bound declaration error. + BoundDeclaration(bound::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An unbound declaration error. + UnboundDeclaration(unbound::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::BoundDeclaration(err) => write!(f, "bound declaration error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::UnboundDeclaration(err) => write!(f, "unbound declaration error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A declaration. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Declaration { + /// A bound declaration. + Bound(bound::Declaration), + + /// A unbound declaration. + Unbound(unbound::Declaration), +} + +impl Declaration { + /// Gets the name from the [`Declaration`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("foo")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// let declaration = Declaration::Bound(bound.clone()); + /// + /// assert_eq!(declaration.name().as_str(), "foo"); + /// + /// let unbound = unbound::Builder::default() + /// .name(Identifier::try_from("bar")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// let declaration = Declaration::Unbound(unbound); + /// + /// assert_eq!(declaration.name().as_str(), "bar"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(&self) -> &Identifier { + match self { + Declaration::Bound(bound) => bound.name(), + Declaration::Unbound(unbound) => unbound.name(), + } + } + + /// Gets the type from the [`Declaration`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("foo")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// let declaration = Declaration::Bound(bound.clone()); + /// + /// assert_eq!(declaration.r#type().kind(), &Kind::Boolean); + /// assert_eq!(declaration.r#type().optional(), false); + /// + /// let unbound = unbound::Builder::default() + /// .name(Identifier::try_from("bar")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// let declaration = Declaration::Unbound(unbound); + /// + /// assert_eq!(declaration.r#type().kind(), &Kind::Boolean); + /// assert_eq!(declaration.r#type().optional(), false); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn r#type(&self) -> &Type { + match self { + Declaration::Bound(bound) => bound.r#type(), + Declaration::Unbound(unbound) => unbound.r#type(), + } + } + + /// Gets the value from the [`Declaration`] by reference (if it exists). + /// + /// * If the declaration is bound, a reference to the value (as an + /// [`Expression`]) is returned wrapped in [`Some`]. + /// * If the declaration is unbound, [`None`] is returned. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("foo")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// let declaration = Declaration::Bound(bound.clone()); + /// + /// assert_eq!( + /// declaration.value(), + /// Some(&Expression::Literal(Literal::None)) + /// ); + /// + /// let unbound = unbound::Builder::default() + /// .name(Identifier::try_from("bar")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// let declaration = Declaration::Unbound(unbound); + /// + /// assert_eq!(declaration.value(), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn value(&self) -> Option<&Expression> { + match self { + Declaration::Bound(bound) => Some(bound.value()), + Declaration::Unbound(_) => None, + } + } + + /// Returns a reference to the [bound declaration](bound::Declaration) + /// wrapped in [`Some`] if the [`Declaration`] is an [`Declaration::Bound`]. + /// Else, [`None`] is returned. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("foo")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// let declaration = Declaration::Bound(bound.clone()); + /// + /// assert_eq!(declaration.as_bound(), Some(&bound)); + /// + /// let unbound = unbound::Builder::default() + /// .name(Identifier::try_from("bar")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// let declaration = Declaration::Unbound(unbound); + /// + /// assert_eq!(declaration.as_bound(), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn as_bound(&self) -> Option<&bound::Declaration> { + match self { + Declaration::Bound(bound) => Some(bound), + _ => None, + } + } + + /// Consumes `self` and returns the [bound declaration](bound::Declaration) + /// wrapped in [`Some`] if the [`Declaration`] is an [`Declaration::Bound`]. + /// Else, [`None`] is returned. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("foo")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// let declaration = Declaration::Bound(bound.clone()); + /// + /// assert_eq!(declaration.into_bound(), Some(bound)); + /// + /// let unbound = unbound::Builder::default() + /// .name(Identifier::try_from("bar")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// let declaration = Declaration::Unbound(unbound); + /// + /// assert_eq!(declaration.into_bound(), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn into_bound(self) -> Option { + match self { + Declaration::Bound(bound) => Some(bound), + _ => None, + } + } + + /// Returns a reference to the [unbound declaration](bound::Declaration) + /// wrapped in [`Some`] if the [`Declaration`] is an + /// [`Declaration::Unbound`]. Else, [`None`] is returned. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("foo")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// let declaration = Declaration::Bound(bound); + /// + /// assert_eq!(declaration.as_unbound(), None); + /// + /// let unbound = unbound::Builder::default() + /// .name(Identifier::try_from("bar")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// let declaration = Declaration::Unbound(unbound.clone()); + /// + /// assert_eq!(declaration.as_unbound(), Some(&unbound)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn as_unbound(&self) -> Option<&unbound::Declaration> { + match self { + Declaration::Unbound(unbound) => Some(unbound), + _ => None, + } + } + + /// Consumes `self` and returns the [unbound + /// declaration](unbound::Declaration) wrapped in [`Some`] if the + /// [`Declaration`] is an [`Declaration::Unbound`]. Else, [`None`] is + /// returned. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("foo")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// let declaration = Declaration::Bound(bound); + /// + /// assert_eq!(declaration.into_unbound(), None); + /// + /// let unbound = unbound::Builder::default() + /// .name(Identifier::try_from("bar")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// let declaration = Declaration::Unbound(unbound.clone()); + /// + /// assert_eq!(declaration.into_unbound(), Some(unbound)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn into_unbound(self) -> Option { + match self { + Declaration::Unbound(unbound) => Some(unbound), + _ => None, + } + } +} + +impl TryFrom> for Declaration { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + match node.as_rule() { + Rule::bound_declaration => { + let declaration = + bound::Declaration::try_from(node).map_err(Error::BoundDeclaration)?; + Ok(Declaration::Bound(declaration)) + } + Rule::unbound_declaration => { + let declaration = + unbound::Declaration::try_from(node).map_err(Error::UnboundDeclaration)?; + Ok(Declaration::Unbound(declaration)) + } + rule => Err(Error::Common(crate::v1::Error::InvalidNode(format!( + "declaration cannot be parsed from node type {:?}", + rule + )))), + } + } +} diff --git a/wdl-ast/src/v1/document/declaration/bound.rs b/wdl-ast/src/v1/document/declaration/bound.rs new file mode 100644 index 000000000..2967a0a36 --- /dev/null +++ b/wdl-ast/src/v1/document/declaration/bound.rs @@ -0,0 +1,216 @@ +//! Bound declarations. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::declaration::r#type; +use crate::v1::document::declaration::r#type::Type; +use crate::v1::document::expression; +use crate::v1::document::Expression; +use crate::v1::macros::check_node; + +pub mod builder; + +pub use builder::Builder; + +/// An error related to a [`Declaration`]. +#[derive(Debug)] +pub enum Error { + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An expression error. + Expression(expression::Error), + + /// An identifier error. + Identifier(singular::Error), + + /// A WDL type error. + Type(r#type::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Expression(err) => write!(f, "expression error: {err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + Error::Type(err) => write!(f, "wdl type error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A bound declaration. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Declaration { + /// The name. + name: Identifier, + + /// The WDL type. + r#type: Type, + + /// The value as an [`Expression`]. + value: Expression, +} + +impl Declaration { + /// Gets the name of the [`Declaration`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound::Builder; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// assert_eq!(declaration.name().as_str(), "hello_world"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(&self) -> &Identifier { + &self.name + } + + /// Gets the WDL [type](Type) of the [`Declaration`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound::Builder; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// assert!(matches!(declaration.r#type().kind(), &Kind::Boolean)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn r#type(&self) -> &Type { + &self.r#type + } + + /// Gets the value of the [`Declaration`] as an [`Expression`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound::Builder; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// assert!(matches!( + /// declaration.value(), + /// &Expression::Literal(Literal::None) + /// )); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn value(&self) -> &Expression { + &self.value + } +} + +impl TryFrom> for Declaration { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, bound_declaration); + let mut builder = Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::wdl_type => { + let r#type = Type::try_from(node).map_err(Error::Type)?; + builder = builder.r#type(r#type).map_err(Error::Builder)?; + } + Rule::bound_declaration_name => { + let name = Identifier::try_from(node.as_str().to_owned()) + .map_err(Error::Identifier)?; + builder = builder.name(name).map_err(Error::Builder)?; + } + Rule::expression => { + let expression = Expression::try_from(node).map_err(Error::Expression)?; + builder = builder.value(expression).map_err(Error::Builder)?; + } + Rule::WHITESPACE => {} + Rule::COMMENT => {} + rule => unreachable!("bound declaration should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::v1::document::declaration::r#type::Kind; + use crate::v1::document::expression::Literal; + use crate::v1::macros; + + #[test] + fn it_parses_from_a_supported_node_type() { + let declaration = + macros::test::valid_node!("Boolean? hello = false", bound_declaration, Declaration); + assert_eq!(declaration.name().as_str(), "hello"); + assert_eq!(declaration.r#type(), &Type::new(Kind::Boolean, true)); + assert_eq!( + declaration.value(), + &Expression::Literal(Literal::Boolean(false)) + ); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + macros::test::invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + bound_declaration, + Declaration + ); + } +} diff --git a/wdl-ast/src/v1/document/declaration/bound/builder.rs b/wdl-ast/src/v1/document/declaration/bound/builder.rs new file mode 100644 index 000000000..ce333e024 --- /dev/null +++ b/wdl-ast/src/v1/document/declaration/bound/builder.rs @@ -0,0 +1,248 @@ +//! Builder for a [bound declaration](Declaration). + +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::declaration::bound::Declaration; +use crate::v1::document::declaration::r#type::Type; +use crate::v1::document::Expression; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A name was not provided to the [`Builder`]. + Name, + + /// A type was not provided to the [`Builder`]. + Type, + + /// An expression was not provided to the [`Builder`]. + Expression, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Name => write!(f, "name"), + MissingError::Type => write!(f, "type"), + MissingError::Expression => write!(f, "expression"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error that occurs when a multiple values were provded for a field that +/// only accepts a single value. +#[derive(Debug)] +pub enum MultipleError { + /// Attempted to set multiple values for the name field within the + /// [`Builder`]. + Name, + + /// Attempted to set multiple values for the type field within the + /// [`Builder`]. + Type, + + /// Attempted to set multiple values for the expression field within the + /// [`Builder`]. + Expression, +} + +impl std::fmt::Display for MultipleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MultipleError::Name => write!(f, "name"), + MultipleError::Type => write!(f, "type"), + MultipleError::Expression => write!(f, "expression"), + } + } +} + +impl std::error::Error for MultipleError {} + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), + + /// Multiple values were provided for a field that accepts a single value. + Multiple(MultipleError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + Error::Multiple(err) => { + write!(f, "multiple values provided for single value field: {err}") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for a [bound declaration](Declaration). +#[derive(Debug, Default)] +pub struct Builder { + /// The name. + name: Option, + + /// The WDL type. + r#type: Option, + + /// The value as an [`Expression`]. + value: Option, +} + +impl Builder { + /// Sets the name of the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound::Builder; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// assert_eq!(declaration.name().as_str(), "hello_world"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(mut self, name: Identifier) -> Result { + if self.name.is_some() { + return Err(Error::Multiple(MultipleError::Name)); + } + + self.name = Some(name); + Ok(self) + } + + /// Sets the WDL [type](Type) of the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound::Builder; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// assert!(matches!(declaration.r#type().kind(), &Kind::Boolean)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn r#type(mut self, r#type: Type) -> Result { + if self.r#type.is_some() { + return Err(Error::Multiple(MultipleError::Type)); + } + + self.r#type = Some(r#type); + Ok(self) + } + + /// Sets the value of the [`Builder`] as an [`Expression`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound::Builder; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// assert!(matches!( + /// declaration.value(), + /// &Expression::Literal(Literal::None) + /// )); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn value(mut self, expression: Expression) -> Result { + if self.value.is_some() { + return Err(Error::Multiple(MultipleError::Expression)); + } + + self.value = Some(expression); + Ok(self) + } + + /// Consumes `self` to attempt to build a [`Declaration`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound::Builder; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_build(self) -> Result { + let name = self + .name + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Name)))?; + + let r#type = self + .r#type + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Type)))?; + + let value = self + .value + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Expression)))?; + + Ok(Declaration { + name, + r#type, + value, + }) + } +} diff --git a/wdl-ast/src/v1/document/declaration/type.rs b/wdl-ast/src/v1/document/declaration/type.rs new file mode 100644 index 000000000..f9f8fa3e0 --- /dev/null +++ b/wdl-ast/src/v1/document/declaration/type.rs @@ -0,0 +1,162 @@ +//! A type of declaration. + +use grammar::v1::Rule; +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::macros::check_node; + +mod kind; + +pub use kind::Kind; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A [`Kind`] was not provided. + Kind, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Kind => write!(f, "kind"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error related to a [`Type`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// An identifier error. + Identifier(singular::Error), + + /// A required field was missing at build time. + Missing(MissingError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + Error::Missing(err) => write!(f, "missing value for field: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A WDL type. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Type { + /// The kind of type. + kind: Kind, + + /// Whether the type is marked as optional. + optional: bool, +} + +impl Type { + /// Creates a new [`Type`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// + /// let r#type = Type::new(Kind::Boolean, false); + /// assert_eq!(r#type.kind(), &Kind::Boolean); + /// assert_eq!(r#type.optional(), false); + /// ``` + pub fn new(kind: Kind, optional: bool) -> Self { + Self { kind, optional } + } + + /// Gets the kind of [`Type`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// + /// let r#type = Type::new(Kind::Boolean, false); + /// assert_eq!(r#type.kind(), &Kind::Boolean); + /// ``` + pub fn kind(&self) -> &Kind { + &self.kind + } + + /// Returns whether the type is optional. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// + /// let r#type = Type::new(Kind::Boolean, false); + /// assert_eq!(r#type.kind(), &Kind::Boolean); + /// assert_eq!(r#type.optional(), false); + /// ``` + pub fn optional(&self) -> bool { + self.optional + } +} + +impl TryFrom> for Type { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, wdl_type); + + let mut kind = None; + let mut optional = false; + + for node in node.into_inner() { + match node.as_rule() { + Rule::map_type => kind = Some(Kind::Map), + Rule::array_type => kind = Some(Kind::Array), + Rule::pair_type => kind = Some(Kind::Pair), + Rule::string_type => kind = Some(Kind::String), + Rule::file_type => kind = Some(Kind::File), + Rule::bool_type => kind = Some(Kind::Boolean), + Rule::int_type => kind = Some(Kind::Integer), + Rule::float_type => kind = Some(Kind::Float), + Rule::object_type => kind = Some(Kind::Object), + Rule::struct_type => { + kind = { + let identifier = Identifier::try_from(node).map_err(Error::Identifier)?; + Some(Kind::Struct(identifier)) + } + } + Rule::OPTION => optional = true, + Rule::WHITESPACE => {} + Rule::COMMENT => {} + rule => unreachable!("type should not contain {:?}", rule), + } + } + + let kind = kind + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Kind)))?; + + Ok(Type { kind, optional }) + } +} diff --git a/wdl-ast/src/v1/document/declaration/type/kind.rs b/wdl-ast/src/v1/document/declaration/type/kind.rs new file mode 100644 index 000000000..55cc51e20 --- /dev/null +++ b/wdl-ast/src/v1/document/declaration/type/kind.rs @@ -0,0 +1,37 @@ +//! Kinds of WDL [`Type`](super::Type)s. + +use crate::v1::document::identifier::singular::Identifier; + +/// A kind of WDL [`Type`](super::Type). +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Kind { + /// A map. + Map, + + /// An array. + Array, + + /// A pair. + Pair, + + /// A string. + String, + + /// A file. + File, + + /// A boolean. + Boolean, + + /// An integer. + Integer, + + /// A float. + Float, + + /// An object. + Object, + + /// A struct. + Struct(Identifier), +} diff --git a/wdl-ast/src/v1/document/declaration/unbound.rs b/wdl-ast/src/v1/document/declaration/unbound.rs new file mode 100644 index 000000000..772427fbe --- /dev/null +++ b/wdl-ast/src/v1/document/declaration/unbound.rs @@ -0,0 +1,162 @@ +//! Unbound declarations. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::declaration::r#type; +use crate::v1::document::declaration::r#type::Type; +use crate::v1::macros::check_node; + +pub mod builder; + +pub use builder::Builder; + +/// An error related to a [`Declaration`]. +#[derive(Debug)] +pub enum Error { + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An identifier error. + Identifier(singular::Error), + + /// A WDL type error. + Type(r#type::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + Error::Type(err) => write!(f, "wdl type error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// An unbound declaration. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Declaration { + /// The name. + name: Identifier, + + /// The WDL type. + r#type: Type, +} + +impl Declaration { + /// Gets the name of the [`Declaration`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound::Builder; + /// use ast::v1::document::declaration::Type; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// + /// assert_eq!(declaration.name().as_str(), "hello_world"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(&self) -> &Identifier { + &self.name + } + + /// Gets the WDL [type](Type) of the [`Declaration`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound::Builder; + /// use ast::v1::document::declaration::Type; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// + /// assert!(matches!(declaration.r#type().kind(), &Kind::Boolean)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn r#type(&self) -> &Type { + &self.r#type + } +} + +impl TryFrom> for Declaration { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, unbound_declaration); + let mut builder = Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::wdl_type => { + let r#type = Type::try_from(node).map_err(Error::Type)?; + builder = builder.r#type(r#type).map_err(Error::Builder)?; + } + Rule::unbound_declaration_name => { + let name = Identifier::try_from(node.as_str().to_owned()) + .map_err(Error::Identifier)?; + builder = builder.name(name).map_err(Error::Builder)?; + } + Rule::WHITESPACE => {} + Rule::COMMENT => {} + rule => unreachable!("unbound declaration should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::v1::document::declaration::r#type::Kind; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + #[test] + fn it_parses_from_a_supported_node_type() { + let declaration = valid_node!("String? hello", unbound_declaration, Declaration); + assert_eq!(declaration.r#type().kind(), &Kind::String); + assert!(declaration.r#type().optional()); + assert_eq!(declaration.name().as_str(), "hello"); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + unbound_declaration, + Declaration + ); + } +} diff --git a/wdl-ast/src/v1/document/declaration/unbound/builder.rs b/wdl-ast/src/v1/document/declaration/unbound/builder.rs new file mode 100644 index 000000000..03885c562 --- /dev/null +++ b/wdl-ast/src/v1/document/declaration/unbound/builder.rs @@ -0,0 +1,187 @@ +//! Builder for a [unbound declaration](Declaration). + +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::declaration::r#type::Type; +use crate::v1::document::declaration::unbound::Declaration; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A name was not provided to the [`Builder`]. + Name, + + /// A type was not provided to the [`Builder`]. + Type, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Name => write!(f, "name"), + MissingError::Type => write!(f, "type"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error that occurs when a multiple values were provded for a field that +/// only accepts a single value. +#[derive(Debug)] +pub enum MultipleError { + /// Attempted to set multiple values for the name field within the + /// [`Builder`]. + Name, + + /// Attempted to set multiple values for the type field within the + /// [`Builder`]. + Type, +} + +impl std::fmt::Display for MultipleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MultipleError::Name => write!(f, "name"), + MultipleError::Type => write!(f, "type"), + } + } +} + +impl std::error::Error for MultipleError {} + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), + + /// Multiple values were provided for a field that accepts a single value. + Multiple(MultipleError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + Error::Multiple(err) => { + write!(f, "multiple values provided for single value field: {err}") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for a [unbound declaration](Declaration). +#[derive(Debug, Default)] +pub struct Builder { + /// The name. + name: Option, + + /// The WDL type. + r#type: Option, +} + +impl Builder { + /// Sets the name of the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound::Builder; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// + /// assert_eq!(declaration.name().as_str(), "hello_world"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(mut self, name: Identifier) -> Result { + if self.name.is_some() { + return Err(Error::Multiple(MultipleError::Name)); + } + + self.name = Some(name); + Ok(self) + } + + /// Sets the WDL [type](Type) of the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound::Builder; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// + /// assert!(matches!(declaration.r#type().kind(), &Kind::Boolean)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn r#type(mut self, r#type: Type) -> Result { + if self.r#type.is_some() { + return Err(Error::Multiple(MultipleError::Type)); + } + + self.r#type = Some(r#type); + Ok(self) + } + + /// Consumes `self` to attempt to build a [`Declaration`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound::Builder; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_build(self) -> Result { + let name = self + .name + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Name)))?; + + let r#type = self + .r#type + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Type)))?; + + Ok(Declaration { name, r#type }) + } +} diff --git a/wdl-ast/src/v1/document/expression.rs b/wdl-ast/src/v1/document/expression.rs new file mode 100644 index 000000000..443394277 --- /dev/null +++ b/wdl-ast/src/v1/document/expression.rs @@ -0,0 +1,453 @@ +//! Expressions. + +use std::num::ParseFloatError; +use std::num::ParseIntError; + +use lazy_static::lazy_static; +use ordered_float::OrderedFloat; +use pest::pratt_parser::Assoc; +use pest::pratt_parser::Op; +use pest::pratt_parser::PrattParser; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::macros::check_node; +use crate::v1::macros::dive_one; + +mod array; +mod r#if; +mod literal; +mod map; +mod pair; +mod unary_signed; + +pub use array::Array; +pub use literal::Literal; +pub use map::Map; +pub use pair::Pair; +pub use r#if::If; +pub use unary_signed::UnarySigned; + +lazy_static! { + static ref PRATT_PARSER: PrattParser = PrattParser::new() + // [#1] Logical OR + .op(Op::infix(Rule::or, Assoc::Left)) + // [#2] Logical AND + .op(Op::infix(Rule::and, Assoc::Left)) + // [#3] Equality | Inequality + .op(Op::infix(Rule::eq, Assoc::Left) | Op::infix(Rule::neq, Assoc::Left)) + // [#4] Less Than | Less Than Or Equal | Greater Than | Greater Than Or Equal + .op(Op::infix(Rule::lt, Assoc::Left) | Op::infix(Rule::lte, Assoc::Left) | Op::infix(Rule::gt, Assoc::Left) | Op::infix(Rule::gte, Assoc::Left)) + // [#5] Addition | Subtraction + .op(Op::infix(Rule::add, Assoc::Left) | Op::infix(Rule::sub, Assoc::Left)) + // [#6] Multiplication | Division | Remainder + .op(Op::infix(Rule::mul, Assoc::Left) | Op::infix(Rule::div, Assoc::Left) | Op::infix(Rule::remainder, Assoc::Left)) + // [#7] Logical NOT | Unary signed positive | Unary signed negative. + .op(Op::prefix(Rule::negation) | Op::prefix(Rule::unary_signed_positive) | Op::prefix(Rule::unary_signed_negative)) + // [#8] Function call. + .op(Op::postfix(Rule::call)) + // [#9] Index. + .op(Op::postfix(Rule::index)) + // [#10] Member access. + .op(Op::postfix(Rule::member)); +} + +/// An error related to an [`Expression`]. +#[derive(Debug)] +pub enum Error { + /// An array error. + Array(array::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An identifier error. + Identifier(singular::Error), + + /// An if error. + If(r#if::Error), + + /// A map error. + Map(map::Error), + + /// A pair error. + Pair(pair::Error), + + /// A [`ParseIntError`]. + ParseInt(ParseIntError), + + /// A [`ParseFloatError`]. + ParseFloat(ParseFloatError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Array(err) => write!(f, "array error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + Error::If(err) => write!(f, "if error: {err}"), + Error::Map(err) => write!(f, "map error: {err}"), + Error::Pair(err) => write!(f, "pair error: {err}"), + Error::ParseInt(err) => write!(f, "parse int error: {err}"), + Error::ParseFloat(err) => write!(f, "parse float error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// An expression. +#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)] +pub enum Expression { + /// Addition. + Add(Box, Box), + + /// Logical AND. + And(Box, Box), + + /// An array literal. + Array(Array), + + /// Function call. + Call(Box), + + /// Division. + Divide(Box, Box), + + /// Equal. + Equal(Box, Box), + + /// A group. + Group(Box), + + /// Greater than. + GreaterThan(Box, Box), + + /// Greater than or equal. + GreaterThanOrEqual(Box, Box), + + /// An if statement. + If(If), + + /// Index access. + Index(Box), + + /// A literal value. + Literal(Literal), + + /// Less than. + LessThan(Box, Box), + + /// Less than or equal. + LessThanOrEqual(Box, Box), + + /// A map literal. + Map(Map), + + /// Member access. + Member(Box), + + /// Multiplication. + Multiply(Box, Box), + + /// Negation. + Negation(Box), + + /// Not equal. + NotEqual(Box, Box), + + /// Logical OR. + Or(Box, Box), + + /// A pair literal. + Pair(Pair), + + /// Remainder. + Remainder(Box, Box), + + /// A struct literal. + Struct, + + /// Subtraction. + Subtract(Box, Box), + + /// Unary signed. + UnarySigned(UnarySigned), +} + +/// Parses an expression using a [`PrattParser`]. +fn parse<'a, P: Iterator>>( + pairs: P, +) -> Result { + let pairs = pairs.filter(|node| { + !matches!(node.as_rule(), wdl_grammar::v1::Rule::WHITESPACE) + && !matches!(node.as_rule(), wdl_grammar::v1::Rule::WHITESPACE) + }); + + PRATT_PARSER + .map_primary(|node| match node.as_rule() { + Rule::group => Ok(Expression::Group(Box::new(parse(node.into_inner())?))), + Rule::expression => parse(node.into_inner()), + Rule::r#if => { + let r#if = If::try_from(node).map_err(Error::If)?; + Ok(Expression::If(r#if)) + } + Rule::object_literal => { + todo!(); + } + Rule::struct_literal => { + todo!(); + } + Rule::map_literal => { + let map = Map::try_from(node).map_err(Error::Map)?; + Ok(Expression::Map(map)) + } + Rule::array_literal => { + let array = Array::try_from(node).map_err(Error::Array)?; + Ok(Expression::Array(array)) + } + Rule::pair_literal => { + let pair = Pair::try_from(node).map_err(Error::Pair)?; + Ok(Expression::Pair(pair)) + } + Rule::boolean => match node.as_str() { + "true" => Ok(Expression::Literal(Literal::Boolean(true))), + "false" => Ok(Expression::Literal(Literal::Boolean(false))), + value => unreachable!("unknown boolean literal value: {}", value), + }, + Rule::integer => Ok(Expression::Literal(Literal::Integer( + node.as_str().parse::().map_err(Error::ParseInt)?, + ))), + Rule::float => Ok(Expression::Literal(Literal::Float( + node.as_str().parse::>().map_err(Error::ParseFloat)?, + ))), + Rule::string => { + let inner = dive_one!(node, string_inner, string, Error::Common)?; + Ok(Expression::Literal(Literal::String( + inner.as_str().to_owned(), + ))) + } + Rule::none => Ok(Expression::Literal(Literal::None)), + Rule::singular_identifier => { + let identifier = + Identifier::try_from(node.as_str().to_owned()).map_err(Error::Identifier)?; + Ok(Expression::Literal(Literal::Identifier(identifier))) + } + _ => unreachable!("unknown primary in expression: {:?}", node.as_rule()), + }) + .map_infix(|lhs, node, rhs| match node.as_rule() { + Rule::or => Ok(Expression::Or(Box::new(lhs?), Box::new(rhs?))), + Rule::and => Ok(Expression::And(Box::new(lhs?), Box::new(rhs?))), + Rule::add => Ok(Expression::Add(Box::new(lhs?), Box::new(rhs?))), + Rule::sub => Ok(Expression::Subtract(Box::new(lhs?), Box::new(rhs?))), + Rule::mul => Ok(Expression::Multiply(Box::new(lhs?), Box::new(rhs?))), + Rule::div => Ok(Expression::Divide(Box::new(lhs?), Box::new(rhs?))), + Rule::remainder => Ok(Expression::Remainder(Box::new(lhs?), Box::new(rhs?))), + Rule::eq => Ok(Expression::Equal(Box::new(lhs?), Box::new(rhs?))), + Rule::neq => Ok(Expression::NotEqual(Box::new(lhs?), Box::new(rhs?))), + Rule::lt => Ok(Expression::LessThan(Box::new(lhs?), Box::new(rhs?))), + Rule::lte => Ok(Expression::LessThanOrEqual(Box::new(lhs?), Box::new(rhs?))), + Rule::gt => Ok(Expression::GreaterThan(Box::new(lhs?), Box::new(rhs?))), + Rule::gte => Ok(Expression::GreaterThanOrEqual( + Box::new(lhs?), + Box::new(rhs?), + )), + _ => unreachable!( + "unknown infix operation in expression: {:?}", + node.as_rule() + ), + }) + .map_prefix(|node, rhs| match node.as_rule() { + Rule::negation => Ok(Expression::Negation(Box::new(rhs?))), + Rule::unary_signed_positive => Ok(Expression::UnarySigned(UnarySigned::Positive(Box::new(rhs?)))), + Rule::unary_signed_negative => Ok(Expression::UnarySigned(UnarySigned::Negative(Box::new(rhs?)))), + _ => unreachable!( + "unknown prefix operation in expression: {:?}", + node.as_rule() + ), + }) + .map_postfix(|lhs, node| match node.as_rule() { + Rule::member => Ok(Expression::Member(Box::new(lhs?))), + Rule::index => Ok(Expression::Index(Box::new(lhs?))), + Rule::call => Ok(Expression::Call(Box::new(lhs?))), + _ => unreachable!( + "unknown postfix operation in expression: {:?}", + node.as_rule() + ), + }) + .parse(pairs) +} + +impl TryFrom> for Expression { + type Error = Error; + + fn try_from(node: pest::iterators::Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, expression); + parse(node.into_inner()) + } +} + +/// Ensures that an expression is a number. This includes floats and integers that are wrapped in +pub fn ensure_number(expr: &Expression) -> Option<&Expression> { + match expr { + Expression::Literal(Literal::Float(_)) => Some(expr), + Expression::Literal(Literal::Integer(_)) => Some(expr), + Expression::UnarySigned(UnarySigned::Positive(inner)) => { + if ensure_number(inner).is_some() { + Some(expr) + } else { + None + } + } + Expression::UnarySigned(UnarySigned::Negative(inner)) => { + if ensure_number(inner).is_some() { + Some(expr) + } else { + None + } + } + _ => None, + } +} + + +#[cfg(test)] +mod tests { + use crate::v1::macros; + use super::*; + + #[test] + fn ensure_number_works_correctly() { + let expr = macros::test::valid_node!("1", expression, Expression); + assert_eq!( + ensure_number(&expr), + Some(&Expression::Literal(Literal::Integer(1))) + ); + + let expr = macros::test::valid_node!("-1", expression, Expression); + assert_eq!( + ensure_number(&expr), + Some(&Expression::UnarySigned(UnarySigned::Negative(Box::new( + Expression::Literal(Literal::Integer(1)) + )))) + ); + + let expr = macros::test::valid_node!("+-1", expression, Expression); + assert_eq!( + ensure_number(&expr), + Some(&Expression::UnarySigned(UnarySigned::Positive(Box::new( + Expression::UnarySigned(UnarySigned::Negative(Box::new(Expression::Literal( + Literal::Integer(1) + )))) + )))) + ); + + let expr = macros::test::valid_node!("-+-1", expression, Expression); + assert_eq!( + ensure_number(&expr), + Some(&Expression::UnarySigned(UnarySigned::Negative(Box::new( + Expression::UnarySigned(UnarySigned::Positive(Box::new(Expression::UnarySigned( + UnarySigned::Negative(Box::new(Expression::Literal(Literal::Integer(1)))) + )))) + )))) + ); + let expr = macros::test::valid_node!("1.0", expression, Expression); + assert_eq!( + ensure_number(&expr), + Some(&Expression::Literal(Literal::Float(OrderedFloat(1.0)))) + ); + + let expr = macros::test::valid_node!("-1.0", expression, Expression); + assert_eq!( + ensure_number(&expr), + Some(&Expression::UnarySigned(UnarySigned::Negative(Box::new( + Expression::Literal(Literal::Float(OrderedFloat(1.0))) + )))) + ); + + let expr = macros::test::valid_node!("+-1.0", expression, Expression); + assert_eq!( + ensure_number(&expr), + Some(&Expression::UnarySigned(UnarySigned::Positive(Box::new( + Expression::UnarySigned(UnarySigned::Negative(Box::new(Expression::Literal( + Literal::Float(OrderedFloat(1.0)) + )))) + )))) + ); + + let expr = macros::test::valid_node!("-+-1.0", expression, Expression); + assert_eq!( + ensure_number(&expr), + Some(&Expression::UnarySigned(UnarySigned::Negative(Box::new( + Expression::UnarySigned(UnarySigned::Positive(Box::new(Expression::UnarySigned( + UnarySigned::Negative(Box::new(Expression::Literal(Literal::Float( + OrderedFloat(1.0) + )))) + )))) + )))) + ); + + let expr = macros::test::valid_node!("-+-false", expression, Expression); + assert_eq!(ensure_number(&expr), None); + } + + #[test] + fn it_correctly_parses_floats() { + let value = macros::test::valid_node!("1.0", expression, Expression); + assert_eq!(value, Expression::Literal(Literal::Float(OrderedFloat(1.0)))); + + let value = macros::test::valid_node!("1.0e0", expression, Expression); + assert_eq!(value, Expression::Literal(Literal::Float(OrderedFloat(1.0)))); + + let value = macros::test::valid_node!("1.e0", expression, Expression); + assert_eq!(value, Expression::Literal(Literal::Float(OrderedFloat(1.0)))); + + let value = macros::test::valid_node!("1e0", expression, Expression); + assert_eq!(value, Expression::Literal(Literal::Float(OrderedFloat(1.0)))); + + let value = macros::test::valid_node!("1e+0", expression, Expression); + assert_eq!(value, Expression::Literal(Literal::Float(OrderedFloat(1.0)))); + + // Positive signed. + + let value = macros::test::valid_node!("+1.0", expression, Expression); + assert_eq!(value, Expression::UnarySigned(UnarySigned::Positive(Box::new(Expression::Literal(Literal::Float(OrderedFloat(1.0))))))); + + let value = macros::test::valid_node!("+1.0e0", expression, Expression); + assert_eq!(value, Expression::UnarySigned(UnarySigned::Positive(Box::new(Expression::Literal(Literal::Float(OrderedFloat(1.0))))))); + + let value = macros::test::valid_node!("+1.e0", expression, Expression); + assert_eq!(value, Expression::UnarySigned(UnarySigned::Positive(Box::new(Expression::Literal(Literal::Float(OrderedFloat(1.0))))))); + + let value = macros::test::valid_node!("+1e0", expression, Expression); + assert_eq!(value, Expression::UnarySigned(UnarySigned::Positive(Box::new(Expression::Literal(Literal::Float(OrderedFloat(1.0))))))); + + let value = macros::test::valid_node!("+1e+0", expression, Expression); + assert_eq!(value, Expression::UnarySigned(UnarySigned::Positive(Box::new(Expression::Literal(Literal::Float(OrderedFloat(1.0))))))); + + // Negative signed. + + let value = macros::test::valid_node!("-1.0", expression, Expression); + assert_eq!(value, Expression::UnarySigned(UnarySigned::Negative(Box::new(Expression::Literal(Literal::Float(OrderedFloat(1.0))))))); + + let value = macros::test::valid_node!("-1.0e0", expression, Expression); + assert_eq!(value, Expression::UnarySigned(UnarySigned::Negative(Box::new(Expression::Literal(Literal::Float(OrderedFloat(1.0))))))); + + let value = macros::test::valid_node!("-1.e0", expression, Expression); + assert_eq!(value, Expression::UnarySigned(UnarySigned::Negative(Box::new(Expression::Literal(Literal::Float(OrderedFloat(1.0))))))); + + let value = macros::test::valid_node!("-1e0", expression, Expression); + assert_eq!(value, Expression::UnarySigned(UnarySigned::Negative(Box::new(Expression::Literal(Literal::Float(OrderedFloat(1.0))))))); + + let value = macros::test::valid_node!("-1e+0", expression, Expression); + assert_eq!(value, Expression::UnarySigned(UnarySigned::Negative(Box::new(Expression::Literal(Literal::Float(OrderedFloat(1.0))))))); + } +} \ No newline at end of file diff --git a/wdl-ast/src/v1/document/expression/array.rs b/wdl-ast/src/v1/document/expression/array.rs new file mode 100644 index 000000000..4184b6ca0 --- /dev/null +++ b/wdl-ast/src/v1/document/expression/array.rs @@ -0,0 +1,160 @@ +//! An array. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::expression; +use crate::v1::document::Expression; +use crate::v1::macros::check_node; + +/// An error related to an [`Array`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// An expression error. + Expression(Box), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Expression(err) => write!(f, "expression error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// An array within an [`Expression`]. +#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)] +pub struct Array(Vec); + +impl Array { + /// Creates an empty [`Array`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::expression::Array; + /// + /// let array = Array::empty(); + /// + /// assert_eq!(array.inner().len(), 0); + /// ``` + pub fn empty() -> Array { + Array(Vec::new()) + } + + /// Gets the inner [`Vec`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::expression::Array; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let expressions = vec![Expression::Literal(Literal::None)]; + /// let array = Array::from(expressions); + /// + /// let mut expressions = array.inner().into_iter(); + /// assert!(matches!( + /// expressions.next().unwrap(), + /// &Expression::Literal(Literal::None) + /// )); + /// ``` + pub fn inner(&self) -> &Vec { + &self.0 + } + + /// Consumes `self` and returns the inner [`Vec`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::expression::Array; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let expressions = vec![Expression::Literal(Literal::None)]; + /// let array = Array::from(expressions); + /// + /// let mut expressions = array.into_inner().into_iter(); + /// assert!(matches!( + /// expressions.next().unwrap(), + /// Expression::Literal(Literal::None) + /// )); + /// ``` + pub fn into_inner(self) -> Vec { + self.0 + } +} + +impl From> for Array { + fn from(array: Vec) -> Self { + Array(array) + } +} + +impl TryFrom> for Array { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, array_literal); + + let expressions = node + .into_inner() + .filter(|node| matches!(node.as_rule(), Rule::expression)) + .map(Expression::try_from) + .collect::, _>>() + .map_err(|err| Error::Expression(Box::new(err)))?; + + Ok(Array(expressions)) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::document::expression::Literal; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + let array = valid_node!(r#"["Hello", false]"#, array_literal, Array); + assert_eq!(array.inner().len(), 2); + + let mut array = array.inner().iter(); + assert!(matches!( + array.next().unwrap(), + Expression::Literal(Literal::String(_)) + )); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + array_literal, + Array + ); + } +} diff --git a/wdl-ast/src/v1/document/expression/if.rs b/wdl-ast/src/v1/document/expression/if.rs new file mode 100644 index 000000000..505ff5106 --- /dev/null +++ b/wdl-ast/src/v1/document/expression/if.rs @@ -0,0 +1,241 @@ +//! An if statement. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::expression; +use crate::v1::document::Expression; +use crate::v1::macros::check_node; + +/// An error related to an [`If`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// An expression error. + Expression(Box), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Expression(err) => write!(f, "expression error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// An if statement within an [`Expression`]. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct If { + /// The conditional clause of the if statement. + condition: Box, + + /// The then clause of the if statement. + then: Box, + + /// The else clause of the if statement. + r#else: Box, +} + +impl If { + /// Creates a new [`If`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::expression::If; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let r#if = If::new( + /// Box::new(Expression::Literal(Literal::Boolean(false))), + /// Box::new(Expression::Literal(Literal::String(String::from("foo")))), + /// Box::new(Expression::Literal(Literal::Boolean(true))), + /// ); + /// + /// assert!(matches!( + /// r#if.condition(), + /// Expression::Literal(Literal::Boolean(false)) + /// )); + /// assert!(matches!( + /// r#if.then(), + /// Expression::Literal(Literal::String(_)) + /// )); + /// assert!(matches!( + /// r#if.r#else(), + /// Expression::Literal(Literal::Boolean(true)) + /// )); + /// ``` + pub fn new(condition: Box, then: Box, r#else: Box) -> Self { + Self { + condition, + then, + r#else, + } + } + + /// Gets the conditional clause of the [`If`] as an [`Expression`] by + /// reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::expression::If; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let r#if = If::new( + /// Box::new(Expression::Literal(Literal::Boolean(false))), + /// Box::new(Expression::Literal(Literal::String(String::from("foo")))), + /// Box::new(Expression::Literal(Literal::Boolean(true))), + /// ); + /// + /// assert!(matches!( + /// r#if.condition(), + /// Expression::Literal(Literal::Boolean(false)) + /// )); + /// ``` + pub fn condition(&self) -> &Expression { + &self.condition + } + + /// Gets the then clause of the [`If`] as an [`Expression`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::expression::If; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let r#if = If::new( + /// Box::new(Expression::Literal(Literal::Boolean(false))), + /// Box::new(Expression::Literal(Literal::String(String::from("foo")))), + /// Box::new(Expression::Literal(Literal::Boolean(true))), + /// ); + /// + /// assert!(matches!( + /// r#if.then(), + /// Expression::Literal(Literal::String(_)) + /// )); + /// ``` + pub fn then(&self) -> &Expression { + &self.then + } + + /// Gets the else clause of the [`If`] as an [`Expression`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::expression::If; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::Expression; + /// + /// let r#if = If::new( + /// Box::new(Expression::Literal(Literal::Boolean(false))), + /// Box::new(Expression::Literal(Literal::String(String::from("foo")))), + /// Box::new(Expression::Literal(Literal::Boolean(true))), + /// ); + /// assert!(matches!( + /// r#if.r#else(), + /// Expression::Literal(Literal::Boolean(true)) + /// )); + /// ``` + pub fn r#else(&self) -> &Expression { + &self.r#else + } +} + +impl TryFrom> for If { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, r#if); + + let expressions = node + .into_inner() + .filter(|node| matches!(node.as_rule(), Rule::expression)) + .collect::>(); + + if expressions.len() != 3 { + unreachable!("incorrect number of expressions in if statement"); + } + + let mut expressions = expressions.into_iter(); + + // SAFETY: we just checked above that there are exactly three elements. + // Thus, this will always unwrap. + let condition_node = expressions.next().unwrap(); + let condition = + Expression::try_from(condition_node).map_err(|err| Error::Expression(Box::new(err)))?; + + let then_node = expressions.next().unwrap(); + let then = + Expression::try_from(then_node).map_err(|err| Error::Expression(Box::new(err)))?; + + let else_node = expressions.next().unwrap(); + let r#else = + Expression::try_from(else_node).map_err(|err| Error::Expression(Box::new(err)))?; + + Ok(If { + condition: Box::new(condition), + then: Box::new(then), + r#else: Box::new(r#else), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::document::expression::Literal; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + let r#if = valid_node!(r#"if true then "foo" else false"#, r#if, If); + assert!(matches!( + r#if.condition(), + Expression::Literal(Literal::Boolean(true)) + )); + assert!(matches!( + r#if.then(), + Expression::Literal(Literal::String(_)) + )); + assert!(matches!( + r#if.r#else(), + Expression::Literal(Literal::Boolean(false)) + )); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + r#if, + If + ); + } +} diff --git a/wdl-ast/src/v1/document/expression/literal.rs b/wdl-ast/src/v1/document/expression/literal.rs new file mode 100644 index 000000000..08cecee12 --- /dev/null +++ b/wdl-ast/src/v1/document/expression/literal.rs @@ -0,0 +1,28 @@ +//! A literal. + +use ordered_float::OrderedFloat; + +use crate::v1::document::identifier::singular::Identifier; + +/// An literal value within an [`Expression`](super::Expression). +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Literal { + /// A boolean. + Boolean(bool), + + /// An integer. + Integer(i64), + + /// A float. + Float(OrderedFloat), + + /// A string. + String(String), + + /// None. + None, + + /// An identifier. + Identifier(Identifier), +} + diff --git a/wdl-ast/src/v1/document/expression/map.rs b/wdl-ast/src/v1/document/expression/map.rs new file mode 100644 index 000000000..6a649816b --- /dev/null +++ b/wdl-ast/src/v1/document/expression/map.rs @@ -0,0 +1,150 @@ +//! A map. + +use std::collections::BTreeMap; + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::expression; +use crate::v1::document::Expression; +use crate::v1::macros::check_node; +use crate::v1::macros::dive_one; +use crate::v1::macros::unwrap_one; + +/// An error related to a [`Map`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// An expression error. + Expression(Box), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Expression(err) => write!(f, "expression error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A map within an [`Expression`]. +#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Map(BTreeMap); + +impl std::ops::Deref for Map { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TryFrom> for Map { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, map_literal); + + let mut map = BTreeMap::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::expression_based_kv_pair => { + //================// + // Key extraction // + //================// + + // (1) Pull the `expression_based_kv_key` out of the + // `expression_based_kv_pair`. + + // TODO: a clone is required here because Pest's `FlatPairs` + // type does not support creating an iterator without taking + // ownership (at the time of writing). This can be made + // better with a PR to Pest. + let key_node = dive_one!( + node.clone(), + expression_based_kv_key, + expression_based_kv_pair, + Error::Common + )?; + + // (2) Pull out the expression. + let key_expression_node = unwrap_one!(key_node, expression_based_kv_key)?; + + // (3) Ensure that the node is an expression. + check_node!(key_expression_node, expression); + + // (4) Parse the key expression. + let key = Expression::try_from(key_expression_node) + .map_err(|err| Error::Expression(Box::new(err)))?; + + //==================// + // Value extraction // + //==================// + + // (1) Pull the `kv_value` out of the + // `expression_based_kv_pair`. + let value_node = + dive_one!(node, kv_value, expression_based_kv_pair, Error::Common)?; + + // (2) Pull out the expression. + let value_expression_node = unwrap_one!(value_node, kv_value)?; + + // (3) Ensure that the node is an expression. + check_node!(value_expression_node, expression); + + // (4) Parse the key expression. + let value = Expression::try_from(value_expression_node) + .map_err(|err| Error::Expression(Box::new(err)))?; + + map.insert(key, value); + } + Rule::WHITESPACE => {} + Rule::COMMA => {} + Rule::COMMENT => {} + rule => unreachable!("map literals should not contain {:?}", rule), + } + } + + Ok(Map(map)) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::document::expression::Literal; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + let map = valid_node!(r#"{"hello": "world"}"#, map_literal, Map); + assert_eq!( + map.get(&Expression::Literal(Literal::String(String::from("hello")))), + Some(&Expression::Literal(Literal::String(String::from("world")))) + ); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + map_literal, + Map + ); + } +} diff --git a/wdl-ast/src/v1/document/expression/pair.rs b/wdl-ast/src/v1/document/expression/pair.rs new file mode 100644 index 000000000..4e46be774 --- /dev/null +++ b/wdl-ast/src/v1/document/expression/pair.rs @@ -0,0 +1,122 @@ +//! A pair. + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::expression; +use crate::v1::document::Expression; +use crate::v1::macros::check_node; + +/// An error related to a [`Pair`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// An expression error. + Expression(Box), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Expression(err) => write!(f, "expression error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A pair within an [`Expression`]. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Pair(Box, Box); + +impl Pair { + /// Creates a new [`Pair`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::expression::Pair; + /// use ast::v1::document::Expression; + /// + /// let pair = Pair::new( + /// Expression::Literal(Literal::Boolean(true)), + /// Expression::Literal(Literal::Boolean(false)), + /// ); + /// ``` + pub fn new(first: Expression, second: Expression) -> Self { + Self(Box::new(first), Box::new(second)) + } +} + +impl TryFrom> for Pair { + type Error = Error; + + fn try_from(node: pest::iterators::Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, pair_literal); + + let expressions = node + .into_inner() + .filter(|node| matches!(node.as_rule(), Rule::expression)) + .collect::>(); + + if expressions.len() != 2 { + unreachable!("incorrect number of expressions in pair"); + } + + let mut expressions = expressions.into_iter(); + + // SAFETY: we just checked above that there are exactly two elements. + // Thus, this will always unwrap. + let first_node = expressions.next().unwrap(); + let first = + Expression::try_from(first_node).map_err(|err| Error::Expression(Box::new(err)))?; + + let second_node = expressions.next().unwrap(); + let second = + Expression::try_from(second_node).map_err(|err| Error::Expression(Box::new(err)))?; + + Ok(Pair(Box::new(first), Box::new(second))) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::document::expression::Literal; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + let pair = valid_node!(r#"(true, false)"#, pair_literal, Pair); + assert_eq!( + pair.0, + Box::new(Expression::Literal(Literal::Boolean(true))) + ); + assert_eq!( + pair.1, + Box::new(Expression::Literal(Literal::Boolean(false))) + ); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + pair_literal, + Pair + ); + } +} diff --git a/wdl-ast/src/v1/document/expression/unary_signed.rs b/wdl-ast/src/v1/document/expression/unary_signed.rs new file mode 100644 index 000000000..88893b68e --- /dev/null +++ b/wdl-ast/src/v1/document/expression/unary_signed.rs @@ -0,0 +1,7 @@ +use crate::v1::document::Expression; + +#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)] +pub enum UnarySigned { + Positive(Box), + Negative(Box) +} \ No newline at end of file diff --git a/wdl-ast/src/v1/document/identifier.rs b/wdl-ast/src/v1/document/identifier.rs new file mode 100644 index 000000000..5fbd200cb --- /dev/null +++ b/wdl-ast/src/v1/document/identifier.rs @@ -0,0 +1,242 @@ +//! Identifiers. +//! +//! Identifiers come in two flavors: +//! +//! * [Singular](singular::Identifier), which represent a non-namespaced +//! identifier that matches the pattern `^[a-zA-Z][a-zA-Z0-9_]*$`, and +//! * [Qualified](qualified::Identifier), which represent multiple singular +//! identifiers concatenated by a [seperator](qualified::SEPARATOR) +//! (effectively, namespaced identifiers). + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +pub mod qualified; +pub mod singular; + +/// An error related to an [`Identifier`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// A qualified identifier error. + Qualified(qualified::Error), + + /// A singular identifier error. + Singular(singular::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Qualified(err) => write!(f, "qualified identifier error: {err}"), + Error::Singular(err) => write!(f, "singular identifier error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// An identifier. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Identifier { + /// A singular identifier. + Singular(singular::Identifier), + + /// A qualified identifier. + Qualified(qualified::Identifier), +} + +impl Identifier { + /// Returns a reference to the [singular identifier](singular::Identifier) + /// wrapped in [`Some`] if the [`Identifier`] is an + /// [`Identifier::Singular`]. Else, [`None`] is returned. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// + /// let identifier = Identifier::Singular(singular::Identifier::try_from("hello_world")?); + /// + /// assert_eq!( + /// identifier + /// .as_singular() + /// .map(|identifier| identifier.as_str()), + /// Some("hello_world") + /// ); + /// assert_eq!(identifier.as_qualified(), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn as_singular(&self) -> Option<&singular::Identifier> { + match self { + Identifier::Singular(ref identifier) => Some(identifier), + _ => None, + } + } + + /// Consumes `self` and returns the [singular + /// identifier](singular::Identifier) wrapped in [`Some`] if the + /// [`Identifier`] is an [`Identifier::Singular`]. Else, [`None`] is + /// returned. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// + /// let identifier = Identifier::Singular(singular::Identifier::try_from("hello_world")?); + /// + /// assert_eq!( + /// identifier.clone().into_singular(), + /// Some(singular::Identifier::try_from("hello_world")?) + /// ); + /// assert_eq!(identifier.into_qualified(), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn into_singular(self) -> Option { + match self { + Identifier::Singular(identifier) => Some(identifier), + _ => None, + } + } + + /// Returns a reference to the [qualified identifier](qualified::Identifier) + /// wrapped in [`Some`] if the [`Identifier`] is an + /// [`Identifier::Qualified`]. Else, [`None`] is returned. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::qualified; + /// use ast::v1::document::Identifier; + /// + /// let identifier = Identifier::Qualified(qualified::Identifier::try_from("hello.there.world")?); + /// + /// assert_eq!(identifier.as_singular(), None); + /// assert_eq!( + /// identifier + /// .as_qualified() + /// .map(|identifier| identifier.to_string()), + /// Some(String::from("hello.there.world")) + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn as_qualified(&self) -> Option<&qualified::Identifier> { + match self { + Identifier::Qualified(ref identifier) => Some(identifier), + _ => None, + } + } + + /// Consumes `self` and returns the [qualified + /// identifier](qualified::Identifier) wrapped in [`Some`] if the + /// [`Identifier`] is an [`Identifier::Qualified`]. Else, [`None`] is + /// returned. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::qualified; + /// use ast::v1::document::Identifier; + /// + /// let identifier = + /// Identifier::Qualified(qualified::Identifier::try_from("hello.there.world").unwrap()); + /// + /// assert_eq!( + /// identifier.clone().into_qualified(), + /// Some(qualified::Identifier::try_from("hello.there.world").unwrap()) + /// ); + /// assert_eq!(identifier.into_singular(), None); + /// ``` + pub fn into_qualified(self) -> Option { + match self { + Identifier::Qualified(identifier) => Some(identifier), + _ => None, + } + } +} + +impl std::fmt::Display for Identifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Identifier::Singular(identifier) => write!(f, "{identifier}"), + Identifier::Qualified(identifier) => write!(f, "{identifier}"), + } + } +} + +impl From for Identifier { + fn from(value: singular::Identifier) -> Self { + Identifier::Singular(value) + } +} + +impl From for Identifier { + fn from(value: qualified::Identifier) -> Self { + Identifier::Qualified(value) + } +} + +impl TryFrom> for Identifier { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + match node.as_rule() { + Rule::singular_identifier => { + let identifier = singular::Identifier::try_from(node).map_err(Error::Singular)?; + Ok(Identifier::Singular(identifier)) + } + Rule::qualified_identifier => { + let identifier = qualified::Identifier::try_from(node).map_err(Error::Qualified)?; + Ok(Identifier::Qualified(identifier)) + } + node => Err(Error::Common(crate::v1::Error::InvalidNode(format!( + "identifier cannot be parsed from node type {:?}", + node + )))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::v1::macros; + + #[test] + fn it_parses_from_a_supported_node_type() { + let identifier = macros::test::valid_node!("hello_world", singular_identifier, Identifier); + assert_eq!(identifier.into_singular().unwrap().as_str(), "hello_world"); + + macros::test::valid_node!("hello.there.world", qualified_identifier, Identifier); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + macros::test::invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + identifier, + Identifier + ); + } +} diff --git a/wdl-ast/src/v1/document/identifier/qualified.rs b/wdl-ast/src/v1/document/identifier/qualified.rs new file mode 100644 index 000000000..17adced56 --- /dev/null +++ b/wdl-ast/src/v1/document/identifier/qualified.rs @@ -0,0 +1,198 @@ +//! Qualified identifiers. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use crate::v1::document::identifier::singular; +use crate::v1::macros::check_node; + +/// The separator between the [`singular::Identifier`]s when serialized. +pub const SEPARATOR: &str = "."; + +/// An error related to an [`Identifier`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// Attempted to create an empty identifier. + Empty, + + /// Attempted to create a qualified identifier with an invalid format. + InvalidFormat(String, String), + + /// A singular identifier error. + /// + /// Generally speaking, this error will be returned if there is any issue + /// parsing the singular identifiers that comprise the qualified identifier. + Singular(singular::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Empty => write!(f, "cannot create an empty identifier"), + Error::InvalidFormat(value, reason) => { + write!(f, "invalid format for \"{value}\": {reason}") + } + Error::Singular(err) => write!(f, "singular identifier error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A qualified identifier. +/// +/// Qualified [`Identifier`]s are comprised of one or more singular +/// [`Identifier`](singular::Identifier)s that are joined together by +/// [`SEPARATOR`] (in the [`Identifier`]'s serialized form). These identifiers +/// are effectively used to enable namespacing of identifiers. +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Identifier(Vec); + +impl std::fmt::Display for Identifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + self.0 + .iter() + .map(|identifier| identifier.as_str()) + .collect::>() + .join(SEPARATOR) + ) + } +} + +// Note: when displaying an [`Identifier`] via the [`std::fmt::Debug`] trait, +// it's more clear to simply serialize the [`Identifier`] as is done in +// [`std::fmt::Display`] rather than to print each element in the inner [`Vec`]. +impl std::fmt::Debug for Identifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"{}\"", self) + } +} + +impl TryFrom<&str> for Identifier { + type Error = Error; + + fn try_from(value: &str) -> Result { + if value.is_empty() { + return Err(Error::Empty); + } + + if !value.contains(SEPARATOR) { + return Err(Error::InvalidFormat( + value.to_owned(), + String::from("cannot create qualified identifier with no scope"), + )); + } + + value + .split(SEPARATOR) + .map(|identifier| singular::Identifier::try_from(identifier).map_err(Error::Singular)) + .collect::>() + } +} + +// Note: this is implemented to facilitate collection via `collect()` on a set +// of singular [`Identifier`](singular::Identifier)s. +impl FromIterator for Identifier { + fn from_iter>(iter: T) -> Self { + Identifier(iter.into_iter().collect()) + } +} + +impl TryFrom> for Identifier { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, qualified_identifier); + + let mut nodes = node.into_inner().collect::>(); + + if nodes.is_empty() { + return Err(Error::Empty); + } else if nodes.len() == 1 { + return Err(Error::InvalidFormat( + // SAFTEY: we just ensured that exactly one node exists. + nodes.pop().unwrap().as_str().to_owned(), + String::from("cannot create qualified identifier with no scope"), + )); + } + + nodes + .into_iter() + .map(singular::Identifier::try_from) + .collect::>() + .map_err(Error::Singular) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + valid_node!("hello.there.world", qualified_identifier, Identifier); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + qualified_identifier, + Identifier + ); + } + + #[test] + fn it_collects_identifiers_into_a_qualified_identifier() { + let identifiers = vec![ + String::from("hello"), + String::from("there"), + String::from("world"), + ]; + + let qualified = identifiers + .into_iter() + .map(crate::v1::document::identifier::singular::Identifier::try_from) + .collect::>() + .unwrap(); + + assert_eq!(qualified.to_string(), "hello.there.world"); + } + + #[test] + fn it_fails_to_create_an_empty_identifier() -> Result<()> { + let err = Identifier::try_from("").unwrap_err(); + assert_eq!( + err.to_string(), + String::from("cannot create an empty identifier") + ); + + Ok(()) + } + + #[test] + fn it_fails_to_create_a_qualified_identifier_from_a_singular_identifier() -> Result<()> { + let err = Identifier::try_from("hello_world").unwrap_err(); + assert_eq!( + err.to_string(), + String::from("invalid format for \"hello_world\": cannot create qualified identifier with no scope") + ); + + Ok(()) + } +} diff --git a/wdl-ast/src/v1/document/identifier/singular.rs b/wdl-ast/src/v1/document/identifier/singular.rs new file mode 100644 index 000000000..be06c03a4 --- /dev/null +++ b/wdl-ast/src/v1/document/identifier/singular.rs @@ -0,0 +1,180 @@ +//! Singular identifiers. + +use std::borrow::Borrow; + +use lazy_static::lazy_static; +use pest::iterators::Pair; +use regex::Regex; + +use wdl_grammar as grammar; + +use crate::v1::macros::check_node; + +lazy_static! { + static ref PATTERN: Regex = Regex::new(r"^[a-zA-Z][a-zA-Z0-9_]*$").unwrap(); +} + +/// An error related to an [`Identifier`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// Attempted to create an empty identifier. + Empty, + + /// Attempted to create an identifier with an invalid format. + InvalidFormat(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Empty => write!(f, "cannot create an empty identifier"), + Error::InvalidFormat(format) => { + write!(f, "invalid format for identifier: \"{format}\"") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A singular identifier. +/// +/// An [`Identifier`] must match the pattern `^[a-zA-Z][a-zA-Z0-9_]*$`. If an ones +/// attempts to create an [`Identifier`] that does not match this pattern, an +/// [`Error::InvalidFormat`] is returned. +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Identifier(String); + +// Note: this is included because we want to allow looking up an [`Identifier`] +// using a `&str` in things like [`HashMap`]s (and similar). +impl Borrow for Identifier { + fn borrow(&self) -> &str { + self.0.as_str() + } +} + +impl std::ops::Deref for Identifier { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::fmt::Display for Identifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +// Note: when displaying an [`Identifier`] via the [`std::fmt::Debug`] trait, +// it's more clear to simply serialize the [`Identifier`] as is done in +// [`std::fmt::Display`] rather than to print it within the tuple struct. +impl std::fmt::Debug for Identifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"{}\"", self) + } +} + +impl TryFrom<&str> for Identifier { + type Error = Error; + + fn try_from(value: &str) -> std::prelude::v1::Result { + ensure_valid(value)?; + Ok(Identifier(value.to_owned())) + } +} + +impl TryFrom for Identifier { + type Error = Error; + + fn try_from(value: String) -> Result { + ensure_valid(&value)?; + Ok(Identifier(value)) + } +} + +/// Ensures that a provided [`&str`](str) is a valid identifier. If it is not, +/// the appropriate error is returned. +fn ensure_valid(value: impl AsRef) -> Result<()> { + let value = value.as_ref(); + + if value.is_empty() { + return Err(Error::Empty); + } + + if !PATTERN.is_match(value) { + return Err(Error::InvalidFormat(value.to_string())); + } + + Ok(()) +} + +impl TryFrom> for Identifier { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, singular_identifier); + Identifier::try_from(node.as_str().to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_creates_a_identifier_with_a_valid_format() -> Result<()> { + Identifier::try_from("hello_world")?; + Identifier::try_from("a1b2c3")?; + + Ok(()) + } + + #[test] + fn it_fails_to_create_an_empty_identifier() -> Result<()> { + let err = Identifier::try_from("").unwrap_err(); + assert_eq!( + err.to_string(), + String::from("cannot create an empty identifier") + ); + + Ok(()) + } + + #[test] + fn it_fails_to_create_invalid_identifiers() -> Result<()> { + let err = Identifier::try_from("0123").unwrap_err(); + assert_eq!( + err.to_string(), + String::from("invalid format for identifier: \"0123\"") + ); + + let err = Identifier::try_from("_").unwrap_err(); + assert_eq!( + err.to_string(), + String::from("invalid format for identifier: \"_\"") + ); + + let err = Identifier::try_from("$hello").unwrap_err(); + assert_eq!( + err.to_string(), + String::from("invalid format for identifier: \"$hello\"") + ); + + Ok(()) + } + + #[test] + fn it_derefences_as_a_string_and_as_str() { + let identifier = Identifier::try_from("hello_world").unwrap(); + assert_eq!(identifier.as_str(), "hello_world"); + } +} diff --git a/wdl-ast/src/v1/document/import.rs b/wdl-ast/src/v1/document/import.rs new file mode 100644 index 000000000..a0f78166e --- /dev/null +++ b/wdl-ast/src/v1/document/import.rs @@ -0,0 +1,230 @@ +//! Imports. + +use std::collections::BTreeMap; + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +mod builder; + +pub use builder::Builder; + +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::macros::check_node; +use crate::v1::macros::dive_one; + +/// An error related to an [`Import`]. +#[derive(Debug)] +pub enum Error { + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An identifier error. + Identifier(singular::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// Aliases within an import. +pub type Aliases = BTreeMap; + +/// An import. +#[derive(Clone, Debug)] +pub struct Import { + /// Aliases (if they exist). + aliases: Option, + + /// As (if it exists). + r#as: Option, + + /// The URI. + uri: String, +} + +impl Import { + /// Gets the aliases from the [`Import`] by reference (if they exist). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::import::Builder; + /// + /// let import = Builder::default() + /// .insert_alias( + /// Identifier::try_from("hello_world")?, + /// Identifier::try_from("foo_bar").unwrap(), + /// ) + /// .r#as(Identifier::try_from("baz_quux").unwrap())? + /// .uri(String::from("../mapping.wdl"))? + /// .try_build()?; + /// + /// assert_eq!( + /// import.aliases().unwrap().get("hello_world"), + /// Some(&Identifier::try_from("foo_bar").unwrap()) + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn aliases(&self) -> Option<&Aliases> { + self.aliases.as_ref() + } + + /// Gets the as from the [`Import`] by reference (if it exists). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::import::Builder; + /// + /// let import = Builder::default() + /// .insert_alias( + /// Identifier::try_from("hello_world")?, + /// Identifier::try_from("foo_bar").unwrap(), + /// ) + /// .r#as(Identifier::try_from("baz_quux").unwrap())? + /// .uri(String::from("../mapping.wdl"))? + /// .try_build()?; + /// + /// assert_eq!( + /// import.r#as().unwrap(), + /// &Identifier::try_from("baz_quux").unwrap() + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn r#as(&self) -> Option<&Identifier> { + self.r#as.as_ref() + } + + /// Gets the URI from the [`Import`] by reference (if it exists). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::import::Builder; + /// + /// let import = Builder::default() + /// .insert_alias( + /// Identifier::try_from("hello_world")?, + /// Identifier::try_from("foo_bar").unwrap(), + /// ) + /// .r#as(Identifier::try_from("baz_quux").unwrap())? + /// .uri(String::from("../mapping.wdl"))? + /// .try_build()?; + /// + /// assert_eq!(import.uri(), "../mapping.wdl"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn uri(&self) -> &str { + self.uri.as_str() + } +} + +impl TryFrom> for Import { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, import); + let mut builder = builder::Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::import_uri => { + let uri = dive_one!(node, string_literal_contents, import_uri, Error::Common)?; + builder = builder + .uri(uri.as_str().to_owned()) + .map_err(Error::Builder)?; + } + Rule::import_as => { + let identifier_node = + dive_one!(node, singular_identifier, import_as, Error::Common)?; + let identifier = Identifier::try_from(identifier_node.as_str().to_string()) + .map_err(Error::Identifier)?; + builder = builder.r#as(identifier).map_err(Error::Builder)?; + } + Rule::import_alias => { + // TODO: a clone is required here because Pest's `FlatPairs` + // type does not support creating an iterator without taking + // ownership (at the time of writing). This can be made + // better with a PR to Pest. + let from_node = + dive_one!(node.clone(), import_alias_from, import_alias, Error::Common)?; + let from = Identifier::try_from(from_node.as_str().to_string()) + .map_err(Error::Identifier)?; + + let to_node = dive_one!(node, import_alias_to, import_alias, Error::Common)?; + let to = Identifier::try_from(to_node.as_str().to_string()) + .map_err(Error::Identifier)?; + + builder = builder.insert_alias(from, to); + } + Rule::COMMENT => {} + Rule::WHITESPACE => {} + rule => unreachable!("import should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_parses_a_complicated_import_correctly( + ) -> std::result::Result<(), Box> { + let import = wdl_grammar::v1::parse( + wdl_grammar::v1::Rule::import, + r#"import "hello.wdl" as hello alias foo as bar alias baz as quux"#, + ) + .unwrap() + .into_inner() + .next() + .unwrap(); + + let import = Import::try_from(import).unwrap(); + assert_eq!(import.uri(), "hello.wdl"); + assert_eq!(import.r#as().map(|x| x.as_str()), Some("hello")); + + let aliases = import.aliases().unwrap(); + assert_eq!( + aliases.get("foo"), + Some(&Identifier::try_from("bar").unwrap()) + ); + assert_eq!( + aliases.get("baz"), + Some(&Identifier::try_from("quux").unwrap()) + ); + + Ok(()) + } +} diff --git a/wdl-ast/src/v1/document/import/builder.rs b/wdl-ast/src/v1/document/import/builder.rs new file mode 100644 index 000000000..97b06b2e4 --- /dev/null +++ b/wdl-ast/src/v1/document/import/builder.rs @@ -0,0 +1,221 @@ +//! Builder for an [`Import`]. + +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::import::Aliases; +use crate::v1::document::Import; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A URI was not provided to the [`Builder`]. + Uri, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Uri => write!(f, "uri"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error that occurs when a multiple values were provded for a field that +/// only accepts a single value. +#[derive(Debug)] +pub enum MultipleError { + /// Attempted to set multiple values for the "as" field within the + /// [`Builder`]. + As, + + /// Attempted to set multiple values for the URI field within the + /// [`Builder`]. + Uri, +} + +impl std::fmt::Display for MultipleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MultipleError::As => write!(f, "as"), + MultipleError::Uri => write!(f, "uri"), + } + } +} + +impl std::error::Error for MultipleError {} + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), + + /// Multiple values were provided for a field that accepts a single value. + Multiple(MultipleError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + Error::Multiple(err) => { + write!(f, "multiple values provided for single value field: {err}") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for an [`Import`]. +#[derive(Debug, Default)] +pub struct Builder { + /// The import aliases. + aliases: Option, + + /// The as clause. + r#as: Option, + + /// The URI. + uri: Option, +} + +impl Builder { + /// Inserts an alias into the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::import::Builder; + /// + /// let import = Builder::default() + /// .insert_alias( + /// Identifier::try_from("hello_world")?, + /// Identifier::try_from("foo_bar").unwrap(), + /// ) + /// .r#as(Identifier::try_from("baz_quux").unwrap())? + /// .uri(String::from("../mapping.wdl"))? + /// .try_build()?; + /// + /// assert_eq!( + /// import.aliases().unwrap().get("hello_world"), + /// Some(&Identifier::try_from("foo_bar").unwrap()) + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn insert_alias(mut self, from: Identifier, to: Identifier) -> Self { + let mut aliases = self.aliases.unwrap_or_default(); + aliases.insert(from, to); + self.aliases = Some(aliases); + self + } + + /// Sets the as for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::import::Builder; + /// + /// let import = Builder::default() + /// .insert_alias( + /// Identifier::try_from("hello_world")?, + /// Identifier::try_from("foo_bar").unwrap(), + /// ) + /// .r#as(Identifier::try_from("baz_quux").unwrap())? + /// .uri(String::from("../mapping.wdl"))? + /// .try_build()?; + /// + /// assert_eq!( + /// import.r#as().unwrap(), + /// &Identifier::try_from("baz_quux").unwrap() + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn r#as(mut self, identifier: Identifier) -> Result { + if self.r#as.is_some() { + return Err(Error::Multiple(MultipleError::As)); + } + + self.r#as = Some(identifier); + Ok(self) + } + + /// Sets the URI for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::import::Builder; + /// + /// let import = Builder::default() + /// .insert_alias( + /// Identifier::try_from("hello_world")?, + /// Identifier::try_from("foo_bar").unwrap(), + /// ) + /// .r#as(Identifier::try_from("baz_quux").unwrap())? + /// .uri(String::from("../mapping.wdl"))? + /// .try_build()?; + /// + /// assert_eq!(import.uri(), "../mapping.wdl"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn uri(mut self, uri: String) -> Result { + if self.uri.is_some() { + return Err(Error::Multiple(MultipleError::Uri)); + } + + self.uri = Some(uri); + Ok(self) + } + + /// Consumes `self` to attempt to build an [`Import`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::import::Builder; + /// + /// let import = Builder::default() + /// .insert_alias( + /// Identifier::try_from("hello_world")?, + /// Identifier::try_from("foo_bar").unwrap(), + /// ) + /// .r#as(Identifier::try_from("baz_quux").unwrap())? + /// .uri(String::from("../mapping.wdl"))? + /// .try_build()?; + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_build(self) -> Result { + let uri = self + .uri + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Uri)))?; + + Ok(Import { + uri, + r#as: self.r#as, + aliases: self.aliases, + }) + } +} diff --git a/wdl-ast/src/v1/document/input.rs b/wdl-ast/src/v1/document/input.rs new file mode 100644 index 000000000..515aeb899 --- /dev/null +++ b/wdl-ast/src/v1/document/input.rs @@ -0,0 +1,128 @@ +//! Inputs. + +use nonempty::NonEmpty; +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::declaration; +use crate::v1::document::Declaration; +use crate::v1::macros::check_node; + +mod builder; + +pub use builder::Builder; + +/// An error related to a [`Input`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// A declaration error. + Declaration(declaration::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Declaration(err) => write!(f, "declaration error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// Bound and unbound declarations in an [`Input`]. +pub type Declarations = NonEmpty; + +/// An input. +/// +/// **Note:** this struct could have been designed as a tuple struct. However, +/// it felt non-ergonomic to wrap an optional type and allow dereferencing as is +/// the convention elsewhere in the code base. As such, it is written as a +/// single field struct. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Input { + /// The bound and unbound declarations (if they exist). + declarations: Option, +} + +impl Input { + /// Gets the [declaration(s)](Declarations) from the [`Input`] by reference + /// (if they exist). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::input::Builder; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// let unbound = unbound::Builder::default() + /// .name(Identifier::try_from("foo_bar")?)? + /// .r#type(Type::new(Kind::Boolean, true))? + /// .try_build()?; + /// + /// let mut input = Builder::default() + /// .push_declaration(Declaration::Bound(bound.clone())) + /// .push_declaration(Declaration::Unbound(unbound.clone())) + /// .build(); + /// + /// let declarations = input.declarations().unwrap(); + /// assert_eq!(declarations.len(), 2); + /// + /// let mut declarations = declarations.iter(); + /// assert_eq!(declarations.next().unwrap().as_bound().unwrap(), &bound); + /// assert_eq!(declarations.next().unwrap().as_unbound().unwrap(), &unbound); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn declarations(&self) -> Option<&Declarations> { + self.declarations.as_ref() + } +} + +impl TryFrom> for Input { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, input); + let mut builder = builder::Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::bound_declaration => { + let declaration = Declaration::try_from(node).map_err(Error::Declaration)?; + builder = builder.push_declaration(declaration); + } + Rule::unbound_declaration => { + let declaration = Declaration::try_from(node).map_err(Error::Declaration)?; + builder = builder.push_declaration(declaration); + } + Rule::COMMENT => {} + Rule::WHITESPACE => {} + rule => unreachable!("workflow input should not contain {:?}", rule), + } + } + + Ok(builder.build()) + } +} diff --git a/wdl-ast/src/v1/document/input/builder.rs b/wdl-ast/src/v1/document/input/builder.rs new file mode 100644 index 000000000..56b5a0d4a --- /dev/null +++ b/wdl-ast/src/v1/document/input/builder.rs @@ -0,0 +1,119 @@ +//! Builder for an [`Input`]. + +use nonempty::NonEmpty; + +use crate::v1::document::input::Declarations; +use crate::v1::document::input::Input; +use crate::v1::document::Declaration; + +/// A builder for an [`Input`]. +#[derive(Debug, Default)] +pub struct Builder { + /// The bound and unbound declarations. + declarations: Option, +} + +impl Builder { + /// Pushes a new [declaration](Declaration) into the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::input::Builder; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// let unbound = unbound::Builder::default() + /// .name(Identifier::try_from("foo_bar")?)? + /// .r#type(Type::new(Kind::Boolean, true))? + /// .try_build()?; + /// + /// let mut input = Builder::default() + /// .push_declaration(Declaration::Bound(bound.clone())) + /// .push_declaration(Declaration::Unbound(unbound.clone())) + /// .build(); + /// + /// let declarations = input.declarations().unwrap(); + /// assert_eq!(declarations.len(), 2); + /// + /// let mut declarations = declarations.iter(); + /// assert_eq!(declarations.next().unwrap().as_bound().unwrap(), &bound); + /// assert_eq!(declarations.next().unwrap().as_unbound().unwrap(), &unbound); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn push_declaration(mut self, declaration: Declaration) -> Self { + let declarations = match self.declarations { + Some(mut declarations) => { + declarations.push(declaration); + declarations + } + None => NonEmpty::new(declaration), + }; + + self.declarations = Some(declarations); + self + } + + /// Consumes `self` to build an [`Input`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::input::Builder; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// let unbound = unbound::Builder::default() + /// .name(Identifier::try_from("foo_bar")?)? + /// .r#type(Type::new(Kind::Boolean, true))? + /// .try_build()?; + /// + /// let mut input = Builder::default() + /// .push_declaration(Declaration::Bound(bound.clone())) + /// .push_declaration(Declaration::Unbound(unbound.clone())) + /// .build(); + /// + /// let declarations = input.declarations().unwrap(); + /// assert_eq!(declarations.len(), 2); + /// + /// let mut declarations = declarations.iter(); + /// assert_eq!(declarations.next().unwrap().as_bound().unwrap(), &bound); + /// assert_eq!(declarations.next().unwrap().as_unbound().unwrap(), &unbound); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn build(self) -> Input { + Input { + declarations: self.declarations, + } + } +} diff --git a/wdl-ast/src/v1/document/metadata.rs b/wdl-ast/src/v1/document/metadata.rs new file mode 100644 index 000000000..aa3341098 --- /dev/null +++ b/wdl-ast/src/v1/document/metadata.rs @@ -0,0 +1,175 @@ +//! Metadata. + +use std::collections::BTreeMap; + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::macros::extract_one; +use crate::v1::macros::unwrap_one; + +pub mod value; + +pub use value::Value; + +/// An error related to a [`Metadata`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// An identifier error. + Identifier(singular::Error), + + /// A value error. + Value(value::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + Error::Value(err) => write!(f, "value error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// The inner map for [`Metadata`]. +type Map = BTreeMap; + +/// A metadata. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Metadata(Map); + +impl From for Metadata { + fn from(metadata: Map) -> Self { + Metadata(metadata) + } +} + +impl Metadata { + /// Returns the inner value of the [`Metadata`] by reference. + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::metadata::Value; + /// use ast::v1::document::Metadata; + /// + /// let mut map = BTreeMap::::new(); + /// map.insert( + /// Identifier::try_from("hello").unwrap(), + /// Value::String(String::from("world")), + /// ); + /// map.insert(Identifier::try_from("foo").unwrap(), Value::Null); + /// + /// let metadata = Metadata::from(map); + /// + /// let inner = metadata.inner(); + /// + /// assert_eq!(inner.get("hello"), Some(&Value::String(String::from("world")))); + /// assert_eq!(inner.get("foo"), Some(&Value::Null)); + /// assert_eq!(inner.get("baz"), None); + /// ``` + pub fn inner(&self) -> &Map { + &self.0 + } + + /// Consumes `self` and returns the inner value of the [`Metadata`]. + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::metadata::Value; + /// use ast::v1::document::Metadata; + /// + /// let mut map = BTreeMap::::new(); + /// map.insert( + /// Identifier::try_from("hello").unwrap(), + /// Value::String(String::from("world")), + /// ); + /// map.insert(Identifier::try_from("foo").unwrap(), Value::Null); + /// + /// let metadata = Metadata::from(map.clone()); + /// + /// assert_eq!(metadata.into_inner(), map); + /// ``` + pub fn into_inner(self) -> Map { + self.0 + } +} + +impl TryFrom> for Metadata { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + match node.as_rule() { + Rule::metadata => {} + Rule::parameter_metadata => {} + rule => { + return Err(Self::Error::Common(crate::v1::Error::InvalidNode(format!( + "{} cannot be parsed from node type {:?}", + stringify!($type_), + rule + )))) + } + } + + let mut metadata = Map::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::metadata_kv => { + //================// + // Key extraction // + //================// + + // TODO: a clone is required here because Pest's `FlatPairs` + // type does not support creating an iterator without taking + // ownership (at the time of writing). This can be made + // better with a PR to Pest. + let key_node = + extract_one!(node.clone(), metadata_key, metadata_kv, Error::Common)?; + let key = Identifier::try_from(unwrap_one!(key_node, metadata_key)?.as_str()) + .map_err(Error::Identifier)?; + + //==================// + // Value extraction // + //==================// + + let value_node = + extract_one!(node, metadata_value, metadata_kv, Error::Common)?; + let value = Value::try_from(value_node).map_err(Error::Value)?; + + metadata.insert(key, value); + } + Rule::WHITESPACE => {} + Rule::COMMENT => {} + rule => unreachable!("parameter metadata should not contain {:?}", rule), + } + } + + Ok(Metadata(metadata)) + } +} diff --git a/wdl-ast/src/v1/document/metadata/value.rs b/wdl-ast/src/v1/document/metadata/value.rs new file mode 100644 index 000000000..2d624cf60 --- /dev/null +++ b/wdl-ast/src/v1/document/metadata/value.rs @@ -0,0 +1,107 @@ +//! Metadata values. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::macros::check_node; +use crate::v1::macros::dive_one; +use crate::v1::macros::unwrap_one; + +mod array; +mod object; + +pub use array::Array; +pub use object::Object; + +/// An error related to a [`Value`]. +#[derive(Debug)] +pub enum Error { + /// An array error. + Array(Box), + + /// A common error. + Common(crate::v1::Error), + + /// An object error. + Object(Box), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Array(err) => write!(f, "array error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Object(err) => write!(f, "object error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A metadata value. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Value { + /// A string. + String(String), + + /// An integer. + Integer(String), + + /// A float. + Float(String), + + /// A boolean. + Boolean(bool), + + /// Null. + Null, + + /// An object. + Object(Object), + + /// An array. + Array(Array), +} + +impl TryFrom> for Value { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, metadata_value); + let node = unwrap_one!(node, metadata_value)?; + + match node.as_rule() { + Rule::string => { + let inner = dive_one!(node, string_inner, string, Error::Common)?; + Ok(Value::String(inner.as_str().to_owned())) + } + Rule::integer => Ok(Value::Integer(node.as_str().to_owned())), + Rule::float => Ok(Value::Float(node.as_str().to_owned())), + Rule::boolean => match node.as_str() { + "true" => Ok(Value::Boolean(true)), + "false" => Ok(Value::Boolean(false)), + value => unreachable!("unknown boolean literal value: {}", value), + }, + Rule::null => Ok(Value::Null), + Rule::metadata_object => { + let object = Object::try_from(node) + .map_err(Box::new) + .map_err(Error::Object)?; + Ok(Value::Object(object)) + } + Rule::metadata_array => { + let array = Array::try_from(node) + .map_err(Box::new) + .map_err(Error::Array)?; + Ok(Value::Array(array)) + } + rule => unreachable!("workflow metadata value should not contain {:?}", rule), + } + } +} diff --git a/wdl-ast/src/v1/document/metadata/value/array.rs b/wdl-ast/src/v1/document/metadata/value/array.rs new file mode 100644 index 000000000..a96ad5ad5 --- /dev/null +++ b/wdl-ast/src/v1/document/metadata/value/array.rs @@ -0,0 +1,58 @@ +//! Metadata array values. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::metadata::value; +use crate::v1::document::metadata::Value; +use crate::v1::macros::check_node; + +/// An error related to an [`Array`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// A value error. + Value(value::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Value(err) => write!(f, "value error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A metadata array value. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Array(Vec); + +impl TryFrom> for Array { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, metadata_array); + + node.into_inner() + .filter(|node| { + !matches!( + node.as_rule(), + Rule::WHITESPACE | Rule::COMMENT | Rule::COMMA + ) + }) + .map(|node| Value::try_from(node).map_err(Error::Value)) + .collect::>>() + .map(Array) + } +} diff --git a/wdl-ast/src/v1/document/metadata/value/object.rs b/wdl-ast/src/v1/document/metadata/value/object.rs new file mode 100644 index 000000000..174ed9b5e --- /dev/null +++ b/wdl-ast/src/v1/document/metadata/value/object.rs @@ -0,0 +1,85 @@ +//! Metadata object values. + +use std::collections::BTreeMap; + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::metadata::value; +use crate::v1::document::metadata::Value; +use crate::v1::macros::check_node; +use crate::v1::macros::extract_one; +use crate::v1::macros::unwrap_one; + +/// An error related to an [`Object`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// An identifier error. + Identifier(singular::Error), + + /// A value error. + Value(value::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + Error::Value(err) => write!(f, "value error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A metadata object value. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Object(BTreeMap); + +impl TryFrom> for Object { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, metadata_object); + let mut inner = BTreeMap::new(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::metadata_kv => { + // TODO: a clone is required here because Pest's `FlatPairs` + // type does not support creating an iterator without taking + // ownership (at the time of writing). This can be made + // better with a PR to Pest. + let key_node = + extract_one!(node.clone(), metadata_key, metadata_kv, Error::Common)?; + let key = Identifier::try_from(unwrap_one!(key_node, metadata_key)?.as_str()) + .map_err(Error::Identifier)?; + + let value_node = + extract_one!(node, metadata_value, metadata_kv, Error::Common)?; + let value = Value::try_from(value_node).map_err(Error::Value)?; + + inner.insert(key, value); + } + Rule::COMMA => {} + Rule::WHITESPACE => {} + Rule::COMMENT => {} + rule => unreachable!("parameter metadata should not contain {:?}", rule), + } + } + + Ok(Object(inner)) + } +} diff --git a/wdl-ast/src/v1/document/output.rs b/wdl-ast/src/v1/document/output.rs new file mode 100644 index 000000000..a2a014b77 --- /dev/null +++ b/wdl-ast/src/v1/document/output.rs @@ -0,0 +1,117 @@ +//! Outputs. + +use nonempty::NonEmpty; +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::declaration::bound; +use crate::v1::document::declaration::bound::Declaration; +use crate::v1::macros::check_node; + +mod builder; + +pub use builder::Builder; + +/// An error related to a [`Output`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// A declaration error. + Declaration(bound::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Declaration(err) => write!(f, "declaration error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// Bound declarations in an [`Output`]. +pub type Declarations = NonEmpty; + +/// An output. +/// +/// **Note:** this struct could have been designed as a tuple struct. However, +/// it felt non-ergonomic to wrap an optional type and allow dereferencing as is +/// the convention elsewhere in the code base. As such, it is written as a +/// single field struct. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Output { + /// The bound declarations (if they exist). + declarations: Option, +} + +impl Output { + /// Gets the [bound declaration(s)](Declarations) from the [`Output`] by + /// reference (if they exist). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::output::Builder; + /// use ast::v1::document::Expression; + /// + /// let declaration = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::Identifier( + /// Identifier::try_from("foo").unwrap(), + /// )))? + /// .try_build()?; + /// + /// let output = Builder::default() + /// .push_bound_declaration(declaration) + /// .build(); + /// + /// let declarations = output.declarations().unwrap(); + /// assert_eq!(declarations.len(), 1); + /// + /// let declaration = declarations.into_iter().next().unwrap(); + /// assert_eq!(declaration.name().as_str(), "hello_world"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn declarations(&self) -> Option<&NonEmpty> { + self.declarations.as_ref() + } +} + +impl TryFrom> for Output { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, output); + let mut builder = builder::Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::bound_declaration => { + let declaration = Declaration::try_from(node).map_err(Error::Declaration)?; + builder = builder.push_bound_declaration(declaration); + } + Rule::COMMENT => {} + Rule::WHITESPACE => {} + rule => unreachable!("workflow output should not contain {:?}", rule), + } + } + + Ok(builder.build()) + } +} diff --git a/wdl-ast/src/v1/document/output/builder.rs b/wdl-ast/src/v1/document/output/builder.rs new file mode 100644 index 000000000..0a55608fa --- /dev/null +++ b/wdl-ast/src/v1/document/output/builder.rs @@ -0,0 +1,138 @@ +//! Builder for an [`Output`]. + +use nonempty::NonEmpty; + +use crate::v1::document::declaration::bound::Declaration; +use crate::v1::document::output::Declarations; +use crate::v1::document::Output; + +/// A builder for an [`Output`]. +#[derive(Debug, Default)] +pub struct Builder { + /// The bound declarations (if they exist). + declarations: Option, +} + +impl Builder { + /// Pushes a [bound declaration](Declaration) into the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::output::Builder; + /// use ast::v1::document::Expression; + /// + /// let declaration = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::Identifier( + /// Identifier::try_from("foo").unwrap(), + /// )))? + /// .try_build()?; + /// + /// let output = Builder::default() + /// .push_bound_declaration(declaration) + /// .build(); + /// + /// let declarations = output.declarations().unwrap(); + /// assert_eq!(declarations.len(), 1); + /// + /// let declaration = declarations.into_iter().next().unwrap(); + /// assert_eq!(declaration.name().as_str(), "hello_world"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn push_bound_declaration(mut self, declaration: Declaration) -> Self { + let declarations = match self.declarations { + Some(mut declarations) => { + declarations.push(declaration); + declarations + } + None => NonEmpty::new(declaration), + }; + + self.declarations = Some(declarations); + self + } + + /// Consumes `self` to build an [`Output`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::output::Builder; + /// use ast::v1::document::Expression; + /// + /// let declaration = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::Identifier( + /// Identifier::try_from("foo").unwrap(), + /// )))? + /// .try_build()?; + /// + /// let output = Builder::default() + /// .push_bound_declaration(declaration) + /// .build(); + /// + /// let declarations = output.declarations().unwrap(); + /// assert_eq!(declarations.len(), 1); + /// + /// let declaration = declarations.into_iter().next().unwrap(); + /// assert_eq!(declaration.name().as_str(), "hello_world"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn build(self) -> Output { + Output { + declarations: self.declarations, + } + } +} + +#[cfg(test)] +mod tests { + use crate::v1::document::declaration::r#type::Kind; + use crate::v1::document::expression::Literal; + use crate::v1::document::Expression; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + let output = valid_node!(r#"output { String baz = None }"#, output, Output); + assert_eq!(output.declarations().unwrap().len(), 1); + + let declaration = output.declarations().unwrap().into_iter().next().unwrap(); + assert_eq!(declaration.name().as_str(), "baz"); + assert_eq!(declaration.r#type().kind(), &Kind::String); + assert!(!declaration.r#type().optional()); + assert_eq!(declaration.value(), &Expression::Literal(Literal::None)); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + output, + Output + ); + } +} diff --git a/wdl-ast/src/v1/document/parameter.rs b/wdl-ast/src/v1/document/parameter.rs new file mode 100644 index 000000000..df087e2ee --- /dev/null +++ b/wdl-ast/src/v1/document/parameter.rs @@ -0,0 +1,5 @@ +//! Parameters. + +pub mod metadata; + +pub use metadata::Metadata; diff --git a/wdl-ast/src/v1/document/private_declarations.rs b/wdl-ast/src/v1/document/private_declarations.rs new file mode 100644 index 000000000..192eba5ac --- /dev/null +++ b/wdl-ast/src/v1/document/private_declarations.rs @@ -0,0 +1,196 @@ +//! Private declarations. + +use nonempty::NonEmpty; +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::declaration::bound; +use crate::v1::document::declaration::bound::Declaration; +use crate::v1::macros::check_node; + +mod builder; + +pub use builder::Builder; + +/// An error related to [`PrivateDeclarations`]. +#[derive(Debug)] +pub enum Error { + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// A declaration error. + Declaration(bound::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Declaration(err) => write!(f, "declaration error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// The inner list of [bound declarations](`Declaration`) for [`PrivateDeclarations`]. +type Declarations = NonEmpty; + +/// A set of private declarations. +/// +/// Private declarations are comprised of one or more [bound +/// declarations](Declaration). +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PrivateDeclarations(Declarations); + +impl PrivateDeclarations { + /// Gets the inner value from the [`PrivateDeclarations`] by reference. + /// + /// # Examples + /// + /// ``` + /// use nonempty::NonEmpty; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound::Builder; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// use ast::v1::document::PrivateDeclarations; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// let private_declarations = PrivateDeclarations::from(NonEmpty::new(declaration.clone())); + /// + /// assert_eq!( + /// private_declarations.inner().into_iter().next().unwrap(), + /// &declaration + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn inner(&self) -> &Declarations { + &self.0 + } + + /// Consumes `self` to return the inner value from the + /// [`PrivateDeclarations`]. + /// + /// # Examples + /// + /// ``` + /// use nonempty::NonEmpty; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound::Builder; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// use ast::v1::document::PrivateDeclarations; + /// + /// let declaration = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// let private_declarations = PrivateDeclarations::from(NonEmpty::new(declaration.clone())); + /// + /// assert_eq!( + /// private_declarations + /// .into_inner() + /// .into_iter() + /// .next() + /// .unwrap(), + /// declaration + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn into_inner(self) -> Declarations { + self.0 + } +} + +impl From for PrivateDeclarations { + fn from(declarations: Declarations) -> Self { + PrivateDeclarations(declarations) + } +} + +impl TryFrom> for PrivateDeclarations { + type Error = Error; + + fn try_from(node: Pair<'_, Rule>) -> Result { + check_node!(node, private_declarations); + let mut builder = Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::bound_declaration => { + let declaration = Declaration::try_from(node).map_err(Error::Declaration)?; + builder = builder.push_bound_declaration(declaration); + } + Rule::WHITESPACE => {} + Rule::COMMENT => {} + rule => unreachable!("private declarations should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::document::declaration::r#type::Kind; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + let declarations = valid_node!( + r#"Boolean hello = false"#, + private_declarations, + PrivateDeclarations + ); + + assert_eq!(declarations.inner().len(), 1); + + let declaration = declarations.inner().iter().next().unwrap(); + assert_eq!(declaration.name().as_str(), "hello"); + assert_eq!(declaration.r#type().kind(), &Kind::Boolean); + assert!(!declaration.r#type().optional()); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + private_declarations, + PrivateDeclarations + ); + } +} diff --git a/wdl-ast/src/v1/document/private_declarations/builder.rs b/wdl-ast/src/v1/document/private_declarations/builder.rs new file mode 100644 index 000000000..391706810 --- /dev/null +++ b/wdl-ast/src/v1/document/private_declarations/builder.rs @@ -0,0 +1,118 @@ +//! Builder for a [`PrivateDeclarations`]. + +use nonempty::NonEmpty; + +use crate::v1::document::PrivateDeclarations; +use crate::v1::document::declaration::bound::Declaration; + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// Attempted to create a [`PrivateDeclarations`] that contained no + /// declarations, which is disallowed by the specification. + EmptyDeclarations, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::EmptyDeclarations => write!(f, "empty declarations"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for [`PrivateDeclarations`]. +#[derive(Debug, Default)] +pub struct Builder { + /// The bound declarations. + declarations: Option>, +} + +impl Builder { + /// Pushes a new [bound declaration](Declaration) into the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::private_declarations::Builder; + /// use ast::v1::document::Expression; + /// + /// let declaration = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// let declarations = Builder::default() + /// .push_bound_declaration(declaration.clone()) + /// .try_build()?; + /// + /// assert_eq!(declarations.inner().len(), 1); + /// assert_eq!(declarations.inner().iter().next().unwrap(), &declaration); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn push_bound_declaration(mut self, declaration: Declaration) -> Self { + let declarations = match self.declarations { + Some(mut declarations) => { + declarations.push(declaration); + declarations + } + None => NonEmpty::new(declaration), + }; + + self.declarations = Some(declarations); + self + } + + /// Consumes `self` to attempt to build a [`PrivateDeclarations`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::private_declarations::Builder; + /// use ast::v1::document::Expression; + /// + /// let declaration = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// let declarations = Builder::default() + /// .push_bound_declaration(declaration.clone()) + /// .try_build()?; + /// + /// assert_eq!(declarations.inner().len(), 1); + /// assert_eq!(declarations.inner().iter().next().unwrap(), &declaration); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_build(self) -> Result { + let declarations = self + .declarations + .map(Ok) + .unwrap_or(Err(Error::EmptyDeclarations))?; + + Ok(PrivateDeclarations::from(declarations)) + } +} diff --git a/wdl-ast/src/v1/document/struct.rs b/wdl-ast/src/v1/document/struct.rs new file mode 100644 index 000000000..7a89b9a70 --- /dev/null +++ b/wdl-ast/src/v1/document/struct.rs @@ -0,0 +1,176 @@ +//! Structs. + +use nonempty::NonEmpty; +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::declaration::unbound; +use crate::v1::document::declaration::unbound::Declaration; +use crate::v1::macros::check_node; + +mod builder; + +pub use builder::Builder; + +/// An error related to a [`Struct`]. +#[derive(Debug)] +pub enum Error { + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An identifier error. + Identifier(singular::Error), + + /// An unbound declaration error. + UnboundDeclaration(unbound::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + Error::UnboundDeclaration(err) => write!(f, "unbound declaration error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// Unbound declarations within a struct. +pub type Declarations = NonEmpty; + +/// A struct. +#[derive(Clone, Debug)] +pub struct Struct { + /// The unbound declarations (if they exist). + declarations: Option, + + /// The name. + name: Identifier, +} + +impl Struct { + /// Gets the [`Declarations`] for this [`Struct`] by reference (if they + /// exist). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::r#struct::Builder; + /// + /// let declaration = declaration::unbound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// + /// let r#struct = Builder::default() + /// .name(Identifier::try_from("a_struct").unwrap())? + /// .push_unbound_declaration(declaration.clone()) + /// .try_build()?; + /// + /// assert_eq!(r#struct.declarations().unwrap().len(), 1); + /// assert_eq!(r#struct.declarations().unwrap().first(), &declaration); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn declarations(&self) -> Option<&Declarations> { + self.declarations.as_ref() + } + + /// Gets the name from the [`Struct`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::r#struct::Builder; + /// + /// let r#struct = Builder::default() + /// .name(Identifier::try_from("a_struct").unwrap())? + /// .try_build()?; + /// + /// assert_eq!(r#struct.name().as_str(), "a_struct"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(&self) -> &Identifier { + &self.name + } +} + +impl TryFrom> for Struct { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, r#struct); + let mut builder = Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::struct_name => { + let identifier = Identifier::try_from(node.as_str().to_owned()) + .map_err(Error::Identifier)?; + builder = builder.name(identifier).map_err(Error::Builder)?; + } + Rule::unbound_declaration => { + let declaration = + Declaration::try_from(node).map_err(Error::UnboundDeclaration)?; + builder = builder.push_unbound_declaration(declaration); + } + Rule::WHITESPACE => {} + Rule::COMMENT => {} + rule => unreachable!("struct should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::document::declaration::r#type::Kind; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + let r#struct = valid_node!(r#"struct Hello { String? world }"#, r#struct, Struct); + assert_eq!(r#struct.name().as_str(), "Hello"); + + let declaration = r#struct.declarations().unwrap().into_iter().next().unwrap(); + assert_eq!(declaration.name().as_str(), "world"); + assert_eq!(declaration.r#type().kind(), &Kind::String); + assert!(declaration.r#type().optional()); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + r#struct, + Struct + ); + } +} diff --git a/wdl-ast/src/v1/document/struct/builder.rs b/wdl-ast/src/v1/document/struct/builder.rs new file mode 100644 index 000000000..e31296c7f --- /dev/null +++ b/wdl-ast/src/v1/document/struct/builder.rs @@ -0,0 +1,191 @@ +//! Builder for a [`Struct`]. + +use nonempty::NonEmpty; + +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::declaration::unbound::Declaration; +use crate::v1::document::r#struct::Declarations; +use crate::v1::document::Struct; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A name was not provided to the [`Builder`]. + Name, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Name => write!(f, "name"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error that occurs when a multiple values were provded for a field that +/// only accepts a single value. +#[derive(Debug)] +pub enum MultipleError { + /// Attempted to set multiple values for the name field within the + /// [`Builder`]. + Name, +} + +impl std::fmt::Display for MultipleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MultipleError::Name => write!(f, "name"), + } + } +} + +impl std::error::Error for MultipleError {} + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), + + /// Multiple values were provided for a field that accepts a single value. + Multiple(MultipleError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + Error::Multiple(err) => { + write!(f, "multiple values provided for single value field: {err}") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for an [`Struct`]. +#[derive(Debug, Default)] +pub struct Builder { + /// The unbound declarations (if they exist). + declarations: Option, + + /// The name. + name: Option, +} + +impl Builder { + /// Sets the name for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::r#struct::Builder; + /// + /// let r#struct = Builder::default() + /// .name(Identifier::try_from("a_struct").unwrap())? + /// .try_build()?; + /// + /// assert_eq!(r#struct.name().as_str(), "a_struct"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(mut self, name: Identifier) -> Result { + if self.name.is_some() { + return Err(Error::Multiple(MultipleError::Name)); + } + + self.name = Some(name); + Ok(self) + } + + /// Pushes an [unbound declaration](Declaration) into this [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::r#struct::Builder; + /// + /// let declaration = declaration::unbound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// + /// let r#struct = Builder::default() + /// .name(Identifier::try_from("a_struct").unwrap())? + /// .push_unbound_declaration(declaration.clone()) + /// .try_build()?; + /// + /// assert_eq!(r#struct.declarations().unwrap().len(), 1); + /// assert_eq!(r#struct.declarations().unwrap().first(), &declaration); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn push_unbound_declaration(mut self, declaration: Declaration) -> Self { + let declarations = match self.declarations { + Some(mut declarations) => { + declarations.push(declaration); + declarations + } + None => NonEmpty::new(declaration), + }; + + self.declarations = Some(declarations); + self + } + + /// Consumes `self` to attempt to build a [`Struct`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::declaration; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::r#struct::Builder; + /// + /// let declaration = declaration::unbound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// + /// let r#struct = Builder::default() + /// .name(Identifier::try_from("a_struct").unwrap())? + /// .push_unbound_declaration(declaration.clone()) + /// .try_build()?; + /// + /// assert_eq!(r#struct.name().as_str(), "a_struct"); + /// assert_eq!(r#struct.declarations().unwrap().len(), 1); + /// assert_eq!(r#struct.declarations().unwrap().first(), &declaration); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_build(self) -> Result { + let name = self + .name + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Name)))?; + + Ok(Struct { + name, + declarations: self.declarations, + }) + } +} diff --git a/wdl-ast/src/v1/document/task.rs b/wdl-ast/src/v1/document/task.rs new file mode 100644 index 000000000..46d97ee98 --- /dev/null +++ b/wdl-ast/src/v1/document/task.rs @@ -0,0 +1,528 @@ +//! Tasks. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +mod builder; +pub mod command; +pub mod runtime; + +pub use builder::Builder; +pub use command::Command; +pub use runtime::Runtime; + +use crate::v1::document; +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::Input; +use crate::v1::document::Metadata; +use crate::v1::document::Output; +use crate::v1::document::PrivateDeclarations; +use crate::v1::macros::check_node; +use crate::v1::macros::unwrap_one; + +/// An error related to a [`Task`]. +#[derive(Debug)] +pub enum Error { + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// A command error. + Command(command::Error), + + /// An identifier error. + Identifier(singular::Error), + + /// A input error. + Input(document::input::Error), + + /// A metadata error. + Metadata(document::metadata::Error), + + /// An output error. + Output(document::output::Error), + + /// A parameter metadata error. + ParameterMetadata(document::metadata::Error), + + /// A private declarations error. + PrivateDeclarations(document::private_declarations::Error), + + /// A runtime error. + Runtime(runtime::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Command(err) => write!(f, "command error: {err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + Error::Input(err) => write!(f, "input error: {err}"), + Error::Metadata(err) => write!(f, "metadata error: {err}"), + Error::Output(err) => write!(f, "output error: {err}"), + Error::ParameterMetadata(err) => write!(f, "parameter metadata error: {err}"), + Error::PrivateDeclarations(err) => write!(f, "private declarations error: {err}"), + Error::Runtime(err) => write!(f, "runtime error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A task. +#[derive(Clone, Debug)] +pub struct Task { + /// The command. + command: Command, + + /// The input. + input: Option, + + /// The metadata. + metadata: Option, + + /// The name. + name: Identifier, + + /// The output. + output: Option, + + /// The parameter metadata. + parameter_metadata: Option, + + /// Private declarations. + private_declarations: Option, + + /// The runtime. + runtime: Option, +} + +impl Task { + /// Gets the command from the [`Task`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::task; + /// + /// let name = Identifier::try_from(String::from("name"))?; + /// let contents = "echo 'Hello, world!'".parse::()?; + /// let command = task::Command::HereDoc(contents); + /// + /// let task = task::Builder::default() + /// .name(name)? + /// .command(command.clone())? + /// .try_build()?; + /// + /// assert_eq!(task.command(), &command); + /// + /// # Ok::<(), Box>(()) + /// ```` + pub fn command(&self) -> &Command { + &self.command + } + + /// Gets the input from the [`Task`] by reference (if it exists). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::input; + /// use ast::v1::document::task; + /// use ast::v1::document::task::Command; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let name = Identifier::try_from(String::from("name"))?; + /// let contents = "echo 'Hello, world!'" + /// .parse::() + /// .unwrap(); + /// let command = Command::HereDoc(contents); + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// let unbound = unbound::Builder::default() + /// .name(Identifier::try_from("foo_bar")?)? + /// .r#type(Type::new(Kind::Boolean, true))? + /// .try_build()?; + /// + /// let mut input = input::Builder::default() + /// .push_declaration(Declaration::Bound(bound.clone())) + /// .push_declaration(Declaration::Unbound(unbound.clone())) + /// .build(); + /// + /// let task = task::Builder::default() + /// .name(name)? + /// .command(command)? + /// .input(input.clone())? + /// .try_build()?; + /// + /// assert_eq!(task.input().unwrap(), &input); + /// + /// # Ok::<(), Box>(()) + /// ```` + pub fn input(&self) -> Option<&Input> { + self.input.as_ref() + } + + /// Gets the metadata from the [`Task`] by reference (if it exists). + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::metadata::Value; + /// use ast::v1::document::task; + /// use ast::v1::document::task::Command; + /// use ast::v1::document::Metadata; + /// + /// let name = Identifier::try_from(String::from("name"))?; + /// let contents = "echo 'Hello, world!'".parse::()?; + /// let command = Command::HereDoc(contents); + /// + /// let mut map = BTreeMap::::new(); + /// map.insert( + /// Identifier::try_from("hello")?, + /// Value::String(String::from("world")), + /// ); + /// map.insert(Identifier::try_from("foo")?, Value::Null); + /// + /// let metadata = Metadata::from(map); + /// + /// let task = task::Builder::default() + /// .name(name)? + /// .command(command)? + /// .metadata(metadata.clone())? + /// .try_build()?; + /// + /// assert_eq!(task.metadata().unwrap(), &metadata); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn metadata(&self) -> Option<&Metadata> { + self.metadata.as_ref() + } + + /// Gets the name from the [`Task`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::task; + /// + /// let name = Identifier::try_from(String::from("name"))?; + /// let contents = "echo 'Hello, world!'".parse::()?; + /// let command = task::Command::HereDoc(contents); + /// + /// let task = task::Builder::default() + /// .name(name.clone())? + /// .command(command)? + /// .try_build() + /// .unwrap(); + /// + /// assert_eq!(task.name(), &name); + /// + /// # Ok::<(), Box>(()) + /// ```` + pub fn name(&self) -> &Identifier { + &self.name + } + + /// Gets the output from the [`Task`] by reference (if it exists). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::output; + /// use ast::v1::document::task; + /// use ast::v1::document::task::Command; + /// use ast::v1::document::Expression; + /// + /// let name = Identifier::try_from(String::from("name"))?; + /// let contents = "echo 'Hello, world!'" + /// .parse::() + /// .unwrap(); + /// let command = Command::HereDoc(contents); + /// + /// let bound = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// let mut output = output::Builder::default() + /// .push_bound_declaration(bound.clone()) + /// .build(); + /// + /// let task = task::Builder::default() + /// .name(name)? + /// .command(command)? + /// .output(output.clone())? + /// .try_build() + /// .unwrap(); + /// + /// assert_eq!(task.output().unwrap(), &output); + /// + /// # Ok::<(), Box>(()) + /// ```` + pub fn output(&self) -> Option<&Output> { + self.output.as_ref() + } + + /// Gets the parameter metadata from the [`Task`] by reference (if it + /// exists). + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::metadata::Value; + /// use ast::v1::document::task; + /// use ast::v1::document::task::Command; + /// use ast::v1::document::Metadata; + /// + /// let name = Identifier::try_from(String::from("name"))?; + /// let contents = "echo 'Hello, world!'" + /// .parse::() + /// .unwrap(); + /// let command = Command::HereDoc(contents); + /// + /// let mut map = BTreeMap::::new(); + /// map.insert( + /// Identifier::try_from("hello").unwrap(), + /// Value::String(String::from("world")), + /// ); + /// map.insert(Identifier::try_from("foo").unwrap(), Value::Null); + /// + /// let parameter_metadata = Metadata::from(map); + /// + /// let task = task::Builder::default() + /// .name(name)? + /// .command(command)? + /// .parameter_metadata(parameter_metadata.clone())? + /// .try_build() + /// .unwrap(); + /// + /// assert_eq!(task.parameter_metadata().unwrap(), ¶meter_metadata); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn parameter_metadata(&self) -> Option<&Metadata> { + self.parameter_metadata.as_ref() + } + + /// Gets the [private declarations](PrivateDeclarations) from the [`Task`] + /// by reference (if they exist). + /// + /// # Examples + /// + /// ``` + /// use nonempty::NonEmpty; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::task; + /// use ast::v1::document::task::Command; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// use ast::v1::document::PrivateDeclarations; + /// + /// let name = Identifier::try_from(String::from("name"))?; + /// let contents = "echo 'Hello, world!'" + /// .parse::() + /// .unwrap(); + /// let command = Command::HereDoc(contents); + /// + /// let declaration = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// let private_declarations = PrivateDeclarations::from(NonEmpty::new(declaration.clone())); + /// + /// let task = task::Builder::default() + /// .name(name)? + /// .command(command)? + /// .push_private_declarations(private_declarations.clone()) + /// .try_build() + /// .unwrap(); + /// + /// assert_eq!(task.private_declarations().unwrap(), &private_declarations); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn private_declarations(&self) -> Option<&PrivateDeclarations> { + self.private_declarations.as_ref() + } + + pub fn runtime(&self) -> Option<&Runtime> { + self.runtime.as_ref() + } +} + +impl TryFrom> for Task { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, task); + let mut builder = builder::Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::singular_identifier => { + let identifier = Identifier::try_from(node.as_str().to_owned()) + .map_err(Error::Identifier)?; + builder = builder.name(identifier).map_err(Error::Builder)?; + } + Rule::task_element => { + let node = unwrap_one!(node, task_element)?; + + match node.as_rule() { + Rule::input => { + let input = Input::try_from(node).map_err(Error::Input)?; + builder = builder.input(input).map_err(Error::Builder)?; + } + Rule::metadata => { + let metadata = Metadata::try_from(node).map_err(Error::Metadata)?; + builder = builder.metadata(metadata).map_err(Error::Builder)?; + } + Rule::output => { + let output = Output::try_from(node).map_err(Error::Output)?; + builder = builder.output(output).map_err(Error::Builder)?; + } + Rule::parameter_metadata => { + let parameter_metadata = + Metadata::try_from(node).map_err(Error::ParameterMetadata)?; + builder = builder + .parameter_metadata(parameter_metadata) + .map_err(Error::Builder)?; + } + Rule::private_declarations => { + let declarations = PrivateDeclarations::try_from(node) + .map_err(Error::PrivateDeclarations)?; + builder = builder.push_private_declarations(declarations); + } + Rule::task_command => { + let command = Command::try_from(node).map_err(Error::Command)?; + builder = builder.command(command).map_err(Error::Builder)?; + } + Rule::task_runtime => { + let runtime = Runtime::try_from(node).map_err(Error::Runtime)?; + builder = builder.runtime(runtime).map_err(Error::Builder)?; + } + rule => unreachable!("task element should not contain {:?}", rule), + } + } + Rule::COMMENT => {} + Rule::WHITESPACE => {} + rule => unreachable!("task should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} + +#[cfg(test)] +mod tests { + use nonempty::NonEmpty; + + use super::*; + + use crate::v1::document::declaration::bound; + use crate::v1::document::declaration::r#type::Kind; + use crate::v1::document::declaration::Type; + use crate::v1::document::expression::Literal; + use crate::v1::document::identifier::singular::Identifier; + use crate::v1::document::task; + use crate::v1::document::task::Command; + use crate::v1::document::Expression; + + #[test] + fn multiple_private_declarations_are_squashed() -> Result<(), Box> { + let name = Identifier::try_from(String::from("name"))?; + let contents = "echo 'Hello, world!'" + .parse::() + .unwrap(); + let command = Command::HereDoc(contents); + + let one = bound::Builder::default() + .name(Identifier::try_from("hello_world")?)? + .r#type(Type::new(Kind::Boolean, false))? + .value(Expression::Literal(Literal::None))? + .try_build()?; + + let two = bound::Builder::default() + .name(Identifier::try_from("foo")?)? + .r#type(Type::new(Kind::String, false))? + .value(Expression::Literal(Literal::String(String::from("baz"))))? + .try_build()?; + + let task = task::Builder::default() + .name(name)? + .command(command)? + .push_private_declarations(PrivateDeclarations::from(NonEmpty::new(one.clone()))) + .push_private_declarations(PrivateDeclarations::from(NonEmpty::new(two.clone()))) + .try_build() + .unwrap(); + + let mut private_declarations = task.private_declarations().unwrap().inner().into_iter(); + + assert_eq!(private_declarations.next(), Some(&one)); + assert_eq!(private_declarations.next(), Some(&two)); + assert_eq!(private_declarations.next(), None); + + Ok(()) + } +} diff --git a/wdl-ast/src/v1/document/task/builder.rs b/wdl-ast/src/v1/document/task/builder.rs new file mode 100644 index 000000000..bfaa9010f --- /dev/null +++ b/wdl-ast/src/v1/document/task/builder.rs @@ -0,0 +1,541 @@ +//! A builder for [`Task`]s. + +use nonempty::NonEmpty; + +use crate::v1::document; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::task::Command; +use crate::v1::document::task::Runtime; +use crate::v1::document::task::Task; +use crate::v1::document::PrivateDeclarations; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A command was not provided to the [`Builder`]. + Command, + + /// A name was not provided to the [`Builder`]. + Name, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Command => write!(f, "command"), + MissingError::Name => write!(f, "name"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error that occurs when a multiple values were provded for a field that +/// only accepts a single value. +#[derive(Debug)] +pub enum MultipleError { + /// Attempted to set multiple values for the command field within the + /// [`Builder`]. + Command, + + /// Attempted to set multiple values for the input field within the + /// [`Builder`]. + Input, + + /// Attempted to set multiple values for the metadata field within the + /// [`Builder`]. + Metadata, + + /// Attempted to set multiple values for the name field within the + /// [`Builder`]. + Name, + + /// Attempted to set multiple values for the output field within the + /// [`Builder`]. + Output, + + /// Attempted to set multiple values for the parameter metadata field within + /// the [`Builder`]. + ParameterMetadata, + + /// Attempted to set multiple values for the runtime field within the + /// [`Builder`]. + Runtime, +} + +impl std::fmt::Display for MultipleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MultipleError::Command => write!(f, "command"), + MultipleError::Input => write!(f, "input"), + MultipleError::Metadata => write!(f, "metadata"), + MultipleError::Name => write!(f, "name"), + MultipleError::Output => write!(f, "output"), + MultipleError::ParameterMetadata => write!(f, "parameter metadata"), + MultipleError::Runtime => write!(f, "runtime"), + } + } +} + +impl std::error::Error for MultipleError {} + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), + + /// Multiple values were provided for a field that accepts a single value. + Multiple(MultipleError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + Error::Multiple(err) => { + write!(f, "multiple values provided for single value field: {err}") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for a [`Task`]. +#[derive(Debug, Default)] +pub struct Builder { + /// The command. + command: Option, + + /// The input. + input: Option, + + /// The metadata. + metadata: Option, + + /// The name. + name: Option, + + /// The output. + output: Option, + + /// The parameter metadata. + parameter_metadata: Option, + + /// Private declarations. + private_declarations: Option>, + + /// The runtime. + runtime: Option, +} + +impl Builder { + /// Sets the name for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::task; + /// + /// let name = Identifier::try_from(String::from("name"))?; + /// let contents = "echo 'Hello, world!'" + /// .parse::() + /// .unwrap(); + /// let command = task::Command::HereDoc(contents); + /// task::Builder::default().name(name)?; + /// + /// # Ok::<(), Box>(()) + /// ```` + pub fn name(mut self, name: Identifier) -> Result { + if self.name.is_some() { + return Err(Error::Multiple(MultipleError::Name)); + } + + self.name = Some(name); + Ok(self) + } + + /// Sets the command for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document::task; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// let name = Identifier::try_from(String::from("name"))?; + /// let contents = "echo 'Hello, world!'" + /// .parse::() + /// .unwrap(); + /// let command = task::Command::HereDoc(contents); + /// task::Builder::default().name(name)?.command(command)?; + /// + /// # Ok::<(), Box>(()) + /// ```` + pub fn command(mut self, command: Command) -> Result { + if self.command.is_some() { + return Err(Error::Multiple(MultipleError::Command)); + } + + self.command = Some(command); + Ok(self) + } + + /// Sets the input for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::input; + /// use ast::v1::document::task; + /// use ast::v1::document::task::Builder; + /// use ast::v1::document::Declaration; + /// + /// let declaration = unbound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// let mut input = input::Builder::default() + /// .push_declaration(Declaration::Unbound(declaration)) + /// .build(); + /// + /// let contents = "echo 'Hello, world!'" + /// .parse::() + /// .unwrap(); + /// let command = task::Command::HereDoc(contents); + /// + /// let task = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .command(command)? + /// .input(input.clone())? + /// .try_build()?; + /// + /// assert_eq!(task.input(), Some(&input)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn input(mut self, input: document::Input) -> Result { + if self.input.is_some() { + return Err(Error::Multiple(MultipleError::Input)); + } + + self.input = Some(input); + Ok(self) + } + + /// Sets the output for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::output; + /// use ast::v1::document::task; + /// use ast::v1::document::task::Builder; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let declaration = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::Identifier( + /// Identifier::try_from("foo").unwrap(), + /// )))? + /// .try_build()?; + /// let output = output::Builder::default() + /// .push_bound_declaration(declaration) + /// .build(); + /// + /// let contents = "echo 'Hello, world!'" + /// .parse::() + /// .unwrap(); + /// let command = task::Command::HereDoc(contents); + /// + /// let task = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .command(command)? + /// .output(output.clone())? + /// .try_build()?; + /// + /// assert_eq!(task.output(), Some(&output)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn output(mut self, output: document::Output) -> Result { + if self.output.is_some() { + return Err(Error::Multiple(MultipleError::Output)); + } + + self.output = Some(output); + Ok(self) + } + + /// Sets the metadata for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::metadata::Value; + /// use ast::v1::document::task; + /// use ast::v1::document::task::Builder; + /// use ast::v1::document::task::Command; + /// use ast::v1::document::Metadata; + /// + /// let mut map = BTreeMap::new(); + /// map.insert( + /// Identifier::try_from(String::from("foo"))?, + /// Value::String(String::from("bar")), + /// ); + /// + /// let metadata = Metadata::from(map); + /// + /// let contents = "echo 'Hello, world!'" + /// .parse::() + /// .unwrap(); + /// let command = Command::HereDoc(contents); + /// + /// let task = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .command(command)? + /// .metadata(metadata)? + /// .try_build()?; + /// + /// let metadata = task.metadata().unwrap().inner(); + /// + /// assert_eq!( + /// metadata.get("foo"), + /// Some(&Value::String(String::from("bar"))) + /// ); + /// assert_eq!(metadata.get("baz"), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn metadata(mut self, metadata: document::Metadata) -> Result { + if self.metadata.is_some() { + return Err(Error::Multiple(MultipleError::Metadata)); + } + + self.metadata = Some(metadata); + Ok(self) + } + + /// Sets the parameter metadata for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::metadata::Value; + /// use ast::v1::document::task; + /// use ast::v1::document::task::Builder; + /// use ast::v1::document::task::Command; + /// use ast::v1::document::Metadata; + /// + /// let mut map = BTreeMap::new(); + /// map.insert( + /// Identifier::try_from(String::from("baz"))?, + /// Value::String(String::from("quux")), + /// ); + /// + /// let parameter_metadata = Metadata::from(map); + /// + /// let contents = "echo 'Hello, world!'" + /// .parse::() + /// .unwrap(); + /// let command = Command::HereDoc(contents); + /// + /// let task = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .command(command)? + /// .parameter_metadata(parameter_metadata)? + /// .try_build()?; + /// + /// let parameter_metadata = task.parameter_metadata().unwrap().inner(); + /// + /// assert_eq!( + /// parameter_metadata.get("baz"), + /// Some(&Value::String(String::from("quux"))) + /// ); + /// assert_eq!(parameter_metadata.get("foo"), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn parameter_metadata(mut self, metadata: document::Metadata) -> Result { + if self.parameter_metadata.is_some() { + return Err(Error::Multiple(MultipleError::ParameterMetadata)); + } + + self.parameter_metadata = Some(metadata); + Ok(self) + } + + /// Pushes a [private declarations](PrivateDeclarations) into the [`Task`]. + /// + /// # Examples + /// + /// ``` + /// use nonempty::NonEmpty; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::task; + /// use ast::v1::document::task::Command; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// use ast::v1::document::PrivateDeclarations; + /// + /// let name = Identifier::try_from(String::from("name"))?; + /// let contents = "echo 'Hello, world!'" + /// .parse::() + /// .unwrap(); + /// let command = Command::HereDoc(contents); + /// + /// let declaration = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::None))? + /// .try_build()?; + /// + /// let private_declarations = PrivateDeclarations::from(NonEmpty::new(declaration.clone())); + /// + /// let task = task::Builder::default() + /// .name(name)? + /// .command(command)? + /// .push_private_declarations(private_declarations.clone()) + /// .try_build() + /// .unwrap(); + /// + /// assert_eq!(task.private_declarations().unwrap(), &private_declarations); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn push_private_declarations( + mut self, + declarations: document::PrivateDeclarations, + ) -> Self { + let declarations = match self.private_declarations { + Some(mut existing) => { + existing.push(declarations); + existing + } + None => NonEmpty::new(declarations), + }; + + self.private_declarations = Some(declarations); + self + } + + pub fn runtime(mut self, runtime: Runtime) -> Result { + if self.runtime.is_some() { + return Err(Error::Multiple(MultipleError::Runtime)); + } + + self.runtime = Some(runtime); + Ok(self) + } + + /// Consumes `self` to attempt to build a [`Task`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// use wdl_grammar as grammar; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::task; + /// + /// let name = Identifier::from(Identifier::try_from(String::from("name"))?); + /// let contents = "".parse::()?; + /// let command = task::Command::HereDoc(contents); + /// + /// let task = task::Builder::default() + /// .name(name.clone())? + /// .command(command)? + /// .try_build()?; + /// + /// # Ok::<(), Box>(()) + /// ```` + pub fn try_build(self) -> Result { + let command = self + .command + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Command)))?; + + let name = self + .name + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Name)))?; + + // Before building the final [`Task`], we must flatten the private + // declarations into a single [`PrivateDeclarations`]. + let private_declarations = self + .private_declarations + .into_iter() + .flat_map(|declarations| { + declarations.flat_map(|declarations| declarations.into_inner()) + }) + .collect::>(); + + let private_declarations = if !private_declarations.is_empty() { + // SAFETY: The check above ensures that the declarations aren't empty, + // so unwrapping is safe here. + let mut private_declarations = private_declarations.into_iter(); + + let mut result = NonEmpty::new(private_declarations.next().unwrap()); + result.extend(private_declarations); + + Some(PrivateDeclarations::from(result)) + } else { + None + }; + + Ok(Task { + command, + input: self.input, + metadata: self.metadata, + name, + output: self.output, + parameter_metadata: self.parameter_metadata, + private_declarations, + runtime: self.runtime, + }) + } +} diff --git a/wdl-ast/src/v1/document/task/command.rs b/wdl-ast/src/v1/document/task/command.rs new file mode 100644 index 000000000..56de070d2 --- /dev/null +++ b/wdl-ast/src/v1/document/task/command.rs @@ -0,0 +1,118 @@ +//! Commands within a task. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::macros::check_node; +use crate::v1::macros::dive_one; +use crate::v1::macros::unwrap_one; + +mod contents; + +pub use contents::Contents; + +/// An error related to a [`Command`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// Contents error. + Contents(contents::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Contents(err) => write!(f, "contents error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A command withing a task. +/// +/// **Note:** this crate does no inspection of the underlying command. Instead, +/// we make the command available for other tools (e.g., +/// [shellcheck](https://www.shellcheck.net/)). +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Command { + /// A heredoc style command. + HereDoc(Contents), + + /// A curly bracket style command. + Curly(Contents), +} + +impl Command { + /// Gets the inner contents of the command as a reference to a [`str`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::task::command::Contents; + /// use ast::v1::document::task::Command; + /// + /// let contents = "echo 'Hello, world!'".parse::().unwrap(); + /// let command = Command::HereDoc(contents); + /// assert_eq!(command.as_str(), "echo 'Hello, world!'"); + /// ``` + pub fn as_str(&self) -> &str { + match self { + Command::HereDoc(contents) => contents.as_str(), + Command::Curly(contents) => contents.as_str(), + } + } +} + +impl std::fmt::Display for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl TryFrom> for Command { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, task_command); + let node = unwrap_one!(node, task_command)?; + + Ok(match node.as_rule() { + Rule::command_heredoc => { + let contents_node = dive_one!( + node, + command_heredoc_contents, + command_heredoc, + Error::Common + )?; + let contents = contents_node + .as_str() + .parse::() + .map_err(Error::Contents)?; + Command::HereDoc(contents) + } + Rule::command_curly => { + let contents_node = + dive_one!(node, command_curly_contents, command_curly, Error::Common)?; + let contents = contents_node + .as_str() + .parse::() + .map_err(Error::Contents)?; + Command::Curly(contents) + } + _ => { + unreachable!( + "a task command's inner element must be either a heredoc or a curly command" + ) + } + }) + } +} diff --git a/wdl-ast/src/v1/document/task/command/contents.rs b/wdl-ast/src/v1/document/task/command/contents.rs new file mode 100644 index 000000000..d4cf895b9 --- /dev/null +++ b/wdl-ast/src/v1/document/task/command/contents.rs @@ -0,0 +1,242 @@ +//! Contents of a command. + +use std::collections::HashMap; + +/// The line ending. +#[cfg(windows)] +const LINE_ENDING: &str = "\r\n"; +/// The line ending. +#[cfg(not(windows))] +const LINE_ENDING: &str = "\n"; + +/// An error when parsing [`Contents`]. +#[derive(Debug)] +pub enum ParseError { + /// Mixed tabs and spaces. + MixedIndentationStyle, +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseError::MixedIndentationStyle => write!(f, "mixed indentation characters"), + } + } +} + +impl std::error::Error for ParseError {} + +/// An error related to [`Contents`]. +#[derive(Debug)] +pub enum Error { + /// A parse error. + Parse(ParseError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Parse(err) => write!(f, "parse error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// Contents of a command. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Contents(String); + +impl std::ops::Deref for Contents { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::str::FromStr for Contents { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Ok(Self(String::new())); + } + + let mut lines = s.lines(); + let mut results = Vec::new(); + + // SAFETY: we just ensured that exactly one line exists, so this + // will unwrap. + let first_line = lines.next().unwrap(); + + // NOTE: the first line is treated separately from the remaining lines. + // This is because the first line is either (a) empty, which is harmless + // and pushes and empty line into the results, or (b) has some content, + // which means it is on the same line as the command. In the case of + // (b), we don't want the spacing for the first line to influence the + // stripping of whitespace on the remaining lines. For example, + // + // ``` + // command <<< echo 'hello' + // echo 'world' + // exit 0 + // >>> + // ``` + // + // Althought the above is considered bad form, the single space on the + // first line should not dictate the stripping of whitespace for the + // remaining lines (which are clearly indented with four spaces). + + if !first_line.is_empty() { + results.push(first_line.to_string()); + } + + results.extend(strip_leading_whitespace(lines.collect())?); + + // If the last line is pure whitespace, ignore it. + if let Some(line) = results.pop() { + if !line.trim().is_empty() { + results.push(line) + } + } + + Ok(Self(results.join(LINE_ENDING))) + } +} + +/// Strips common leading whitespace from a [`Vec<&str>`]. +fn strip_leading_whitespace(lines: Vec<&str>) -> Result> { + // Count up all preceeding whitespace characters (including if whitespace + // characters are mixed). + let whitespace_by_line = lines + .iter() + .filter(|line| !line.trim().is_empty()) + .map(|line| { + line.chars() + .take_while(|c| c.is_whitespace()) + .fold(HashMap::new(), |mut counts, c| { + *counts.entry(c).or_insert(0usize) += 1usize; + counts + }) + }) + .collect::>>(); + + let all_whitespace = + whitespace_by_line + .iter() + .fold(HashMap::new(), |mut total_counts, line_counts| { + for (c, count) in line_counts { + *total_counts.entry(*c).or_insert(0usize) += count + } + + total_counts + }); + + if all_whitespace.len() > 1 { + return Err(Error::Parse(ParseError::MixedIndentationStyle)); + } + + let indent = whitespace_by_line + .into_iter() + .map(|counts| counts.values().sum()) + .min() + .unwrap_or_default(); + + Ok(lines + .iter() + .map(|line| { + if line.len() >= indent { + line.chars().skip(indent).collect() + } else { + line.to_string() + } + }) + .collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_parses_contents_with_spaces_correctly( + ) -> std::result::Result<(), Box> { + let contents = "echo 'hello,' + echo 'there' + echo 'world'" + .parse::()?; + + assert_eq!( + contents.as_str(), + "echo 'hello,'\necho 'there'\necho 'world'" + ); + + let contents = " + echo 'hello,' + echo 'there' + echo 'world'" + .parse::()?; + + assert_eq!( + contents.as_str(), + "echo 'hello,'\necho 'there'\necho 'world'" + ); + + let contents = " + echo 'hello,' + echo 'there' + echo 'world'" + .parse::()?; + + assert_eq!( + contents.as_str(), + " echo 'hello,'\necho 'there'\necho 'world'" + ); + + Ok(()) + } + + #[test] + fn it_parses_contents_with_tabs_correctly( + ) -> std::result::Result<(), Box> { + let contents = " +\t\techo 'hello,' +\t\techo 'there' +\t\techo 'world'" + .parse::()?; + assert_eq!( + contents.as_str(), + "echo 'hello,'\necho 'there'\necho 'world'" + ); + + Ok(()) + } + + #[test] + fn it_keeps_preceeding_whitespace_on_the_same_line_as_the_command( + ) -> std::result::Result<(), Box> { + let contents = " \nhello".parse::()?; + assert_eq!(contents.as_str(), " \nhello"); + + Ok(()) + } + + #[test] + fn it_fails_on_mixed_spaces_and_tabs() { + let err = " + \techo 'hello,' + echo 'there' + echo 'world'" + .parse::() + .unwrap_err(); + + assert!(matches!( + err, + Error::Parse(ParseError::MixedIndentationStyle) + )); + } +} diff --git a/wdl-ast/src/v1/document/task/metadata.rs b/wdl-ast/src/v1/document/task/metadata.rs new file mode 100644 index 000000000..e69de29bb diff --git a/wdl-ast/src/v1/document/task/runtime.rs b/wdl-ast/src/v1/document/task/runtime.rs new file mode 100644 index 000000000..aeec5a7e1 --- /dev/null +++ b/wdl-ast/src/v1/document/task/runtime.rs @@ -0,0 +1,130 @@ +use std::collections::BTreeMap; + + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::expression; + +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::Expression; +use crate::v1::macros::check_node; +use crate::v1::macros::extract_one; +use crate::v1::macros::unwrap_one; + +mod builder; +mod container; +mod cpu; + +pub use builder::Builder; + +/// An error related to a [`Runtime`]. +#[derive(Debug)] +pub enum Error { + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An error with the `container` entry. + Container(container::Error), + + /// An error with the `cpu` entry. + Cpu(cpu::Error), + + /// An expression error. + Expression(expression::Error), + + /// An identifier error. + Identifier(singular::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Container(err) => write!(f, "`container` entry error: {err}"), + Error::Cpu(err) => write!(f, "`cpu` entry error: {err}"), + Error::Expression(err) => write!(f, "expression error: {err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A runtime section. +#[derive(Clone, Debug)] +pub struct Runtime { + /// The container entry. + container: Option, + + /// The cpu entry. + cpu: cpu::Value, + + /// Other included runtime hints. + hints: Option>, +} + +impl TryFrom> for Runtime { + type Error = Error; + + fn try_from(node: Pair<'_, Rule>) -> Result { + check_node!(node, task_runtime); + let mut builder = Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::task_runtime_mapping => { + let key_node = extract_one!( + node.clone(), + task_runtime_mapping_key, + task_runtime_mapping, + Error::Common + )?; + let value_node = extract_one!( + node, + task_runtime_mapping_value, + task_runtime_mapping, + Error::Common + )?; + + match key_node.as_str() { + "container" | "docker" => { + let container = + container::Value::try_from(value_node).map_err(Error::Container)?; + builder = builder.container(container).map_err(Error::Builder)? + } + "cpu" => { + let cpu = cpu::Value::try_from(value_node).map_err(Error::Cpu)?; + builder = builder.cpu(cpu).map_err(Error::Builder)?; + } + _ => { + let identifier_node = unwrap_one!(key_node, task_runtime_mapping_key)?; + let key = + Identifier::try_from(identifier_node).map_err(Error::Identifier)?; + + let expression_node = + unwrap_one!(value_node, task_runtime_mapping_value)?; + let value = + Expression::try_from(expression_node).map_err(Error::Expression)?; + + builder = builder.insert_hint(key, value); + } + } + } + Rule::WHITESPACE => {} + Rule::COMMENT => {} + rule => unreachable!("task runtime should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} diff --git a/wdl-ast/src/v1/document/task/runtime/builder.rs b/wdl-ast/src/v1/document/task/runtime/builder.rs new file mode 100644 index 000000000..3410d9340 --- /dev/null +++ b/wdl-ast/src/v1/document/task/runtime/builder.rs @@ -0,0 +1,124 @@ +use std::collections::BTreeMap; + +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::task::runtime::container; +use crate::v1::document::task::runtime::cpu; +use crate::v1::document::task::Runtime; +use crate::v1::document::Expression; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A version was not provided to the [`Builder`]. + Container, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Container => write!(f, "version"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error that occurs when a multiple values were provded for a field that +/// only accepts a single value. +#[derive(Debug)] +pub enum MultipleError { + /// Attempted to set multiple values for the container field within the + /// [`Builder`]. + Container, + + /// Attempted to set multiple values for the cpu field within the + /// [`Builder`]. + Cpu, +} + +impl std::fmt::Display for MultipleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MultipleError::Container => write!(f, "container"), + MultipleError::Cpu => write!(f, "cpu") + } + } +} + +impl std::error::Error for MultipleError {} + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), + + /// Multiple values were provided for a field that accepts a single value. + Multiple(MultipleError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + Error::Multiple(err) => { + write!(f, "multiple values provided for single value field: {err}") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for a [`Runtime`]. +#[derive(Debug, Default)] +pub struct Builder { + /// The container entry. + container: Option, + + /// The container entry. + cpu: Option, + + /// Other included runtime hints. + hints: Option>, +} + +impl Builder { + pub fn container(mut self, container: container::Value) -> Result { + if self.container.is_some() { + return Err(Error::Multiple(MultipleError::Container)); + } + + self.container = Some(container); + Ok(self) + } + + pub fn cpu(mut self, cpu: cpu::Value) -> Result { + if self.cpu.is_some() { + return Err(Error::Multiple(MultipleError::Cpu)); + } + + self.cpu = Some(cpu); + Ok(self) + } + + pub fn insert_hint(mut self, key: Identifier, value: Expression) -> Self { + let mut hints = self.hints.unwrap_or_default(); + hints.insert(key, value); + self.hints = Some(hints); + self + } + + pub fn try_build(self) -> Result { + let cpu = self.cpu.unwrap_or_default(); + + Ok(Runtime { + container: self.container, + cpu, + hints: self.hints, + }) + } +} diff --git a/wdl-ast/src/v1/document/task/runtime/container.rs b/wdl-ast/src/v1/document/task/runtime/container.rs new file mode 100644 index 000000000..01623a507 --- /dev/null +++ b/wdl-ast/src/v1/document/task/runtime/container.rs @@ -0,0 +1,152 @@ +//! `container`. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::expression; +use crate::v1::document::expression::Literal; +use crate::v1::document::Expression; +use crate::v1::macros::check_node; +use crate::v1::macros::unwrap_one; + +/// An error related to a [`Value`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// Attempted to create an empty array. + EmptyArray, + + /// An expression error. + Expression(expression::Error), + + /// An invalid format for a [`Value`]. + InvalidFormat(Expression), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::EmptyArray => write!(f, "empty array"), + Error::Expression(err) => write!(f, "expression error: {err}"), + Error::InvalidFormat(_) => write!(f, "invalid value"), + } + } +} + +impl std::error::Error for Error {} + +/// A value for the runtime section's `container` entry. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Value { + /// A single URI. + Single(String), + + /// Multiple URIs. + Multiple(Vec), +} + +impl TryFrom> for Value { + type Error = Error; + + fn try_from(node: Pair<'_, Rule>) -> Result { + check_node!(node, task_runtime_mapping_value); + + let expression_node = unwrap_one!(node, task_runtime_mapping_value)?; + let expression = Expression::try_from(expression_node).map_err(Error::Expression)?; + + match expression { + Expression::Array(array) => { + if array.inner().is_empty() { + return Err(Error::EmptyArray); + } + + let values = + array + .clone() + .into_inner() + .into_iter() + .try_fold(Vec::new(), |mut acc, expr| match expr { + Expression::Literal(Literal::String(value)) => { + acc.push(value); + Some(acc) + } + _ => None, + }); + + match values { + Some(values) => Ok(Value::Multiple(values)), + None => Err(Error::InvalidFormat(Expression::Array(array))), + } + } + Expression::Literal(Literal::String(value)) => Ok(Value::Single(value)), + expr => Err(Error::InvalidFormat(expr)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::v1::macros; + + #[test] + fn it_correctly_parses_a_single_string() { + let value = macros::test::valid_node!(r#""ubuntu:latest""#, task_runtime_mapping_value, Value); + assert_eq!(value, Value::Single(String::from("ubuntu:latest"))); + } + + #[test] + fn it_correctly_parses_a_array_of_strings() { + let value = + macros::test::valid_node!(r#"["ubuntu:latest", "debian:latest"]"#, task_runtime_mapping_value, Value); + assert_eq!( + value, + Value::Multiple(vec![ + String::from("ubuntu:latest"), + String::from("debian:latest") + ]) + ); + } + + #[test] + fn it_fails_to_parse_from_an_invalid_expression() { + let parse_node = wdl_grammar::v1::parse(Rule::task_runtime_mapping_value, "None") + .unwrap() + .into_inner() + .next() + .unwrap(); + + let err = Value::try_from(parse_node).unwrap_err(); + matches!(err, Error::InvalidFormat(_)); + } + + #[test] + fn it_fails_to_parse_from_an_array_with_a_non_string() { + let parse_node = wdl_grammar::v1::parse(Rule::task_runtime_mapping_value, r#"["ubuntu:latest", None]"#) + .unwrap() + .into_inner() + .next() + .unwrap(); + + let err = Value::try_from(parse_node).unwrap_err(); + matches!(err, Error::InvalidFormat(_)); + } + + #[test] + fn it_fails_to_parse_from_an_empty_array() { + let parse_node = wdl_grammar::v1::parse(Rule::task_runtime_mapping_value, "[]") + .unwrap() + .into_inner() + .next() + .unwrap(); + + let err = Value::try_from(parse_node).unwrap_err(); + matches!(err, Error::EmptyArray); + } +} diff --git a/wdl-ast/src/v1/document/task/runtime/cpu.rs b/wdl-ast/src/v1/document/task/runtime/cpu.rs new file mode 100644 index 000000000..cc4a27b87 --- /dev/null +++ b/wdl-ast/src/v1/document/task/runtime/cpu.rs @@ -0,0 +1,177 @@ +//! `cpu`. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::expression; +use crate::v1::document::expression::ensure_number; +use crate::v1::document::expression::Literal; +use crate::v1::document::Expression; +use crate::v1::macros::check_node; +use crate::v1::macros::unwrap_one; + +/// An error related to a [`Value`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// An expression error. + Expression(expression::Error), + + /// An invalid format for a [`Value`]. + InvalidFormat(Expression), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Expression(err) => write!(f, "expression error: {err}"), + Error::InvalidFormat(_) => write!(f, "invalid value"), + } + } +} + +impl std::error::Error for Error {} + +/// A value for the runtime section's `container` entry. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Value(Expression); + +impl Default for Value { + fn default() -> Self { + Self(Expression::Literal(Literal::Integer(1))) + } +} + +impl Value { + pub fn inner(&self) -> &Expression { + &self.0 + } + + pub fn into_inner(self) -> Expression { + self.0 + } +} + +impl TryFrom> for Value { + type Error = Error; + + fn try_from(node: Pair<'_, Rule>) -> Result { + check_node!(node, task_runtime_mapping_value); + + let expression_node = unwrap_one!(node, task_runtime_mapping_value)?; + let expression = Expression::try_from(expression_node).map_err(Error::Expression)?; + + if ensure_number(&expression).is_some() { + Ok(Value(expression)) + } else { + Err(Error::InvalidFormat(expression)) + } + } +} + +#[cfg(test)] +mod tests { + use ordered_float::OrderedFloat; + + use super::*; + use crate::v1::document::expression::Literal; + use crate::v1::document::expression::UnarySigned; + use crate::v1::macros; + + #[test] + fn the_default_value_is_correct() { + assert_eq!( + Value::default(), + Value(Expression::Literal(Literal::Integer(1))) + ); + } + + #[test] + fn it_correctly_parses_integers() { + let value = macros::test::valid_node!("1", task_runtime_mapping_value, Value); + assert_eq!(value.into_inner(), Expression::Literal(Literal::Integer(1))); + + let value = macros::test::valid_node!("+1", task_runtime_mapping_value, Value); + assert_eq!( + value.into_inner(), + Expression::UnarySigned(UnarySigned::Positive(Box::new(Expression::Literal( + Literal::Integer(1) + )))) + ); + + let value = macros::test::valid_node!("-1", task_runtime_mapping_value, Value); + assert_eq!( + value.into_inner(), + Expression::UnarySigned(UnarySigned::Negative(Box::new(Expression::Literal( + Literal::Integer(1) + )))) + ); + + let value = macros::test::valid_node!("-+--1", task_runtime_mapping_value, Value); + assert_eq!( + value.into_inner(), + Expression::UnarySigned(UnarySigned::Negative(Box::new(Expression::UnarySigned( + UnarySigned::Positive(Box::new(Expression::UnarySigned(UnarySigned::Negative( + Box::new(Expression::UnarySigned(UnarySigned::Negative(Box::new( + Expression::Literal(Literal::Integer(1)) + )))) + )))) + )))) + ) + } + + #[test] + fn it_correctly_parses_floats() { + let value = macros::test::valid_node!("1.0", task_runtime_mapping_value, Value); + assert_eq!( + value.into_inner(), + Expression::Literal(Literal::Float(OrderedFloat(1.0))) + ); + + let value = macros::test::valid_node!("+1.0", task_runtime_mapping_value, Value); + assert_eq!( + value.into_inner(), + Expression::UnarySigned(UnarySigned::Positive(Box::new(Expression::Literal( + Literal::Float(OrderedFloat(1.0)) + )))) + ); + + let value = macros::test::valid_node!("-1.0", task_runtime_mapping_value, Value); + assert_eq!( + value.into_inner(), + Expression::UnarySigned(UnarySigned::Negative(Box::new(Expression::Literal( + Literal::Float(OrderedFloat(1.0)) + )))) + ); + + let value = macros::test::valid_node!("-+--1.5", task_runtime_mapping_value, Value); + assert_eq!( + value.into_inner(), + Expression::UnarySigned(UnarySigned::Negative(Box::new(Expression::UnarySigned( + UnarySigned::Positive(Box::new(Expression::UnarySigned(UnarySigned::Negative( + Box::new(Expression::UnarySigned(UnarySigned::Negative(Box::new( + Expression::Literal(Literal::Float(OrderedFloat(1.5))) + )))) + )))) + )))) + ) + } + + #[test] + fn it_fails_to_parse_from_an_invalid_expression() { + let parse_node = wdl_grammar::v1::parse(Rule::expression, "None") + .unwrap() + .into_inner() + .next() + .unwrap(); + + let err = Value::try_from(parse_node).unwrap_err(); + matches!(err, Error::InvalidFormat(_)); + } +} diff --git a/wdl-ast/src/v1/document/version.rs b/wdl-ast/src/v1/document/version.rs new file mode 100644 index 000000000..303a225ec --- /dev/null +++ b/wdl-ast/src/v1/document/version.rs @@ -0,0 +1,123 @@ +//! Document versions. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::macros::check_node; + +/// An error when parsing a [`Version`]. +#[derive(Debug)] +pub enum ParseError { + /// An unknown version. + UnknownVersion(String), +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseError::UnknownVersion(version) => write!(f, "unknown version: {version}"), + } + } +} + +impl std::error::Error for ParseError {} + +/// An error related to a [`Version`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// A parse error. + Parse(ParseError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Parse(err) => write!(f, "parse error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A document version. +#[derive(Clone, Eq, PartialEq)] +pub enum Version { + /// WDL v1.0 + OneDotZero, + + /// WDL v1.1 + OneDotOne, +} + +impl std::fmt::Debug for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Version::OneDotZero => write!(f, "WDL v1.0"), + Version::OneDotOne => write!(f, "WDL v1.1"), + } + } +} + +impl std::str::FromStr for Version { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "1.1" => Ok(Self::OneDotOne), + "1.0" => Ok(Self::OneDotZero), + _ => Err(Error::Parse(ParseError::UnknownVersion(s.to_string()))), + } + } +} + +impl TryFrom> for Version { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, version); + + for node in node.into_inner().flatten() { + if node.as_rule() == Rule::version_release { + return node.as_str().parse(); + } + } + + unreachable!( + "`version` node must be required by the grammar to always contain \ + a `version_release` node" + ) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::macros; + + #[test] + fn it_parses_valid_document_versions() { + macros::test::parse_document!("version 1.0").unwrap(); + macros::test::parse_document!("version 1.1").unwrap(); + } + + #[test] + fn it_fails_to_parse_an_invalid_version() { + let err = macros::test::parse_document!("version 1.2").unwrap_err(); + assert_eq!( + err.to_string(), + "parse error:\n\ndocument error: version error: parse error: unknown version: 1.2" + ); + } +} diff --git a/wdl-ast/src/v1/document/workflow.rs b/wdl-ast/src/v1/document/workflow.rs new file mode 100644 index 000000000..0fd04db3f --- /dev/null +++ b/wdl-ast/src/v1/document/workflow.rs @@ -0,0 +1,356 @@ +//! Workflows. + +use nonempty::NonEmpty; +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document; +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::workflow; +use crate::v1::macros::check_node; + +mod builder; +pub mod execution; + +pub use builder::Builder; + +/// An error related to a [`Workflow`]. +#[derive(Debug)] +pub enum Error { + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An execution statement error. + ExecutionStatement(execution::statement::Error), + + /// An identifier error. + Identifier(singular::Error), + + /// A input error. + Input(document::input::Error), + + /// A metadata error. + Metadata(document::metadata::Error), + + /// An output error. + Output(document::output::Error), + + /// A parameter metadata error. + ParameterMetadata(document::metadata::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::ExecutionStatement(err) => write!(f, "execution statement error: {err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + Error::Input(err) => write!(f, "input error: {err}"), + Error::Metadata(err) => write!(f, "metadata error: {err}"), + Error::Output(err) => write!(f, "output error: {err}"), + Error::ParameterMetadata(err) => write!(f, "parameter metadata error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A workflow. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Workflow { + /// The input. + input: Option, + + /// The metadata. + metadata: Option, + + /// The name. + name: Identifier, + + /// The output. + output: Option, + + /// The parameter metadata. + parameter_metadata: Option, + + /// The workflow execution statements. + statements: Option>, +} + +impl Workflow { + /// Gets the input from the [`Workflow`] by reference (if it exists). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::input; + /// use ast::v1::document::workflow::Builder; + /// use ast::v1::document::Declaration; + /// + /// let declaration = unbound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// let mut input = input::Builder::default() + /// .push_declaration(Declaration::Unbound(declaration)) + /// .build(); + /// let workflow = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .input(input.clone())? + /// .try_build()?; + /// + /// assert_eq!(workflow.input(), Some(&input)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn input(&self) -> Option<&document::Input> { + self.input.as_ref() + } + + /// Gets the metadata from the [`Workflow`] by reference (if it exists). + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::metadata::Value; + /// use ast::v1::document::workflow::Builder; + /// use ast::v1::document::Metadata; + /// + /// let mut map = BTreeMap::new(); + /// map.insert( + /// Identifier::try_from(String::from("foo"))?, + /// Value::String(String::from("bar")), + /// ); + /// + /// let metadata = Metadata::from(map); + /// + /// let workflow = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .metadata(metadata)? + /// .try_build()?; + /// + /// let metadata = workflow.metadata().unwrap().inner(); + /// + /// assert_eq!( + /// metadata.get("foo"), + /// Some(&Value::String(String::from("bar"))) + /// ); + /// assert_eq!(metadata.get("baz"), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn metadata(&self) -> Option<&document::Metadata> { + self.metadata.as_ref() + } + + /// Gets the name from the [`Workflow`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::workflow::Builder; + /// + /// let workflow = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .try_build()?; + /// + /// assert_eq!(workflow.name().as_str(), "hello_world"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(&self) -> &Identifier { + &self.name + } + + /// Gets the output from the [`Workflow`] by reference (if it exists). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::output; + /// use ast::v1::document::workflow::Builder; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// + /// let declaration = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::Identifier( + /// Identifier::try_from("foo").unwrap(), + /// )))? + /// .try_build()?; + /// let output = output::Builder::default() + /// .push_bound_declaration(declaration) + /// .build(); + /// let workflow = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .output(output.clone())? + /// .try_build()?; + /// + /// assert_eq!(workflow.output(), Some(&output)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn output(&self) -> Option<&document::Output> { + self.output.as_ref() + } + + /// Gets the parameter metadata from the [`Workflow`] by reference (if it + /// exists). + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::metadata::Value; + /// use ast::v1::document::workflow::Builder; + /// use ast::v1::document::Metadata; + /// + /// let mut map = BTreeMap::new(); + /// map.insert( + /// Identifier::try_from(String::from("baz")).unwrap(), + /// Value::String(String::from("quux")), + /// ); + /// + /// let parameter_metadata = Metadata::from(map); + /// + /// let workflow = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .parameter_metadata(parameter_metadata)? + /// .try_build()?; + /// + /// let parameter_metadata = workflow.parameter_metadata().unwrap().inner(); + /// + /// assert_eq!( + /// parameter_metadata.get("baz"), + /// Some(&Value::String(String::from("quux"))) + /// ); + /// assert_eq!(parameter_metadata.get("foo"), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn parameter_metadata(&self) -> Option<&document::Metadata> { + self.parameter_metadata.as_ref() + } + + /// Gets the workflow execution statements from the [`Workflow`] by + /// reference (if they exist). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::workflow::Builder; + /// use ast::v1::document::Identifier; + /// + /// let call = call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from("foo")?))? + /// .try_build()?; + /// + /// let statement = Statement::Call(call); + /// + /// let workflow = Builder::default() + /// .name(singular::Identifier::try_from("hello_world")?)? + /// .push_workflow_execution_statement(statement) + /// .try_build()?; + /// assert_eq!(workflow.statements().unwrap().len(), 1); + /// + /// let call = match workflow.statements().unwrap().into_iter().next().unwrap() { + /// Statement::Call(call) => call, + /// _ => unreachable!(), + /// }; + /// + /// assert_eq!(call.name().to_string(), "foo"); + /// assert_eq!(call.body(), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn statements(&self) -> Option<&NonEmpty> { + self.statements.as_ref() + } +} + +impl TryFrom> for Workflow { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, workflow); + let mut builder = builder::Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::workflow_name => { + let name = Identifier::try_from(node.as_str().to_owned()) + .map_err(Error::Identifier)?; + builder = builder.name(name).map_err(Error::Builder)?; + } + Rule::input => { + let input = document::Input::try_from(node).map_err(Error::Input)?; + builder = builder.input(input).map_err(Error::Builder)?; + } + Rule::workflow_execution_statement => { + let statement = workflow::execution::Statement::try_from(node) + .map_err(Error::ExecutionStatement)?; + builder = builder.push_workflow_execution_statement(statement); + } + Rule::output => { + let output = document::Output::try_from(node).map_err(Error::Output)?; + builder = builder.output(output).map_err(Error::Builder)?; + } + Rule::metadata => { + let metadata = document::Metadata::try_from(node).map_err(Error::Metadata)?; + builder = builder.metadata(metadata).map_err(Error::Builder)?; + } + Rule::parameter_metadata => { + let parameter_metadata = + document::Metadata::try_from(node).map_err(Error::ParameterMetadata)?; + builder = builder + .parameter_metadata(parameter_metadata) + .map_err(Error::Builder)?; + } + Rule::COMMENT => {} + Rule::WHITESPACE => {} + rule => unreachable!("workflow should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} diff --git a/wdl-ast/src/v1/document/workflow/builder.rs b/wdl-ast/src/v1/document/workflow/builder.rs new file mode 100644 index 000000000..fa026628d --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/builder.rs @@ -0,0 +1,511 @@ +//! Builder for a [`Workflow`]. + +use nonempty::NonEmpty; + +use crate::v1::document; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::workflow::execution; +use crate::v1::document::Workflow; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A name was not provided to the [`Builder`]. + Name, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Name => write!(f, "name"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error that occurs when a multiple values were provded for a field that +/// only accepts a single value. +#[derive(Debug)] +pub enum MultipleError { + /// Attempted to set multiple values for the input field within the + /// [`Builder`]. + Input, + + /// Attempted to set multiple values for the metadata field within the + /// [`Builder`]. + Metadata, + + /// Attempted to set multiple values for the name field within the + /// [`Builder`]. + Name, + + /// Attempted to set multiple values for the output field within the + /// [`Builder`]. + Output, + + /// Attempted to set multiple values for the parameter metadata field within + /// the [`Builder`]. + ParameterMetadata, +} + +impl std::fmt::Display for MultipleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MultipleError::Input => write!(f, "input"), + MultipleError::Metadata => write!(f, "metadata"), + MultipleError::Name => write!(f, "name"), + MultipleError::Output => write!(f, "output"), + MultipleError::ParameterMetadata => write!(f, "parameter metadata"), + } + } +} + +impl std::error::Error for MultipleError {} + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), + + /// Multiple values were provided for a field that accepts a single value. + Multiple(MultipleError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + Error::Multiple(err) => { + write!(f, "multiple values provided for single value field: {err}") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for a [`Workflow`]. +#[derive(Debug, Default)] +pub struct Builder { + /// The input. + input: Option, + + /// The metadata. + metadata: Option, + + /// The name. + name: Option, + + /// The output. + output: Option, + + /// The parameter metadata. + parameter_metadata: Option, + + /// The workflow execution statements. + statements: Option>, +} + +impl Builder { + /// Sets the name for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::workflow::Builder; + /// + /// let workflow = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .try_build()?; + /// + /// assert_eq!(workflow.name().as_str(), "hello_world"); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(mut self, name: Identifier) -> Result { + if self.name.is_some() { + return Err(Error::Multiple(MultipleError::Name)); + } + + self.name = Some(name); + Ok(self) + } + + /// Sets the input for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::input; + /// use ast::v1::document::workflow::Builder; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Input; + /// + /// let declaration = unbound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// let mut input = input::Builder::default() + /// .push_declaration(Declaration::Unbound(declaration)) + /// .build(); + /// let workflow = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .input(input.clone())? + /// .try_build()?; + /// + /// assert_eq!(workflow.input(), Some(&input)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn input(mut self, input: document::Input) -> Result { + if self.input.is_some() { + return Err(Error::Multiple(MultipleError::Input)); + } + + self.input = Some(input); + Ok(self) + } + + /// Sets the output for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::output; + /// use ast::v1::document::workflow::Builder; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// use ast::v1::document::Output; + /// + /// let declaration = bound::Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::Identifier( + /// Identifier::try_from("foo").unwrap(), + /// )))? + /// .try_build()?; + /// let output = output::Builder::default() + /// .push_bound_declaration(declaration) + /// .build(); + /// let workflow = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .output(output.clone())? + /// .try_build()?; + /// + /// assert_eq!(workflow.output(), Some(&output)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn output(mut self, output: document::Output) -> Result { + if self.output.is_some() { + return Err(Error::Multiple(MultipleError::Output)); + } + + self.output = Some(output); + Ok(self) + } + + /// Pushes a workflow execution statement into the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::workflow::Builder; + /// use ast::v1::document::Identifier; + /// + /// let call = call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from("foo")?))? + /// .try_build()?; + /// + /// let statement = Statement::Call(call); + /// + /// let workflow = Builder::default() + /// .name(singular::Identifier::try_from("hello_world")?)? + /// .push_workflow_execution_statement(statement) + /// .try_build()?; + /// assert_eq!(workflow.statements().unwrap().len(), 1); + /// + /// let call = match workflow.statements().unwrap().into_iter().next().unwrap() { + /// Statement::Call(call) => call, + /// _ => unreachable!(), + /// }; + /// + /// assert_eq!(call.name().to_string(), "foo"); + /// assert_eq!(call.body(), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn push_workflow_execution_statement(mut self, statement: execution::Statement) -> Self { + let statements = match self.statements { + Some(mut statements) => { + statements.push(statement); + statements + } + None => NonEmpty::new(statement), + }; + + self.statements = Some(statements); + self + } + + /// Sets the metadata for the [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::metadata::Value; + /// use ast::v1::document::workflow::Builder; + /// use ast::v1::document::Metadata; + /// + /// let mut map = BTreeMap::new(); + /// map.insert( + /// Identifier::try_from(String::from("foo"))?, + /// Value::String(String::from("bar")), + /// ); + /// + /// let metadata = Metadata::from(map); + /// + /// let workflow = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .metadata(metadata)? + /// .try_build()?; + /// + /// let metadata = workflow.metadata().unwrap().inner(); + /// + /// assert_eq!( + /// metadata.get("foo"), + /// Some(&Value::String(String::from("bar"))) + /// ); + /// assert_eq!(metadata.get("baz"), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn metadata(mut self, metadata: document::Metadata) -> Result { + if self.metadata.is_some() { + return Err(Error::Multiple(MultipleError::Metadata)); + } + + self.metadata = Some(metadata); + Ok(self) + } + + /// Pushes a parameter metadata into the [`Builder`]. + /// + /// **Note:** although the convention is to only ever include _one_ + /// `parameter_meta` section, technically the specification for WDL v1.x + /// allows for multiple `parameter_meta` blocks. + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular::Identifier; + /// use ast::v1::document::metadata::Value; + /// use ast::v1::document::workflow::Builder; + /// use ast::v1::document::Metadata; + /// + /// let mut map = BTreeMap::new(); + /// map.insert( + /// Identifier::try_from(String::from("baz"))?, + /// Value::String(String::from("quux")), + /// ); + /// + /// let parameter_metadata = Metadata::from(map); + /// + /// let workflow = Builder::default() + /// .name(Identifier::try_from("hello_world")?)? + /// .parameter_metadata(parameter_metadata)? + /// .try_build()?; + /// + /// let parameter_metadata = workflow.parameter_metadata().unwrap().inner(); + /// + /// assert_eq!( + /// parameter_metadata.get("baz"), + /// Some(&Value::String(String::from("quux"))) + /// ); + /// assert_eq!(parameter_metadata.get("foo"), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn parameter_metadata(mut self, metadata: document::Metadata) -> Result { + if self.parameter_metadata.is_some() { + return Err(Error::Multiple(MultipleError::ParameterMetadata)); + } + + self.parameter_metadata = Some(metadata); + Ok(self) + } + + /// Consumes `self` to attempt to build a [`Workflow`]. + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use nonempty::NonEmpty; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::declaration::bound; + /// use ast::v1::document::declaration::r#type::Kind; + /// use ast::v1::document::declaration::unbound; + /// use ast::v1::document::declaration::Type; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::input; + /// use ast::v1::document::metadata; + /// use ast::v1::document::metadata::Value; + /// use ast::v1::document::output; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::workflow::Builder; + /// use ast::v1::document::Declaration; + /// use ast::v1::document::Expression; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::Input; + /// use ast::v1::document::Metadata; + /// use ast::v1::document::Output; + /// + /// // Creating the input. + /// let declaration = unbound::Builder::default() + /// .name(singular::Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .try_build()?; + /// let mut input = input::Builder::default() + /// .push_declaration(Declaration::Unbound(declaration)) + /// .build(); + /// + /// // Creating the output. + /// let declaration = bound::Builder::default() + /// .name(singular::Identifier::try_from("hello_world")?)? + /// .r#type(Type::new(Kind::Boolean, false))? + /// .value(Expression::Literal(Literal::Identifier( + /// singular::Identifier::try_from("foo").unwrap(), + /// )))? + /// .try_build()?; + /// let output = output::Builder::default() + /// .push_bound_declaration(declaration) + /// .build(); + /// + /// // Creating the workflow execution statement. + /// let call = call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from("foo")?))? + /// .try_build()?; + /// + /// let statement = Statement::Call(call); + /// + /// // Creating the metadata. + /// let mut metadata_map = BTreeMap::new(); + /// metadata_map.insert( + /// singular::Identifier::try_from(String::from("foo"))?, + /// Value::String(String::from("bar")), + /// ); + /// let metadata = Metadata::from(metadata_map); + /// + /// // Creating the parameter metadata. + /// let mut parameter_metadata_map = BTreeMap::new(); + /// parameter_metadata_map.insert( + /// singular::Identifier::try_from(String::from("baz"))?, + /// Value::String(String::from("quux")), + /// ); + /// let parameter_metadata = Metadata::from(parameter_metadata_map); + /// + /// // Building the workflow. + /// let workflow = Builder::default() + /// .name(singular::Identifier::try_from("hello_world")?)? + /// .input(input.clone())? + /// .output(output.clone())? + /// .push_workflow_execution_statement(statement) + /// .metadata(metadata)? + /// .parameter_metadata(parameter_metadata)? + /// .try_build()?; + /// + /// // Check workflow name. + /// assert_eq!(workflow.name().as_str(), "hello_world"); + /// + /// // Check workflow input. + /// assert_eq!(workflow.input(), Some(&input)); + /// + /// // Check workflow output. + /// assert_eq!(workflow.output(), Some(&output)); + /// + /// // Check workflow execution statements. + /// let call = match workflow.statements().unwrap().into_iter().next().unwrap() { + /// Statement::Call(call) => call, + /// _ => unreachable!(), + /// }; + /// + /// assert_eq!(call.name().to_string(), "foo"); + /// assert_eq!(call.body(), None); + /// + /// // Check workflow metadata. + /// let metadata = workflow.metadata().unwrap().inner(); + /// + /// assert_eq!( + /// metadata.get("foo"), + /// Some(&Value::String(String::from("bar"))) + /// ); + /// assert_eq!(metadata.get("baz"), None); + /// + /// // Check workflow parameter metadata. + /// let parameter_metadata = workflow.parameter_metadata().unwrap().inner(); + /// + /// assert_eq!( + /// parameter_metadata.get("baz"), + /// Some(&Value::String(String::from("quux"))) + /// ); + /// assert_eq!(parameter_metadata.get("foo"), None); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_build(self) -> Result { + let name = self + .name + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Name)))?; + + Ok(Workflow { + input: self.input, + metadata: self.metadata, + name, + output: self.output, + parameter_metadata: self.parameter_metadata, + statements: self.statements, + }) + } +} diff --git a/wdl-ast/src/v1/document/workflow/execution.rs b/wdl-ast/src/v1/document/workflow/execution.rs new file mode 100644 index 000000000..e624d3a67 --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/execution.rs @@ -0,0 +1,5 @@ +//! Execution. + +pub mod statement; + +pub use statement::Statement; diff --git a/wdl-ast/src/v1/document/workflow/execution/statement.rs b/wdl-ast/src/v1/document/workflow/execution/statement.rs new file mode 100644 index 000000000..5016477f9 --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/execution/statement.rs @@ -0,0 +1,122 @@ +//! Statements. + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::private_declarations; +use crate::v1::document::private_declarations::PrivateDeclarations; +use crate::v1::macros::check_node; + +pub mod call; +pub mod conditional; +pub mod scatter; + +pub use call::Call; +pub use conditional::Conditional; +pub use scatter::Scatter; + +/// An error related to a [`Statement`]. +#[derive(Debug)] +pub enum Error { + /// A workflow call error. + Call(call::Error), + + /// A common error. + Common(crate::v1::Error), + + /// A conditional error. + Conditional(Box), + + /// A workflow execution statement had no children when one was expected. + MissingChildren, + + /// A workflow execution statement has more than one child node when only + /// one is expected. + MultipleChildren, + + /// A private declarations error. + PrivateDeclarations(private_declarations::Error), + + /// A scatter error. + Scatter(Box), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Call(err) => write!(f, "call error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Conditional(err) => write!(f, "conditional error: {err}"), + Error::MissingChildren => { + write!(f, "no children found for workflow execution statement") + } + Error::MultipleChildren => write!( + f, + "multiple children found for workflow execution statement" + ), + Error::PrivateDeclarations(err) => write!(f, "private declarations error: {err}"), + Error::Scatter(err) => write!(f, "scatter error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A workflow execution statement. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Statement { + /// A conditional statement. + Conditional(Conditional), + + /// A scatter statement. + Scatter(Scatter), + + /// A function call statement. + Call(Call), + + /// A set of private declarations. + PrivateDeclarations(PrivateDeclarations), +} + +impl TryFrom> for Statement { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, workflow_execution_statement); + let mut children = node.into_inner(); + + let node = match children.len() { + 0 => return Err(Error::MissingChildren), + // SAFETY: we ensure by checking the length that this will always + // unwrap. + 1 => children.next().unwrap(), + _n => return Err(Error::MultipleChildren), + }; + + match node.as_rule() { + Rule::workflow_conditional => { + let conditional = + Conditional::try_from(node).map_err(|err| Error::Conditional(Box::new(err)))?; + Ok(Statement::Conditional(conditional)) + } + Rule::workflow_scatter => { + let scatter = + Scatter::try_from(node).map_err(|err| Error::Scatter(Box::new(err)))?; + Ok(Statement::Scatter(scatter)) + } + Rule::workflow_call => { + let call = Call::try_from(node).map_err(Error::Call)?; + Ok(Statement::Call(call)) + } + Rule::private_declarations => { + let declarations = + PrivateDeclarations::try_from(node).map_err(Error::PrivateDeclarations)?; + Ok(Statement::PrivateDeclarations(declarations)) + } + rule => unreachable!("workflow execution statement should not contain {:?}", rule), + } + } +} diff --git a/wdl-ast/src/v1/document/workflow/execution/statement/call.rs b/wdl-ast/src/v1/document/workflow/execution/statement/call.rs new file mode 100644 index 000000000..636569afc --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/execution/statement/call.rs @@ -0,0 +1,277 @@ +//! Calls. + +use nonempty::NonEmpty; +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::identifier; +use crate::v1::document::identifier::singular; +use crate::v1::document::Identifier; +use crate::v1::macros::check_node; +use crate::v1::macros::dive_one; +use crate::v1::macros::unwrap_one; + +pub mod body; +mod builder; + +pub use body::Body; +pub use builder::Builder; + +/// An error rleated to a [`Call`]. +#[derive(Debug)] +pub enum Error { + /// A body error. + Body(body::Error), + + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An identifier error. + Identifier(identifier::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Body(err) => write!(f, "body error: {err}"), + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A call statement. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Call { + /// The after clauses. + afters: Option>, + + /// The body. + body: Option, + + /// The name. + name: Identifier, + + /// The as clause. + r#as: Option, +} + +impl Call { + /// Gets the after clauses from the [`Call`] by reference (if the exist). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::workflow::execution::statement::call::Builder; + /// + /// let name = Identifier::from(singular::Identifier::try_from("foo")?); + /// let after = singular::Identifier::try_from("baz")?; + /// let call = Builder::default() + /// .name(name)? + /// .push_after(after.clone()) + /// .try_build()?; + /// + /// assert_eq!(call.afters().unwrap().len(), 1); + /// assert_eq!(call.afters().unwrap().iter().next().unwrap(), &after); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn afters(&self) -> Option<&NonEmpty> { + self.afters.as_ref() + } + + /// Gets the body for this [`Call`] by reference (if it exists). + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::workflow::execution::statement::call::body::Value; + /// use ast::v1::document::workflow::execution::statement::call::Body; + /// use ast::v1::document::workflow::execution::statement::call::Builder; + /// + /// let mut map = BTreeMap::new(); + /// map.insert(singular::Identifier::try_from("a")?, Value::ImplicitBinding); + /// + /// let name = Identifier::from(singular::Identifier::try_from("foo")?); + /// let body = Body::from(map); + /// + /// let call = Builder::default() + /// .name(name)? + /// .body(body.clone())? + /// .try_build()?; + /// + /// assert_eq!(call.body(), Some(&body)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn body(&self) -> Option<&Body> { + self.body.as_ref() + } + + /// Gets the name for this [`Call`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::workflow::execution::statement::call::Builder; + /// + /// let name = Identifier::from(singular::Identifier::try_from("foo")?); + /// let call = Builder::default().name(name.clone())?.try_build()?; + /// + /// assert_eq!(call.name(), &name); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(&self) -> &Identifier { + &self.name + } + + /// Gets the as clause into this [`Call`] by reference (if it exists). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::workflow::execution::statement::call::Builder; + /// + /// let name = Identifier::from(singular::Identifier::try_from("foo")?); + /// let r#as = singular::Identifier::try_from("bar")?; + /// let call = Builder::default() + /// .name(name)? + /// .r#as(r#as.clone())? + /// .try_build()?; + /// + /// assert_eq!(call.r#as().unwrap(), &r#as); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn r#as(&self) -> Option<&singular::Identifier> { + self.r#as.as_ref() + } +} + +impl TryFrom> for Call { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, workflow_call); + let mut builder = Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::workflow_call_name => { + let inner = unwrap_one!(node, workflow_call_name)?; + let name = Identifier::try_from(inner).map_err(Error::Identifier)?; + builder = builder.name(name).map_err(Error::Builder)?; + } + Rule::workflow_call_body => { + let body = Body::try_from(node).map_err(Error::Body)?; + builder = builder.body(body).map_err(Error::Builder)?; + } + Rule::workflow_call_as => { + let identifier_node = + dive_one!(node, singular_identifier, workflow_call_as, Error::Common)?; + let identifier = singular::Identifier::try_from(identifier_node) + .map_err(|err| Error::Identifier(identifier::Error::Singular(err)))?; + builder = builder.r#as(identifier).map_err(Error::Builder)?; + } + Rule::workflow_call_after => { + let identifier_node = dive_one!( + node, + singular_identifier, + workflow_call_after, + Error::Common + )?; + let identifier = singular::Identifier::try_from(identifier_node) + .map_err(|err| Error::Identifier(identifier::Error::Singular(err)))?; + builder = builder.push_after(identifier); + } + Rule::WHITESPACE => {} + Rule::COMMENT => {} + rule => unreachable!("workflow call should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::document::expression::Literal; + use crate::v1::document::workflow::execution::statement::call::body::Value; + use crate::v1::document::Expression; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + let call = valid_node!(r#"call foo {input: a, b=true, c=baz}"#, workflow_call, Call); + + assert_eq!(call.name().as_singular().unwrap().as_str(), "foo"); + + let body = call.body.unwrap(); + assert_eq!(body.get("a"), Some(&Value::ImplicitBinding)); + assert_eq!( + body.get("b"), + Some(&Value::Expression(Expression::Literal(Literal::Boolean( + true + )))) + ); + assert_eq!( + body.get("c"), + Some(&Value::Expression(Expression::Literal( + Literal::Identifier(singular::Identifier::try_from("baz").unwrap()) + ))) + ); + + let call = valid_node!( + r#"call foo.bar {input: a, b=true, c=baz}"#, + workflow_call, + Call + ); + + assert_eq!(call.name().as_qualified().unwrap().to_string(), "foo.bar"); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + workflow_call, + Call + ); + } +} diff --git a/wdl-ast/src/v1/document/workflow/execution/statement/call/body.rs b/wdl-ast/src/v1/document/workflow/execution/statement/call/body.rs new file mode 100644 index 000000000..8691faae1 --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/execution/statement/call/body.rs @@ -0,0 +1,155 @@ +//! Call bodies. + +use std::collections::BTreeMap; + +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::expression; +use crate::v1::document::Expression; +use crate::v1::document::identifier::singular; +use crate::v1::macros::check_node; + +mod value; + +pub use value::Value; + +/// An error related to a [`Body`]. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(crate::v1::Error), + + /// An expression error. + Expression(expression::Error), + + /// An identifier error. + Identifier(singular::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::Expression(err) => write!(f, "expression error: {err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A body for a [`Call`](super::Call). +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Body(BTreeMap); + +impl std::ops::Deref for Body { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for Body { + fn from(body: BTreeMap) -> Self { + Self(body) + } +} + +impl TryFrom> for Body { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, workflow_call_body); + + let mut body = BTreeMap::new(); + + let nodes = node + .into_inner() + .filter(|node| matches!(node.as_rule(), Rule::workflow_call_input)) + .collect::>(); + + for node in nodes { + let inner = node + .into_inner() + .filter(|node| { + !matches!(node.as_rule(), Rule::WHITESPACE) + && !matches!(node.as_rule(), Rule::COMMENT) + }) + .collect::>(); + + if inner.len() != 1 && inner.len() != 2 { + unreachable!( + "invalid number of nodes for workflow call input: {}", + inner.len() + ); + } + + let mut nodes = inner.into_iter(); + + // SAFETY: we just checked above that at least one node exists. + let identifier_node = nodes.next().unwrap(); + let identifier = + singular::Identifier::try_from(identifier_node).map_err(Error::Identifier)?; + + let value = match nodes.next() { + Some(node) => match node.as_rule() { + Rule::expression => { + Value::Expression(Expression::try_from(node).map_err(Error::Expression)?) + } + rule => unreachable!("workflow call input value should not contain {:?}", rule), + }, + None => Value::ImplicitBinding, + }; + + body.insert(identifier, value); + } + + Ok(Body(body)) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::document::expression::Literal; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + let body = valid_node!(r#"{input: x, y=true, z=beta}"#, workflow_call_body, Body); + assert_eq!(body.get("x"), Some(&Value::ImplicitBinding)); + assert_eq!( + body.get("y"), + Some(&Value::Expression(Expression::Literal(Literal::Boolean( + true + )))) + ); + assert_eq!( + body.get("z"), + Some(&Value::Expression(Expression::Literal( + Literal::Identifier(singular::Identifier::try_from("beta").unwrap()) + ))) + ); + assert_eq!(body.get("q"), None); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + workflow_call_body, + Body + ); + } +} diff --git a/wdl-ast/src/v1/document/workflow/execution/statement/call/body/value.rs b/wdl-ast/src/v1/document/workflow/execution/statement/call/body/value.rs new file mode 100644 index 000000000..6f824bbca --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/execution/statement/call/body/value.rs @@ -0,0 +1,16 @@ +//! Values within a call body. + +use crate::v1::document::Expression; + +/// A value within a call [`Body`](super::Body). +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Value { + /// An expression. + Expression(Expression), + + /// An implicit binding. + /// + /// In an implicit binding, the value is inferred to be the same identifier + /// as the key to which the value belongs in the [`Body`](super::Body). + ImplicitBinding, +} diff --git a/wdl-ast/src/v1/document/workflow/execution/statement/call/builder.rs b/wdl-ast/src/v1/document/workflow/execution/statement/call/builder.rs new file mode 100644 index 000000000..73f7d14a0 --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/execution/statement/call/builder.rs @@ -0,0 +1,283 @@ +//! Builder for a [`Call`]. + +use nonempty::NonEmpty; + +use crate::v1::document::identifier::singular; +use crate::v1::document::workflow::execution::statement::call::Body; +use crate::v1::document::workflow::execution::statement::Call; +use crate::v1::document::Identifier; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A name was not provided to the [`Builder`]. + Name, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Name => write!(f, "name"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error that occurs when a multiple values were provded for a field that +/// only accepts a single value. +#[derive(Debug)] +pub enum MultipleError { + /// Attempted to set multiple values for the "as" field within the + /// [`Builder`]. + As, + + /// Attempted to set multiple values for the body field within the + /// [`Builder`]. + Body, + + /// Attempted to set multiple values for the name field within the + /// [`Builder`]. + Name, +} + +impl std::fmt::Display for MultipleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MultipleError::As => write!(f, "as"), + MultipleError::Name => write!(f, "name"), + MultipleError::Body => write!(f, "body"), + } + } +} + +impl std::error::Error for MultipleError {} + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), + + /// Multiple values were provided for a field that accepts a single value. + Multiple(MultipleError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + Error::Multiple(err) => { + write!(f, "multiple values provided for single value field: {err}") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for an [`Call`]. +#[derive(Debug, Default)] +pub struct Builder { + /// The after clauses. + after: Option>, + + /// The body. + body: Option, + + /// The name. + name: Option, + + /// The as clause. + r#as: Option, +} + +impl Builder { + /// Sets the body for this [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::workflow::execution::statement::call::body::Value; + /// use ast::v1::document::workflow::execution::statement::call::Body; + /// use ast::v1::document::workflow::execution::statement::call::Builder; + /// + /// let mut map = BTreeMap::new(); + /// map.insert(singular::Identifier::try_from("a")?, Value::ImplicitBinding); + /// + /// let name = Identifier::from(singular::Identifier::try_from("foo")?); + /// let body = Body::from(map); + /// + /// let call = Builder::default() + /// .name(name)? + /// .body(body.clone())? + /// .try_build()?; + /// + /// assert_eq!(call.body(), Some(&body)); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn body(mut self, body: Body) -> Result { + if self.body.is_some() { + return Err(Error::Multiple(MultipleError::Body)); + } + + self.body = Some(body); + Ok(self) + } + + /// Sets the name for this [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::workflow::execution::statement::call::Builder; + /// + /// let name = Identifier::from(singular::Identifier::try_from("foo")?); + /// let call = Builder::default().name(name.clone())?.try_build()?; + /// + /// assert_eq!(call.name(), &name); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn name(mut self, identifier: Identifier) -> Result { + if self.name.is_some() { + return Err(Error::Multiple(MultipleError::Name)); + } + + self.name = Some(identifier); + Ok(self) + } + + /// Pushes an after clause into this [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::workflow::execution::statement::call::Builder; + /// + /// let name = Identifier::from(singular::Identifier::try_from("foo")?); + /// let after = singular::Identifier::try_from("baz")?; + /// let call = Builder::default() + /// .name(name)? + /// .push_after(after.clone()) + /// .try_build()?; + /// + /// assert_eq!(call.afters().unwrap().len(), 1); + /// assert_eq!(call.afters().unwrap().iter().next().unwrap(), &after); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn push_after(mut self, identifier: singular::Identifier) -> Self { + let after = match self.after { + Some(mut after) => { + after.push(identifier); + after + } + None => NonEmpty::new(identifier), + }; + + self.after = Some(after); + self + } + + /// Sets the as clause into this [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::workflow::execution::statement::call::Builder; + /// + /// let name = Identifier::from(singular::Identifier::try_from("foo")?); + /// let r#as = singular::Identifier::try_from("bar")?; + /// let call = Builder::default() + /// .name(name)? + /// .r#as(r#as.clone())? + /// .try_build()?; + /// + /// assert_eq!(call.r#as().unwrap(), &r#as); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn r#as(mut self, identifier: singular::Identifier) -> Result { + if self.r#as.is_some() { + return Err(Error::Multiple(MultipleError::As)); + } + + self.r#as = Some(identifier); + Ok(self) + } + + /// Consumes `self` to attempt to build a [`Call`]. + /// + /// # Examples + /// + /// ``` + /// use std::collections::BTreeMap; + /// + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::workflow::execution::statement::call::body::Value; + /// use ast::v1::document::workflow::execution::statement::call::Body; + /// use ast::v1::document::workflow::execution::statement::call::Builder; + /// + /// let mut map = BTreeMap::new(); + /// map.insert(singular::Identifier::try_from("a")?, Value::ImplicitBinding); + /// + /// let name = Identifier::from(singular::Identifier::try_from("foo")?); + /// let body = Body::from(map); + /// let r#as = singular::Identifier::try_from("bar")?; + /// let after = singular::Identifier::try_from("baz")?; + /// + /// let call = Builder::default() + /// .name(name.clone())? + /// .body(body.clone())? + /// .r#as(r#as.clone())? + /// .push_after(after.clone()) + /// .try_build()?; + /// + /// assert_eq!(call.name(), &name); + /// assert_eq!(call.body(), Some(&body)); + /// assert_eq!(call.r#as().unwrap(), &r#as); + /// assert_eq!(call.afters().unwrap().len(), 1); + /// assert_eq!(call.afters().unwrap().iter().next().unwrap(), &after); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_build(self) -> Result { + let name = self + .name + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Name)))?; + + Ok(Call { + afters: self.after, + body: self.body, + name, + r#as: self.r#as, + }) + } +} diff --git a/wdl-ast/src/v1/document/workflow/execution/statement/call/name.rs b/wdl-ast/src/v1/document/workflow/execution/statement/call/name.rs new file mode 100644 index 000000000..b7f3f3eef --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/execution/statement/call/name.rs @@ -0,0 +1 @@ +pub struct Name(Identifier); \ No newline at end of file diff --git a/wdl-ast/src/v1/document/workflow/execution/statement/conditional.rs b/wdl-ast/src/v1/document/workflow/execution/statement/conditional.rs new file mode 100644 index 000000000..553da1447 --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/execution/statement/conditional.rs @@ -0,0 +1,211 @@ +//! Conditionals. + +use nonempty::NonEmpty; + +use pest::iterators::Pair; +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::expression; +use crate::v1::document::workflow::execution::statement; +use crate::v1::document::workflow::execution::Statement; +use crate::v1::document::Expression; +use crate::v1::macros::check_node; +use crate::v1::macros::unwrap_one; + +mod builder; + +pub use builder::Builder; + +/// An error rleated to a [`Conditional`]. +#[derive(Debug)] +pub enum Error { + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An expression error. + Expression(expression::Error), + + /// A workflow execution statement error. + Statement(statement::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Expression(err) => write!(f, "expression error: {err}"), + Error::Statement(err) => write!(f, "workflow execution statement error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A conditional statement. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Conditional { + /// The condition clause. + condition: Expression, + + /// The workflow execution statements. + statements: Option>>, +} + +impl Conditional { + /// Gets the condition clause from this [`Conditional`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::conditional::Builder; + /// use ast::v1::document::Expression; + /// + /// let condition = Expression::Literal(Literal::Boolean(true)); + /// let conditional = Builder::default() + /// .condition(condition.clone())? + /// .try_build()?; + /// + /// assert_eq!(conditional.condition(), &condition); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn condition(&self) -> &Expression { + &self.condition + } + + /// Gets the [workflow execution statement(s)](Statement) from this + /// [Conditional] by reference (if they exist). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::statement::conditional::Builder; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::Expression; + /// + /// let statement = Statement::Call( + /// call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from( + /// "hello_world", + /// )?))? + /// .try_build()?, + /// ); + /// + /// let condition = Expression::Literal(Literal::Boolean(true)); + /// + /// let conditional = Builder::default() + /// .condition(condition)? + /// .push_workflow_execution_statement(statement.clone()) + /// .try_build()?; + /// + /// assert_eq!(conditional.statements().unwrap().len(), 1); + /// assert_eq!( + /// conditional + /// .statements() + /// .unwrap() + /// .iter() + /// .next() + /// .unwrap() + /// .as_ref(), + /// &statement + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn statements(&self) -> Option<&NonEmpty>> { + self.statements.as_ref() + } +} + +impl TryFrom> for Conditional { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, workflow_conditional); + let mut builder = Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::workflow_conditional_condition => { + let condition_node = unwrap_one!(node, workflow_conditional_condition)?; + let condition = + Expression::try_from(condition_node).map_err(Error::Expression)?; + builder = builder.condition(condition).map_err(Error::Builder)?; + } + Rule::workflow_execution_statement => { + let statement = Statement::try_from(node).map_err(Error::Statement)?; + builder = builder.push_workflow_execution_statement(statement); + } + Rule::WHITESPACE => {} + Rule::COMMENT => {} + rule => unreachable!("workflow call should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::document::expression::Literal; + use crate::v1::document::Expression; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + let conditional = valid_node!( + r#"if (true) { + call foo + }"#, + workflow_conditional, + Conditional + ); + + assert_eq!( + conditional.condition(), + &Expression::Literal(Literal::Boolean(true)) + ); + + let statements = conditional.statements().unwrap(); + assert_eq!(statements.len(), 1); + + let first_call = match statements.iter().next().unwrap().as_ref() { + Statement::Call(call) => call, + _ => unreachable!(), + }; + assert_eq!(first_call.name().to_string(), "foo"); + assert_eq!(first_call.body(), None); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + workflow_conditional, + Conditional + ); + } +} diff --git a/wdl-ast/src/v1/document/workflow/execution/statement/conditional/builder.rs b/wdl-ast/src/v1/document/workflow/execution/statement/conditional/builder.rs new file mode 100644 index 000000000..27828b098 --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/execution/statement/conditional/builder.rs @@ -0,0 +1,225 @@ +//! Builder for a [`Conditional`]. + +use nonempty::NonEmpty; + +use crate::v1::document::workflow::execution::statement::Conditional; +use crate::v1::document::workflow::execution::Statement; +use crate::v1::document::Expression; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A condition clause was not provided to the [`Builder`]. + Condition, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Condition => write!(f, "condition"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error that occurs when a multiple values were provded for a field that +/// only accepts a single value. +#[derive(Debug)] +pub enum MultipleError { + /// Attempted to set multiple values for the condition clause field within + /// the [`Builder`]. + Condition, +} + +impl std::fmt::Display for MultipleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MultipleError::Condition => write!(f, "condition"), + } + } +} + +impl std::error::Error for MultipleError {} + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), + + /// Multiple values were provided for a field that accepts a single value. + Multiple(MultipleError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + Error::Multiple(err) => { + write!(f, "multiple values provided for single value field: {err}") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for a [`Conditional`]. +#[derive(Debug, Default)] +pub struct Builder { + /// The condition clause. + condition: Option, + + /// The workflow execution statements. + statements: Option>>, +} + +impl Builder { + /// Sets the condition clause for this [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::conditional::Builder; + /// use ast::v1::document::Expression; + /// + /// let condition = Expression::Literal(Literal::Boolean(true)); + /// let conditional = Builder::default() + /// .condition(condition.clone())? + /// .try_build()?; + /// + /// assert_eq!(conditional.condition(), &condition); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn condition(mut self, condition: Expression) -> Result { + if self.condition.is_some() { + return Err(Error::Multiple(MultipleError::Condition)); + } + + self.condition = Some(condition); + Ok(self) + } + + /// Pushes a [workflow execution statement](Statement) into this + /// [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::statement::conditional::Builder; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::Expression; + /// + /// let statement = Statement::Call( + /// call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from( + /// "hello_world", + /// )?))? + /// .try_build()?, + /// ); + /// + /// let condition = Expression::Literal(Literal::Boolean(true)); + /// + /// let conditional = Builder::default() + /// .condition(condition)? + /// .push_workflow_execution_statement(statement.clone()) + /// .try_build()?; + /// + /// assert_eq!(conditional.statements().unwrap().len(), 1); + /// assert_eq!( + /// conditional + /// .statements() + /// .unwrap() + /// .iter() + /// .next() + /// .unwrap() + /// .as_ref(), + /// &statement + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn push_workflow_execution_statement(mut self, statement: Statement) -> Self { + let statements = match self.statements { + Some(mut statements) => { + statements.push(Box::new(statement)); + statements + } + None => NonEmpty::new(Box::new(statement)), + }; + + self.statements = Some(statements); + self + } + + /// Consumes `self` to attempt to build a [`Conditional`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::statement::conditional::Builder; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::Expression; + /// + /// let statement = Statement::Call( + /// call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from( + /// "hello_world", + /// )?))? + /// .try_build()?, + /// ); + /// + /// let condition = Expression::Literal(Literal::Boolean(true)); + /// + /// let conditional = Builder::default() + /// .condition(condition.clone())? + /// .push_workflow_execution_statement(statement.clone()) + /// .try_build()?; + /// + /// assert_eq!(conditional.condition(), &condition); + /// assert_eq!(conditional.statements().unwrap().len(), 1); + /// assert_eq!( + /// conditional + /// .statements() + /// .unwrap() + /// .iter() + /// .next() + /// .unwrap() + /// .as_ref(), + /// &statement + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_build(self) -> Result { + let condition = self + .condition + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Condition)))?; + + Ok(Conditional { + condition, + statements: self.statements, + }) + } +} diff --git a/wdl-ast/src/v1/document/workflow/execution/statement/scatter.rs b/wdl-ast/src/v1/document/workflow/execution/statement/scatter.rs new file mode 100644 index 000000000..0cd1a942e --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/execution/statement/scatter.rs @@ -0,0 +1,341 @@ +//! Scatters. + +use nonempty::NonEmpty; +use pest::iterators::Pair; + +use wdl_grammar as grammar; + +use grammar::v1::Rule; + +use crate::v1::document::identifier::singular; +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::expression; +use crate::v1::document::workflow::execution::statement; +use crate::v1::document::workflow::execution::Statement; +use crate::v1::document::Expression; +use crate::v1::macros::check_node; +use crate::v1::macros::dive_one; +use crate::v1::macros::unwrap_one; + +mod builder; + +pub use builder::Builder; + +/// An error related to [`Scatter`]. +#[derive(Debug)] +pub enum Error { + /// A builder error. + Builder(builder::Error), + + /// A common error. + Common(crate::v1::Error), + + /// An expression error. + Expression(expression::Error), + + /// An identifier error. + Identifier(singular::Error), + + /// A workflow execution statement error. + Statement(statement::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Builder(err) => write!(f, "builder error: {err}"), + Error::Common(err) => write!(f, "{err}"), + Error::Expression(err) => write!(f, "expression error: {err}"), + Error::Identifier(err) => write!(f, "identifier error: {err}"), + Error::Statement(err) => write!(f, "workflow execution statement error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A scatter statement. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Scatter { + /// The entities being scattered over. + iterable: Expression, + + /// The workflow execution statements for each entity. + statements: Option>>, + + /// The variable name for each entity. + variable: Identifier, +} + +impl Scatter { + /// Gets the iterables from the [`Scatter`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::statement::scatter::Builder; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::Expression; + /// + /// let variable = singular::Identifier::try_from("entity")?; + /// let iterable = Expression::Literal(Literal::Identifier(singular::Identifier::try_from( + /// "entities", + /// )?)); + /// let statement = Statement::Call( + /// call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from( + /// "hello_world", + /// )?))? + /// .try_build()?, + /// ); + /// + /// let scatter = Builder::default() + /// .variable(variable)? + /// .iterable(iterable.clone())? + /// .push_workflow_execution_statement(statement) + /// .try_build()?; + /// + /// assert_eq!(scatter.iterable(), &iterable); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn iterable(&self) -> &Expression { + &self.iterable + } + + /// Gets the [workflow execution statement(s)](Statement) from this + /// [`Scatter`] by reference (if they exist). + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::statement::scatter::Builder; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::Expression; + /// + /// let variable = singular::Identifier::try_from("entity")?; + /// let iterable = Expression::Literal(Literal::Identifier(singular::Identifier::try_from( + /// "entities", + /// )?)); + /// let statement = Statement::Call( + /// call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from( + /// "hello_world", + /// )?))? + /// .try_build()?, + /// ); + /// + /// let scatter = Builder::default() + /// .variable(variable)? + /// .iterable(iterable)? + /// .push_workflow_execution_statement(statement.clone()) + /// .try_build()?; + /// + /// assert_eq!(scatter.statements().unwrap().len(), 1); + /// assert_eq!( + /// scatter + /// .statements() + /// .unwrap() + /// .iter() + /// .next() + /// .unwrap() + /// .as_ref(), + /// &statement + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn statements(&self) -> Option<&NonEmpty>> { + self.statements.as_ref() + } + + /// Gets the variable for this [`Scatter`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::statement::scatter::Builder; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::Expression; + /// + /// let variable = singular::Identifier::try_from("entity")?; + /// let iterable = Expression::Literal(Literal::Identifier(singular::Identifier::try_from( + /// "entities", + /// )?)); + /// let statement = Statement::Call( + /// call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from( + /// "hello_world", + /// )?))? + /// .try_build()?, + /// ); + /// + /// let scatter = Builder::default() + /// .variable(variable.clone())? + /// .iterable(iterable)? + /// .push_workflow_execution_statement(statement) + /// .try_build()?; + /// + /// assert_eq!(scatter.variable(), &variable); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn variable(&self) -> &Identifier { + &self.variable + } +} + +impl TryFrom> for Scatter { + type Error = Error; + + fn try_from(node: Pair<'_, grammar::v1::Rule>) -> Result { + check_node!(node, workflow_scatter); + let mut builder = Builder::default(); + + for node in node.into_inner() { + match node.as_rule() { + Rule::workflow_scatter_iteration_statement => { + // TODO: a clone is required here because Pest's `FlatPairs` + // type does not support creating an iterator without taking + // ownership (at the time of writing). This can be made + // better with a PR to Pest. + let variable_node = dive_one!( + node.clone(), + workflow_scatter_iteration_statement_variable, + workflow_scatter_iteration_statement, + Error::Common + )?; + let variable_node = + unwrap_one!(variable_node, workflow_scatter_iteration_statement_variable)?; + let variable = + singular::Identifier::try_from(variable_node).map_err(Error::Identifier)?; + + let iterable_node = dive_one!( + node.clone(), + workflow_scatter_iteration_statement_iterable, + workflow_scatter_iteration_statement, + Error::Common + )?; + let iterable_node = + unwrap_one!(iterable_node, workflow_scatter_iteration_statement_iterable)?; + let iterable = + Expression::try_from(iterable_node).map_err(Error::Expression)?; + + builder = builder.variable(variable).map_err(Error::Builder)?; + builder = builder.iterable(iterable).map_err(Error::Builder)?; + } + Rule::workflow_execution_statement => { + let statement = Statement::try_from(node).map_err(Error::Statement)?; + builder = builder.push_workflow_execution_statement(statement); + } + Rule::WHITESPACE => {} + Rule::COMMENT => {} + rule => unreachable!("scatter should not contain {:?}", rule), + } + } + + builder.try_build().map_err(Error::Builder) + } +} + +#[cfg(test)] +mod tests { + use crate::v1::document::expression::Literal; + use crate::v1::document::workflow::execution::statement::call::body::Value; + use crate::v1::macros::test::invalid_node; + use crate::v1::macros::test::valid_node; + + use super::*; + + #[test] + fn it_parses_from_a_supported_node_type() { + let scatter = valid_node!( + r#"scatter (file in files) { + call read { + input: file, foo=bar + } + + call external.write { + input: file, baz=false + } + }"#, + workflow_scatter, + Scatter + ); + + assert_eq!(scatter.variable().as_str(), "file"); + assert_eq!( + scatter.iterable(), + &Expression::Literal(Literal::Identifier( + singular::Identifier::try_from("files").unwrap() + )) + ); + + let statements = scatter.statements().unwrap(); + assert_eq!(statements.len(), 2); + + let mut statements = statements.into_iter(); + + let first_call = match statements.next().unwrap().as_ref() { + Statement::Call(call) => call, + _ => unreachable!(), + }; + assert_eq!(first_call.name().to_string(), "read"); + assert_eq!( + first_call.body().unwrap().get("file"), + Some(&Value::ImplicitBinding) + ); + assert_eq!( + first_call.body().unwrap().get("foo"), + Some(&Value::Expression(Expression::Literal( + Literal::Identifier(singular::Identifier::try_from("bar").unwrap()) + ))) + ); + assert_eq!(first_call.body().unwrap().get("does_not_exist"), None); + + let second_call = match statements.next().unwrap().as_ref() { + Statement::Call(call) => call, + _ => unreachable!(), + }; + assert_eq!(second_call.name().to_string(), "external.write"); + assert_eq!( + second_call.body().unwrap().get("file"), + Some(&Value::ImplicitBinding) + ); + assert_eq!( + second_call.body().unwrap().get("baz"), + Some(&Value::Expression(Expression::Literal(Literal::Boolean( + false + )))) + ); + assert_eq!(second_call.body().unwrap().get("does_not_exist"), None); + } + + #[test] + fn it_fails_to_parse_from_an_unsupported_node_type() { + invalid_node!( + "version 1.1\n\ntask hello { command <<<>>> }", + document, + workflow_scatter, + Scatter + ); + } +} diff --git a/wdl-ast/src/v1/document/workflow/execution/statement/scatter/builder.rs b/wdl-ast/src/v1/document/workflow/execution/statement/scatter/builder.rs new file mode 100644 index 000000000..999a5b95a --- /dev/null +++ b/wdl-ast/src/v1/document/workflow/execution/statement/scatter/builder.rs @@ -0,0 +1,314 @@ +//! Builder for a [`Scatter`]. + +use nonempty::NonEmpty; + +use crate::v1::document::identifier::singular::Identifier; +use crate::v1::document::workflow::execution::statement::Scatter; +use crate::v1::document::workflow::execution::Statement; +use crate::v1::document::Expression; + +/// An error that occurs when a required field is missing at build time. +#[derive(Debug)] +pub enum MissingError { + /// A iterable was not provided to the [`Builder`]. + Iterable, + + /// A variable was not provided to the [`Builder`]. + Variable, +} + +impl std::fmt::Display for MissingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MissingError::Iterable => write!(f, "iterable"), + MissingError::Variable => write!(f, "variable"), + } + } +} + +impl std::error::Error for MissingError {} + +/// An error that occurs when a multiple values were provded for a field that +/// only accepts a single value. +#[derive(Debug)] +pub enum MultipleError { + /// Attempted to set multiple values for the iterable field within the + /// [`Builder`]. + Iterable, + + /// Attempted to set multiple values for the variable field within the + /// [`Builder`]. + Variable, +} + +impl std::fmt::Display for MultipleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MultipleError::Iterable => write!(f, "iterable"), + MultipleError::Variable => write!(f, "variable"), + } + } +} + +impl std::error::Error for MultipleError {} + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), + + /// Multiple values were provided for a field that accepts a single value. + Multiple(MultipleError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + Error::Multiple(err) => { + write!(f, "multiple values provided for single value field: {err}") + } + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A builder for a [`Scatter`]. +#[derive(Debug, Default)] +pub struct Builder { + /// The entities being scattered over. + iterable: Option, + + /// The workflow execution statements. + statements: Option>>, + + /// The variable name. + variable: Option, +} + +impl Builder { + /// Sets the iterable for this [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::statement::scatter::Builder; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::Expression; + /// + /// let variable = singular::Identifier::try_from("entity")?; + /// let iterable = Expression::Literal(Literal::Identifier(singular::Identifier::try_from( + /// "entities", + /// )?)); + /// let statement = Statement::Call( + /// call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from( + /// "hello_world", + /// )?))? + /// .try_build()?, + /// ); + /// + /// let scatter = Builder::default() + /// .variable(variable)? + /// .iterable(iterable.clone())? + /// .push_workflow_execution_statement(statement) + /// .try_build()?; + /// + /// assert_eq!(scatter.iterable(), &iterable); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn iterable(mut self, iterable: Expression) -> Result { + if self.iterable.is_some() { + return Err(Error::Multiple(MultipleError::Iterable)); + } + + self.iterable = Some(iterable); + Ok(self) + } + + /// Pushes a [workflow execution statement](Statement) into this + /// [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::statement::scatter::Builder; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::Expression; + /// + /// let variable = singular::Identifier::try_from("entity")?; + /// let iterable = Expression::Literal(Literal::Identifier(singular::Identifier::try_from( + /// "entities", + /// )?)); + /// let statement = Statement::Call( + /// call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from( + /// "hello_world", + /// )?))? + /// .try_build()?, + /// ); + /// + /// let scatter = Builder::default() + /// .variable(variable)? + /// .iterable(iterable)? + /// .push_workflow_execution_statement(statement.clone()) + /// .try_build()?; + /// + /// assert_eq!(scatter.statements().unwrap().len(), 1); + /// assert_eq!( + /// scatter + /// .statements() + /// .unwrap() + /// .iter() + /// .next() + /// .unwrap() + /// .as_ref(), + /// &statement + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn push_workflow_execution_statement(mut self, statement: Statement) -> Self { + let statements = match self.statements { + Some(mut statements) => { + statements.push(Box::new(statement)); + statements + } + None => NonEmpty::new(Box::new(statement)), + }; + + self.statements = Some(statements); + self + } + + /// Sets the variable for this [`Builder`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::statement::scatter::Builder; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::Expression; + /// + /// let variable = singular::Identifier::try_from("entity")?; + /// let iterable = Expression::Literal(Literal::Identifier(singular::Identifier::try_from( + /// "entities", + /// )?)); + /// let statement = Statement::Call( + /// call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from( + /// "hello_world", + /// )?))? + /// .try_build()?, + /// ); + /// + /// let scatter = Builder::default() + /// .variable(variable.clone())? + /// .iterable(iterable)? + /// .push_workflow_execution_statement(statement) + /// .try_build()?; + /// + /// assert_eq!(scatter.variable(), &variable); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn variable(mut self, variable: Identifier) -> Result { + if self.variable.is_some() { + return Err(Error::Multiple(MultipleError::Variable)); + } + + self.variable = Some(variable); + Ok(self) + } + + /// Consumes `self` to attempt to build a [`Scatter`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_ast as ast; + /// + /// use ast::v1::document::identifier::singular; + /// use ast::v1::document::Identifier; + /// use ast::v1::document::expression::Literal; + /// use ast::v1::document::workflow::execution::statement::call; + /// use ast::v1::document::workflow::execution::statement::scatter::Builder; + /// use ast::v1::document::workflow::execution::Statement; + /// use ast::v1::document::Expression; + /// + /// let variable = singular::Identifier::try_from("entity")?; + /// let iterable = Expression::Literal(Literal::Identifier(singular::Identifier::try_from( + /// "entities", + /// )?)); + /// let statement = Statement::Call( + /// call::Builder::default() + /// .name(Identifier::from(singular::Identifier::try_from( + /// "hello_world", + /// )?))? + /// .try_build()?, + /// ); + /// + /// let scatter = Builder::default() + /// .variable(variable.clone())? + /// .iterable(iterable.clone())? + /// .push_workflow_execution_statement(statement.clone()) + /// .try_build()?; + /// + /// assert_eq!(scatter.iterable(), &iterable); + /// assert_eq!(scatter.variable(), &variable); + /// assert_eq!(scatter.statements().unwrap().len(), 1); + /// assert_eq!( + /// scatter + /// .statements() + /// .unwrap() + /// .iter() + /// .next() + /// .unwrap() + /// .as_ref(), + /// &statement + /// ); + /// + /// # Ok::<(), Box>(()) + /// ``` + pub fn try_build(self) -> Result { + let iterable = self + .iterable + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Iterable)))?; + + let variable = self + .variable + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Variable)))?; + + Ok(Scatter { + iterable, + statements: self.statements, + variable, + }) + } +} diff --git a/wdl-ast/src/v1/lint.rs b/wdl-ast/src/v1/lint.rs new file mode 100644 index 000000000..0fa10e83e --- /dev/null +++ b/wdl-ast/src/v1/lint.rs @@ -0,0 +1,18 @@ +//! Lint rules for WDL 1.x abstract syntax trees. + +use lazy_static::lazy_static; + +use wdl_core as core; + +use core::lint::Rule; + +use crate::v1; + +mod matching_parameter_meta; + +pub use matching_parameter_meta::MatchingParameterMeta; + +lazy_static! { + /// All lint rules available for WDL 1.x abstract syntax trees. + pub static ref RULES: Vec Rule<&'a v1::Document>>> = vec![Box::new(MatchingParameterMeta)]; +} diff --git a/wdl-ast/src/v1/lint/matching_parameter_meta.rs b/wdl-ast/src/v1/lint/matching_parameter_meta.rs new file mode 100644 index 000000000..1d8a9f141 --- /dev/null +++ b/wdl-ast/src/v1/lint/matching_parameter_meta.rs @@ -0,0 +1,229 @@ +//! Inputs within a task/workflow _must_ have a `parameter_meta` entry. + +use core::Version; +use std::collections::HashSet; +use std::num::NonZeroUsize; +use std::ops::Sub; + +use wdl_core as core; + +use crate::v1; +use crate::v1::document::Task; +use crate::v1::document::Workflow; +use core::lint; +use core::lint::Group; +use core::lint::Rule; +use core::Code; +use core::Location; + +/// The context within which a `matching_parameter_meta` error occurs. +enum Context<'a> { + /// A missing `parameter_meta` entry for a task. + Task(&'a Task), + + /// A missing `parameter_meta` entry for a workflow. + Workflow(&'a Workflow), +} + +impl<'a> std::fmt::Display for Context<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Context::Task(_) => write!(f, "task"), + Context::Workflow(_) => write!(f, "workflow"), + } + } +} + +/// Every input parameter within a task/workflow _must_ have a matching entry in +/// a `parameter_meta` block. The key must exactly match the name of the input. +#[derive(Debug)] +pub struct MatchingParameterMeta; + +impl<'a> MatchingParameterMeta { + /// Ensures that each input has a corresponding `parameter_meta` element. + fn missing_parameter_meta<'b>( + &self, + parameter: &str, + context: &Context<'b>, + line_no: NonZeroUsize, + col_no: NonZeroUsize, + ) -> lint::Warning + where + Self: Rule<&'a v1::Document>, + { + // SAFETY: this error is written so that it will always unwrap. + lint::warning::Builder::default() + .code(self.code()) + .level(lint::Level::Medium) + .group(lint::Group::Completeness) + .location(Location::LineCol { line_no, col_no }) + .subject(format!( + "missing parameter meta within {}: {}", + context, parameter + )) + .body(format!( + "Each input parameter within a {} should have an associated \ + `parameter_meta` entry with a detailed description of the \ + input.", + context + )) + .fix( + "Add a key to a `parameter_meta` block matching the parameter's exact \ + name with a detailed description of the input.", + ) + .try_build() + .unwrap() + } + + /// Ensures that each input has a corresponding `parameter_meta` element. + fn extraneous_parameter_meta<'b>( + &self, + parameter: &str, + context: &Context<'b>, + line_no: NonZeroUsize, + col_no: NonZeroUsize, + ) -> lint::Warning + where + Self: Rule<&'a v1::Document>, + { + // SAFETY: this error is written so that it will always unwrap. + lint::warning::Builder::default() + .code(self.code()) + .level(lint::Level::Medium) + .group(lint::Group::Completeness) + .location(Location::LineCol { line_no, col_no }) + .subject(format!( + "extraneous parameter meta within {}: {}", + context, parameter + )) + .body( + "A parameter meta entry with no corresponding input \ + parameter was found", + ) + .fix("Remove the parameter meta entry") + .try_build() + .unwrap() + } +} + +impl<'a> Rule<&'a v1::Document> for MatchingParameterMeta { + fn code(&self) -> Code { + // SAFETY: this manually crafted to unwrap successfully every time. + Code::try_new(Version::V1, 2).unwrap() + } + + fn group(&self) -> lint::Group { + Group::Completeness + } + + fn check(&self, tree: &'a v1::Document) -> lint::Result { + let mut results = Vec::new(); + + for (_, task) in tree.tasks() { + let context = Context::Task(task); + + let meta_keys = get_parameter_meta_keys(&context); + let input_parameters = get_input_parameter_names(&context); + + results.extend(report_errors(&context, input_parameters, meta_keys)); + } + + if let Some(workflow) = tree.workflow() { + let context = Context::Workflow(workflow); + + let meta_keys = get_parameter_meta_keys(&context); + let input_parameters = get_input_parameter_names(&context); + + results.extend(report_errors(&context, input_parameters, meta_keys)); + } + + match results.is_empty() { + true => Ok(None), + false => Ok(Some(results)), + } + } +} + +/// Gets the defined `parameter_meta` keys for this context. +fn get_parameter_meta_keys(context: &Context<'_>) -> HashSet { + match context { + Context::Task(task) => task + .parameter_metadata() + .cloned() + .into_iter() + .flat_map(|meta| meta.into_inner().into_keys()) + .map(|identifier| identifier.as_str().to_string()) + .collect::>(), + Context::Workflow(workflow) => workflow + .parameter_metadata() + .cloned() + .into_iter() + .flat_map(|meta| meta.into_inner().into_keys()) + .map(|identifier| identifier.as_str().to_string()) + .collect::>(), + } +} + +/// Gets the defined input parameter names for this context. +fn get_input_parameter_names(context: &Context<'_>) -> HashSet { + match context { + Context::Task(task) => task + .input() + .cloned() + .into_iter() + .flat_map(|input| input.declarations().cloned()) + .flatten() + .map(|declaration| declaration.name().to_string()) + .collect::>(), + Context::Workflow(workflow) => workflow + .input() + .cloned() + .into_iter() + .flat_map(|input| input.declarations().cloned()) + .flatten() + .map(|declaration| declaration.name().to_string()) + .collect::>(), + } +} + +/// Reports errors within a particular context for the given input parameters +/// and defined parameter meta keys. +fn report_errors( + context: &Context<'_>, + input_parameters: HashSet, + meta_keys: HashSet, +) -> Vec { + let mut results = Vec::new(); + + // Report existing parameters that have no matching parameter meta entry. + results.extend( + input_parameters + .sub(&meta_keys) + .into_iter() + .map(|parameter| { + MatchingParameterMeta.missing_parameter_meta( + ¶meter, + context, + NonZeroUsize::try_from(1).unwrap(), + NonZeroUsize::try_from(1).unwrap(), + ) + }), + ); + + // Report existing parameters that have no matching parameter meta entry. + results.extend( + meta_keys + .sub(&input_parameters) + .into_iter() + .map(|parameter| { + MatchingParameterMeta.extraneous_parameter_meta( + ¶meter, + context, + NonZeroUsize::try_from(1).unwrap(), + NonZeroUsize::try_from(1).unwrap(), + ) + }), + ); + + results +} diff --git a/wdl-ast/src/v1/macros.rs b/wdl-ast/src/v1/macros.rs new file mode 100644 index 000000000..e67b0d4e0 --- /dev/null +++ b/wdl-ast/src/v1/macros.rs @@ -0,0 +1,139 @@ +//! Macros. + +#[cfg(test)] +pub(crate) mod test; + +/// Checks to ensure that a node is of a certain [`Rule`](wdl_grammar::v1::Rule) +/// type. If the node does not match the specified `$type_`, an +/// [`Error::InvalidNode`](crate::Error::InvalidNode) is returned. +/// +/// # Arguments +/// +/// * `$node` - the [`Pair`](pest::iterators::Pair) (or "node") to examine. +/// * `$type_` - the rule type that the node must match. Note that only the rule +/// name is provided: `wdl_grammar::v1::Rule::` is prepended to the +/// expression. +pub macro check_node($node:expr, $type_:ident) { + if $node.as_rule() != wdl_grammar::v1::Rule::$type_ { + return Err(Self::Error::Common(crate::v1::Error::InvalidNode(format!( + "{} cannot be parsed from node type {:?}", + stringify!($type_), + $node.as_rule(), + )))); + } +} + +/// Unwraps exactly one node from a [`Pair`](pest::iterators::Pair), regardless +/// of node type. This macro is intended for situations where a node contains +/// one and only one child node and you'd like to unwrap to the inner node. If +/// either zero nodes or more than one nodes are found, the respective +/// [`Error`](crate::Error) is thrown. +/// +/// **Note:** the type of the node is not considered in this macro. If you'd +/// like to extract a particular node type, see [`extract_one!()`]. +/// +/// # Arguments +/// +/// * `$node` - the [`Pair`](pest::iterators::Pair) (or "node") to extract a +/// node from. +/// * `$within` - the name of the rule we're extracting from (needed for +/// constructing the error message if zero or more than one nodes are found). +pub macro unwrap_one($node:expr, $within:ident) {{ + let mut nodes = $node.into_inner(); + + match nodes.len() { + 0 => Err(Self::Error::Common(crate::v1::Error::MissingNode(format!( + "expected one node within a {} node", + stringify!($within) + )))), + 1 => Ok(nodes.next().unwrap()), + _ => Err(Self::Error::Common(crate::v1::Error::MultipleNodes( + format!("expected one node within a {} node", stringify!($within)), + ))), + } +}} + + +/// Extracts exactly one node of a particular type from a +/// [`Pair`](pest::iterators::Pair). This macro is intended for situations where +/// a node contains one and only one child node of a particular type and you'd +/// like to extract that node. For the immediate children of the node being +/// examined, if either zero nodes match the desired node type or multiple nodes +/// match the desired node type, the respective [`Error`](crate::Error) is +/// thrown. +/// +/// **Note:** if the node only has one child, [`unwrap_one!()`] is recommended. +/// +/// **Note:** if you'd like to do a depth-first search of the entire tree rather +/// than simply examining the node's immediate children, [`dive_one!()`] is +/// recommended. +/// +/// # Arguments +/// +/// * `$node` - the [`Pair`](pest::iterators::Pair) (or "node") to dive within. +/// * `$type_` - the rule type to dive for. Note that only the rule name is +/// provided: `wdl_grammar::v1::Rule::` is prepended to the expression. +/// * `$within` - the name of the rule we're diving into (needed for +/// constructing the error message if zero or more than one nodes matching +/// `$type_` are found). +pub macro extract_one($node:expr, $type_:ident, $within:ident, $err:path) {{ + let mut nodes = $node + .into_inner() + .filter(|x| matches!(x.as_rule(), wdl_grammar::v1::Rule::$type_)) + .collect::>(); + + match nodes.len() { + 0 => Err($err(crate::v1::Error::MissingNode(format!( + "expected one {} node within a {} node", + stringify!($type_), + stringify!($within) + )))), + 1 => Ok(nodes.pop().unwrap()), + _ => Err($err(crate::v1::Error::MissingNode(format!( + "expected one {} node within a {} node", + stringify!($type_), + stringify!($within) + )))), + } + +}} + +/// Dives into a [`Pair`](pest::iterators::Pair) to find exactly one node +/// matching the provided `$type_`. Notably, this method does a depth-first +/// search of the entire parse tree underneath the provided node—not just the +/// immediate level below. +/// +/// **Note:** if the node only has one child, [`unwrap_one!()`] is recommended. +/// +/// **Note:** if you'd like to examining only the node's immediate children, +/// [`extract_one!()`] is recommended. +/// +/// # Arguments +/// +/// * `$node` - the [`Pair`](pest::iterators::Pair) (or "node") to dive within. +/// * `$type_` - the rule type to dive for. Note that only the rule name is +/// provided: `wdl_grammar::v1::Rule::` is prepended to the expression. +/// * `$within` - the name of the rule we're diving into (needed for +/// constructing the error message if zero or more than one nodes matching +/// `$type_` are found). +pub macro dive_one($node:expr, $type_:ident, $within:ident, $err:path) {{ + let mut nodes = $node + .into_inner() + .flatten() + .filter(|x| matches!(x.as_rule(), wdl_grammar::v1::Rule::$type_)) + .collect::>(); + + match nodes.len() { + 0 => Err($err(crate::v1::Error::MissingNode(format!( + "expected one {} node within a {} node", + stringify!($type_), + stringify!($within) + )))), + 1 => Ok(nodes.pop().unwrap()), + _ => Err($err(crate::v1::Error::MissingNode(format!( + "expected one {} node within a {} node", + stringify!($type_), + stringify!($within) + )))), + } +}} diff --git a/wdl-ast/src/v1/macros/test.rs b/wdl-ast/src/v1/macros/test.rs new file mode 100644 index 000000000..2b23e83eb --- /dev/null +++ b/wdl-ast/src/v1/macros/test.rs @@ -0,0 +1,63 @@ +/// Parses a [`Document`](crate::v1::Document) from the provided input (and +/// assumes that the grammar parsing will `unwrap()`). This method is helpful +/// for quickly bootstrapping up a [`Document`](crate::v1::Document) in your +/// tests. +/// +/// # Arguments +/// +/// * `$document` - a [`&str`](str) defining the +/// [`Document`](crate::v1::Document). +pub macro parse_document($document:literal) {{ + let parse_tree = wdl_grammar::v1::parse(wdl_grammar::v1::Rule::document, $document).unwrap(); + crate::v1::parse(parse_tree) +}} + +/// Scaffolds a test to ensure that a targeted entity is able to be constructed +/// from a valid [parsed node](wdl_grammar::v1::Rule) using `try_from()`. +/// +/// # Arguments +/// +/// * `$input` - a [`&str`](str) that is parsed into the defined `$type_`. +/// * `$type_` - the name of the [rule](wdl_grammar::v1::Rule) to parse the +/// `$input` as. +/// * `$target` - the name of the entity to attempt to construct from the +/// `$type_`. +pub macro valid_node($input:literal, $type_:ident, $target:ident) {{ + let parse_node = wdl_grammar::v1::parse(wdl_grammar::v1::Rule::$type_, $input) + .unwrap() + .into_inner() + .next() + .unwrap(); + + $target::try_from(parse_node).unwrap() +}} + +/// Scaffolds a test to ensure that a targeted entity fails to be constructed +/// from an invalid [parsed node](wdl_grammar::v1::Rule) using `try_from()`. +/// +/// # Arguments +/// +/// * `$input` - a [`&str`](str) that is parsed into the defined `$type_`. +/// * `$type_` - the name of the [rule](wdl_grammar::v1::Rule) to parse the +/// `$input` as. +/// * `$name` - the name of the target entity included in the error message. +/// * `$target` - the name of the target entity to attempt to construct from the +/// `$type_` as a Rust identifier. +pub macro invalid_node($input:literal, $type_:ident, $name:ident, $target:ident) { + let parse_node = wdl_grammar::v1::parse(wdl_grammar::v1::Rule::$type_, $input) + .unwrap() + .into_inner() + .next() + .unwrap(); + + let err = $target::try_from(parse_node).unwrap_err(); + + assert_eq!( + err.to_string(), + format!( + "invalid node: {} cannot be parsed from node type {:?}", + stringify!($name), + wdl_grammar::v1::Rule::$type_ + ) + ); +} diff --git a/wdl-ast/src/v1/validation.rs b/wdl-ast/src/v1/validation.rs new file mode 100644 index 000000000..25d1de251 --- /dev/null +++ b/wdl-ast/src/v1/validation.rs @@ -0,0 +1,14 @@ +//! Validation rules for WDL 1.x abstract syntax trees. + +use lazy_static::lazy_static; + +use wdl_core as core; + +use core::validation::Rule; + +use crate::v1; + +lazy_static! { + /// All validation rules available for WDL 1.x abstract syntax trees. + pub static ref RULES: Vec Rule<&'a v1::Document>>> = vec![]; +} \ No newline at end of file diff --git a/wdl-core/Cargo.toml b/wdl-core/Cargo.toml new file mode 100644 index 000000000..01ed44e87 --- /dev/null +++ b/wdl-core/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "wdl-core" +license.workspace = true +edition.workspace = true +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { workspace = true } +pest = { workspace = true } +serde = { workspace = true } +to_snake_case = "0.1.1" \ No newline at end of file diff --git a/wdl-grammar/src/core/code.rs b/wdl-core/src/code.rs similarity index 89% rename from wdl-grammar/src/core/code.rs rename to wdl-core/src/code.rs index 01c8e9202..372de9a5b 100644 --- a/wdl-grammar/src/core/code.rs +++ b/wdl-core/src/code.rs @@ -40,10 +40,10 @@ impl Code { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// - /// use grammar::core::Code; - /// use grammar::Version; + /// use core::Code; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// assert_eq!(code.grammar(), &Version::V1); @@ -62,10 +62,10 @@ impl Code { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// - /// use grammar::core::Code; - /// use grammar::Version; + /// use core::Code; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// assert_eq!(code.grammar(), &Version::V1); @@ -81,10 +81,10 @@ impl Code { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// - /// use grammar::core::Code; - /// use grammar::Version; + /// use core::Code; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// assert_eq!(code.index().get(), 1); diff --git a/wdl-grammar/src/core/code/identity.rs b/wdl-core/src/code/identity.rs similarity index 100% rename from wdl-grammar/src/core/code/identity.rs rename to wdl-core/src/code/identity.rs diff --git a/wdl-grammar/src/core.rs b/wdl-core/src/lib.rs similarity index 54% rename from wdl-grammar/src/core.rs rename to wdl-core/src/lib.rs index 08a8806b1..fb8f8c123 100644 --- a/wdl-grammar/src/core.rs +++ b/wdl-core/src/lib.rs @@ -1,11 +1,9 @@ -//! Core functionality used across all grammar versions. - mod code; pub mod lint; mod location; -mod tree; pub mod validation; +mod version; pub use code::Code; pub use location::Location; -pub use tree::Tree; +pub use version::Version; diff --git a/wdl-core/src/lint.rs b/wdl-core/src/lint.rs new file mode 100644 index 000000000..2c50195fc --- /dev/null +++ b/wdl-core/src/lint.rs @@ -0,0 +1,33 @@ +//! Linting. + +use to_snake_case::ToSnakeCase as _; + +mod group; +mod level; +pub mod warning; + +pub use group::Group; +pub use level::Level; +pub use warning::Warning; + +use crate::Code; + +/// A [`Result`](std::result::Result) returned from a lint check. +pub type Result = std::result::Result>, Box>; + +/// A lint rule. +pub trait Rule: std::fmt::Debug + Sync { + /// The name of the lint rule. + fn name(&self) -> String { + format!("{:?}", self).to_snake_case() + } + + /// Get the code for this lint rule. + fn code(&self) -> Code; + + /// Get the lint group for this lint rule. + fn group(&self) -> Group; + + /// Checks the tree according to the implemented lint rule. + fn check(&self, tree: E) -> Result; +} diff --git a/wdl-grammar/src/core/lint/group.rs b/wdl-core/src/lint/group.rs similarity index 59% rename from wdl-grammar/src/core/lint/group.rs rename to wdl-core/src/lint/group.rs index 3e8fb24a5..96ef3f721 100644 --- a/wdl-grammar/src/core/lint/group.rs +++ b/wdl-core/src/lint/group.rs @@ -3,16 +3,22 @@ /// A lint group. #[derive(Clone, Debug, Eq, PartialEq)] pub enum Group { - /// Rules associated with the style of an input. + /// Rules associated with having a complete document. + Completeness, + + /// Rules associated with the style of a document. Style, /// Rules often considered overly opinionated. + /// + /// These rules are disabled by default but can be turned on individually. Pedantic, } impl std::fmt::Display for Group { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Group::Completeness => write!(f, "Completeness"), Group::Style => write!(f, "Style"), Group::Pedantic => write!(f, "Pedantic"), } diff --git a/wdl-grammar/src/core/lint/level.rs b/wdl-core/src/lint/level.rs similarity index 100% rename from wdl-grammar/src/core/lint/level.rs rename to wdl-core/src/lint/level.rs diff --git a/wdl-grammar/src/core/lint/warning.rs b/wdl-core/src/lint/warning.rs similarity index 78% rename from wdl-grammar/src/core/lint/warning.rs rename to wdl-core/src/lint/warning.rs index 4c79403b6..9bfcf1d63 100644 --- a/wdl-grammar/src/core/lint/warning.rs +++ b/wdl-core/src/lint/warning.rs @@ -1,9 +1,9 @@ //! Lint warnings. -use crate::core::lint::Group; -use crate::core::lint::Level; -use crate::core::Code; -use crate::core::Location; +use crate::lint::Group; +use crate::lint::Level; +use crate::Code; +use crate::Location; mod builder; pub mod display; @@ -41,16 +41,16 @@ impl Warning { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// use std::path::PathBuf; /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -77,16 +77,16 @@ impl Warning { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// use std::path::PathBuf; /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -112,16 +112,16 @@ impl Warning { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// use std::path::PathBuf; /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -147,16 +147,16 @@ impl Warning { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// use std::path::PathBuf; /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -182,16 +182,16 @@ impl Warning { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// use std::path::PathBuf; /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -217,16 +217,16 @@ impl Warning { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// use std::path::PathBuf; /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -252,16 +252,16 @@ impl Warning { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// use std::path::PathBuf; /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -287,18 +287,18 @@ impl Warning { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// use std::fmt::Write as _; /// use std::path::PathBuf; /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::warning::display; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::warning::display; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() diff --git a/wdl-grammar/src/core/lint/warning/builder.rs b/wdl-core/src/lint/warning/builder.rs similarity index 72% rename from wdl-grammar/src/core/lint/warning/builder.rs rename to wdl-core/src/lint/warning/builder.rs index c97345cf4..5976fe068 100644 --- a/wdl-grammar/src/core/lint/warning/builder.rs +++ b/wdl-core/src/lint/warning/builder.rs @@ -1,42 +1,42 @@ //! A builder for a lint [`Warning`]. -use crate::core::lint::Group; -use crate::core::lint::Level; -use crate::core::lint::Warning; -use crate::core::Code; -use crate::core::Location; +use crate::lint::Group; +use crate::lint::Level; +use crate::lint::Warning; +use crate::Code; +use crate::Location; -/// An error related to building a lint warning. +/// An error that occurs when a required field is missing at build time. #[derive(Debug)] pub enum MissingError { - /// A code was not provided. + /// A code was not provided to the [`Builder`]. Code, - /// A lint level was not provided. + /// A lint level was not provided to the [`Builder`]. Level, - /// A lint group was not provided. + /// A lint group was not provided to the [`Builder`]. Group, - /// A location was not provided. + /// A location was not provided to the [`Builder`]. Location, - /// A subject was not provided. + /// A subject was not provided to the [`Builder`]. Subject, - /// A body was not provided. + /// A body was not provided to the [`Builder`]. Body, } impl std::fmt::Display for MissingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - MissingError::Code => write!(f, "missing code"), - MissingError::Level => write!(f, "missing level"), - MissingError::Group => write!(f, "missing group"), - MissingError::Location => write!(f, "missing location"), - MissingError::Subject => write!(f, "missing subject"), - MissingError::Body => write!(f, "missing body"), + MissingError::Code => write!(f, "code"), + MissingError::Level => write!(f, "level"), + MissingError::Group => write!(f, "group"), + MissingError::Location => write!(f, "location"), + MissingError::Subject => write!(f, "subject"), + MissingError::Body => write!(f, "body"), } } } @@ -77,14 +77,14 @@ impl Builder { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -112,15 +112,15 @@ impl Builder { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -146,15 +146,15 @@ impl Builder { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -180,15 +180,15 @@ impl Builder { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -217,15 +217,15 @@ impl Builder { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -252,15 +252,15 @@ impl Builder { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -287,14 +287,14 @@ impl Builder { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -321,15 +321,15 @@ impl Builder { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// - /// use grammar::core::lint::warning::Builder; - /// use grammar::core::lint::Group; - /// use grammar::core::lint::Level; - /// use grammar::core::Code; - /// use grammar::core::Location; - /// use grammar::Version; + /// use core::lint::warning::Builder; + /// use core::lint::Group; + /// use core::lint::Level; + /// use core::Code; + /// use core::Location; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() diff --git a/wdl-grammar/src/core/lint/warning/display.rs b/wdl-core/src/lint/warning/display.rs similarity index 100% rename from wdl-grammar/src/core/lint/warning/display.rs rename to wdl-core/src/lint/warning/display.rs diff --git a/wdl-grammar/src/core/location.rs b/wdl-core/src/location.rs similarity index 97% rename from wdl-grammar/src/core/location.rs rename to wdl-core/src/location.rs index d74e87526..e4cbb836b 100644 --- a/wdl-grammar/src/core/location.rs +++ b/wdl-core/src/location.rs @@ -36,11 +36,11 @@ impl Location { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// /// use std::num::NonZeroUsize; /// - /// use grammar::core::Location; + /// use core::Location; /// /// assert_eq!(Location::File.to_string(), None); /// assert_eq!( diff --git a/wdl-core/src/validation.rs b/wdl-core/src/validation.rs new file mode 100644 index 000000000..132fbe87c --- /dev/null +++ b/wdl-core/src/validation.rs @@ -0,0 +1,29 @@ +//! Validation. + +use to_snake_case::ToSnakeCase as _; + +pub mod error; + +pub use error::Error; + +use crate::Code; + +/// A [`Result`](std::result::Result) with a validation [`Error`]. +pub type Result = std::result::Result<(), Error>; + +/// A validation rule. +pub trait Rule: std::fmt::Debug + Sync { + /// The name of the validation rule. + /// + /// This is what will show up in style guides, it is required to be snake + /// case (even though the rust struct is camel case). + fn name(&self) -> String { + format!("{:?}", self).to_snake_case() + } + + /// Get the code for this validation rule. + fn code(&self) -> Code; + + /// Checks the tree according to the implemented validation rule. + fn validate(&self, tree: E) -> Result; +} diff --git a/wdl-grammar/src/core/validation/error.rs b/wdl-core/src/validation/error.rs similarity index 85% rename from wdl-grammar/src/core/validation/error.rs rename to wdl-core/src/validation/error.rs index 1281b469c..d881b7ed8 100644 --- a/wdl-grammar/src/core/validation/error.rs +++ b/wdl-core/src/validation/error.rs @@ -4,7 +4,7 @@ mod builder; pub use builder::Builder; -use crate::core::Code; +use crate::Code; /// A validation error. #[derive(Clone, Debug)] @@ -22,11 +22,11 @@ impl Error { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// - /// use grammar::core::validation::error::Builder; - /// use grammar::core::Code; - /// use grammar::Version; + /// use core::validation::error::Builder; + /// use core::Code; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -47,11 +47,11 @@ impl Error { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// - /// use grammar::core::validation::error::Builder; - /// use grammar::core::Code; - /// use grammar::Version; + /// use core::validation::error::Builder; + /// use core::Code; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() diff --git a/wdl-grammar/src/core/validation/error/builder.rs b/wdl-core/src/validation/error/builder.rs similarity index 63% rename from wdl-grammar/src/core/validation/error/builder.rs rename to wdl-core/src/validation/error/builder.rs index c6ef0089d..0c2c993bd 100644 --- a/wdl-grammar/src/core/validation/error/builder.rs +++ b/wdl-core/src/validation/error/builder.rs @@ -1,31 +1,47 @@ //! A builder for a validation [`Error`](super::Error). -use crate::core::validation; -use crate::core::Code; +use crate::validation; +use crate::Code; -/// An error related to building a validation error. +/// An error that occurs when a required field is missing at build time. #[derive(Debug)] pub enum MissingError { - /// A code was not provided. + /// A code was not provided to the [`Builder`]. Code, - /// A message was not provided. + /// A message was not provided to the [`Builder`]. Message, } impl std::fmt::Display for MissingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - MissingError::Code => write!(f, "missing code"), - MissingError::Message => write!(f, "missing message"), + MissingError::Code => write!(f, "code"), + MissingError::Message => write!(f, "message"), } } } impl std::error::Error for MissingError {} +#[derive(Debug)] +pub enum Error { + /// A required field was missing at build time. + Missing(MissingError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Missing(err) => write!(f, "missing value for field: {err}"), + } + } +} + +impl std::error::Error for Error {} + /// A [`Result`](std::result::Result) with a [`MissingError`]. -pub type Result = std::result::Result; +pub type Result = std::result::Result; /// A builder for an [`Error`](validation::Error). #[derive(Debug, Default)] @@ -43,11 +59,11 @@ impl Builder { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// - /// use grammar::core::validation::error::Builder; - /// use grammar::core::Code; - /// use grammar::Version; + /// use core::validation::error::Builder; + /// use core::Code; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -69,11 +85,11 @@ impl Builder { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// - /// use grammar::core::validation::error::Builder; - /// use grammar::core::Code; - /// use grammar::Version; + /// use core::validation::error::Builder; + /// use core::Code; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -95,11 +111,11 @@ impl Builder { /// # Examples /// /// ``` - /// use wdl_grammar as grammar; + /// use wdl_core as core; /// - /// use grammar::core::validation::error::Builder; - /// use grammar::core::Code; - /// use grammar::Version; + /// use core::validation::error::Builder; + /// use core::Code; + /// use core::Version; /// /// let code = Code::try_new(Version::V1, 1)?; /// let warning = Builder::default() @@ -114,8 +130,14 @@ impl Builder { /// /// # Ok::<(), Box>(()) pub fn try_build(self) -> Result { - let code = self.code.map(Ok).unwrap_or(Err(MissingError::Code))?; - let message = self.message.map(Ok).unwrap_or(Err(MissingError::Message))?; + let code = self + .code + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Code)))?; + let message = self + .message + .map(Ok) + .unwrap_or(Err(Error::Missing(MissingError::Message)))?; Ok(validation::Error { code, message }) } diff --git a/wdl-grammar/src/version.rs b/wdl-core/src/version.rs similarity index 89% rename from wdl-grammar/src/version.rs rename to wdl-core/src/version.rs index 6dbcc8810..5e31561b6 100644 --- a/wdl-grammar/src/version.rs +++ b/wdl-core/src/version.rs @@ -1,13 +1,11 @@ //! Workflow Description Language (WDL) grammar versions. -#[cfg(feature = "binaries")] use clap::ValueEnum; use serde::Deserialize; use serde::Serialize; /// A Workflow Description Language (WDL) grammar version. -#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -#[cfg_attr(feature = "binaries", derive(ValueEnum))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ValueEnum)] #[serde(rename_all = "lowercase")] pub enum Version { /// Version 1.x of the WDL specification. diff --git a/wdl-gauntlet/Cargo.toml b/wdl-gauntlet/Cargo.toml new file mode 100644 index 000000000..0a93f4c0a --- /dev/null +++ b/wdl-gauntlet/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "wdl-gauntlet" +license.workspace = true +edition.workspace = true +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-recursion = "1.0.5" +chrono = "0.4.31" +clap.workspace = true +colored.workspace = true +dirs = "5.0.1" +env_logger.workspace = true +indexmap.workspace = true +log.workspace = true +octocrab = "0.32.0" +reqwest = "0.11.22" +serde.workspace = true +serde_with.workspace = true +tokio.workspace = true +toml.workspace = true +wdl-ast = { path = "../wdl-ast", version = "0.1.0" } +wdl-core = { path = "../wdl-core", version = "0.1.0" } +wdl-grammar = { path = "../wdl-grammar", version = "0.1.0" } \ No newline at end of file diff --git a/wdl-grammar/src/commands/gauntlet/config.rs b/wdl-gauntlet/src/config.rs similarity index 56% rename from wdl-grammar/src/commands/gauntlet/config.rs rename to wdl-gauntlet/src/config.rs index 9ac986f4a..f6b07ffbc 100644 --- a/wdl-grammar/src/commands/gauntlet/config.rs +++ b/wdl-gauntlet/src/config.rs @@ -1,22 +1,21 @@ //! Configuration. -use std::collections::HashSet; use std::fs::File; use std::io::Write; use std::path::PathBuf; -use grammar::Version; use log::debug; use log::log_enabled; use log::trace; -use wdl_grammar as grammar; + +use wdl_core as core; + +use core::Version; pub mod inner; pub use inner::Inner; -use crate::commands::gauntlet::repository; - /// The default directory name for the `wdl-grammar` configuration file and /// cache. const DEFAULT_CONFIG_DIR: &str = "wdl-grammar"; @@ -33,6 +32,10 @@ pub enum Error { /// An input/output error. InputOutput(std::io::Error), + /// Attempted to save a config without a backing `path` (i.e., an anonymous + /// configuration file that is only meant to be used for testing). + SaveOnAnonymousConfig, + /// An error serializing TOML. SerializeToml(toml::ser::Error), } @@ -42,6 +45,7 @@ impl std::fmt::Display for Error { match self { Error::DeserializeToml(err) => write!(f, "deserialize toml error: {err}"), Error::InputOutput(err) => write!(f, "i/e error: {err}"), + Error::SaveOnAnonymousConfig => write!(f, "attempted to save an anonymous config"), Error::SerializeToml(err) => write!(f, "serialize toml error: {err}"), } } @@ -58,10 +62,10 @@ type Result = std::result::Result; /// configuration itself. Notably, the path to the configuration file should /// _not_ be part of the serialized configuration value. Thus, I split the /// concept of the path and the actual configuration into two different structs. -#[derive(Default)] +#[derive(Debug, Default)] pub struct Config { /// The path to the configuration file. - path: PathBuf, + path: Option, /// The inner configuration values. inner: Inner, @@ -100,72 +104,118 @@ impl Config { /// In both cases, the `path` will be stored within the [`Config`]. This has /// the effect of ensuring the value loaded here will be saved to the /// inteded location (should [`Config::save()`] be called). - pub fn load_or_new(path: PathBuf, version: grammar::Version) -> Result { + pub fn load_or_new(path: PathBuf, version: Version) -> Result { if !path.exists() { return Ok(Self { - path, + path: Some(path), inner: Inner::from(version), }); } debug!("loading from {}.", path.display()); let contents = std::fs::read_to_string(&path).map_err(Error::InputOutput)?; - let inner = toml::from_str(&contents).map_err(Error::DeserializeToml)?; + let mut inner: Inner = toml::from_str(&contents).map_err(Error::DeserializeToml)?; + inner.sort(); - let result = Self { path, inner }; + let result = Self { + path: Some(path), + inner, + }; if log_enabled!(log::Level::Trace) { trace!("Loaded configuration file with the following:"); - trace!(" -> {} repositories.", result.repositories().len()); - let num_ignored_errors = result.ignored_errors().len(); + trace!(" -> {} repositories.", result.inner().repositories().len()); + let num_ignored_errors = result.inner().ignored_errors().len(); trace!(" -> {} ignored errors.", num_ignored_errors); } Ok(result) } - /// Gets the [`Version`] from the [`Config`] by reference. - pub fn version(&self) -> &Version { - &self.inner.version - } - - /// Gets the [`inner::Repositories`] from the [`Config`] by reference. - pub fn repositories(&self) -> &inner::Repositories { - &self.inner.repositories - } - - /// Gets the [`inner::Repositories`] from the [`Config`] by mutable - /// reference. - pub fn repositories_mut(&mut self) -> &mut HashSet { - &mut self.inner.repositories - } - - /// Gets the [`inner::Errors`] from the [`Config`] by reference. - pub fn ignored_errors(&self) -> &inner::Errors { - &self.inner.ignored_errors + /// Gets the [`Inner`] configuration by reference. + /// + /// ``` + /// use wdl_core as core; + /// use wdl_gauntlet as gauntlet; + /// + /// let config = r#"version = "v1" + /// + /// [[repositories]] + /// organization = "Foo" + /// name = "Bar""# + /// .parse::()?; + /// + /// assert_eq!(config.inner().version(), &core::Version::V1); + /// assert_eq!(config.inner().repositories().len(), 1); + /// assert_eq!(config.inner().ignored_errors().len(), 0); + /// + /// Ok::<(), Box>(()) + /// ``` + pub fn inner(&self) -> &Inner { + &self.inner } - /// Gets the [`inner::Errors`] from the [`Config`] by mutable reference. - pub fn ignored_errors_mut(&mut self) -> &mut inner::Errors { - &mut self.inner.ignored_errors + /// Gets the [`Inner`] configuration by mutable reference. + /// + /// ``` + /// use wdl_core as core; + /// use wdl_gauntlet as gauntlet; + /// + /// let mut config = r#"version = "v1" + /// + /// [[repositories]] + /// organization = "Foo" + /// name = "Bar""# + /// .parse::()?; + /// + /// config + /// .inner_mut() + /// .extend_repositories(vec!["Foo/Baz".parse::()?]); + /// + /// config.inner_mut().extend_ignored_errors(vec![( + /// "Foo/Bar:Quux".parse::()?, + /// String::from("Hello, world!"), + /// )]); + /// + /// assert_eq!(config.inner().version(), &core::Version::V1); + /// assert_eq!(config.inner().repositories().len(), 2); + /// assert_eq!(config.inner().ignored_errors().len(), 1); + /// + /// Ok::<(), Box>(()) + /// ``` + pub fn inner_mut(&mut self) -> &mut Inner { + &mut self.inner } /// Attempts to save the contents of the [`Config`] (in particular, the - /// [`Self::inner`] stored within the [`Config`]) to the path pointed to - /// [`Self::path`]. + /// [`Self::inner`] stored within the [`Config`]) to the path backing the + /// [`Config`]. pub fn save(&self) -> Result<()> { - if log_enabled!(log::Level::Debug) { - if self.path.exists() { - debug!("overwriting configuration at {}", self.path.display()); - } else { - debug!("saving configuration to {}", self.path.display()); + if let Some(ref path) = self.path { + if log_enabled!(log::Level::Debug) { + if path.exists() { + debug!("overwriting configuration at {}", path.display()); + } else { + debug!("saving configuration to {}", path.display()); + } } + + let mut file = File::create(path).map_err(Error::InputOutput)?; + let contents = toml::to_string_pretty(&self.inner).map_err(Error::SerializeToml)?; + + write!(file, "{}", contents).map_err(Error::InputOutput) + } else { + Err(Error::SaveOnAnonymousConfig) } + } +} - let mut file = File::create(&self.path).map_err(Error::InputOutput)?; - let contents = toml::to_string_pretty(&self.inner).map_err(Error::SerializeToml)?; +impl std::str::FromStr for Config { + type Err = Error; - write!(file, "{}", contents).map_err(Error::InputOutput) + fn from_str(s: &str) -> Result { + let inner = toml::from_str(s).map_err(Error::DeserializeToml)?; + Ok(Self { path: None, inner }) } } diff --git a/wdl-gauntlet/src/config/inner.rs b/wdl-gauntlet/src/config/inner.rs new file mode 100644 index 000000000..fae934f79 --- /dev/null +++ b/wdl-gauntlet/src/config/inner.rs @@ -0,0 +1,253 @@ +//! An inner representation for the configuration object. +//! +//! This struct holds the configuration values. + +use indexmap::IndexMap; +use indexmap::IndexSet; +use serde::Deserialize; +use serde::Serialize; +use serde_with::serde_as; + +use wdl_core as core; + +mod repr; + +pub use repr::ErrorsAsReprs; + +use crate::document; +use crate::repository; + +/// Parsing errors as [`String`]s associated with a [document +/// identifier](document::Identifier). +pub type Errors = IndexMap; + +/// A unique set of [repository identifiers](repository::Identifier). +pub type Repositories = IndexSet; + +/// The inner configuration object for a [`Config`](super::Config). +/// +/// This object stores the actual configuration values for this subcommand. +#[serde_as] +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct Inner { + /// The WDL version. + version: core::Version, + + /// The repositories. + #[serde(default)] + repositories: Repositories, + + /// The ignored errors. + #[serde_as(as = "ErrorsAsReprs")] + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + ignored_errors: Errors, +} + +impl Inner { + /// Gets the [`Version`](core::Version) for this [`Inner`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_core as core; + /// use wdl_gauntlet as gauntlet; + /// + /// use gauntlet::config::Inner; + /// + /// let config = r#"version = "v1" + /// + /// [[repositories]] + /// organization = "Foo" + /// name = "Bar""#; + /// + /// let inner: Inner = toml::from_str(&config).unwrap(); + /// assert_eq!(inner.version(), &core::Version::V1); + /// ``` + pub fn version(&self) -> &core::Version { + &self.version + } + + /// Gets the [`Repositories`] for this [`Inner`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_core as core; + /// use wdl_gauntlet as gauntlet; + /// + /// use gauntlet::config::Inner; + /// + /// let config = r#"version = "v1" + /// + /// [[repositories]] + /// organization = "Foo" + /// name = "Bar""#; + /// + /// let inner: Inner = toml::from_str(&config).unwrap(); + /// assert_eq!(inner.repositories().len(), 1); + /// ``` + pub fn repositories(&self) -> &Repositories { + &self.repositories + } + + /// Extends the [`Repositories`] for this [`Inner`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_core as core; + /// use wdl_gauntlet as gauntlet; + /// + /// use indexmap::IndexSet; + /// + /// use gauntlet::config::Inner; + /// + /// let config = r#"version = "v1" + /// + /// [[repositories]] + /// organization = "Foo" + /// name = "Bar""#; + /// + /// let mut inner: Inner = toml::from_str(&config).unwrap(); + /// + /// let mut repositories = IndexSet::new(); + /// repositories.insert( + /// "Foo/Baz" + /// .parse::() + /// .unwrap(), + /// ); + /// + /// inner.extend_repositories(repositories); + /// + /// assert_eq!(inner.repositories().len(), 2); + /// ``` + pub fn extend_repositories>( + &mut self, + items: T, + ) { + self.repositories.extend(items.into_iter()); + self.repositories.sort(); + } + + /// Gets the [`Errors`] for this [`Inner`] by reference. + /// + /// # Examples + /// + /// ``` + /// use wdl_core as core; + /// use wdl_gauntlet as gauntlet; + /// + /// use gauntlet::config::Inner; + /// + /// let config = r#"version = "v1" + /// + /// [[ignored_errors]] + /// document = "Foo/Bar:baz.wdl" + /// error = '''an error'''"#; + /// + /// let mut inner: Inner = toml::from_str(&config).unwrap(); + /// + /// assert_eq!(inner.ignored_errors().len(), 1); + /// ``` + pub fn ignored_errors(&self) -> &Errors { + &self.ignored_errors + } + + /// Replaces the [`Errors`] for this [`Inner`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_core as core; + /// use wdl_gauntlet as gauntlet; + /// + /// use indexmap::IndexMap; + /// + /// use gauntlet::config::Inner; + /// + /// let config = r#"version = "v1" + /// + /// [[ignored_errors]] + /// document = "Foo/Bar:baz.wdl" + /// error = '''an error'''"#; + /// + /// let mut inner: Inner = toml::from_str(&config).unwrap(); + /// + /// let mut errors = IndexMap::new(); + /// errors.insert( + /// "Foo/Baz:quux.wdl" + /// .parse::() + /// .unwrap(), + /// String::from("another error"), + /// ); + /// + /// inner.replace_ignored_errors(errors); + /// + /// assert_eq!(inner.ignored_errors().len(), 1); + /// let (document, error) = inner.ignored_errors().first().unwrap(); + /// assert_eq!(error, &String::from("another error")); + /// ``` + pub fn replace_ignored_errors>( + &mut self, + items: T, + ) { + self.ignored_errors = items.into_iter().collect(); + self.ignored_errors.sort_keys(); + } + + /// Extends the [`Errors`] for this [`Inner`]. + /// + /// # Examples + /// + /// ``` + /// use wdl_core as core; + /// use wdl_gauntlet as gauntlet; + /// + /// use indexmap::IndexMap; + /// + /// use gauntlet::config::Inner; + /// + /// let config = r#"version = "v1" + /// + /// [[ignored_errors]] + /// document = "Foo/Bar:baz.wdl" + /// error = '''an error'''"#; + /// + /// let mut inner: Inner = toml::from_str(&config).unwrap(); + /// + /// let mut errors = IndexMap::new(); + /// errors.insert( + /// "Foo/Baz:quux.wdl" + /// .parse::() + /// .unwrap(), + /// String::from("another error"), + /// ); + /// + /// inner.extend_ignored_errors(errors); + /// + /// assert_eq!(inner.ignored_errors().len(), 2); + /// ``` + pub fn extend_ignored_errors>( + &mut self, + items: T, + ) { + self.ignored_errors.extend(items.into_iter()); + self.ignored_errors.sort_keys(); + } + + /// Sorts the [`Repositories`] and the [`Errors`] (by key). + pub fn sort(&mut self) { + self.repositories.sort(); + self.ignored_errors.sort_keys(); + } +} + +impl From for Inner { + fn from(version: core::Version) -> Self { + Self { + version, + repositories: Default::default(), + ignored_errors: Default::default(), + } + } +} diff --git a/wdl-grammar/src/commands/gauntlet/config/inner/repr.rs b/wdl-gauntlet/src/config/inner/repr.rs similarity index 83% rename from wdl-grammar/src/commands/gauntlet/config/inner/repr.rs rename to wdl-gauntlet/src/config/inner/repr.rs index 4071c7f8c..3dda96e09 100644 --- a/wdl-grammar/src/commands/gauntlet/config/inner/repr.rs +++ b/wdl-gauntlet/src/config/inner/repr.rs @@ -1,10 +1,15 @@ //! Representation of ignored errors as stored in the configuration file. +// This compiler attribute is added because `serde_with` generates a struct +// below that does not have any documentation. The only way to silence the +// warning is to allow missing docs for this file. +#![allow(missing_docs)] + use serde::Deserialize; use serde::Serialize; -use crate::commands::gauntlet::config::inner::Errors; -use crate::commands::gauntlet::document; +use crate::config::inner::Errors; +use crate::document; /// A representation of an error to ignore as serialized in the configuration /// file. diff --git a/wdl-grammar/src/commands/gauntlet/document.rs b/wdl-gauntlet/src/document.rs similarity index 100% rename from wdl-grammar/src/commands/gauntlet/document.rs rename to wdl-gauntlet/src/document.rs diff --git a/wdl-grammar/src/commands/gauntlet/document/identifier.rs b/wdl-gauntlet/src/document/identifier.rs similarity index 95% rename from wdl-grammar/src/commands/gauntlet/document/identifier.rs rename to wdl-gauntlet/src/document/identifier.rs index 07a71d171..dff0f9d6d 100644 --- a/wdl-grammar/src/commands/gauntlet/document/identifier.rs +++ b/wdl-gauntlet/src/document/identifier.rs @@ -5,7 +5,7 @@ use serde::Serialize; use serde_with::serde_as; use serde_with::DisplayFromStr; -use crate::commands::gauntlet::repository; +use crate::repository; /// The character that separates the repository from the path in the identifier. const SEPARATOR: char = ':'; @@ -102,6 +102,8 @@ impl std::str::FromStr for Identifier { .parse::() .map_err(|err| Error::Parse(ParseError::RepositoryIdentifier(err)))?; + // SAFETY: we just checked above that two elements exist, so this will + // always unwrap. let path = parts.next().unwrap().to_string(); Ok(Self { repository, path }) diff --git a/wdl-grammar/src/commands/gauntlet.rs b/wdl-gauntlet/src/lib.rs similarity index 68% rename from wdl-grammar/src/commands/gauntlet.rs rename to wdl-gauntlet/src/lib.rs index bd2f36940..7b8b67ead 100644 --- a/wdl-grammar/src/commands/gauntlet.rs +++ b/wdl-gauntlet/src/lib.rs @@ -1,5 +1,13 @@ //! `wdl-grammar gauntlet` +#![feature(let_chains)] +#![warn(missing_docs)] +#![warn(rust_2018_idioms)] +#![warn(rust_2021_compatibility)] +#![warn(missing_debug_implementations)] +#![warn(clippy::missing_docs_in_private_items)] +#![warn(rustdoc::broken_intra_doc_links)] + use std::collections::HashSet; use std::path::PathBuf; use std::process; @@ -9,20 +17,22 @@ use colored::Colorize as _; use log::debug; use log::trace; +use wdl_ast as ast; +use wdl_core as core; +use wdl_grammar as grammar; + pub mod config; pub mod document; -mod report; +pub mod report; pub mod repository; pub use config::Config; pub use report::Report; pub use repository::Repository; -use wdl_grammar as grammar; - -use crate::commands::gauntlet::report::Status; -use crate::commands::gauntlet::repository::options; -use crate::commands::gauntlet::repository::Identifier; +use crate::report::Status; +use crate::repository::options; +use crate::repository::Identifier; /// The exit code to emit when any test unexpectedly fails. const EXIT_CODE_FAILED: i32 = 1; @@ -66,51 +76,67 @@ impl std::error::Error for Error {} /// A [`Result`](std::result::Result) with an [`Error`]. type Result = std::result::Result; -/// Arguments for the `wdl-grammar gauntlet` subcommand. -#[derive(Debug, Parser)] +/// A command-line utility for testing the compatibility of `wdl-grammar` and +/// `wdl-ast` against a wide variety of community WDL repositories. +#[derive(Parser, Debug)] +#[command(author, version, about, long_about)] pub struct Args { /// The GitHub repositories to evaluate (e.g., "stjudecloud/workflows"). - repositories: Option>, + pub repositories: Option>, /// The location of the cache directory. #[arg(long)] - cache_dir: Option, + pub cache_dir: Option, /// The location of the config file. #[arg(short, long)] - config_file: Option, + pub config_file: Option, + + /// Detailed information, including debug information, is logged in the + /// console. + #[arg(short, long, global = true)] + pub debug: bool, + + /// Enables logging for all modules (not just `wdl-grammar`). + #[arg(short, long, global = true)] + pub log_all_modules: bool, /// Don't load any configuration from the cache. #[arg(short, long, global = true)] - no_cache: bool, + pub no_cache: bool, - /// Only errors are printed to the stderr stream. + /// Only errors are logged to the console. #[arg(short, long, global = true)] - quiet: bool, + pub quiet: bool, /// Overwrites the configuration file. #[arg(long, global = true)] - save_config: bool, + pub save_config: bool, /// Silences printing detailed error information. #[arg(long, global = true)] - silence_error_details: bool, + pub silence_error_details: bool, /// Skips the retreiving of remote objects. #[arg(long, global = true)] - skip_remote: bool, + pub skip_remote: bool, /// Displays warnings as part of the report output. #[arg(long, global = true)] - show_warnings: bool, + pub show_warnings: bool, /// The Workflow Description Language (WDL) specification version to use. #[arg(value_name = "VERSION", short = 's', long, default_value_t, value_enum)] - specification_version: grammar::Version, + pub specification_version: core::Version, + + /// All available information, including trace information, is logged in the + /// console. + #[arg(short, long, global = true)] + pub trace: bool, - /// All available information, including debug information, is logged. + /// Additional information is logged in the console. #[arg(short, long, global = true)] - verbose: bool, + pub verbose: bool, } /// Main function for this subcommand. @@ -127,7 +153,7 @@ pub async fn gauntlet(args: Args) -> Result<()> { }; if let Some(repositories) = args.repositories { - config.repositories_mut().extend( + config.inner_mut().extend_repositories( repositories .into_iter() .map(|value| { @@ -136,12 +162,12 @@ pub async fn gauntlet(args: Args) -> Result<()> { .map_err(Error::RepositoryIdentifier) }) .collect::>>()?, - ); + ) } - let mut report = Report::new(std::io::stdout().lock()); + let mut report = Report::from(std::io::stdout().lock()); - for (index, repository_identifier) in config.repositories().iter().enumerate() { + for (index, repository_identifier) in config.inner().repositories().iter().enumerate() { let mut repository = repository::Builder::default().identifier(repository_identifier.clone()); @@ -169,40 +195,43 @@ pub async fn gauntlet(args: Args) -> Result<()> { let document_identifier = document::Identifier::new(repository_identifier.clone(), path); - match config.version() { - grammar::Version::V1 => { - match grammar::v1::parse(grammar::v1::Rule::document, &content) { - Ok(tree) => match tree.warnings() { - Some(warnings) => { - trace!( - "{}: successfully parsed with {} warnings.", - document_identifier, - warnings.len() - ); - report - .register(document_identifier, Status::Warning) - .map_err(Error::InputOutput)?; + match config.inner().version() { + core::Version::V1 => { + let pt = match grammar::v1::parse(grammar::v1::Rule::document, &content) { + Ok(tree) => { + match tree.warnings() { + Some(warnings) => { + trace!( + "{}: successfully parsed with {} warnings.", + document_identifier, + warnings.len() + ); + report + .register(document_identifier, Status::Warning) + .map_err(Error::InputOutput)?; - if args.show_warnings { - for warning in warnings { - report - .report_warning(warning) - .map_err(Error::InputOutput)?; + if args.show_warnings { + for warning in warnings { + report + .report_warning(warning) + .map_err(Error::InputOutput)?; + } } } + None => { + trace!("{}: succesfully parsed.", document_identifier,); + report + .register(document_identifier, Status::Success) + .map_err(Error::InputOutput)?; + } } - None => { - trace!("{}: succesfully parsed.", document_identifier,); - report - .register(document_identifier, Status::Success) - .map_err(Error::InputOutput)?; - } - }, + tree + } Err(err) => { let actual_error = err.to_string(); if let Some(expected_error) = - config.ignored_errors().get(&document_identifier) + config.inner().ignored_errors().get(&document_identifier) { if expected_error == &actual_error { trace!( @@ -230,7 +259,21 @@ pub async fn gauntlet(args: Args) -> Result<()> { .register(document_identifier, Status::Error(actual_error)) .map_err(Error::InputOutput)?; } + + continue; } + }; + + let document = match ast::v1::parse(pt) { + Ok(document) => document, + Err(err) => { + eprint!("parse error: {err}"); + continue; + } + }; + + if let Some(workflow) = document.workflow() { + dbg!(workflow.statements()); } } } @@ -250,7 +293,7 @@ pub async fn gauntlet(args: Args) -> Result<()> { .map_err(Error::InputOutput)?; report.next_section().map_err(Error::InputOutput)?; - if index != config.repositories().len() - 1 { + if index != config.inner().repositories().len() - 1 { println!(); } } @@ -275,6 +318,7 @@ pub async fn gauntlet(args: Args) -> Result<()> { .collect::>(); let ignored_errors = config + .inner() .ignored_errors() .clone() .into_iter() @@ -290,13 +334,14 @@ pub async fn gauntlet(args: Args) -> Result<()> { undetected_ignored_errors.len() ); - *config.ignored_errors_mut() = (&ignored_errors - &undetected_ignored_errors) - .union(&unignored_errors) - .cloned() - .collect(); + config.inner_mut().replace_ignored_errors( + (&ignored_errors - &undetected_ignored_errors) + .union(&unignored_errors) + .cloned(), + ); } - config.ignored_errors_mut().extend( + config.inner_mut().extend_ignored_errors( unignored_errors .into_iter() .map(|(id, message)| (id.clone(), message.clone())) diff --git a/wdl-gauntlet/src/main.rs b/wdl-gauntlet/src/main.rs new file mode 100644 index 000000000..75980ea2c --- /dev/null +++ b/wdl-gauntlet/src/main.rs @@ -0,0 +1,65 @@ +//! A command-line tool for parsing Workflow Description Language (WDL) +//! documents. +//! +//! **Note:** this tool is intended to be used as a utility to test and develop +//! the [`wdl-grammar`](https://crates.io/crates/wdl-grammar) and +//! [`wdl-ast`](https://crates.io/crates/wdl-ast) crates. It is not intended to +//! be used by a general audience. + +#![warn(missing_docs)] +#![warn(rust_2018_idioms)] +#![warn(rust_2021_compatibility)] +#![warn(missing_debug_implementations)] +#![warn(clippy::missing_docs_in_private_items)] +#![warn(rustdoc::broken_intra_doc_links)] + +use clap::Parser as _; +use log::LevelFilter; + +use wdl_gauntlet as gauntlet; + +/// The inner function for `wdl-gauntlet`. +async fn inner() -> Result<(), Box> { + let args = gauntlet::Args::parse(); + + let level = if args.trace { + LevelFilter::max() + } else if args.debug { + LevelFilter::Debug + } else if args.verbose { + LevelFilter::Info + } else if args.quiet { + LevelFilter::Error + } else { + LevelFilter::Warn + }; + + let module = match args.log_all_modules { + true => None, + false => Some("wdl_gauntlet"), + }; + + env_logger::builder().filter(module, level).init(); + gauntlet::gauntlet(args).await?; + + Ok(()) +} + +#[tokio::main] +async fn main() { + match inner().await { + Ok(_) => {} + Err(err) => eprintln!("error: {}", err), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_arguments() { + use clap::CommandFactory; + gauntlet::Args::command().debug_assert() + } +} diff --git a/wdl-grammar/src/commands/gauntlet/report.rs b/wdl-gauntlet/src/report.rs similarity index 97% rename from wdl-grammar/src/commands/gauntlet/report.rs rename to wdl-gauntlet/src/report.rs index ce5687b62..3a5b90a62 100644 --- a/wdl-grammar/src/commands/gauntlet/report.rs +++ b/wdl-gauntlet/src/report.rs @@ -5,12 +5,12 @@ use std::collections::HashMap; use colored::Colorize as _; use indexmap::IndexMap; -use wdl_grammar as grammar; +use wdl_core as core; -use grammar::core::lint; +use core::lint; -use crate::commands::gauntlet::repository; -use crate::gauntlet::document; +use crate::document; +use crate::repository; /// The status of a single parsing test. #[derive(Clone, Debug, Eq, Hash, PartialEq)] @@ -93,7 +93,7 @@ impl std::fmt::Display for Status { /// A mapping between [document identifiers](document::Identifier) and the /// [status](Status) of their parsing test. -type Results = HashMap; +pub type Results = HashMap; /// A terminal-based report. #[derive(Debug)] @@ -120,16 +120,6 @@ pub struct Report { } impl Report { - /// Creates a new [`Report`] that points to a [writer](std::io::Write). - pub fn new(inner: T) -> Self { - Self { - inner, - section: Section::Title, - results: Default::default(), - printed: false, - } - } - /// Gets the [`Results`] for this [`Report`] by reference. /// /// **Note:** see the note at [Self::results] for a caveat on interpretation @@ -335,3 +325,14 @@ impl Report { Ok(()) } } + +impl From for Report { + fn from(inner: T) -> Self { + Self { + inner, + section: Section::Title, + results: Default::default(), + printed: false, + } + } +} diff --git a/wdl-grammar/src/commands/gauntlet/repository.rs b/wdl-gauntlet/src/repository.rs similarity index 100% rename from wdl-grammar/src/commands/gauntlet/repository.rs rename to wdl-gauntlet/src/repository.rs diff --git a/wdl-grammar/src/commands/gauntlet/repository/builder.rs b/wdl-gauntlet/src/repository/builder.rs similarity index 86% rename from wdl-grammar/src/commands/gauntlet/repository/builder.rs rename to wdl-gauntlet/src/repository/builder.rs index 9963a6ef5..2ebee7de5 100644 --- a/wdl-grammar/src/commands/gauntlet/repository/builder.rs +++ b/wdl-gauntlet/src/repository/builder.rs @@ -6,17 +6,17 @@ use std::path::PathBuf; use log::debug; use octocrab::Octocrab; -use crate::commands::gauntlet::config::default_config_dir; -use crate::commands::gauntlet::repository::cache; -use crate::commands::gauntlet::repository::cache::Cache; -use crate::commands::gauntlet::repository::options; -use crate::commands::gauntlet::repository::Identifier; -use crate::commands::gauntlet::repository::Options; -use crate::commands::gauntlet::Repository; +use crate::config::default_config_dir; +use crate::repository::cache; +use crate::repository::cache::Cache; +use crate::repository::options; +use crate::repository::Identifier; +use crate::repository::Options; +use crate::Repository; /// The environment variables within which a GitHub personal access token can be -/// stored. See (this -/// link)[https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens] +/// stored. See [this +/// link](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) /// for more details. const GITHUB_TOKEN_ENV: &[&str] = &["GITHUB_TOKEN", "GH_TOKEN"]; diff --git a/wdl-grammar/src/commands/gauntlet/repository/cache.rs b/wdl-gauntlet/src/repository/cache.rs similarity index 100% rename from wdl-grammar/src/commands/gauntlet/repository/cache.rs rename to wdl-gauntlet/src/repository/cache.rs diff --git a/wdl-grammar/src/commands/gauntlet/repository/cache/entry.rs b/wdl-gauntlet/src/repository/cache/entry.rs similarity index 100% rename from wdl-grammar/src/commands/gauntlet/repository/cache/entry.rs rename to wdl-gauntlet/src/repository/cache/entry.rs diff --git a/wdl-grammar/src/commands/gauntlet/repository/cache/registry.rs b/wdl-gauntlet/src/repository/cache/registry.rs similarity index 100% rename from wdl-grammar/src/commands/gauntlet/repository/cache/registry.rs rename to wdl-gauntlet/src/repository/cache/registry.rs diff --git a/wdl-grammar/src/commands/gauntlet/repository/identifier.rs b/wdl-gauntlet/src/repository/identifier.rs similarity index 100% rename from wdl-grammar/src/commands/gauntlet/repository/identifier.rs rename to wdl-gauntlet/src/repository/identifier.rs diff --git a/wdl-grammar/src/commands/gauntlet/repository/options.rs b/wdl-gauntlet/src/repository/options.rs similarity index 100% rename from wdl-grammar/src/commands/gauntlet/repository/options.rs rename to wdl-gauntlet/src/repository/options.rs diff --git a/wdl-grammar/src/commands/gauntlet/repository/options/builder.rs b/wdl-gauntlet/src/repository/options/builder.rs similarity index 82% rename from wdl-grammar/src/commands/gauntlet/repository/options/builder.rs rename to wdl-gauntlet/src/repository/options/builder.rs index 8d6a0e3f2..cb8a7c4c8 100644 --- a/wdl-grammar/src/commands/gauntlet/repository/options/builder.rs +++ b/wdl-gauntlet/src/repository/options/builder.rs @@ -1,6 +1,6 @@ //! A builder for an [`Options`]. -use crate::commands::gauntlet::repository::Options; +use crate::repository::Options; /// A builder for an [`Options`]. #[derive(Debug)] @@ -10,8 +10,7 @@ pub struct Builder { } impl Builder { - /// Sets whether or not the - /// [`Repository`](crate::commands::gauntlet::Repository) will hydrate + /// Sets whether or not the [`Repository`](crate::Repository) will hydrate /// itself from remote sources (or, in contrast, if it will rely purely on /// the local files it already has cached). pub fn hydrate_remote(mut self, value: bool) -> Self { diff --git a/wdl-grammar/Cargo.toml b/wdl-grammar/Cargo.toml index a8622e93f..db62551ff 100644 --- a/wdl-grammar/Cargo.toml +++ b/wdl-grammar/Cargo.toml @@ -10,36 +10,28 @@ repository = "https://github.com/stjude-rust-labs/wdl" documentation = "https://docs.rs/wdl-grammar" [dependencies] -async-recursion = { version = "1.0.5", optional = true } -chrono = { version = "0.4.31", optional = true } clap = { workspace = true, optional = true } -colored = { version = "2.0.4", optional = true } -dirs = { version = "5.0.1", optional = true } +colored = { workspace = true, optional = true } env_logger = { workspace = true, optional = true } indexmap = { workspace = true, optional = true } +lazy_static.workspace = true log = { workspace = true, optional = true } -octocrab = { version = "0.32.0", optional = true } pest = { workspace = true } pest_derive = { workspace = true } -reqwest = { version = "0.11.22", optional = true } -serde = { workspace = true } +serde = { workspace = true, optional = true } serde_with = { workspace = true, optional = true } -to_snake_case = "0.1.1" tokio = { workspace = true, optional = true } toml = { workspace = true, optional = true } +wdl-core = { path = "../wdl-core", version = "0.1.0" } [features] binaries = [ - "dep:async-recursion", - "dep:chrono", "dep:clap", "dep:colored", - "dep:dirs", "dep:env_logger", "dep:indexmap", "dep:log", - "dep:octocrab", - "dep:reqwest", + "dep:serde", "dep:serde_with", "dep:tokio", "dep:toml", diff --git a/wdl-grammar/src/commands.rs b/wdl-grammar/src/commands.rs index de7284ae1..515865d5d 100644 --- a/wdl-grammar/src/commands.rs +++ b/wdl-grammar/src/commands.rs @@ -3,7 +3,6 @@ use log::debug; pub mod create_test; -pub mod gauntlet; pub mod parse; /// An error common to any subcommand. diff --git a/wdl-grammar/src/commands/create_test.rs b/wdl-grammar/src/commands/create_test.rs index d6884d6e1..443e4eb70 100644 --- a/wdl-grammar/src/commands/create_test.rs +++ b/wdl-grammar/src/commands/create_test.rs @@ -5,6 +5,7 @@ use log::warn; use pest::iterators::Pair; use pest::RuleType; +use wdl_core as core; use wdl_grammar as grammar; use crate::commands::get_contents_stdin; @@ -27,7 +28,7 @@ pub enum Error { name: String, /// The grammar being used. - grammar: grammar::Version, + grammar: core::Version, }, } @@ -58,7 +59,7 @@ pub struct Args { /// The Workflow Description Language (WDL) specification version to use. #[arg(value_name = "VERSION", short = 's', long, default_value_t, value_enum)] - specification_version: grammar::Version, + specification_version: core::Version, /// The parser rule to evaluate. #[arg(value_name = "RULE", short = 'r', long, default_value = "document")] @@ -68,7 +69,7 @@ pub struct Args { /// Main function for this subcommand. pub fn create_test(args: Args) -> Result<()> { let rule = match args.specification_version { - grammar::Version::V1 => grammar::v1::get_rule(&args.rule) + core::Version::V1 => grammar::v1::get_rule(&args.rule) .map(Ok) .unwrap_or_else(|| { Err(Error::UnknownRule { @@ -83,8 +84,8 @@ pub fn create_test(args: Args) -> Result<()> { .map(Ok) .unwrap_or_else(|| get_contents_stdin().map_err(Error::Common))?; - let mut parse_tree = match args.specification_version { - grammar::Version::V1 => grammar::v1::parse(rule, &input).map_err(Error::GrammarV1)?, + let parse_tree = match args.specification_version { + core::Version::V1 => grammar::v1::parse(rule, &input).map_err(Error::GrammarV1)?, }; if let Some(warnings) = parse_tree.warnings() { @@ -98,7 +99,12 @@ pub fn create_test(args: Args) -> Result<()> { // completed. As such, we should always have at least one parsed // element. 0 => unreachable!(), - 1 => parse_tree.next().unwrap(), + 1 => { + let mut parse_tree = parse_tree.into_inner(); + // SAFETY: we just checked to ensure that exactly one element + // exists. Thus, this will always unwrap. + parse_tree.next().unwrap() + } _ => return Err(Error::MultipleRootNodes), }; diff --git a/wdl-grammar/src/commands/gauntlet/config/inner.rs b/wdl-grammar/src/commands/gauntlet/config/inner.rs deleted file mode 100644 index 0bb7b2747..000000000 --- a/wdl-grammar/src/commands/gauntlet/config/inner.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! An inner representation for the configuration object. -//! -//! This struct holds the configuration values. -use std::collections::HashMap; -use std::collections::HashSet; - -use serde::Deserialize; -use serde::Serialize; -use serde_with::serde_as; -use wdl_grammar as grammar; - -mod repr; - -pub use repr::ErrorsAsReprs; - -use crate::commands::gauntlet::document; -use crate::commands::gauntlet::repository; - -/// Parsing errors as [`String`]s associated with a [document -/// identifier](document::Identifier). -pub type Errors = HashMap; - -/// A unique set of [repository identifiers](repository::Identifier). -pub type Repositories = HashSet; - -/// The inner configuration object for a [`Config`](super::Config). -/// -/// This object stores the actual configuration values for this subcommand. -#[serde_as] -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct Inner { - /// The WDL version. - pub(super) version: grammar::Version, - - /// The repositories. - #[serde(default)] - pub(super) repositories: Repositories, - - /// The ignored errors. - #[serde_as(as = "ErrorsAsReprs")] - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub(super) ignored_errors: Errors, -} - -impl From for Inner { - fn from(version: grammar::Version) -> Self { - Self { - version, - repositories: Default::default(), - ignored_errors: Default::default(), - } - } -} diff --git a/wdl-grammar/src/commands/parse.rs b/wdl-grammar/src/commands/parse.rs index 0846273de..62db31526 100644 --- a/wdl-grammar/src/commands/parse.rs +++ b/wdl-grammar/src/commands/parse.rs @@ -3,6 +3,7 @@ use clap::Parser; use log::warn; +use wdl_core as core; use wdl_grammar as grammar; use crate::commands::get_contents_stdin; @@ -25,7 +26,7 @@ pub enum Error { name: String, /// The grammar being used. - grammar: grammar::Version, + grammar: core::Version, }, } @@ -58,7 +59,7 @@ pub struct Args { /// The Workflow Description Language (WDL) specification version to use. #[arg(value_name = "VERSION", short = 's', long, default_value_t, value_enum)] - specification_version: grammar::Version, + specification_version: core::Version, /// The parser rule to evaluate. #[arg(value_name = "RULE", short = 'r', long, default_value = "document")] @@ -72,7 +73,7 @@ pub struct Args { /// Main function for this subcommand. pub fn parse(args: Args) -> Result<()> { let rule = match args.specification_version { - grammar::Version::V1 => grammar::v1::get_rule(&args.rule) + core::Version::V1 => grammar::v1::get_rule(&args.rule) .map(Ok) .unwrap_or_else(|| { Err(Error::UnknownRule { @@ -87,8 +88,8 @@ pub fn parse(args: Args) -> Result<()> { .map(Ok) .unwrap_or_else(|| get_contents_stdin().map_err(Error::Common))?; - let mut parse_tree = match args.specification_version { - grammar::Version::V1 => grammar::v1::parse(rule, &input).map_err(Error::GrammarV1)?, + let parse_tree = match args.specification_version { + core::Version::V1 => grammar::v1::parse(rule, &input).map_err(Error::GrammarV1)?, }; if let Some(warnings) = parse_tree.warnings() { @@ -98,15 +99,18 @@ pub fn parse(args: Args) -> Result<()> { } if args.children_only { + let mut parse_tree = parse_tree.into_inner(); let children = match parse_tree.next() { Some(root) => root.into_inner(), None => return Err(Error::ChildrenOnlyWithEmptyParseTree), }; + // Note: this `dbg!()` statement is intended to be permanent. for child in children { dbg!(child); } } else { + // Note: this `dbg!()` statement is intended to be permanent. dbg!(parse_tree); }; diff --git a/wdl-grammar/src/common.rs b/wdl-grammar/src/common.rs new file mode 100644 index 000000000..ef883d1e8 --- /dev/null +++ b/wdl-grammar/src/common.rs @@ -0,0 +1,9 @@ +//! Common functionality used across all WDL grammar versions. + +mod linter; +mod tree; +mod validator; + +pub use linter::Linter; +pub use tree::Tree; +pub use validator::Validator; diff --git a/wdl-grammar/src/core/lint/linter.rs b/wdl-grammar/src/common/linter.rs similarity index 61% rename from wdl-grammar/src/core/lint/linter.rs rename to wdl-grammar/src/common/linter.rs index 0a3b14d2a..686176aab 100644 --- a/wdl-grammar/src/core/lint/linter.rs +++ b/wdl-grammar/src/common/linter.rs @@ -1,23 +1,29 @@ -//! Linters. +//! A parse tree linter. use pest::iterators::Pairs; -use pest::RuleType; +use wdl_core as core; -use crate::core::lint; -use crate::core::lint::Rule; -use crate::core::lint::Warning; +use core::lint; +use core::lint::Warning; -/// A [`Result`](std::result::Result) for the [`Linter::lint`] function. +/// A [`Result`](std::result::Result) for [`Linter::lint`]. pub type Result = std::result::Result>, Box>; -/// A linter for a WDL parse tree. +/// A parse tree linter. #[derive(Debug)] pub struct Linter; impl Linter { - /// Lints a WDL parse tree according to a set of lint rules. - pub fn lint(tree: Pairs<'_, R>, rules: &[Box>]) -> Result { - let warnings = rules + /// Lints a WDL parse tree according to a set of lint rules and returns a + /// set of lint warnings (if any are detected). + /// + /// **Note:** it would be much better to pass a reference to the parse tree + /// (`&Pairs<'a, R>`) here to avoid unnecessary cloning of the tree. + /// Unfortunately, the [`Pest`](https://pest.rs) library does not support a + /// reference to [`Pairs`] being turned into an iterator at the time of + /// writing. + pub fn lint(tree: Pairs<'_, crate::v1::Rule>) -> Result { + let warnings = crate::v1::lint::RULES .iter() .map(|rule| rule.check(tree.clone())) .collect::>>, Box>>()? @@ -45,8 +51,7 @@ mod tests { #[test] fn baseline() -> std::result::Result<(), Box> { let tree = Parser::parse(Rule::document, "version 1.1 \n \n")?; - let rules = crate::v1::lint::rules(); - let mut results = Linter::lint(tree, rules.as_ref())?.unwrap(); + let mut results = Linter::lint(tree)?.unwrap(); assert_eq!(results.len(), 2); assert_eq!( diff --git a/wdl-grammar/src/core/tree.rs b/wdl-grammar/src/common/tree.rs similarity index 87% rename from wdl-grammar/src/core/tree.rs rename to wdl-grammar/src/common/tree.rs index f005711d8..65e1f8994 100644 --- a/wdl-grammar/src/core/tree.rs +++ b/wdl-grammar/src/common/tree.rs @@ -3,7 +3,9 @@ use pest::iterators::Pairs; use pest::RuleType; -use crate::core::lint; +use wdl_core as core; + +use core::lint; /// A parse tree with a set of lint [`Warning`](lint::Warning)s. /// @@ -50,25 +52,19 @@ impl<'a, R: RuleType> std::ops::Deref for Tree<'a, R> { } } -impl<'a, R: RuleType> std::ops::DerefMut for Tree<'a, R> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - #[cfg(test)] mod tests { use pest::Parser as _; use super::*; - use crate::core::lint::Linter; + use crate::common::Linter; use crate::v1::Parser; use crate::v1::Rule; #[test] fn new() -> Result<(), Box> { let tree = Parser::parse(Rule::document, "version 1.1\n \n")?; - let lints = Linter::lint(tree.clone(), &crate::v1::lint::rules())?; + let lints = Linter::lint(tree.clone())?; let tree = Tree::new(tree, lints); assert_eq!( diff --git a/wdl-grammar/src/core/validation/validator.rs b/wdl-grammar/src/common/validator.rs similarity index 66% rename from wdl-grammar/src/core/validation/validator.rs rename to wdl-grammar/src/common/validator.rs index 3cdf4ebb7..7b6d21ddc 100644 --- a/wdl-grammar/src/core/validation/validator.rs +++ b/wdl-grammar/src/common/validator.rs @@ -1,22 +1,19 @@ -//! Validators. +//! A parse tree validator. use pest::iterators::Pairs; -use pest::RuleType; -use crate::core::validation; -use crate::core::validation::Rule; +use wdl_core as core; -/// A validator for a WDL parse tree. +use core::validation; + +/// A parse tree validator. #[derive(Debug)] pub struct Validator; impl Validator { /// Validates a WDL parse tree according to a set of validation rules. - pub fn validate( - tree: Pairs<'_, R>, - rules: &[Box>], - ) -> validation::Result { - rules + pub fn validate(tree: Pairs<'_, crate::v1::Rule>) -> validation::Result { + crate::v1::validation::RULES .iter() .try_for_each(|rule| rule.validate(tree.clone())) } @@ -42,8 +39,8 @@ task test { } }", )?; - let rules = crate::v1::validation::rules(); - let err = Validator::validate(tree, rules.as_ref()).unwrap_err(); + + let err = Validator::validate(tree).unwrap_err(); assert_eq!( err.to_string(), String::from("[v1::001] invalid escape character '\\.' in string at line 4:25") diff --git a/wdl-grammar/src/core/lint.rs b/wdl-grammar/src/core/lint.rs deleted file mode 100644 index f658d0c56..000000000 --- a/wdl-grammar/src/core/lint.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Linting. - -use pest::iterators::Pairs; -use pest::RuleType; -use to_snake_case::ToSnakeCase as _; - -mod group; -mod level; -mod linter; -pub mod warning; - -pub use group::Group; -pub use level::Level; -pub use linter::Linter; -pub use warning::Warning; - -use crate::core::Code; - -/// A [`Result`](std::result::Result) returned from a lint check. -pub type Result = std::result::Result>, Box>; - -/// A lint rule. -pub trait Rule: std::fmt::Debug { - /// The name of the lint rule. - /// - /// This is what will show up in style guides, it is required to be snake - /// case (even though the rust struct is camel case). - fn name(&self) -> String { - format!("{:?}", self).to_snake_case() - } - - /// Get the code for this lint rule. - fn code(&self) -> Code; - - /// Get the lint group for this lint rule. - fn group(&self) -> Group; - - /// Checks the parse tree according to the implemented lint rule. - /// - /// **Note:** it would be much better to pass a reference to the parse tree - /// (`&Pairs<'a, R>`) here to avoid unnecessary cloning of the tree. - /// Unfortunately, the [`Pest`](https://pest.rs) library does not support a - /// reference to [`Pairs`] being turned into an iterator at the moment. - fn check(&self, tree: Pairs<'_, R>) -> Result; -} diff --git a/wdl-grammar/src/core/validation.rs b/wdl-grammar/src/core/validation.rs deleted file mode 100644 index 7bbf81e16..000000000 --- a/wdl-grammar/src/core/validation.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Validation. - -use pest::iterators::Pairs; -use pest::RuleType; -use to_snake_case::ToSnakeCase as _; - -pub mod error; -pub mod validator; - -pub use error::Error; -pub use validator::Validator; - -use crate::core::Code; - -/// A [`Result`](std::result::Result) with a validation [`Error`]. -pub type Result = std::result::Result<(), Error>; - -/// A validation rule. -pub trait Rule: std::fmt::Debug { - /// The name of the validation rule. - /// - /// This is what will show up in style guides, it is required to be snake - /// case (even though the rust struct is camel case). - fn name(&self) -> String { - format!("{:?}", self).to_snake_case() - } - - /// Get the code for this validation rule. - fn code(&self) -> Code; - - /// Checks the parse tree according to the implemented validation rule. - /// - /// **Note:** it would be much better to pass a reference to the parse tree - /// (`&Pairs<'a, R>`) here to avoid unnecessary cloning of the tree. - /// Unfortunately, the [`Pest`](https://pest.rs) library does not support a - /// reference to [`Pairs`] being turned into an iterator at the moment. - fn validate(&self, tree: Pairs<'_, R>) -> Result; -} diff --git a/wdl-grammar/src/lib.rs b/wdl-grammar/src/lib.rs index 13ef10224..c1e7e0f26 100644 --- a/wdl-grammar/src/lib.rs +++ b/wdl-grammar/src/lib.rs @@ -11,11 +11,10 @@ use pest::RuleType; -pub mod core; -pub mod v1; -mod version; +use wdl_core as core; -pub use version::Version; +pub mod common; +pub mod v1; /// An error that can occur when parsing. /// @@ -25,6 +24,9 @@ pub use version::Version; #[derive(Debug)] pub enum Error { /// An error occurred while linting a parse tree. + /// + /// **Note:** this is not a lint _warning_! A lint error is an unrecoverable + /// error that occurs during the process of linting. Lint(Box), /// An error occurred while Pest was parsing the parse tree. diff --git a/wdl-grammar/src/main.rs b/wdl-grammar/src/main.rs index f82b972b9..6e578292a 100644 --- a/wdl-grammar/src/main.rs +++ b/wdl-grammar/src/main.rs @@ -20,7 +20,6 @@ use log::LevelFilter; mod commands; use crate::commands::create_test; -use crate::commands::gauntlet; use crate::commands::parse; /// Subcommands for the `wdl-grammar` command-line tool. @@ -29,9 +28,6 @@ pub enum Command { /// Creates a test for a given input and grammar rule. CreateTest(create_test::Args), - /// Performs a gauntlet of parsing tests. - Gauntlet(gauntlet::Args), - /// Parses an input according to the specified grammar rule. Parse(parse::Args), } @@ -92,7 +88,6 @@ async fn inner() -> Result<(), Box> { match args.command { Command::CreateTest(args) => create_test::create_test(args)?, - Command::Gauntlet(args) => gauntlet::gauntlet(args).await?, Command::Parse(args) => parse::parse(args)?, }; diff --git a/wdl-grammar/src/v1.rs b/wdl-grammar/src/v1.rs index 8fc5e8c2b..3638ee88d 100644 --- a/wdl-grammar/src/v1.rs +++ b/wdl-grammar/src/v1.rs @@ -11,9 +11,9 @@ use pest::Parser as _; -use crate::core::lint::Linter; -use crate::core::validation::Validator; -use crate::core::Tree; +use crate::common::Linter; +use crate::common::Tree; +use crate::common::Validator; use crate::Error; use crate::Result; @@ -79,13 +79,11 @@ pub fn parse(rule: Rule, input: &str) -> Result, Rule> { .map_err(Box::new) .map_err(Error::Parse)?; - let validations = validation::rules(); - Validator::validate(tree.clone(), validations.as_ref()) + Validator::validate(tree.clone()) .map_err(Box::new) .map_err(Error::Validation)?; - let lints = lint::rules(); - let warnings = Linter::lint(tree.clone(), lints.as_ref()).map_err(Error::Lint)?; + let warnings = Linter::lint(tree.clone()).map_err(Error::Lint)?; Ok(Tree::new(tree, warnings)) } @@ -101,7 +99,7 @@ pub fn parse(rule: Rule, input: &str) -> Result, Rule> { /// assert!(matches!(rule, Some(_))); /// /// let rule = wdl::v1::get_rule("foo-bar-baz-rule"); -/// assert!(!matches!(rule, Some(_))); +/// assert!(matches!(rule, None)); /// ``` pub fn get_rule(rule: &str) -> Option { for candidate in Rule::all_rules() { diff --git a/wdl-grammar/src/v1/lint.rs b/wdl-grammar/src/v1/lint.rs index 14dfcf68c..35592dcd5 100644 --- a/wdl-grammar/src/v1/lint.rs +++ b/wdl-grammar/src/v1/lint.rs @@ -1,4 +1,12 @@ -//! Lint rules for WDL 1.x. +//! Lint rules for WDL 1.x parse trees. + +use lazy_static::lazy_static; + +use pest::iterators::Pairs; + +use wdl_core as core; + +use crate::v1; mod no_curly_commands; mod whitespace; @@ -6,10 +14,12 @@ mod whitespace; pub use no_curly_commands::NoCurlyCommands; pub use whitespace::Whitespace; -use crate::core::lint::Rule; -use crate::v1; +/// A boxed lint rule for the v1 grammar. +type Rule = Box core::lint::Rule>>; -/// Gets all lint rules available for WDL 1.x. -pub fn rules() -> Vec>> { - vec![Box::new(Whitespace), Box::new(NoCurlyCommands)] +lazy_static! { + /// All lint rules available for WDL 1.x parse trees. + pub static ref RULES: Vec = vec![ + Box::new(Whitespace), Box::new(NoCurlyCommands) + ]; } diff --git a/wdl-grammar/src/v1/lint/no_curly_commands.rs b/wdl-grammar/src/v1/lint/no_curly_commands.rs index e05d9ad78..0ef922c0f 100644 --- a/wdl-grammar/src/v1/lint/no_curly_commands.rs +++ b/wdl-grammar/src/v1/lint/no_curly_commands.rs @@ -1,16 +1,18 @@ //! Replace curly command blocks with heredoc command blocks. +use core::Version; use std::num::NonZeroUsize; use pest::iterators::Pairs; -use crate::core::lint; -use crate::core::lint::Group; -use crate::core::lint::Rule; -use crate::core::Code; -use crate::core::Location; +use wdl_core as core; + use crate::v1; -use crate::Version; +use core::lint; +use core::lint::Group; +use core::lint::Rule; +use core::Code; +use core::Location; /// Replace curly command blocks with heredoc command blocks. /// @@ -20,11 +22,11 @@ use crate::Version; #[derive(Debug)] pub struct NoCurlyCommands; -impl NoCurlyCommands { +impl<'a> NoCurlyCommands { /// Creates an error corresponding to a line with a trailing tab. fn no_curly_commands(&self, line_no: NonZeroUsize, col_no: NonZeroUsize) -> lint::Warning where - Self: Rule, + Self: Rule>, { // SAFETY: this error is written so that it will always unwrap. lint::warning::Builder::default() @@ -43,7 +45,7 @@ impl NoCurlyCommands { } } -impl Rule for NoCurlyCommands { +impl<'a> Rule> for NoCurlyCommands { fn code(&self) -> Code { // SAFETY: this manually crafted to unwrap successfully every time. Code::try_new(Version::V1, 2).unwrap() @@ -77,9 +79,9 @@ impl Rule for NoCurlyCommands { mod tests { use pest::Parser as _; - use crate::core::lint::Rule as _; use crate::v1::parse::Parser; use crate::v1::Rule; + use wdl_core::lint::Rule as _; use super::*; diff --git a/wdl-grammar/src/v1/lint/whitespace.rs b/wdl-grammar/src/v1/lint/whitespace.rs index 2db58242b..4a3209eca 100644 --- a/wdl-grammar/src/v1/lint/whitespace.rs +++ b/wdl-grammar/src/v1/lint/whitespace.rs @@ -1,27 +1,29 @@ //! Various lints for undesired whitespace. +use core::Version; use std::num::NonZeroUsize; use pest::iterators::Pairs; -use crate::core::lint; -use crate::core::lint::Group; -use crate::core::lint::Rule; -use crate::core::Code; -use crate::core::Location; +use wdl_core as core; + use crate::v1; -use crate::Version; +use core::lint; +use core::lint::Group; +use core::lint::Rule; +use core::Code; +use core::Location; /// Various lints for undesired whitespace. #[derive(Debug)] pub struct Whitespace; -impl Whitespace { +impl<'a> Whitespace { /// Creates an error corresponding to a line being filled only with blank /// spaces. fn empty_line(&self, line_no: NonZeroUsize) -> lint::Warning where - Self: Rule, + Self: Rule>, { // SAFETY: this error is written so that it will always unwrap. lint::warning::Builder::default() @@ -42,7 +44,7 @@ impl Whitespace { /// Creates an error corresponding to a line with a trailing space. fn trailing_space(&self, line_no: NonZeroUsize, col_no: NonZeroUsize) -> lint::Warning where - Self: Rule, + Self: Rule>, { // SAFETY: this error is written so that it will always unwrap. lint::warning::Builder::default() @@ -65,7 +67,7 @@ impl Whitespace { /// Creates an error corresponding to a line with a trailing tab. fn trailing_tab(&self, line_no: NonZeroUsize, col_no: NonZeroUsize) -> lint::Warning where - Self: Rule, + Self: Rule>, { // SAFETY: this error is written so that it will always unwrap. lint::warning::Builder::default() @@ -86,7 +88,7 @@ impl Whitespace { } } -impl Rule for Whitespace { +impl<'a> Rule> for Whitespace { fn code(&self) -> Code { // SAFETY: this manually crafted to unwrap successfully every time. Code::try_new(Version::V1, 1).unwrap() @@ -96,7 +98,7 @@ impl Rule for Whitespace { Group::Style } - fn check(&self, tree: Pairs<'_, v1::Rule>) -> lint::Result { + fn check(&self, tree: Pairs<'a, v1::Rule>) -> lint::Result { let mut results = Vec::new(); for (i, line) in tree.as_str().lines().enumerate() { @@ -136,9 +138,9 @@ impl Rule for Whitespace { mod tests { use pest::Parser as _; - use crate::core::lint::Rule as _; use crate::v1::parse::Parser; use crate::v1::Rule; + use wdl_core::lint::Rule as _; use super::*; diff --git a/wdl-grammar/src/v1/parse.rs b/wdl-grammar/src/v1/parse.rs index e79acd8c6..f7b893eb2 100644 --- a/wdl-grammar/src/v1/parse.rs +++ b/wdl-grammar/src/v1/parse.rs @@ -5,11 +5,11 @@ use pest_derive::Parser; /// A Pest [`pest::Parser`] for the WDL 1.x grammar. /// -/// **Note:** this [`Parser`] is not exposed directly to the user. Instead, you -/// should use the provided [`parse`] method, which performs additional -/// validation outside of the PEG grammar itself (the choice was made to do some -/// validation outside of the PEG grammar to give users better error messages in -/// some use cases). +/// **Note:** this [`Parser`](struct@Parser) is not exposed directly to the +/// user. Instead, you should use the provided [`parse()`](crate::v1::parse()) +/// method, which performs additional validation outside of the PEG grammar +/// itself (the choice was made to do some validation outside of the PEG grammar +/// to give users better error messages in some use cases). #[derive(Debug, Parser)] #[grammar = "v1/wdl.pest"] pub(crate) struct Parser; diff --git a/wdl-grammar/src/v1/tests/expression.rs b/wdl-grammar/src/v1/tests/expression.rs index ba917e565..755cc46b5 100644 --- a/wdl-grammar/src/v1/tests/expression.rs +++ b/wdl-grammar/src/v1/tests/expression.rs @@ -4,7 +4,7 @@ use pest::parses_to; use crate::v1::Parser as WdlParser; use crate::v1::Rule; -mod core; +mod atom; mod infix; mod prefix; mod suffix; @@ -181,7 +181,7 @@ else // `a` identifier_based_kv_key(69, 70, [ // `a` - identifier(69, 70), + singular_identifier(69, 70), ]), WHITESPACE(71, 72, [ SPACE(71, 72), @@ -199,7 +199,7 @@ else // `.a` member(77, 79, [ // `a` - identifier(78, 79), + singular_identifier(78, 79), ]), WHITESPACE(79, 80, [ SPACE(79, 80), @@ -351,11 +351,11 @@ else // `-struct {b: 10}.b` expression(140, 157, [ // `-` - unary_signed(140, 141), + unary_signed_negative(140, 141), // `struct {b: 10}` struct_literal(141, 155, [ // `struct` - identifier(141, 147), + singular_identifier(141, 147), WHITESPACE(147, 148, [ SPACE(147, 148), ]), @@ -364,7 +364,7 @@ else // `b` identifier_based_kv_key(149, 150, [ // `b` - identifier(149, 150), + singular_identifier(149, 150), ]), WHITESPACE(151, 152, [ SPACE(151, 152), @@ -385,7 +385,7 @@ else // `.b` member(155, 157, [ // `b` - identifier(156, 157), + singular_identifier(156, 157), ]), ]), ]), @@ -569,7 +569,7 @@ else // `-3e+10` expression(229, 235, [ // `-` - unary_signed(229, 230), + unary_signed_negative(229, 230), // `3e+10` float(230, 235, [ // `3e+10` @@ -578,11 +578,11 @@ else ]), ]), // `(zulu)` - apply(236, 242, [ + call(236, 242, [ // `zulu` expression(237, 241, [ // `zulu` - identifier(237, 241), + singular_identifier(237, 241), ]), ]), ]), diff --git a/wdl-grammar/src/v1/tests/expression/core.rs b/wdl-grammar/src/v1/tests/expression/atom.rs similarity index 66% rename from wdl-grammar/src/v1/tests/expression/core.rs rename to wdl-grammar/src/v1/tests/expression/atom.rs index 1a6443c1d..5924118e7 100644 --- a/wdl-grammar/src/v1/tests/expression/core.rs +++ b/wdl-grammar/src/v1/tests/expression/atom.rs @@ -15,52 +15,67 @@ mod struct_literal; #[test] fn it_successfully_parses_an_array_literal_with_spaces_inside() { parses_to! { - parser: WdlParser, - input: "[if a then b else c, \"Hello, world!\"]", - rule: Rule::core, - tokens: [ - // `[if a then b else c, "Hello, world!"]` - array_literal(0, 37, [ - // `if a then b else c` - expression(1, 19, [ - // `if a then b else c` - r#if(1, 19, [ - WHITESPACE(3, 4, [SPACE(3, 4)]), - // `a` - expression(4, 5, [ - // `a` - identifier(4, 5), - ]), - WHITESPACE(5, 6, [SPACE(5, 6)]), - WHITESPACE(10, 11, [SPACE(10, 11)]), - // `b` - expression(11, 12, [ - // `b` - identifier(11, 12), - ]), - WHITESPACE(12, 13, [SPACE(12, 13)]), - WHITESPACE(17, 18, [SPACE(17, 18)]), - // `c` - expression(18, 19, [ - // `c` - identifier(18, 19), - ]), - ]), + parser: WdlParser, + input: "[if a then b else c, \"Hello, world!\"]", + rule: Rule::atom, + tokens: [ + // `[if a then b else c, "Hello, world!"]` + array_literal(0, 37, [ + // `if a then b else c` + expression(1, 19, [ + // `if a then b else c` + r#if(1, 19, [ + WHITESPACE(3, 4, [ + SPACE(3, 4), ]), - // `,` - COMMA(19, 20), - WHITESPACE(20, 21, [SPACE(20, 21)]), - // `"Hello, world!"` - expression(21, 36, [ - // `"Hello, world!"` - string(21, 36, [ - // `"` - double_quote(21, 22), - // `Hello, world!` - string_literal_contents(22, 35), - ]), + // `a` + expression(4, 5, [ + // `a` + singular_identifier(4, 5), ]), - ]) + WHITESPACE(5, 6, [ + SPACE(5, 6), + ]), + WHITESPACE(10, 11, [ + SPACE(10, 11), + ]), + // `b` + expression(11, 12, [ + // `b` + singular_identifier(11, 12), + ]), + WHITESPACE(12, 13, [ + SPACE(12, 13), + ]), + WHITESPACE(17, 18, [ + SPACE(17, 18), + ]), + // `c` + expression(18, 19, [ + // `c` + singular_identifier(18, 19), + ]), + ]), + ]), + // `,` + COMMA(19, 20), + WHITESPACE(20, 21, [ + SPACE(20, 21), + ]), + // `"Hello, world!"` + expression(21, 36, [ + // `"Hello, world!"` + string(21, 36, [ + // `"` + double_quote(21, 22), + // `Hello, world!` + string_inner(22, 35, [ + // `Hello, world!` + string_literal_contents(22, 35), + ]), + ]), + ]), + ]) ] } } @@ -70,12 +85,12 @@ fn it_successfully_parses_a_group_with_spaces() { parses_to! { parser: WdlParser, input: "( hello )", - rule: Rule::core, + rule: Rule::atom, tokens: [ group(0, 9, [ WHITESPACE(1, 2, [SPACE(1, 2)]), expression(2, 7, [ - identifier(2, 7) + singular_identifier(2, 7) ]), WHITESPACE(7, 8, [SPACE(7, 8)]), ]) @@ -92,7 +107,7 @@ fn it_successfully_parses_a_group_without_including_the_trailing_space() { tokens: [ group(0, 7, [ expression(1, 6, [ - identifier(1, 6) + singular_identifier(1, 6) ]), ]) ] @@ -104,7 +119,7 @@ fn it_successfully_parses_an_if_statement() { parses_to! { parser: WdlParser, input: "if true then a else b", - rule: Rule::core, + rule: Rule::atom, tokens: [ r#if(0, 21, [ WHITESPACE(2, 3, [SPACE(2, 3)]), @@ -114,12 +129,12 @@ fn it_successfully_parses_an_if_statement() { WHITESPACE(7, 8, [SPACE(7, 8)]), WHITESPACE(12, 13, [SPACE(12, 13)]), expression(13, 14, [ - identifier(13, 14) + singular_identifier(13, 14) ]), WHITESPACE(14, 15, [SPACE(14, 15)]), WHITESPACE(19, 20, [SPACE(19, 20)]), expression(20, 21, [ - identifier(20, 21) + singular_identifier(20, 21) ]), ]) ] @@ -131,7 +146,7 @@ fn it_successfully_parses_a_map_with_an_expression_as_the_key() { parses_to! { parser: WdlParser, input: "{ if a then b else c : true }", - rule: Rule::core, + rule: Rule::atom, tokens: [ map_literal(0, 29, [ WHITESPACE(1, 2, [SPACE(1, 2)]), @@ -141,17 +156,17 @@ fn it_successfully_parses_a_map_with_an_expression_as_the_key() { r#if(2, 20, [ WHITESPACE(4, 5, [SPACE(4, 5)]), expression(5, 6, [ - identifier(5, 6) + singular_identifier(5, 6) ]), WHITESPACE(6, 7, [SPACE(6, 7)]), WHITESPACE(11, 12, [SPACE(11, 12)]), expression(12, 13, [ - identifier(12, 13) + singular_identifier(12, 13) ]), WHITESPACE(13, 14, [SPACE(13, 14)]), WHITESPACE(18, 19, [SPACE(18, 19)]), expression(19, 20, [ - identifier(19, 20) + singular_identifier(19, 20) ]), ]) ]) @@ -175,14 +190,14 @@ fn it_successfully_parses_an_object_literal_with_spaces_inside_and_a_comma() { parses_to! { parser: WdlParser, input: "object { hello : true, }", - rule: Rule::core, + rule: Rule::atom, tokens: [ object_literal(0, 24, [ WHITESPACE(6, 7, [SPACE(6, 7)]), WHITESPACE(8, 9, [SPACE(8, 9)]), identifier_based_kv_pair(9, 21, [ identifier_based_kv_key(9, 14, [ - identifier(9, 14) + singular_identifier(9, 14) ]), WHITESPACE(14, 15, [SPACE(14, 15)]), WHITESPACE(16, 17, [SPACE(16, 17)]), @@ -204,16 +219,16 @@ fn it_successfully_parses_a_pair_literal_with_spaces_inside() { parses_to! { parser: WdlParser, input: "(a, b)", - rule: Rule::core, + rule: Rule::atom, tokens: [ pair_literal(0, 6, [ expression(1, 2, [ - identifier(1, 2) + singular_identifier(1, 2) ]), COMMA(2, 3), WHITESPACE(3, 4, [SPACE(3, 4)]), expression(4, 5, [ - identifier(4, 5) + singular_identifier(4, 5) ]), ]) ] @@ -225,13 +240,13 @@ fn it_successfully_parses_a_struct_literal_with_a_comma() { parses_to! { parser: WdlParser, input: "struct{hello:true,}", - rule: Rule::core, + rule: Rule::atom, tokens: [ struct_literal(0, 19, [ - identifier(0, 6), + singular_identifier(0, 6), identifier_based_kv_pair(7, 17, [ identifier_based_kv_key(7, 12, [ - identifier(7, 12), + singular_identifier(7, 12), ]), kv_value(13, 17, [ expression(13, 17, [ diff --git a/wdl-grammar/src/v1/tests/expression/core/array_literal.rs b/wdl-grammar/src/v1/tests/expression/atom/array_literal.rs similarity index 53% rename from wdl-grammar/src/v1/tests/expression/core/array_literal.rs rename to wdl-grammar/src/v1/tests/expression/atom/array_literal.rs index 8753ceaf4..aedd11641 100644 --- a/wdl-grammar/src/v1/tests/expression/core/array_literal.rs +++ b/wdl-grammar/src/v1/tests/expression/atom/array_literal.rs @@ -36,57 +36,60 @@ fn it_successfully_parses_an_array_literal() { input: "[if a then b else c,\"Hello, world!\"]", rule: Rule::array_literal, tokens: [ - // `[if a then b else c,"Hello, world!"]` - array_literal(0, 36, [ + // `[if a then b else c,"Hello, world!"]` + array_literal(0, 36, [ + // `if a then b else c` + expression(1, 19, [ // `if a then b else c` - expression(1, 19, [ - // `if a then b else c` - r#if(1, 19, [ - WHITESPACE(3, 4, [ - SPACE(3, 4), - ]), + r#if(1, 19, [ + WHITESPACE(3, 4, [ + SPACE(3, 4), + ]), + // `a` + expression(4, 5, [ // `a` - expression(4, 5, [ - // `a` - identifier(4, 5), - ]), - WHITESPACE(5, 6, [ - SPACE(5, 6), - ]), - WHITESPACE(10, 11, [ - SPACE(10, 11), - ]), + singular_identifier(4, 5), + ]), + WHITESPACE(5, 6, [ + SPACE(5, 6), + ]), + WHITESPACE(10, 11, [ + SPACE(10, 11), + ]), + // `b` + expression(11, 12, [ // `b` - expression(11, 12, [ - // `b` - identifier(11, 12), - ]), - WHITESPACE(12, 13, [ - SPACE(12, 13), - ]), - WHITESPACE(17, 18, [ - SPACE(17, 18), - ]), + singular_identifier(11, 12), + ]), + WHITESPACE(12, 13, [ + SPACE(12, 13), + ]), + WHITESPACE(17, 18, [ + SPACE(17, 18), + ]), + // `c` + expression(18, 19, [ // `c` - expression(18, 19, [ - // `c` - identifier(18, 19), - ]), + singular_identifier(18, 19), ]), ]), - // `,` - COMMA(19, 20), + ]), + // `,` + COMMA(19, 20), + // `"Hello, world!"` + expression(20, 35, [ // `"Hello, world!"` - expression(20, 35, [ - // `"Hello, world!"` - string(20, 35, [ - // `"` - double_quote(20, 21), + string(20, 35, [ + // `"` + double_quote(20, 21), + // `Hello, world!` + string_inner(21, 34, [ // `Hello, world!` string_literal_contents(21, 34), ]), ]), - ]) + ]), + ]) ] } } @@ -110,7 +113,7 @@ fn it_successfully_parses_an_array_literal_without_the_trailing_space() { // `a` expression(4, 5, [ // `a` - identifier(4, 5), + singular_identifier(4, 5), ]), WHITESPACE(5, 6, [ SPACE(5, 6), @@ -121,7 +124,7 @@ fn it_successfully_parses_an_array_literal_without_the_trailing_space() { // `b` expression(11, 12, [ // `b` - identifier(11, 12), + singular_identifier(11, 12), ]), WHITESPACE(12, 13, [ SPACE(12, 13), @@ -132,7 +135,7 @@ fn it_successfully_parses_an_array_literal_without_the_trailing_space() { // `c` expression(18, 19, [ // `c` - identifier(18, 19), + singular_identifier(18, 19), ]), ]), ]), @@ -148,7 +151,10 @@ fn it_successfully_parses_an_array_literal_without_the_trailing_space() { // `"` double_quote(21, 22), // `Hello, world!` - string_literal_contents(22, 35), + string_inner(22, 35, [ + // `Hello, world!` + string_literal_contents(22, 35), + ]), ]), ]), ]) @@ -163,60 +169,63 @@ fn it_successfully_parses_an_array_literal_with_spaces_inside() { input: "[if a then b else c, \"Hello, world!\"]", rule: Rule::array_literal, tokens: [ - // `[if a then b else c, "Hello, world!"]` - array_literal(0, 37, [ + // `[if a then b else c, "Hello, world!"]` + array_literal(0, 37, [ + // `if a then b else c` + expression(1, 19, [ // `if a then b else c` - expression(1, 19, [ - // `if a then b else c` - r#if(1, 19, [ - WHITESPACE(3, 4, [ - SPACE(3, 4), - ]), + r#if(1, 19, [ + WHITESPACE(3, 4, [ + SPACE(3, 4), + ]), + // `a` + expression(4, 5, [ // `a` - expression(4, 5, [ - // `a` - identifier(4, 5), - ]), - WHITESPACE(5, 6, [ - SPACE(5, 6), - ]), - WHITESPACE(10, 11, [ - SPACE(10, 11), - ]), + singular_identifier(4, 5), + ]), + WHITESPACE(5, 6, [ + SPACE(5, 6), + ]), + WHITESPACE(10, 11, [ + SPACE(10, 11), + ]), + // `b` + expression(11, 12, [ // `b` - expression(11, 12, [ - // `b` - identifier(11, 12), - ]), - WHITESPACE(12, 13, [ - SPACE(12, 13), - ]), - WHITESPACE(17, 18, [ - SPACE(17, 18), - ]), + singular_identifier(11, 12), + ]), + WHITESPACE(12, 13, [ + SPACE(12, 13), + ]), + WHITESPACE(17, 18, [ + SPACE(17, 18), + ]), + // `c` + expression(18, 19, [ // `c` - expression(18, 19, [ - // `c` - identifier(18, 19), - ]), + singular_identifier(18, 19), ]), ]), - // `,` - COMMA(19, 20), - WHITESPACE(20, 21, [ - SPACE(20, 21), - ]), + ]), + // `,` + COMMA(19, 20), + WHITESPACE(20, 21, [ + SPACE(20, 21), + ]), + // `"Hello, world!"` + expression(21, 36, [ // `"Hello, world!"` - expression(21, 36, [ - // `"Hello, world!"` - string(21, 36, [ - // `"` - double_quote(21, 22), + string(21, 36, [ + // `"` + double_quote(21, 22), + // `Hello, world!` + string_inner(22, 35, [ // `Hello, world!` string_literal_contents(22, 35), ]), ]), - ]) + ]), + ]) ] } } diff --git a/wdl-grammar/src/v1/tests/expression/core/group.rs b/wdl-grammar/src/v1/tests/expression/atom/group.rs similarity index 93% rename from wdl-grammar/src/v1/tests/expression/core/group.rs rename to wdl-grammar/src/v1/tests/expression/atom/group.rs index 1a1baf5ee..b966db357 100644 --- a/wdl-grammar/src/v1/tests/expression/core/group.rs +++ b/wdl-grammar/src/v1/tests/expression/atom/group.rs @@ -52,7 +52,7 @@ fn it_successfully_parses_a_group() { tokens: [ group(0, 7, [ expression(1, 6, [ - identifier(1, 6) + singular_identifier(1, 6) ]), ]) ] @@ -69,7 +69,7 @@ fn it_successfully_parses_a_group_with_spaces() { group(0, 9, [ WHITESPACE(1, 2, [SPACE(1, 2)]), expression(2, 7, [ - identifier(2, 7) + singular_identifier(2, 7) ]), WHITESPACE(7, 8, [SPACE(7, 8)]), ]) @@ -86,7 +86,7 @@ fn it_successfully_parses_a_group_without_including_the_trailing_space() { tokens: [ group(0, 7, [ expression(1, 6, [ - identifier(1, 6) + singular_identifier(1, 6) ]), ]) ] diff --git a/wdl-grammar/src/v1/tests/expression/core/if.rs b/wdl-grammar/src/v1/tests/expression/atom/if.rs similarity index 94% rename from wdl-grammar/src/v1/tests/expression/core/if.rs rename to wdl-grammar/src/v1/tests/expression/atom/if.rs index b203fe8ee..fa1b5067f 100644 --- a/wdl-grammar/src/v1/tests/expression/core/if.rs +++ b/wdl-grammar/src/v1/tests/expression/atom/if.rs @@ -77,7 +77,7 @@ fn it_fails_to_parse_an_if_statement_with_no_spaces_between_the_expression_and_t Rule::gt, Rule::member, Rule::index, - Rule::apply, + Rule::call, ], negatives: vec![], pos: 19 @@ -123,12 +123,12 @@ fn it_successfully_parses_an_if_statement() { WHITESPACE(7, 8, [SPACE(7, 8)]), WHITESPACE(12, 13, [SPACE(12, 13)]), expression(13, 14, [ - identifier(13, 14) + singular_identifier(13, 14) ]), WHITESPACE(14, 15, [SPACE(14, 15)]), WHITESPACE(19, 20, [SPACE(19, 20)]), expression(20, 21, [ - identifier(20, 21) + singular_identifier(20, 21) ]), ]) ] @@ -150,12 +150,12 @@ fn it_successfully_parses_an_if_statement_without_including_the_trailing_space() WHITESPACE(7, 8, [SPACE(7, 8)]), WHITESPACE(12, 13, [SPACE(12, 13)]), expression(13, 14, [ - identifier(13, 14) + singular_identifier(13, 14) ]), WHITESPACE(14, 15, [SPACE(14, 15)]), WHITESPACE(19, 20, [SPACE(19, 20)]), expression(20, 21, [ - identifier(20, 21) + singular_identifier(20, 21) ]), ]) ] diff --git a/wdl-grammar/src/v1/tests/expression/core/map_literal.rs b/wdl-grammar/src/v1/tests/expression/atom/map_literal.rs similarity index 92% rename from wdl-grammar/src/v1/tests/expression/core/map_literal.rs rename to wdl-grammar/src/v1/tests/expression/atom/map_literal.rs index 651d42d27..3043eea18 100644 --- a/wdl-grammar/src/v1/tests/expression/core/map_literal.rs +++ b/wdl-grammar/src/v1/tests/expression/atom/map_literal.rs @@ -40,7 +40,7 @@ fn it_successfully_parses_a_map_literal() { expression_based_kv_pair(1, 11, [ expression_based_kv_key(1, 6, [ expression(1, 6, [ - identifier(1, 6) + singular_identifier(1, 6) ]) ]), kv_value(7, 11, [ @@ -65,7 +65,7 @@ fn it_successfully_parses_a_map_literal_with_a_comma() { expression_based_kv_pair(1, 11, [ expression_based_kv_key(1, 6, [ expression(1, 6, [ - identifier(1, 6) + singular_identifier(1, 6) ]) ]), kv_value(7, 11, [ @@ -91,7 +91,7 @@ fn it_successfully_parses_a_map_literal_without_the_trailing_space() { expression_based_kv_pair(1, 11, [ expression_based_kv_key(1, 6, [ expression(1, 6, [ - identifier(1, 6) + singular_identifier(1, 6) ]) ]), kv_value(7, 11, [ @@ -117,7 +117,7 @@ fn it_successfully_parses_a_map_literal_with_spaces_inside() { expression_based_kv_pair(2, 14, [ expression_based_kv_key(2, 7, [ expression(2, 7, [ - identifier(2, 7) + singular_identifier(2, 7) ]) ]), WHITESPACE(7, 8, [SPACE(7, 8)]), @@ -146,7 +146,7 @@ fn it_successfully_parses_a_map_literal_with_spaces_inside_and_a_comma() { expression_based_kv_pair(2, 14, [ expression_based_kv_key(2, 7, [ expression(2, 7, [ - identifier(2, 7) + singular_identifier(2, 7) ]) ]), WHITESPACE(7, 8, [SPACE(7, 8)]), @@ -179,17 +179,17 @@ fn it_successfully_parses_an_expression_as_the_key() { r#if(2, 20, [ WHITESPACE(4, 5, [SPACE(4, 5)]), expression(5, 6, [ - identifier(5, 6) + singular_identifier(5, 6) ]), WHITESPACE(6, 7, [SPACE(6, 7)]), WHITESPACE(11, 12, [SPACE(11, 12)]), expression(12, 13, [ - identifier(12, 13) + singular_identifier(12, 13) ]), WHITESPACE(13, 14, [SPACE(13, 14)]), WHITESPACE(18, 19, [SPACE(18, 19)]), expression(19, 20, [ - identifier(19, 20) + singular_identifier(19, 20) ]), ]) ]) diff --git a/wdl-grammar/src/v1/tests/expression/core/object_literal.rs b/wdl-grammar/src/v1/tests/expression/atom/object_literal.rs similarity index 94% rename from wdl-grammar/src/v1/tests/expression/core/object_literal.rs rename to wdl-grammar/src/v1/tests/expression/atom/object_literal.rs index a59bffbbb..73f3362aa 100644 --- a/wdl-grammar/src/v1/tests/expression/core/object_literal.rs +++ b/wdl-grammar/src/v1/tests/expression/atom/object_literal.rs @@ -39,7 +39,7 @@ fn it_successfully_parses_an_object_literal() { object_literal(0, 18, [ identifier_based_kv_pair(7, 17, [ identifier_based_kv_key(7, 12, [ - identifier(7, 12) + singular_identifier(7, 12) ]), kv_value(13, 17, [ expression(13, 17, [ @@ -62,7 +62,7 @@ fn it_successfully_parses_an_object_literal_with_a_comma() { object_literal(0, 19, [ identifier_based_kv_pair(7, 17, [ identifier_based_kv_key(7, 12, [ - identifier(7, 12) + singular_identifier(7, 12) ]), kv_value(13, 17, [ expression(13, 17, [ @@ -86,7 +86,7 @@ fn it_successfully_parses_an_object_literal_without_the_trailing_space() { object_literal(0, 18, [ identifier_based_kv_pair(7, 17, [ identifier_based_kv_key(7, 12, [ - identifier(7, 12) + singular_identifier(7, 12) ]), kv_value(13, 17, [ expression(13, 17, [ @@ -111,7 +111,7 @@ fn it_successfully_parses_an_object_literal_with_spaces_inside() { WHITESPACE(8, 9, [SPACE(8, 9)]), identifier_based_kv_pair(9, 21, [ identifier_based_kv_key(9, 14, [ - identifier(9, 14) + singular_identifier(9, 14) ]), WHITESPACE(14, 15, [SPACE(14, 15)]), WHITESPACE(16, 17, [SPACE(16, 17)]), @@ -139,7 +139,7 @@ fn it_successfully_parses_an_object_literal_with_spaces_inside_and_a_comma() { WHITESPACE(8, 9, [SPACE(8, 9)]), identifier_based_kv_pair(9, 21, [ identifier_based_kv_key(9, 14, [ - identifier(9, 14) + singular_identifier(9, 14) ]), WHITESPACE(14, 15, [SPACE(14, 15)]), WHITESPACE(16, 17, [SPACE(16, 17)]), diff --git a/wdl-grammar/src/v1/tests/expression/core/pair_literal.rs b/wdl-grammar/src/v1/tests/expression/atom/pair_literal.rs similarity index 90% rename from wdl-grammar/src/v1/tests/expression/core/pair_literal.rs rename to wdl-grammar/src/v1/tests/expression/atom/pair_literal.rs index de4d9761d..7f6bb4e3a 100644 --- a/wdl-grammar/src/v1/tests/expression/core/pair_literal.rs +++ b/wdl-grammar/src/v1/tests/expression/atom/pair_literal.rs @@ -54,7 +54,7 @@ fn it_fails_to_parse_a_pair_literal_with_one_member() { Rule::gt, Rule::member, Rule::index, - Rule::apply, + Rule::call, ], negatives: vec![], pos: 2 @@ -85,7 +85,7 @@ fn it_fails_to_parse_a_pair_literal_with_three_members() { Rule::gt, Rule::member, Rule::index, - Rule::apply, + Rule::call, ], negatives: vec![], pos: 5 @@ -101,11 +101,11 @@ fn it_successfully_parses_a_pair_literal() { tokens: [ pair_literal(0, 5, [ expression(1, 2, [ - identifier(1, 2) + singular_identifier(1, 2) ]), COMMA(2, 3), expression(3, 4, [ - identifier(3, 4) + singular_identifier(3, 4) ]), ]) ] @@ -121,11 +121,11 @@ fn it_successfully_parses_a_pair_literal_without_the_trailing_space() { tokens: [ pair_literal(0, 5, [ expression(1, 2, [ - identifier(1, 2) + singular_identifier(1, 2) ]), COMMA(2, 3), expression(3, 4, [ - identifier(3, 4) + singular_identifier(3, 4) ]), ]) ] @@ -141,12 +141,12 @@ fn it_successfully_parses_a_pair_literal_with_spaces_inside() { tokens: [ pair_literal(0, 6, [ expression(1, 2, [ - identifier(1, 2) + singular_identifier(1, 2) ]), COMMA(2, 3), WHITESPACE(3, 4, [SPACE(3, 4)]), expression(4, 5, [ - identifier(4, 5) + singular_identifier(4, 5) ]), ]) ] diff --git a/wdl-grammar/src/v1/tests/expression/core/struct_literal.rs b/wdl-grammar/src/v1/tests/expression/atom/struct_literal.rs similarity index 87% rename from wdl-grammar/src/v1/tests/expression/core/struct_literal.rs rename to wdl-grammar/src/v1/tests/expression/atom/struct_literal.rs index 22f29c892..26a18fa3b 100644 --- a/wdl-grammar/src/v1/tests/expression/core/struct_literal.rs +++ b/wdl-grammar/src/v1/tests/expression/atom/struct_literal.rs @@ -11,7 +11,7 @@ fn it_fails_to_parse_an_empty_string() { parser: WdlParser, input: "", rule: Rule::struct_literal, - positives: vec![Rule::identifier], + positives: vec![Rule::singular_identifier], negatives: vec![], pos: 0 } @@ -23,7 +23,7 @@ fn it_fails_to_parse_a_struct_literal_with_spaces_outside_the_input() { parser: WdlParser, input: " struct { hello: true } ", rule: Rule::struct_literal, - positives: vec![Rule::identifier], + positives: vec![Rule::singular_identifier], negatives: vec![], pos: 0 } @@ -37,10 +37,10 @@ fn it_successfully_parses_a_struct_literal() { rule: Rule::struct_literal, tokens: [ struct_literal(0, 18, [ - identifier(0, 6), + singular_identifier(0, 6), identifier_based_kv_pair(7, 17, [ identifier_based_kv_key(7, 12, [ - identifier(7, 12), + singular_identifier(7, 12), ]), kv_value(13, 17, [ expression(13, 17, [ @@ -61,10 +61,10 @@ fn it_successfully_parses_a_struct_literal_with_a_comma() { rule: Rule::struct_literal, tokens: [ struct_literal(0, 19, [ - identifier(0, 6), + singular_identifier(0, 6), identifier_based_kv_pair(7, 17, [ identifier_based_kv_key(7, 12, [ - identifier(7, 12), + singular_identifier(7, 12), ]), kv_value(13, 17, [ expression(13, 17, [ @@ -86,10 +86,10 @@ fn it_successfully_parses_a_struct_literal_without_the_trailing_space() { rule: Rule::struct_literal, tokens: [ struct_literal(0, 18, [ - identifier(0, 6), + singular_identifier(0, 6), identifier_based_kv_pair(7, 17, [ identifier_based_kv_key(7, 12, [ - identifier(7, 12), + singular_identifier(7, 12), ]), kv_value(13, 17, [ expression(13, 17, [ @@ -110,12 +110,12 @@ fn it_successfully_parses_a_struct_literal_with_spaces_inside() { rule: Rule::struct_literal, tokens: [ struct_literal(0, 23, [ - identifier(0, 6), + singular_identifier(0, 6), WHITESPACE(6, 7, [SPACE(6, 7)]), WHITESPACE(8, 9, [SPACE(8, 9)]), identifier_based_kv_pair(9, 21, [ identifier_based_kv_key(9, 14, [ - identifier(9, 14), + singular_identifier(9, 14), ]), WHITESPACE(14, 15, [SPACE(14, 15)]), WHITESPACE(16, 17, [SPACE(16, 17)]), @@ -139,12 +139,12 @@ fn it_successfully_parses_a_struct_literal_with_spaces_inside_and_a_comma() { rule: Rule::struct_literal, tokens: [ struct_literal(0, 24, [ - identifier(0, 6), + singular_identifier(0, 6), WHITESPACE(6, 7, [SPACE(6, 7)]), WHITESPACE(8, 9, [SPACE(8, 9)]), identifier_based_kv_pair(9, 21, [ identifier_based_kv_key(9, 14, [ - identifier(9, 14), + singular_identifier(9, 14), ]), WHITESPACE(14, 15, [SPACE(14, 15)]), WHITESPACE(16, 17, [SPACE(16, 17)]), diff --git a/wdl-grammar/src/v1/tests/expression/prefix.rs b/wdl-grammar/src/v1/tests/expression/prefix.rs index ef96bdd15..e8b0f4761 100644 --- a/wdl-grammar/src/v1/tests/expression/prefix.rs +++ b/wdl-grammar/src/v1/tests/expression/prefix.rs @@ -23,7 +23,7 @@ fn it_successfully_parses_positive_unary() { parser: WdlParser, input: "+", rule: Rule::prefix, - tokens: [unary_signed(0, 1)] + tokens: [unary_signed_positive(0, 1)] } } @@ -33,6 +33,6 @@ fn it_successfully_parses_negative_unary() { parser: WdlParser, input: "-", rule: Rule::prefix, - tokens: [unary_signed(0, 1)] + tokens: [unary_signed_negative(0, 1)] } } diff --git a/wdl-grammar/src/v1/tests/expression/prefix/unary.rs b/wdl-grammar/src/v1/tests/expression/prefix/unary.rs index 7db0c4782..5bd0ed2fd 100644 --- a/wdl-grammar/src/v1/tests/expression/prefix/unary.rs +++ b/wdl-grammar/src/v1/tests/expression/prefix/unary.rs @@ -35,7 +35,7 @@ fn it_successfully_parses_positive_unary_signed() { parser: WdlParser, input: "+", rule: Rule::unary_signed, - tokens: [unary_signed(0, 1)] + tokens: [unary_signed_positive(0, 1)] } } @@ -45,6 +45,6 @@ fn it_successfully_parses_negative_unary_signed() { parser: WdlParser, input: "-", rule: Rule::unary_signed, - tokens: [unary_signed(0, 1)] + tokens: [unary_signed_negative(0, 1)] } } diff --git a/wdl-grammar/src/v1/tests/expression/prefix/unary_signed.rs b/wdl-grammar/src/v1/tests/expression/prefix/unary_signed.rs index 7db0c4782..4ec046a6e 100644 --- a/wdl-grammar/src/v1/tests/expression/prefix/unary_signed.rs +++ b/wdl-grammar/src/v1/tests/expression/prefix/unary_signed.rs @@ -6,45 +6,69 @@ use crate::v1::Parser as WdlParser; use crate::v1::Rule; #[test] -fn it_fails_to_parse_an_empty_unary_signed() { +fn it_fails_to_parse_an_empty_unary_signed_positive() { fails_with! { parser: WdlParser, input: "", - rule: Rule::unary_signed, - positives: vec![Rule::unary_signed], + rule: Rule::unary_signed_positive, + positives: vec![Rule::unary_signed_positive], negatives: vec![], pos: 0 } } #[test] -fn it_fails_to_parse_a_value_that_is_not_unary_signed() { +fn it_fails_to_parse_an_empty_unary_signed_negative() { + fails_with! { + parser: WdlParser, + input: "", + rule: Rule::unary_signed_negative, + positives: vec![Rule::unary_signed_negative], + negatives: vec![], + pos: 0 + } +} + +#[test] +fn it_fails_to_parse_a_value_that_is_not_unary_signed_positive() { + fails_with! { + parser: WdlParser, + input: "!", + rule: Rule::unary_signed_positive, + positives: vec![Rule::unary_signed_positive], + negatives: vec![], + pos: 0 + } +} + +#[test] +fn it_fails_to_parse_a_value_that_is_not_unary_signed_negative() { fails_with! { parser: WdlParser, input: "!", - rule: Rule::unary_signed, - positives: vec![Rule::unary_signed], + rule: Rule::unary_signed_negative, + positives: vec![Rule::unary_signed_negative], negatives: vec![], pos: 0 } } #[test] -fn it_successfully_parses_positive_unary_signed() { +fn it_successfully_parses_a_unary_signed_positive() { parses_to! { parser: WdlParser, input: "+", - rule: Rule::unary_signed, - tokens: [unary_signed(0, 1)] + rule: Rule::unary_signed_positive, + tokens: [unary_signed_positive(0, 1)] } } #[test] -fn it_successfully_parses_negative_unary_signed() { +fn it_successfully_parses_a_unary_signed_negative() { parses_to! { parser: WdlParser, input: "-", - rule: Rule::unary_signed, - tokens: [unary_signed(0, 1)] + rule: Rule::unary_signed_negative, + tokens: [unary_signed_negative(0, 1)] } } diff --git a/wdl-grammar/src/v1/tests/expression/suffix.rs b/wdl-grammar/src/v1/tests/expression/suffix.rs index 0ee61971f..5557db86a 100644 --- a/wdl-grammar/src/v1/tests/expression/suffix.rs +++ b/wdl-grammar/src/v1/tests/expression/suffix.rs @@ -1,3 +1,3 @@ -mod apply; +mod call; mod index; mod member; diff --git a/wdl-grammar/src/v1/tests/expression/suffix/apply.rs b/wdl-grammar/src/v1/tests/expression/suffix/call.rs similarity index 80% rename from wdl-grammar/src/v1/tests/expression/suffix/apply.rs rename to wdl-grammar/src/v1/tests/expression/suffix/call.rs index d7ada85af..613c8d7b2 100644 --- a/wdl-grammar/src/v1/tests/expression/suffix/apply.rs +++ b/wdl-grammar/src/v1/tests/expression/suffix/call.rs @@ -6,12 +6,12 @@ use crate::v1::Parser as WdlParser; use crate::v1::Rule; #[test] -fn it_fails_to_parse_an_empty_apply() { +fn it_fails_to_parse_an_empty_call() { fails_with! { parser: WdlParser, input: "", - rule: Rule::apply, - positives: vec![Rule::apply], + rule: Rule::call, + positives: vec![Rule::call], negatives: vec![], pos: 0 } @@ -22,7 +22,7 @@ fn it_fails_to_parse_an_apply_with_just_a_comma() { fails_with! { parser: WdlParser, input: "(,)", - rule: Rule::apply, + rule: Rule::call, positives: vec![Rule::WHITESPACE, Rule::COMMENT, Rule::expression], negatives: vec![], pos: 1 @@ -30,12 +30,12 @@ fn it_fails_to_parse_an_apply_with_just_a_comma() { } #[test] -fn it_fails_to_parse_a_value_that_is_not_apply() { +fn it_fails_to_parse_a_value_that_is_not_call() { fails_with! { parser: WdlParser, input: ".field", - rule: Rule::apply, - positives: vec![Rule::apply], + rule: Rule::call, + positives: vec![Rule::call], negatives: vec![], pos: 0 } @@ -46,8 +46,8 @@ fn it_successfully_parses_an_apply_with_no_elements() { parses_to! { parser: WdlParser, input: "()", - rule: Rule::apply, - tokens: [apply(0, 2)] + rule: Rule::call, + tokens: [call(0, 2)] } } @@ -56,8 +56,8 @@ fn it_successfully_parses_an_apply_with_one_element() { parses_to! { parser: WdlParser, input: "(if true then a else b)", - rule: Rule::apply, - tokens: [apply(0, 23, [ + rule: Rule::call, + tokens: [call(0, 23, [ expression(1, 22, [ r#if(1, 22, [ WHITESPACE(3, 4, [SPACE(3, 4)]), @@ -67,12 +67,12 @@ fn it_successfully_parses_an_apply_with_one_element() { WHITESPACE(8, 9, [SPACE(8, 9)]), WHITESPACE(13, 14, [SPACE(13, 14)]), expression(14, 15, [ - identifier(14, 15) + singular_identifier(14, 15) ]), WHITESPACE(15, 16, [SPACE(15, 16)]), WHITESPACE(20, 21, [SPACE(20, 21)]), expression(21, 22, [ - identifier(21, 22) + singular_identifier(21, 22) ]), ]) ]), @@ -85,8 +85,8 @@ fn it_successfully_parses_an_apply_with_one_element_and_a_comma() { parses_to! { parser: WdlParser, input: "(if true then a else b,)", - rule: Rule::apply, - tokens: [apply(0, 24, [ + rule: Rule::call, + tokens: [call(0, 24, [ expression(1, 22, [ r#if(1, 22, [ WHITESPACE(3, 4, [SPACE(3, 4)]), @@ -96,12 +96,12 @@ fn it_successfully_parses_an_apply_with_one_element_and_a_comma() { WHITESPACE(8, 9, [SPACE(8, 9)]), WHITESPACE(13, 14, [SPACE(13, 14)]), expression(14, 15, [ - identifier(14, 15) + singular_identifier(14, 15) ]), WHITESPACE(15, 16, [SPACE(15, 16)]), WHITESPACE(20, 21, [SPACE(20, 21)]), expression(21, 22, [ - identifier(21, 22) + singular_identifier(21, 22) ]), ]) ]), @@ -115,8 +115,8 @@ fn it_successfully_parses_an_apply_with_one_element_without_including_the_traili parses_to! { parser: WdlParser, input: "(if true then a else b) ", - rule: Rule::apply, - tokens: [apply(0, 23, [ + rule: Rule::call, + tokens: [call(0, 23, [ expression(1, 22, [ r#if(1, 22, [ WHITESPACE(3, 4, [SPACE(3, 4)]), @@ -126,12 +126,12 @@ fn it_successfully_parses_an_apply_with_one_element_without_including_the_traili WHITESPACE(8, 9, [SPACE(8, 9)]), WHITESPACE(13, 14, [SPACE(13, 14)]), expression(14, 15, [ - identifier(14, 15) + singular_identifier(14, 15) ]), WHITESPACE(15, 16, [SPACE(15, 16)]), WHITESPACE(20, 21, [SPACE(20, 21)]), expression(21, 22, [ - identifier(21, 22) + singular_identifier(21, 22) ]), ]) ]), @@ -144,9 +144,9 @@ fn it_successfully_parses_an_apply_with_two_elements() { parses_to! { parser: WdlParser, input: "(if true then a else b, world)", - rule: Rule::apply, + rule: Rule::call, tokens: [ - apply(0, 30, [ + call(0, 30, [ expression(1, 22, [ r#if(1, 22, [ WHITESPACE(3, 4, [SPACE(3, 4)]), @@ -156,19 +156,19 @@ fn it_successfully_parses_an_apply_with_two_elements() { WHITESPACE(8, 9, [SPACE(8, 9)]), WHITESPACE(13, 14, [SPACE(13, 14)]), expression(14, 15, [ - identifier(14, 15) + singular_identifier(14, 15) ]), WHITESPACE(15, 16, [SPACE(15, 16)]), WHITESPACE(20, 21, [SPACE(20, 21)]), expression(21, 22, [ - identifier(21, 22) + singular_identifier(21, 22) ]), ]) ]), COMMA(22, 23), WHITESPACE(23, 24, [SPACE(23, 24)]), expression(24, 29, [ - identifier(24, 29) + singular_identifier(24, 29) ]) ]), ] diff --git a/wdl-grammar/src/v1/tests/expression/suffix/index.rs b/wdl-grammar/src/v1/tests/expression/suffix/index.rs index 9139c8816..37708eede 100644 --- a/wdl-grammar/src/v1/tests/expression/suffix/index.rs +++ b/wdl-grammar/src/v1/tests/expression/suffix/index.rs @@ -45,12 +45,12 @@ fn it_successfully_parses_an_index() { WHITESPACE(8, 9, [SPACE(8, 9)]), WHITESPACE(13, 14, [SPACE(13, 14)]), expression(14, 15, [ - identifier(14, 15) + singular_identifier(14, 15) ]), WHITESPACE(15, 16, [SPACE(15, 16)]), WHITESPACE(20, 21, [SPACE(20, 21)]), expression(21, 22, [ - identifier(21, 22) + singular_identifier(21, 22) ]), ]) ]) @@ -74,12 +74,12 @@ fn it_successfully_parses_an_index_without_including_the_trailing_space() { WHITESPACE(8, 9, [SPACE(8, 9)]), WHITESPACE(13, 14, [SPACE(13, 14)]), expression(14, 15, [ - identifier(14, 15) + singular_identifier(14, 15) ]), WHITESPACE(15, 16, [SPACE(15, 16)]), WHITESPACE(20, 21, [SPACE(20, 21)]), expression(21, 22, [ - identifier(21, 22) + singular_identifier(21, 22) ]), ]) ]) diff --git a/wdl-grammar/src/v1/tests/expression/suffix/member.rs b/wdl-grammar/src/v1/tests/expression/suffix/member.rs index eee436526..b685852d7 100644 --- a/wdl-grammar/src/v1/tests/expression/suffix/member.rs +++ b/wdl-grammar/src/v1/tests/expression/suffix/member.rs @@ -35,7 +35,7 @@ fn it_fails_to_parse_a_member_from_an_expression() { parser: WdlParser, input: ".(if a then b else c)", rule: Rule::member, - positives: vec![Rule::identifier], + positives: vec![Rule::singular_identifier], negatives: vec![], pos: 1 } @@ -48,7 +48,7 @@ fn it_successfully_parses_a_member() { input: ".field", rule: Rule::member, tokens: [member(0, 6, [ - identifier(1, 6) + singular_identifier(1, 6) ])] } } @@ -60,7 +60,7 @@ fn it_successfully_parses_a_member_without_including_the_trailing_space() { input: ".field ", rule: Rule::member, tokens: [member(0, 6, [ - identifier(1, 6) + singular_identifier(1, 6) ])] } } diff --git a/wdl-grammar/src/v1/tests/literal.rs b/wdl-grammar/src/v1/tests/literal.rs index 51f2841a9..0e8852934 100644 --- a/wdl-grammar/src/v1/tests/literal.rs +++ b/wdl-grammar/src/v1/tests/literal.rs @@ -17,7 +17,7 @@ fn it_fails_to_parse_an_empty_literal() { Rule::integer, Rule::float, Rule::string, - Rule::identifier, + Rule::singular_identifier, ], negatives: vec![], pos: 0 @@ -135,11 +135,12 @@ fn it_successfully_parses_an_empty_double_quoted_string() { input: "\"\"", rule: Rule::literal, tokens: [ - // `""` - string(0, 2, [ - // `"` - double_quote(0, 1), - ]) + // `""` + string(0, 2, [ + // `"` + double_quote(0, 1), + string_inner(1, 1), + ]) ] } } @@ -151,11 +152,12 @@ fn it_successfully_parses_an_empty_single_quoted_string() { input: "''", rule: Rule::literal, tokens: [ - // `''` - string(0, 2, [ - // `'` - single_quote(0, 1), - ]) + // `''` + string(0, 2, [ + // `'` + single_quote(0, 1), + string_inner(1, 1), + ]) ] } } @@ -167,13 +169,16 @@ fn it_successfully_parses_a_double_quoted_string_with_a_unicode_character() { input: "\"😀\"", rule: Rule::literal, tokens: [ - // `"😀"` - string(0, 6, [ - // `"` - double_quote(0, 1), - // `😀` - string_literal_contents(1, 5), - ]) + // `"😀"` + string(0, 6, [ + // `"` + double_quote(0, 1), + // `😀` + string_inner(1, 5, [ + // `😀` + string_literal_contents(1, 5), + ]), + ]) ] } } @@ -185,13 +190,16 @@ fn it_successfully_parses_a_single_quoted_string_with_a_unicode_character() { input: "'😀'", rule: Rule::literal, tokens: [ - // `'😀'` - string(0, 6, [ - // `'` - single_quote(0, 1), - // `😀` - string_literal_contents(1, 5), - ]) + // `'😀'` + string(0, 6, [ + // `'` + single_quote(0, 1), + // `😀` + string_inner(1, 5, [ + // `😀` + string_literal_contents(1, 5), + ]), + ]) ] } } @@ -203,13 +211,16 @@ fn it_successfully_parses_a_double_quoted_string() { input: "\"Hello, world!\"", rule: Rule::literal, tokens: [ - // `"Hello, world!"` - string(0, 15, [ - // `"` - double_quote(0, 1), - // `Hello, world!` - string_literal_contents(1, 14), - ]) + // `"Hello, world!"` + string(0, 15, [ + // `"` + double_quote(0, 1), + // `Hello, world!` + string_inner(1, 14, [ + // `Hello, world!` + string_literal_contents(1, 14), + ]), + ]) ] } } @@ -221,13 +232,16 @@ fn it_successfully_parses_a_single_quoted_string() { input: "'Hello, world!'", rule: Rule::literal, tokens: [ - // `'Hello, world!'` - string(0, 15, [ - // `'` - single_quote(0, 1), - // `Hello, world!` - string_literal_contents(1, 14), - ]) + // `'Hello, world!'` + string(0, 15, [ + // `'` + single_quote(0, 1), + // `Hello, world!` + string_inner(1, 14, [ + // `Hello, world!` + string_literal_contents(1, 14), + ]), + ]) ] } } @@ -248,13 +262,13 @@ fn it_successfully_parses_an_identifier() { parser: WdlParser, input: "hello_world", rule: Rule::literal, - tokens: [identifier(0, 11)] + tokens: [singular_identifier(0, 11)] } parses_to! { parser: WdlParser, input: "HelloWorld", rule: Rule::literal, - tokens: [identifier(0, 10)] + tokens: [singular_identifier(0, 10)] } } diff --git a/wdl-grammar/src/v1/tests/primitives.rs b/wdl-grammar/src/v1/tests/primitives.rs index 8fd7e9919..8eba285e7 100644 --- a/wdl-grammar/src/v1/tests/primitives.rs +++ b/wdl-grammar/src/v1/tests/primitives.rs @@ -1,8 +1,8 @@ mod boolean; mod char; mod float; -mod identifier; mod integer; mod none; mod number; +mod singular_identifier; mod string; diff --git a/wdl-grammar/src/v1/tests/primitives/identifier.rs b/wdl-grammar/src/v1/tests/primitives/singular_identifier.rs similarity index 73% rename from wdl-grammar/src/v1/tests/primitives/identifier.rs rename to wdl-grammar/src/v1/tests/primitives/singular_identifier.rs index 260ca0ecc..2b280087e 100644 --- a/wdl-grammar/src/v1/tests/primitives/identifier.rs +++ b/wdl-grammar/src/v1/tests/primitives/singular_identifier.rs @@ -10,8 +10,8 @@ fn it_fails_to_parse_an_empty_identifier() { fails_with! { parser: WdlParser, input: "", - rule: Rule::identifier, - positives: vec![Rule::identifier], + rule: Rule::singular_identifier, + positives: vec![Rule::singular_identifier], negatives: vec![], pos: 0 } @@ -22,8 +22,8 @@ fn it_fails_to_parse_an_identifier_starting_with_a_number() { fails_with! { parser: WdlParser, input: "0hello", - rule: Rule::identifier, - positives: vec![Rule::identifier], + rule: Rule::singular_identifier, + positives: vec![Rule::singular_identifier], negatives: vec![], pos: 0 } @@ -34,8 +34,8 @@ fn it_fail_to_parse_an_identifier_with_a_unicode_character() { fails_with! { parser: WdlParser, input: "😀", - rule: Rule::identifier, - positives: vec![Rule::identifier], + rule: Rule::singular_identifier, + positives: vec![Rule::singular_identifier], negatives: vec![], pos: 0 } @@ -50,8 +50,8 @@ fn it_successfully_parses_part_of_an_identifier_with_a_dash() { parses_to! { parser: WdlParser, input: "hello-world", - rule: Rule::identifier, - tokens: [identifier(0, 5)] + rule: Rule::singular_identifier, + tokens: [singular_identifier(0, 5)] } } @@ -64,8 +64,8 @@ fn it_successfully_parses_part_of_an_identifier_with_a_space() { parses_to! { parser: WdlParser, input: "hello world", - rule: Rule::identifier, - tokens: [identifier(0, 5)] + rule: Rule::singular_identifier, + tokens: [singular_identifier(0, 5)] } } @@ -74,7 +74,7 @@ fn it_successfully_parses_an_identifer() { parses_to! { parser: WdlParser, input: "testing", - rule: Rule::identifier, - tokens: [identifier(0, 7)] + rule: Rule::singular_identifier, + tokens: [singular_identifier(0, 7)] } } diff --git a/wdl-grammar/src/v1/tests/primitives/string.rs b/wdl-grammar/src/v1/tests/primitives/string.rs index 28db8fef1..85cfa271e 100644 --- a/wdl-grammar/src/v1/tests/primitives/string.rs +++ b/wdl-grammar/src/v1/tests/primitives/string.rs @@ -67,6 +67,7 @@ fn it_successfully_parses_an_empty_double_quoted_string() { string(0, 2, [ // `"` double_quote(0, 1), + string_inner(1, 1), ]) ] } @@ -83,6 +84,7 @@ fn it_successfully_parses_an_empty_single_quoted_string() { string(0, 2, [ // `'` single_quote(0, 1), + string_inner(1, 1), ]) ] } @@ -100,7 +102,10 @@ fn it_successfully_parses_a_double_quoted_string_with_a_unicode_character() { // `"` double_quote(0, 1), // `😀` - string_literal_contents(1, 5), + string_inner(1, 5, [ + // `😀` + string_literal_contents(1, 5), + ]), ]) ] } @@ -118,7 +123,10 @@ fn it_successfully_parses_a_single_quoted_string_with_a_unicode_character() { // `'` single_quote(0, 1), // `😀` - string_literal_contents(1, 5), + string_inner(1, 5, [ + // `😀` + string_literal_contents(1, 5), + ]), ]) ] } @@ -136,7 +144,10 @@ fn it_successfully_parses_a_double_quoted_string() { // `"` double_quote(0, 1), // `Hello, world!` - string_literal_contents(1, 14), + string_inner(1, 14, [ + // `Hello, world!` + string_literal_contents(1, 14), + ]), ]) ] } @@ -154,7 +165,10 @@ fn it_successfully_parses_a_single_quoted_string() { // `'` single_quote(0, 1), // `Hello, world!` - string_literal_contents(1, 14), + string_inner(1, 14, [ + // `Hello, world!` + string_literal_contents(1, 14), + ]), ]) ] } diff --git a/wdl-grammar/src/v1/tests/primitives/string/double_quoted_string.rs b/wdl-grammar/src/v1/tests/primitives/string/double_quoted_string.rs index 5a827c78f..ff88e4317 100644 --- a/wdl-grammar/src/v1/tests/primitives/string/double_quoted_string.rs +++ b/wdl-grammar/src/v1/tests/primitives/string/double_quoted_string.rs @@ -48,11 +48,12 @@ fn it_parses_an_empty_double_quoted_string() { input: "\"\"", rule: Rule::string, tokens: [ - // `""` - string(0, 2, [ - // `"` - double_quote(0, 1), - ]) + // `""` + string(0, 2, [ + // `"` + double_quote(0, 1), + string_inner(1, 1), + ]) ] } } @@ -68,11 +69,12 @@ fn it_successfully_parses_the_first_two_double_quotes() { input: "\"\"\"", rule: Rule::string, tokens: [ - // `""` - string(0, 2, [ - // `"` - double_quote(0, 1), - ]) + // `""` + string(0, 2, [ + // `"` + double_quote(0, 1), + string_inner(1, 1), + ]) ] } } @@ -89,7 +91,10 @@ fn it_parses_a_double_quoted_string() { // `"` double_quote(0, 1), // `Hello, world!` - string_literal_contents(1, 14), + string_inner(1, 14, [ + // `Hello, world!` + string_literal_contents(1, 14), + ]), ]) ] } diff --git a/wdl-grammar/src/v1/tests/primitives/string/single_quoted_string.rs b/wdl-grammar/src/v1/tests/primitives/string/single_quoted_string.rs index 4f6037b00..7e0e62a85 100644 --- a/wdl-grammar/src/v1/tests/primitives/string/single_quoted_string.rs +++ b/wdl-grammar/src/v1/tests/primitives/string/single_quoted_string.rs @@ -52,6 +52,7 @@ fn it_parses_an_empty_single_quoted_string() { string(0, 2, [ // `'` single_quote(0, 1), + string_inner(1, 1), ]) ] } @@ -72,6 +73,7 @@ fn it_successfully_parses_the_first_two_double_quotes() { string(0, 2, [ // `'` single_quote(0, 1), + string_inner(1, 1), ]) ] } @@ -89,7 +91,10 @@ fn it_parses_a_single_quoted_string() { // `'` single_quote(0, 1), // `Hello, world!` - string_literal_contents(1, 14), + string_inner(1, 14, [ + // `Hello, world!` + string_literal_contents(1, 14), + ]), ]) ] } diff --git a/wdl-grammar/src/v1/tests/workflow_elements/call.rs b/wdl-grammar/src/v1/tests/workflow_elements/call.rs index 75d18fc44..95319500b 100644 --- a/wdl-grammar/src/v1/tests/workflow_elements/call.rs +++ b/wdl-grammar/src/v1/tests/workflow_elements/call.rs @@ -26,7 +26,7 @@ fn it_fails_to_parse_just_call() { positives: vec![ Rule::WHITESPACE, Rule::COMMENT, - Rule::identifier, + Rule::workflow_call_name, ], negatives: vec![], pos: 5 @@ -39,9 +39,20 @@ fn it_successfully_parses_plain_call() { parser: WdlParser, input: "call my_task", rule: Rule::workflow_call, - tokens: [workflow_call(0, 12, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), identifier(5, 12) - ])] + tokens: [ + // `call my_task` + workflow_call(0, 12, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `my_task` + workflow_call_name(5, 12, [ + // `my_task` + singular_identifier(5, 12), + ]), + ]) + ] + } } @@ -51,9 +62,19 @@ fn it_successfully_excludes_trailing_whitespace() { parser: WdlParser, input: "call my_task ", rule: Rule::workflow_call, - tokens: [workflow_call(0, 12, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), identifier(5, 12) - ])] + tokens: [ + // `call my_task` + workflow_call(0, 12, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `my_task` + workflow_call_name(5, 12, [ + // `my_task` + singular_identifier(5, 12), + ]), + ]) + ] } } @@ -63,11 +84,21 @@ fn it_successfully_parses_call_with_empty_body() { parser: WdlParser, input: "call my_task{}", rule: Rule::workflow_call, - tokens: [workflow_call(0, 14, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - identifier(5, 12), - workflow_call_body(12, 14) - ])] + tokens: [ + // `call my_task{}` + workflow_call(0, 14, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `my_task` + workflow_call_name(5, 12, [ + // `my_task` + singular_identifier(5, 12), + ]), + // `{}` + workflow_call_body(12, 14), + ]) + ] } } @@ -77,13 +108,27 @@ fn it_successfully_parses_call_with_implicitly_declared_input() { parser: WdlParser, input: "call my_task{input:a}", rule: Rule::workflow_call, - tokens: [workflow_call(0, 21, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - identifier(5, 12), + tokens: [ + // `call my_task{input:a}` + workflow_call(0, 21, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `my_task` + workflow_call_name(5, 12, [ + // `my_task` + singular_identifier(5, 12), + ]), + // `{input:a}` workflow_call_body(12, 21, [ - workflow_call_input(19, 20, [identifier(19, 20)]) - ]) - ])] + // `a` + workflow_call_input(19, 20, [ + // `a` + singular_identifier(19, 20), + ]), + ]), + ]) + ] } } @@ -93,13 +138,27 @@ fn it_successfully_parses_call_with_implicitly_declared_input_without_trailing_w parser: WdlParser, input: "call my_task{input:a} ", rule: Rule::workflow_call, - tokens: [workflow_call(0, 21, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - identifier(5, 12), + tokens: [ + // `call my_task{input:a}` + workflow_call(0, 21, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `my_task` + workflow_call_name(5, 12, [ + // `my_task` + singular_identifier(5, 12), + ]), + // `{input:a}` workflow_call_body(12, 21, [ - workflow_call_input(19, 20, [identifier(19, 20)]) - ]) - ])] + // `a` + workflow_call_input(19, 20, [ + // `a` + singular_identifier(19, 20), + ]), + ]), + ]) + ] } } @@ -110,25 +169,30 @@ fn it_successfully_parses_call_with_explicitly_declared_input() { input: "call my_task{input:a=b}", rule: Rule::workflow_call, tokens: [ - // `call my_task{input:a=b}` - workflow_call(0, 23, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - // `my_task` - identifier(5, 12), - // `{input:a=b}` - workflow_call_body(12, 23, [ - // `a=b` - workflow_call_input(19, 22, [ - // `a` - identifier(19, 20), - // `b` - expression(21, 22, [ - // `b` - identifier(21, 22), - ]), - ]), + // `call my_task{input:a=b}` + workflow_call(0, 23, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `my_task` + workflow_call_name(5, 12, [ + // `my_task` + singular_identifier(5, 12), + ]), + // `{input:a=b}` + workflow_call_body(12, 23, [ + // `a=b` + workflow_call_input(19, 22, [ + // `a` + singular_identifier(19, 20), + // `b` + expression(21, 22, [ + // `b` + singular_identifier(21, 22), ]), - ]) + ]), + ]), + ]) ] } } @@ -140,25 +204,30 @@ fn it_successfully_parses_call_with_explicitly_declared_input_without_trailing_w input: "call my_task{input:a=b} ", rule: Rule::workflow_call, tokens: [ - // `call my_task{input:a=b}` - workflow_call(0, 23, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - // `my_task` - identifier(5, 12), - // `{input:a=b}` - workflow_call_body(12, 23, [ - // `a=b` - workflow_call_input(19, 22, [ - // `a` - identifier(19, 20), - // `b` - expression(21, 22, [ - // `b` - identifier(21, 22), - ]), - ]), + // `call my_task{input:a=b}` + workflow_call(0, 23, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `my_task` + workflow_call_name(5, 12, [ + // `my_task` + singular_identifier(5, 12), + ]), + // `{input:a=b}` + workflow_call_body(12, 23, [ + // `a=b` + workflow_call_input(19, 22, [ + // `a` + singular_identifier(19, 20), + // `b` + expression(21, 22, [ + // `b` + singular_identifier(21, 22), ]), - ]) + ]), + ]), + ]) ] } } @@ -169,21 +238,51 @@ fn it_successfully_parses_call_with_multiple_inputs() { parser: WdlParser, input: "call my_task{input:a,b=b,c=z}", rule: Rule::workflow_call, - tokens: [workflow_call(0, 29, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - identifier(5, 12), + tokens: [ + // `call my_task{input:a,b=b,c=z}` + workflow_call(0, 29, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `my_task` + workflow_call_name(5, 12, [ + // `my_task` + singular_identifier(5, 12), + ]), + // `{input:a,b=b,c=z}` workflow_call_body(12, 29, [ - workflow_call_input(19, 20, [identifier(19, 20)]), - COMMA(20, 21), - workflow_call_input(21, 24, [identifier(21, 22), expression(23, 24, [ - identifier(23, 24) - ])]), - COMMA(24, 25), - workflow_call_input(25, 28, [identifier(25, 26), expression(27, 28, [ - identifier(27, 28) - ])]), - ]) - ])] + // `a` + workflow_call_input(19, 20, [ + // `a` + singular_identifier(19, 20), + ]), + // `,` + COMMA(20, 21), + // `b=b` + workflow_call_input(21, 24, [ + // `b` + singular_identifier(21, 22), + // `b` + expression(23, 24, [ + // `b` + singular_identifier(23, 24), + ]), + ]), + // `,` + COMMA(24, 25), + // `c=z` + workflow_call_input(25, 28, [ + // `c` + singular_identifier(25, 26), + // `z` + expression(27, 28, [ + // `z` + singular_identifier(27, 28), + ]), + ]), + ]), + ]) + ] } } @@ -193,21 +292,51 @@ fn it_successfully_parses_call_with_multiple_inputs_without_trailing_whitespace( parser: WdlParser, input: "call my_task{input:a,b=b,c=z} ", rule: Rule::workflow_call, - tokens: [workflow_call(0, 29, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - identifier(5, 12), + tokens: [ + // `call my_task{input:a,b=b,c=z}` + workflow_call(0, 29, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `my_task` + workflow_call_name(5, 12, [ + // `my_task` + singular_identifier(5, 12), + ]), + // `{input:a,b=b,c=z}` workflow_call_body(12, 29, [ - workflow_call_input(19, 20, [identifier(19, 20)]), - COMMA(20, 21), - workflow_call_input(21, 24, [identifier(21, 22), expression(23, 24, [ - identifier(23, 24) - ])]), - COMMA(24, 25), - workflow_call_input(25, 28, [identifier(25, 26), expression(27, 28, [ - identifier(27, 28) - ])]), - ]) - ])] + // `a` + workflow_call_input(19, 20, [ + // `a` + singular_identifier(19, 20), + ]), + // `,` + COMMA(20, 21), + // `b=b` + workflow_call_input(21, 24, [ + // `b` + singular_identifier(21, 22), + // `b` + expression(23, 24, [ + // `b` + singular_identifier(23, 24), + ]), + ]), + // `,` + COMMA(24, 25), + // `c=z` + workflow_call_input(25, 28, [ + // `c` + singular_identifier(25, 26), + // `z` + expression(27, 28, [ + // `z` + singular_identifier(27, 28), + ]), + ]), + ]), + ]) + ] } } @@ -217,15 +346,30 @@ fn it_successfully_parses_call_with_as() { parser: WdlParser, input: "call my_task as different_task", rule: Rule::workflow_call, - tokens: [workflow_call(0, 30, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - identifier(5, 12), - WHITESPACE(12, 13, [SPACE(12, 13)]), + tokens: [ + // `call my_task as different_task` + workflow_call(0, 30, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `my_task` + workflow_call_name(5, 12, [ + // `my_task` + singular_identifier(5, 12), + ]), + WHITESPACE(12, 13, [ + SPACE(12, 13), + ]), + // `as different_task` workflow_call_as(13, 30, [ - WHITESPACE(15, 16, [SPACE(15, 16)]), - identifier(16, 30) - ]) - ])] + WHITESPACE(15, 16, [ + SPACE(15, 16), + ]), + // `different_task` + singular_identifier(16, 30), + ]), + ]) + ] } } @@ -235,18 +379,35 @@ fn it_successfully_parses_call_with_after() { parser: WdlParser, input: "call imported_doc.my_task after different_task", rule: Rule::workflow_call, - tokens: [workflow_call(0, 46, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - qualified_identifier(5, 25, [ - identifier(5, 17), - identifier(18, 25) + tokens: [ + // `call imported_doc.my_task after different_task` + workflow_call(0, 46, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `imported_doc.my_task` + workflow_call_name(5, 25, [ + // `imported_doc.my_task` + qualified_identifier(5, 25, [ + // `imported_doc` + singular_identifier(5, 17), + // `my_task` + singular_identifier(18, 25), + ]), + ]), + WHITESPACE(25, 26, [ + SPACE(25, 26), ]), - WHITESPACE(25, 26, [SPACE(25, 26)]), + // `after different_task` workflow_call_after(26, 46, [ - WHITESPACE(31, 32, [SPACE(31, 32)]), - identifier(32, 46) - ]) - ])] + WHITESPACE(31, 32, [ + SPACE(31, 32), + ]), + // `different_task` + singular_identifier(32, 46), + ]), + ]) + ] } } @@ -256,18 +417,35 @@ fn it_successfully_parses_call_with_after_without_trailing_whitespace() { parser: WdlParser, input: "call imported_doc.my_task after different_task ", rule: Rule::workflow_call, - tokens: [workflow_call(0, 46, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - qualified_identifier(5, 25, [ - identifier(5, 17), - identifier(18, 25) + tokens: [ + // `call imported_doc.my_task after different_task` + workflow_call(0, 46, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `imported_doc.my_task` + workflow_call_name(5, 25, [ + // `imported_doc.my_task` + qualified_identifier(5, 25, [ + // `imported_doc` + singular_identifier(5, 17), + // `my_task` + singular_identifier(18, 25), + ]), ]), - WHITESPACE(25, 26, [SPACE(25, 26)]), + WHITESPACE(25, 26, [ + SPACE(25, 26), + ]), + // `after different_task` workflow_call_after(26, 46, [ - WHITESPACE(31, 32, [SPACE(31, 32)]), - identifier(32, 46) - ]) - ])] + WHITESPACE(31, 32, [ + SPACE(31, 32), + ]), + // `different_task` + singular_identifier(32, 46), + ]), + ]) + ] } } @@ -277,45 +455,104 @@ fn it_successfully_parses_call_with_all_options() { parser: WdlParser, input: "call imported_doc.my_task as their_task after different_task { input: a, b = b, c=z, }", rule: Rule::workflow_call, - tokens: [workflow_call(0, 86, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - qualified_identifier(5, 25, [ - identifier(5, 17), - identifier(18, 25) + tokens: [ + // `call imported_doc.my_task as their_task after different_task { input: a, b = b, c=z, }` + workflow_call(0, 86, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), ]), - WHITESPACE(25, 26, [SPACE(25, 26)]), + // `imported_doc.my_task` + workflow_call_name(5, 25, [ + // `imported_doc.my_task` + qualified_identifier(5, 25, [ + // `imported_doc` + singular_identifier(5, 17), + // `my_task` + singular_identifier(18, 25), + ]), + ]), + WHITESPACE(25, 26, [ + SPACE(25, 26), + ]), + // `as their_task` workflow_call_as(26, 39, [ - WHITESPACE(28, 29, [SPACE(28, 29)]), - identifier(29, 39) + WHITESPACE(28, 29, [ + SPACE(28, 29), + ]), + // `their_task` + singular_identifier(29, 39), + ]), + WHITESPACE(39, 40, [ + SPACE(39, 40), ]), - WHITESPACE(39, 40, [SPACE(39, 40)]), + // `after different_task` workflow_call_after(40, 60, [ - WHITESPACE(45, 46, [SPACE(45, 46)]), - identifier(46, 60) + WHITESPACE(45, 46, [ + SPACE(45, 46), + ]), + // `different_task` + singular_identifier(46, 60), ]), - WHITESPACE(60, 61, [SPACE(60, 61)]), + WHITESPACE(60, 61, [ + SPACE(60, 61), + ]), + // `{ input: a, b = b, c=z, }` workflow_call_body(61, 86, [ - WHITESPACE(62, 63, [SPACE(62, 63)]), - WHITESPACE(69, 70, [SPACE(69, 70)]), - workflow_call_input(70, 71, [identifier(70, 71)]), - COMMA(71, 72), - WHITESPACE(72, 73, [SPACE(72, 73)]), - workflow_call_input(73, 78, [ - identifier(73, 74), - WHITESPACE(74, 75, [SPACE(74, 75)]), - WHITESPACE(76, 77, [SPACE(76, 77)]), - expression(77, 78, [identifier(77, 78)]) + WHITESPACE(62, 63, [ + SPACE(62, 63), + ]), + WHITESPACE(69, 70, [ + SPACE(69, 70), + ]), + // `a` + workflow_call_input(70, 71, [ + // `a` + singular_identifier(70, 71), + ]), + // `,` + COMMA(71, 72), + WHITESPACE(72, 73, [ + SPACE(72, 73), + ]), + // `b = b` + workflow_call_input(73, 78, [ + // `b` + singular_identifier(73, 74), + WHITESPACE(74, 75, [ + SPACE(74, 75), + ]), + WHITESPACE(76, 77, [ + SPACE(76, 77), ]), - COMMA(78, 79), - WHITESPACE(79, 80, [SPACE(79, 80)]), - workflow_call_input(80, 83, [ - identifier(80, 81), - expression(82, 83, [identifier(82, 83)]) + // `b` + expression(77, 78, [ + // `b` + singular_identifier(77, 78), ]), - COMMA(83, 84), - WHITESPACE(84, 85, [SPACE(84, 85)]), + ]), + // `,` + COMMA(78, 79), + WHITESPACE(79, 80, [ + SPACE(79, 80), + ]), + // `c=z` + workflow_call_input(80, 83, [ + // `c` + singular_identifier(80, 81), + // `z` + expression(82, 83, [ + // `z` + singular_identifier(82, 83), + ]), + ]), + // `,` + COMMA(83, 84), + WHITESPACE(84, 85, [ + SPACE(84, 85), + ]), ]), - ])] + ]) + ] } } @@ -325,44 +562,103 @@ fn it_successfully_parses_call_with_all_options_without_trailing_whitespace() { parser: WdlParser, input: "call imported_doc.my_task as their_task after different_task { input: a, b = b, c=z, } ", rule: Rule::workflow_call, - tokens: [workflow_call(0, 86, [ - WHITESPACE(4, 5, [SPACE(4, 5)]), - qualified_identifier(5, 25, [ - identifier(5, 17), - identifier(18, 25) + tokens: [ + // `call imported_doc.my_task as their_task after different_task { input: a, b = b, c=z, }` + workflow_call(0, 86, [ + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `imported_doc.my_task` + workflow_call_name(5, 25, [ + // `imported_doc.my_task` + qualified_identifier(5, 25, [ + // `imported_doc` + singular_identifier(5, 17), + // `my_task` + singular_identifier(18, 25), + ]), + ]), + WHITESPACE(25, 26, [ + SPACE(25, 26), ]), - WHITESPACE(25, 26, [SPACE(25, 26)]), + // `as their_task` workflow_call_as(26, 39, [ - WHITESPACE(28, 29, [SPACE(28, 29)]), - identifier(29, 39) + WHITESPACE(28, 29, [ + SPACE(28, 29), + ]), + // `their_task` + singular_identifier(29, 39), ]), - WHITESPACE(39, 40, [SPACE(39, 40)]), + WHITESPACE(39, 40, [ + SPACE(39, 40), + ]), + // `after different_task` workflow_call_after(40, 60, [ - WHITESPACE(45, 46, [SPACE(45, 46)]), - identifier(46, 60) + WHITESPACE(45, 46, [ + SPACE(45, 46), + ]), + // `different_task` + singular_identifier(46, 60), + ]), + WHITESPACE(60, 61, [ + SPACE(60, 61), ]), - WHITESPACE(60, 61, [SPACE(60, 61)]), + // `{ input: a, b = b, c=z, }` workflow_call_body(61, 86, [ - WHITESPACE(62, 63, [SPACE(62, 63)]), - WHITESPACE(69, 70, [SPACE(69, 70)]), - workflow_call_input(70, 71, [identifier(70, 71)]), - COMMA(71, 72), - WHITESPACE(72, 73, [SPACE(72, 73)]), - workflow_call_input(73, 78, [ - identifier(73, 74), - WHITESPACE(74, 75, [SPACE(74, 75)]), - WHITESPACE(76, 77, [SPACE(76, 77)]), - expression(77, 78, [identifier(77, 78)]) + WHITESPACE(62, 63, [ + SPACE(62, 63), + ]), + WHITESPACE(69, 70, [ + SPACE(69, 70), + ]), + // `a` + workflow_call_input(70, 71, [ + // `a` + singular_identifier(70, 71), + ]), + // `,` + COMMA(71, 72), + WHITESPACE(72, 73, [ + SPACE(72, 73), + ]), + // `b = b` + workflow_call_input(73, 78, [ + // `b` + singular_identifier(73, 74), + WHITESPACE(74, 75, [ + SPACE(74, 75), + ]), + WHITESPACE(76, 77, [ + SPACE(76, 77), ]), - COMMA(78, 79), - WHITESPACE(79, 80, [SPACE(79, 80)]), - workflow_call_input(80, 83, [ - identifier(80, 81), - expression(82, 83, [identifier(82, 83)]) + // `b` + expression(77, 78, [ + // `b` + singular_identifier(77, 78), ]), - COMMA(83, 84), - WHITESPACE(84, 85, [SPACE(84, 85)]), + ]), + // `,` + COMMA(78, 79), + WHITESPACE(79, 80, [ + SPACE(79, 80), + ]), + // `c=z` + workflow_call_input(80, 83, [ + // `c` + singular_identifier(80, 81), + // `z` + expression(82, 83, [ + // `z` + singular_identifier(82, 83), + ]), + ]), + // `,` + COMMA(83, 84), + WHITESPACE(84, 85, [ + SPACE(84, 85), + ]), ]), - ])] + ]) + ] } } diff --git a/wdl-grammar/src/v1/tests/workflow_elements/conditional.rs b/wdl-grammar/src/v1/tests/workflow_elements/conditional.rs index 4a4c443ae..e742a422c 100644 --- a/wdl-grammar/src/v1/tests/workflow_elements/conditional.rs +++ b/wdl-grammar/src/v1/tests/workflow_elements/conditional.rs @@ -39,14 +39,17 @@ fn it_successfully_parses_conditional_without_space() { // `if(true){Int a=10}` workflow_conditional(0, 18, [ // `true` - expression(3, 7, [ + workflow_conditional_condition(3, 7, [ // `true` - boolean(3, 7), + expression(3, 7, [ + // `true` + boolean(3, 7), + ]), ]), // `Int a=10` workflow_execution_statement(9, 17, [ // `Int a=10` - workflow_private_declarations(9, 17, [ + private_declarations(9, 17, [ // `Int a=10` bound_declaration(9, 17, [ // `Int` @@ -58,7 +61,10 @@ fn it_successfully_parses_conditional_without_space() { SPACE(12, 13), ]), // `a` - identifier(13, 14), + bound_declaration_name(13, 14, [ + // `a` + singular_identifier(13, 14), + ]), // `10` expression(15, 17, [ // `10` @@ -70,7 +76,8 @@ fn it_successfully_parses_conditional_without_space() { ]), ]), ]), - ])] + ]) + ] } } @@ -80,11 +87,19 @@ fn it_successfully_parses_conditional_with_empty_body() { parser: WdlParser, input: "if(true){}", rule: Rule::workflow_conditional, - tokens: [workflow_conditional(0, 10, [ - expression(3, 7, [ - boolean(3, 7) + tokens: [ + // `if(true){}` + workflow_conditional(0, 10, [ + // `true` + workflow_conditional_condition(3, 7, [ + // `true` + expression(3, 7, [ + // `true` + boolean(3, 7), + ]), ]), - ])] + ]) + ] } } @@ -98,14 +113,17 @@ fn it_successfully_excludes_trailing_whitespace() { // `if(true){Int a=10}` workflow_conditional(0, 18, [ // `true` - expression(3, 7, [ + workflow_conditional_condition(3, 7, [ // `true` - boolean(3, 7), + expression(3, 7, [ + // `true` + boolean(3, 7), + ]), ]), // `Int a=10` workflow_execution_statement(9, 17, [ // `Int a=10` - workflow_private_declarations(9, 17, [ + private_declarations(9, 17, [ // `Int a=10` bound_declaration(9, 17, [ // `Int` @@ -117,7 +135,10 @@ fn it_successfully_excludes_trailing_whitespace() { SPACE(12, 13), ]), // `a` - identifier(13, 14), + bound_declaration_name(13, 14, [ + // `a` + singular_identifier(13, 14), + ]), // `10` expression(15, 17, [ // `10` @@ -150,9 +171,12 @@ fn it_successfully_parses_conditional_with_space() { SPACE(4, 5), ]), // `true` - expression(5, 9, [ + workflow_conditional_condition(5, 9, [ // `true` - boolean(5, 9), + expression(5, 9, [ + // `true` + boolean(5, 9), + ]), ]), WHITESPACE(9, 10, [ SPACE(9, 10), @@ -166,7 +190,7 @@ fn it_successfully_parses_conditional_with_space() { // `Int a=10` workflow_execution_statement(14, 23, [ // `Int a=10` - workflow_private_declarations(14, 23, [ + private_declarations(14, 23, [ // `Int a=10` bound_declaration(14, 22, [ // `Int` @@ -178,7 +202,10 @@ fn it_successfully_parses_conditional_with_space() { SPACE(17, 18), ]), // `a` - identifier(18, 19), + bound_declaration_name(18, 19, [ + // `a` + singular_identifier(18, 19), + ]), // `10` expression(20, 22, [ // `10` @@ -193,7 +220,8 @@ fn it_successfully_parses_conditional_with_space() { ]), ]), ]), - ])] + ]) + ] } } @@ -211,9 +239,12 @@ fn it_successfully_parses_conditional_with_multiple_statements() { // `if(true){ Int a=10 call my_task{input:foo=a} call no_inputs{} }` workflow_conditional(0, 75, [ // `true` - expression(3, 7, [ + workflow_conditional_condition(3, 7, [ // `true` - boolean(3, 7), + expression(3, 7, [ + // `true` + boolean(3, 7), + ]), ]), WHITESPACE(9, 10, [ NEWLINE(9, 10), @@ -233,7 +264,7 @@ fn it_successfully_parses_conditional_with_multiple_statements() { // `Int a=10` workflow_execution_statement(14, 27, [ // `Int a=10` - workflow_private_declarations(14, 27, [ + private_declarations(14, 27, [ // `Int a=10` bound_declaration(14, 22, [ // `Int` @@ -245,7 +276,10 @@ fn it_successfully_parses_conditional_with_multiple_statements() { SPACE(17, 18), ]), // `a` - identifier(18, 19), + bound_declaration_name(18, 19, [ + // `a` + singular_identifier(18, 19), + ]), // `10` expression(20, 22, [ // `10` @@ -280,17 +314,20 @@ fn it_successfully_parses_conditional_with_multiple_statements() { SPACE(31, 32), ]), // `my_task` - identifier(32, 39), + workflow_call_name(32, 39, [ + // `my_task` + singular_identifier(32, 39), + ]), // `{input:foo=a}` workflow_call_body(39, 52, [ // `foo=a` workflow_call_input(46, 51, [ // `foo` - identifier(46, 49), + singular_identifier(46, 49), // `a` expression(50, 51, [ // `a` - identifier(50, 51), + singular_identifier(50, 51), ]), ]), ]), @@ -319,7 +356,10 @@ fn it_successfully_parses_conditional_with_multiple_statements() { SPACE(61, 62), ]), // `no_inputs` - identifier(62, 71), + workflow_call_name(62, 71, [ + // `no_inputs` + singular_identifier(62, 71), + ]), // `{}` workflow_call_body(71, 73), ]), diff --git a/wdl-grammar/src/v1/tests/workflow_elements/qualified_identifier.rs b/wdl-grammar/src/v1/tests/workflow_elements/qualified_identifier.rs index 85e6d148e..8dbe63798 100644 --- a/wdl-grammar/src/v1/tests/workflow_elements/qualified_identifier.rs +++ b/wdl-grammar/src/v1/tests/workflow_elements/qualified_identifier.rs @@ -11,7 +11,7 @@ fn it_fails_to_parse_an_empty_string() { parser: WdlParser, input: "", rule: Rule::qualified_identifier, - positives: vec![Rule::identifier], + positives: vec![Rule::singular_identifier], negatives: vec![], pos: 0 } @@ -35,7 +35,7 @@ fn it_fails_to_parse_an_identifier_followed_by_a_period() { parser: WdlParser, input: "foo.", rule: Rule::qualified_identifier, - positives: vec![Rule::identifier], + positives: vec![Rule::singular_identifier], negatives: vec![], pos: 4 } @@ -47,7 +47,7 @@ fn it_fails_to_parse_an_identifier_proceeded_by_a_period() { parser: WdlParser, input: ".foo", rule: Rule::qualified_identifier, - positives: vec![Rule::identifier], + positives: vec![Rule::singular_identifier], negatives: vec![], pos: 0 } @@ -60,8 +60,8 @@ fn it_successfully_parses_a_qualified_identifier() { input: "foo.bar", rule: Rule::qualified_identifier, tokens: [qualified_identifier(0, 7, [ - identifier(0, 3), - identifier(4, 7) + singular_identifier(0, 3), + singular_identifier(4, 7) ])] } } @@ -73,8 +73,8 @@ fn it_successfully_excludes_trailing_whitespace() { input: "foo.bar ", rule: Rule::qualified_identifier, tokens: [qualified_identifier(0, 7, [ - identifier(0, 3), - identifier(4, 7) + singular_identifier(0, 3), + singular_identifier(4, 7) ])] } } @@ -86,12 +86,12 @@ fn it_successfully_parses_a_long_qualified_identifier() { input: "foo.bar.baz.qux.corge.grault", rule: Rule::qualified_identifier, tokens: [qualified_identifier(0, 28, [ - identifier(0, 3), - identifier(4, 7), - identifier(8, 11), - identifier(12, 15), - identifier(16, 21), - identifier(22, 28), + singular_identifier(0, 3), + singular_identifier(4, 7), + singular_identifier(8, 11), + singular_identifier(12, 15), + singular_identifier(16, 21), + singular_identifier(22, 28), ])] } } diff --git a/wdl-grammar/src/v1/tests/workflow_elements/scatter.rs b/wdl-grammar/src/v1/tests/workflow_elements/scatter.rs index d6c0b6d85..ae63b51ad 100644 --- a/wdl-grammar/src/v1/tests/workflow_elements/scatter.rs +++ b/wdl-grammar/src/v1/tests/workflow_elements/scatter.rs @@ -35,29 +35,58 @@ fn it_successfully_parses_scatter_without_spaces() { parser: WdlParser, input: "scatter(i in range(10)){call my_task}", rule: Rule::workflow_scatter, - tokens: [workflow_scatter(0, 37, [ + tokens: [ + // `scatter(i in range(10)){call my_task}` + workflow_scatter(0, 37, [ + // `(i in range(10))` workflow_scatter_iteration_statement(7, 23, [ - identifier(8, 9), - WHITESPACE(9, 10, [SPACE(9, 10)]), - WHITESPACE(12, 13, [SPACE(12, 13)]), + // `i` + workflow_scatter_iteration_statement_variable(8, 9, [ + // `i` + singular_identifier(8, 9), + ]), + WHITESPACE(9, 10, [ + SPACE(9, 10), + ]), + WHITESPACE(12, 13, [ + SPACE(12, 13), + ]), + // `range(10)` + workflow_scatter_iteration_statement_iterable(13, 22, [ + // `range(10)` expression(13, 22, [ - identifier(13, 18), - apply(18, 22, [ - expression(19, 21, [ - integer(19, 21, [ - integer_decimal(19, 21) - ]) - ]) - ]) + // `range` + singular_identifier(13, 18), + // `(10)` + call(18, 22, [ + // `10` + expression(19, 21, [ + // `10` + integer(19, 21, [ + // `10` + integer_decimal(19, 21), + ]), + ]), + ]), ]), + ]), ]), + // `call my_task` workflow_execution_statement(24, 36, [ - workflow_call(24, 36, [ - WHITESPACE(28, 29, [SPACE(28, 29)]), - identifier(29, 36) - ]) - ]) - ])] + // `call my_task` + workflow_call(24, 36, [ + WHITESPACE(28, 29, [ + SPACE(28, 29), + ]), + // `my_task` + workflow_call_name(29, 36, [ + // `my_task` + singular_identifier(29, 36), + ]), + ]), + ]), + ]) + ] } } @@ -67,29 +96,58 @@ fn it_successfully_excludes_trailing_whitespace() { parser: WdlParser, input: "scatter(i in range(10)){call my_task} ", rule: Rule::workflow_scatter, - tokens: [workflow_scatter(0, 37, [ + tokens: [ + // `scatter(i in range(10)){call my_task}` + workflow_scatter(0, 37, [ + // `(i in range(10))` workflow_scatter_iteration_statement(7, 23, [ - identifier(8, 9), - WHITESPACE(9, 10, [SPACE(9, 10)]), - WHITESPACE(12, 13, [SPACE(12, 13)]), + // `i` + workflow_scatter_iteration_statement_variable(8, 9, [ + // `i` + singular_identifier(8, 9), + ]), + WHITESPACE(9, 10, [ + SPACE(9, 10), + ]), + WHITESPACE(12, 13, [ + SPACE(12, 13), + ]), + // `range(10)` + workflow_scatter_iteration_statement_iterable(13, 22, [ + // `range(10)` expression(13, 22, [ - identifier(13, 18), - apply(18, 22, [ - expression(19, 21, [ - integer(19, 21, [ - integer_decimal(19, 21) - ]) - ]) - ]) + // `range` + singular_identifier(13, 18), + // `(10)` + call(18, 22, [ + // `10` + expression(19, 21, [ + // `10` + integer(19, 21, [ + // `10` + integer_decimal(19, 21), + ]), + ]), + ]), ]), + ]), ]), + // `call my_task` workflow_execution_statement(24, 36, [ - workflow_call(24, 36, [ - WHITESPACE(28, 29, [SPACE(28, 29)]), - identifier(29, 36) - ]) - ]) - ])] + // `call my_task` + workflow_call(24, 36, [ + WHITESPACE(28, 29, [ + SPACE(28, 29), + ]), + // `my_task` + workflow_call_name(29, 36, [ + // `my_task` + singular_identifier(29, 36), + ]), + ]), + ]), + ]) + ] } } @@ -99,23 +157,44 @@ fn it_successfully_parses_scatter_with_empty_body() { parser: WdlParser, input: "scatter(i in range(10)){}", rule: Rule::workflow_scatter, - tokens: [workflow_scatter(0, 25, [ + tokens: [ + // `scatter(i in range(10)){}` + workflow_scatter(0, 25, [ + // `(i in range(10))` workflow_scatter_iteration_statement(7, 23, [ - identifier(8, 9), - WHITESPACE(9, 10, [SPACE(9, 10)]), - WHITESPACE(12, 13, [SPACE(12, 13)]), + // `i` + workflow_scatter_iteration_statement_variable(8, 9, [ + // `i` + singular_identifier(8, 9), + ]), + WHITESPACE(9, 10, [ + SPACE(9, 10), + ]), + WHITESPACE(12, 13, [ + SPACE(12, 13), + ]), + // `range(10)` + workflow_scatter_iteration_statement_iterable(13, 22, [ + // `range(10)` expression(13, 22, [ - identifier(13, 18), - apply(18, 22, [ - expression(19, 21, [ - integer(19, 21, [ - integer_decimal(19, 21) - ]) - ]) - ]) + // `range` + singular_identifier(13, 18), + // `(10)` + call(18, 22, [ + // `10` + expression(19, 21, [ + // `10` + integer(19, 21, [ + // `10` + integer_decimal(19, 21), + ]), + ]), + ]), ]), + ]), ]), - ])] + ]) + ] } } @@ -125,37 +204,82 @@ fn it_successfully_parses_scatter_with_spaces() { parser: WdlParser, input: "scatter ( i in range( 10 ) ) { call my_task }", rule: Rule::workflow_scatter, - tokens: [workflow_scatter(0, 45, [ - WHITESPACE(7, 8, [SPACE(7, 8)]), + tokens: [ + // `scatter ( i in range( 10 ) ) { call my_task }` + workflow_scatter(0, 45, [ + WHITESPACE(7, 8, [ + SPACE(7, 8), + ]), + // `( i in range( 10 ) )` workflow_scatter_iteration_statement(8, 28, [ - WHITESPACE(9, 10, [SPACE(9, 10)]), - identifier(10, 11), - WHITESPACE(11, 12, [SPACE(11, 12)]), - WHITESPACE(14, 15, [SPACE(14, 15)]), + WHITESPACE(9, 10, [ + SPACE(9, 10), + ]), + // `i` + workflow_scatter_iteration_statement_variable(10, 11, [ + // `i` + singular_identifier(10, 11), + ]), + WHITESPACE(11, 12, [ + SPACE(11, 12), + ]), + WHITESPACE(14, 15, [ + SPACE(14, 15), + ]), + // `range( 10 )` + workflow_scatter_iteration_statement_iterable(15, 26, [ + // `range( 10 )` expression(15, 26, [ - identifier(15, 20), - apply(20, 26, [ - WHITESPACE(21, 22, [SPACE(21, 22)]), - expression(22, 24, [ - integer(22, 24, [ - integer_decimal(22, 24) - ]) - ]), - WHITESPACE(24, 25, [SPACE(24, 25)]), - ]) + // `range` + singular_identifier(15, 20), + // `( 10 )` + call(20, 26, [ + WHITESPACE(21, 22, [ + SPACE(21, 22), + ]), + // `10` + expression(22, 24, [ + // `10` + integer(22, 24, [ + // `10` + integer_decimal(22, 24), + ]), + ]), + WHITESPACE(24, 25, [ + SPACE(24, 25), + ]), + ]), ]), - WHITESPACE(26, 27, [SPACE(26, 27)]), + ]), + WHITESPACE(26, 27, [ + SPACE(26, 27), + ]), ]), - WHITESPACE(28, 29, [SPACE(28, 29)]), - WHITESPACE(30, 31, [SPACE(30, 31)]), + WHITESPACE(28, 29, [ + SPACE(28, 29), + ]), + WHITESPACE(30, 31, [ + SPACE(30, 31), + ]), + // `call my_task` workflow_execution_statement(31, 43, [ - workflow_call(31, 43, [ - WHITESPACE(35, 36, [SPACE(35, 36)]), - identifier(36, 43) - ]) + // `call my_task` + workflow_call(31, 43, [ + WHITESPACE(35, 36, [ + SPACE(35, 36), + ]), + // `my_task` + workflow_call_name(36, 43, [ + // `my_task` + singular_identifier(36, 43), + ]), + ]), + ]), + WHITESPACE(43, 44, [ + SPACE(43, 44), ]), - WHITESPACE(43, 44, [SPACE(43, 44)]), - ])] + ]) + ] } } @@ -164,226 +288,162 @@ fn it_successfully_parses_scatter_with_multiple_calls() { parses_to! { parser: WdlParser, input: "scatter (i in range(10)){ - call my_task - call other_task {} - call another_task {input:foo=bar} - }", + call my_task + call other_task {} + call another_task {input:foo=bar} +}", rule: Rule::workflow_scatter, - tokens: [workflow_scatter(0, 137, [ - WHITESPACE(7, 8, [ - SPACE(7, 8), - ]), - workflow_scatter_iteration_statement(8, 24, [ - identifier(9, 10), - WHITESPACE(10, 11, [ - SPACE(10, 11), - ]), - WHITESPACE(13, 14, [ - SPACE(13, 14), - ]), - expression(14, 23, [ - identifier(14, 19), - apply(19, 23, [ - expression(20, 22, [ - integer(20, 22, [ - integer_decimal(20, 22), - ]), - ]), - ]), + tokens: [ + // `scatter (i in range(10)){ call my_task call other_task {} call another_task {input:foo=bar} }` + workflow_scatter(0, 105, [ + WHITESPACE(7, 8, [ + SPACE(7, 8), ]), - ]), - // `` - WHITESPACE(25, 26, [ - // `` - NEWLINE(25, 26), - ]), - WHITESPACE(26, 27, [ - SPACE(26, 27), - ]), - WHITESPACE(27, 28, [ - SPACE(27, 28), - ]), - WHITESPACE(28, 29, [ - SPACE(28, 29), - ]), - WHITESPACE(29, 30, [ - SPACE(29, 30), - ]), - WHITESPACE(30, 31, [ - SPACE(30, 31), - ]), - WHITESPACE(31, 32, [ - SPACE(31, 32), - ]), - WHITESPACE(32, 33, [ - SPACE(32, 33), - ]), - WHITESPACE(33, 34, [ - SPACE(33, 34), - ]), - WHITESPACE(34, 35, [ - SPACE(34, 35), - ]), - WHITESPACE(35, 36, [ - SPACE(35, 36), - ]), - WHITESPACE(36, 37, [ - SPACE(36, 37), - ]), - WHITESPACE(37, 38, [ - SPACE(37, 38), - ]), - workflow_execution_statement(38, 50, [ - workflow_call(38, 50, [ - WHITESPACE(42, 43, [ - SPACE(42, 43), + // `(i in range(10))` + workflow_scatter_iteration_statement(8, 24, [ + // `i` + workflow_scatter_iteration_statement_variable(9, 10, [ + // `i` + singular_identifier(9, 10), ]), - identifier(43, 50), - ]), - ]), - // `` - WHITESPACE(50, 51, [ - // `` - NEWLINE(50, 51), - ]), - WHITESPACE(51, 52, [ - SPACE(51, 52), - ]), - WHITESPACE(52, 53, [ - SPACE(52, 53), - ]), - WHITESPACE(53, 54, [ - SPACE(53, 54), - ]), - WHITESPACE(54, 55, [ - SPACE(54, 55), - ]), - WHITESPACE(55, 56, [ - SPACE(55, 56), - ]), - WHITESPACE(56, 57, [ - SPACE(56, 57), - ]), - WHITESPACE(57, 58, [ - SPACE(57, 58), - ]), - WHITESPACE(58, 59, [ - SPACE(58, 59), - ]), - WHITESPACE(59, 60, [ - SPACE(59, 60), - ]), - WHITESPACE(60, 61, [ - SPACE(60, 61), - ]), - WHITESPACE(61, 62, [ - SPACE(61, 62), - ]), - WHITESPACE(62, 63, [ - SPACE(62, 63), - ]), - workflow_execution_statement(63, 81, [ - workflow_call(63, 81, [ - WHITESPACE(67, 68, [ - SPACE(67, 68), + WHITESPACE(10, 11, [ + SPACE(10, 11), ]), - identifier(68, 78), - WHITESPACE(78, 79, [ - SPACE(78, 79), + WHITESPACE(13, 14, [ + SPACE(13, 14), ]), - workflow_call_body(79, 81), - ]), - ]), - // `` - WHITESPACE(81, 82, [ - // `` - NEWLINE(81, 82), - ]), - WHITESPACE(82, 83, [ - SPACE(82, 83), - ]), - WHITESPACE(83, 84, [ - SPACE(83, 84), - ]), - WHITESPACE(84, 85, [ - SPACE(84, 85), - ]), - WHITESPACE(85, 86, [ - SPACE(85, 86), - ]), - WHITESPACE(86, 87, [ - SPACE(86, 87), - ]), - WHITESPACE(87, 88, [ - SPACE(87, 88), - ]), - WHITESPACE(88, 89, [ - SPACE(88, 89), - ]), - WHITESPACE(89, 90, [ - SPACE(89, 90), - ]), - WHITESPACE(90, 91, [ - SPACE(90, 91), - ]), - WHITESPACE(91, 92, [ - SPACE(91, 92), - ]), - WHITESPACE(92, 93, [ - SPACE(92, 93), - ]), - WHITESPACE(93, 94, [ - SPACE(93, 94), - ]), - workflow_execution_statement(94, 127, [ - workflow_call(94, 127, [ - WHITESPACE(98, 99, [ - SPACE(98, 99), + // `range(10)` + workflow_scatter_iteration_statement_iterable(14, 23, [ + // `range(10)` + expression(14, 23, [ + // `range` + singular_identifier(14, 19), + // `(10)` + call(19, 23, [ + // `10` + expression(20, 22, [ + // `10` + integer(20, 22, [ + // `10` + integer_decimal(20, 22), + ]), + ]), + ]), + ]), ]), - identifier(99, 111), - WHITESPACE(111, 112, [ - SPACE(111, 112), + ]), + WHITESPACE(25, 26, [ + NEWLINE(25, 26), + ]), + WHITESPACE(26, 27, [ + SPACE(26, 27), + ]), + WHITESPACE(27, 28, [ + SPACE(27, 28), + ]), + WHITESPACE(28, 29, [ + SPACE(28, 29), + ]), + WHITESPACE(29, 30, [ + SPACE(29, 30), + ]), + // `call my_task` + workflow_execution_statement(30, 42, [ + // `call my_task` + workflow_call(30, 42, [ + WHITESPACE(34, 35, [ + SPACE(34, 35), + ]), + // `my_task` + workflow_call_name(35, 42, [ + // `my_task` + singular_identifier(35, 42), + ]), ]), - workflow_call_body(112, 127, [ - workflow_call_input(119, 126, [ - identifier(119, 122), - expression(123, 126, [ - identifier(123, 126), + ]), + WHITESPACE(42, 43, [ + NEWLINE(42, 43), + ]), + WHITESPACE(43, 44, [ + SPACE(43, 44), + ]), + WHITESPACE(44, 45, [ + SPACE(44, 45), + ]), + WHITESPACE(45, 46, [ + SPACE(45, 46), + ]), + WHITESPACE(46, 47, [ + SPACE(46, 47), + ]), + // `call other_task {}` + workflow_execution_statement(47, 65, [ + // `call other_task {}` + workflow_call(47, 65, [ + WHITESPACE(51, 52, [ + SPACE(51, 52), + ]), + // `other_task` + workflow_call_name(52, 62, [ + // `other_task` + singular_identifier(52, 62), + ]), + WHITESPACE(62, 63, [ + SPACE(62, 63), + ]), + // `{}` + workflow_call_body(63, 65), + ]), + ]), + WHITESPACE(65, 66, [ + NEWLINE(65, 66), + ]), + WHITESPACE(66, 67, [ + SPACE(66, 67), + ]), + WHITESPACE(67, 68, [ + SPACE(67, 68), + ]), + WHITESPACE(68, 69, [ + SPACE(68, 69), + ]), + WHITESPACE(69, 70, [ + SPACE(69, 70), + ]), + // `call another_task {input:foo=bar}` + workflow_execution_statement(70, 103, [ + // `call another_task {input:foo=bar}` + workflow_call(70, 103, [ + WHITESPACE(74, 75, [ + SPACE(74, 75), + ]), + // `another_task` + workflow_call_name(75, 87, [ + // `another_task` + singular_identifier(75, 87), + ]), + WHITESPACE(87, 88, [ + SPACE(87, 88), + ]), + // `{input:foo=bar}` + workflow_call_body(88, 103, [ + // `foo=bar` + workflow_call_input(95, 102, [ + // `foo` + singular_identifier(95, 98), + // `bar` + expression(99, 102, [ + // `bar` + singular_identifier(99, 102), + ]), ]), ]), ]), ]), - ]), - // `` - WHITESPACE(127, 128, [ - // `` - NEWLINE(127, 128), - ]), - WHITESPACE(128, 129, [ - SPACE(128, 129), - ]), - WHITESPACE(129, 130, [ - SPACE(129, 130), - ]), - WHITESPACE(130, 131, [ - SPACE(130, 131), - ]), - WHITESPACE(131, 132, [ - SPACE(131, 132), - ]), - WHITESPACE(132, 133, [ - SPACE(132, 133), - ]), - WHITESPACE(133, 134, [ - SPACE(133, 134), - ]), - WHITESPACE(134, 135, [ - SPACE(134, 135), - ]), - WHITESPACE(135, 136, [ - SPACE(135, 136), - ]), - ]) - ] + WHITESPACE(103, 104, [ + NEWLINE(103, 104), + ]), + ]) + ] } } diff --git a/wdl-grammar/src/v1/validation.rs b/wdl-grammar/src/v1/validation.rs index e53537081..effa3c606 100644 --- a/wdl-grammar/src/v1/validation.rs +++ b/wdl-grammar/src/v1/validation.rs @@ -1,13 +1,22 @@ -//! Validation rules for WDL 1.x. +//! Validation rules for WDL 1.x parse trees. + +use lazy_static::lazy_static; +use pest::iterators::Pairs; + +use wdl_core as core; mod invalid_escape_character; pub use invalid_escape_character::InvalidEscapeCharacter; -use crate::core::validation::Rule; use crate::v1; -/// Gets all validation rules available for WDL 1.x. -pub fn rules() -> Vec>> { - vec![Box::new(InvalidEscapeCharacter)] +/// A boxed validation rule for the v1 grammar. +type Rule = Box core::validation::Rule>>; + +lazy_static! { + /// All validation rules available for WDL 1.x parse trees. + pub static ref RULES: Vec = vec![ + Box::new(InvalidEscapeCharacter) + ]; } diff --git a/wdl-grammar/src/v1/validation/invalid_escape_character.rs b/wdl-grammar/src/v1/validation/invalid_escape_character.rs index 4eb364e1d..b1b17740d 100644 --- a/wdl-grammar/src/v1/validation/invalid_escape_character.rs +++ b/wdl-grammar/src/v1/validation/invalid_escape_character.rs @@ -2,23 +2,26 @@ use pest::iterators::Pairs; -use crate::core::validation; -use crate::core::validation::Rule; -use crate::core::Code; +use wdl_core as core; + +use core::validation; +use core::validation::Rule; +use core::Code; +use core::Version; + use crate::v1; -use crate::Version; /// An invalid escape character within a string. #[derive(Debug)] pub struct InvalidEscapeCharacter; -impl Rule for InvalidEscapeCharacter { +impl<'a> Rule> for InvalidEscapeCharacter { fn code(&self) -> Code { // SAFETY: this manually crafted to unwrap successfully every time. Code::try_new(Version::V1, 1).unwrap() } - fn validate(&self, tree: Pairs<'_, v1::Rule>) -> validation::Result { + fn validate(&self, tree: Pairs<'a, v1::Rule>) -> validation::Result { tree.flatten().try_for_each(|node| match node.as_rule() { v1::Rule::char_escaped_invalid => { let (line_no, col) = node.line_col(); @@ -42,9 +45,9 @@ impl Rule for InvalidEscapeCharacter { mod tests { use pest::Parser as _; - use crate::core::validation::Rule as _; use crate::v1::parse::Parser; use crate::v1::Rule; + use wdl_core::validation::Rule as _; use super::*; diff --git a/wdl-grammar/src/v1/wdl.pest b/wdl-grammar/src/v1/wdl.pest index 37c074f23..f8e190d18 100644 --- a/wdl-grammar/src/v1/wdl.pest +++ b/wdl-grammar/src/v1/wdl.pest @@ -1,6 +1,6 @@ -// ============// +// ========== // // Whitespace // -// ============// +// ========== // // Pest provides relatively good support for whitespace out of the box. However, // we decided that we are our parse tree to include details on the specific @@ -18,27 +18,30 @@ LINE_ENDING = _{ NEWLINE | CARRIAGE_RETURN_NEWLINE | CARRIAGE_RETURN WHITESPACE = ${ LINE_ENDING | INDENT } -// ==========// +// ======== // // Comments // -// ==========// +// ======== // COMMENT = { "#" ~ (!LINE_ENDING ~ ANY)* } -// =======// +// ===== // // Atoms // -// =======// +// ===== // OPTION = { "?" } ONE_OR_MORE = { "+" } COMMA = { "," } -// ==========// +// ======== // // Literals // -// ==========// +// ======== // // None. none = { "None" } +// Null. +null = { "null" } + // Boolean. boolean = { "true" | "false" } @@ -88,7 +91,6 @@ char_special = ${ char_escaped | char_hex | char_unicode | char_octal | char_other = @{ !("\\" | "\n") ~ ANY } // String. - double_quote = { "\"" } single_quote = { "\'" } string_expression_placeholder_start = { "~{" | "${" } @@ -112,19 +114,21 @@ string_placeholder = ${ // compound-atomic (`$`). This is because we don't want rules eating up // whitespace within a string. This is not checked by the compiler, so you must // ensure it remains true. -string = ${ - PUSH(double_quote | single_quote) ~ (string_placeholder | string_literal_contents)* ~ POP +string_inner = ${ (string_placeholder | string_literal_contents)* } +string = ${ + PUSH(double_quote | single_quote) ~ string_inner ~ POP } -// Identifier. -identifier = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* } +// Identifiers. +singular_identifier = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* } +qualified_identifier = ${ singular_identifier ~ ("." ~ singular_identifier)+ } // Literal. -literal = _{ boolean | number | string | none | identifier } +literal = _{ boolean | number | string | none | singular_identifier } -// =============// +// =========== // // Expressions // -// =============// +// =========== // // Prefix. or = { "||" } @@ -145,16 +149,17 @@ infix = _{ or | and | add | sub | mul | div | remainder | eq | neq | lte | gte | // Prefix. negation = { "!" } -unary_signed = { "+" | "-" } +unary_signed_positive = { "+" } +unary_signed_negative = { "-" } -prefix = _{ negation | unary_signed } +prefix = _{ negation | unary_signed_positive | unary_signed_negative } // Postfix. -member = ${ "." ~ identifier } +member = ${ "." ~ singular_identifier } index = !{ "[" ~ expression ~ "]" } -apply = !{ "(" ~ (expression ~ (COMMA ~ expression)* ~ COMMA?)* ~ ")" } +call = !{ "(" ~ (expression ~ (COMMA ~ expression)* ~ COMMA?)* ~ ")" } -postfix = _{ member | index | apply } +postfix = _{ member | index | call } // Core elements. // @@ -165,7 +170,7 @@ postfix = _{ member | index | apply } // make comma delimiters optional when parsing and enforce style rules via a // linter built on top of this parser. -identifier_based_kv_key = { identifier } +identifier_based_kv_key = { singular_identifier } expression_based_kv_key = { expression } kv_value = { expression } identifier_based_kv_pair = { identifier_based_kv_key ~ ":" ~ kv_value } @@ -176,12 +181,12 @@ if = ${ "if" ~ (WHITESPACE | COMMENT)+ ~ expression ~ (WHITESPACE | COMMENT)+ ~ "then" ~ (WHITESPACE | COMMENT)+ ~ expression ~ (WHITESPACE | COMMENT)+ ~ "else" ~ (WHITESPACE | COMMENT)+ ~ expression } object_literal = !{ "object" ~ "{" ~ (identifier_based_kv_pair ~ (COMMA? ~ identifier_based_kv_pair)* ~ COMMA?)* ~ "}" } -struct_literal = !{ identifier ~ "{" ~ (identifier_based_kv_pair ~ (COMMA? ~ identifier_based_kv_pair)* ~ COMMA?)* ~ "}" } +struct_literal = !{ singular_identifier ~ "{" ~ (identifier_based_kv_pair ~ (COMMA? ~ identifier_based_kv_pair)* ~ COMMA?)* ~ "}" } map_literal = !{ "{" ~ (expression_based_kv_pair ~ (COMMA? ~ expression_based_kv_pair)* ~ COMMA?)* ~ "}" } array_literal = !{ "[" ~ (expression ~ (COMMA ~ expression)* ~ COMMA?)* ~ "]" } pair_literal = !{ "(" ~ expression ~ COMMA ~ expression ~ ")" } -core = _{ +atom = _{ group | if | object_literal @@ -190,7 +195,6 @@ core = _{ | array_literal | pair_literal | literal - | identifier } // Expression. @@ -206,9 +210,9 @@ core = _{ // As such, you will see that none of the permutations of the rule below end in // an optional token. That is by design to avoid the problem above. expression = ${ - prefix* ~ (WHITESPACE | COMMENT)* ~ core ~ (WHITESPACE | COMMENT)* ~ postfix* ~ ((WHITESPACE | COMMENT)* ~ infix ~ (WHITESPACE | COMMENT)* ~ prefix* ~ (WHITESPACE | COMMENT)* ~ core ~ (WHITESPACE | COMMENT)* ~ postfix+ | (WHITESPACE | COMMENT)* ~ infix ~ (WHITESPACE | COMMENT)* ~ prefix* ~ (WHITESPACE | COMMENT)* ~ core)+ - | prefix* ~ (WHITESPACE | COMMENT)* ~ core ~ (WHITESPACE | COMMENT)* ~ postfix+ - | prefix* ~ (WHITESPACE | COMMENT)* ~ core + prefix* ~ (WHITESPACE | COMMENT)* ~ atom ~ (WHITESPACE | COMMENT)* ~ postfix* ~ ((WHITESPACE | COMMENT)* ~ infix ~ (WHITESPACE | COMMENT)* ~ prefix* ~ (WHITESPACE | COMMENT)* ~ atom ~ (WHITESPACE | COMMENT)* ~ postfix+ | (WHITESPACE | COMMENT)* ~ infix ~ (WHITESPACE | COMMENT)* ~ prefix* ~ (WHITESPACE | COMMENT)* ~ atom)+ + | prefix* ~ (WHITESPACE | COMMENT)* ~ atom ~ (WHITESPACE | COMMENT)* ~ postfix+ + | prefix* ~ (WHITESPACE | COMMENT)* ~ atom } // Types. @@ -245,7 +249,7 @@ wdl_type_inner = ${ | (int_type ~ OPTION) | (float_type ~ OPTION) | (object_type ~ OPTION) - | (identifier ~ OPTION) + | (singular_identifier ~ OPTION) | map_type | array_type | pair_type @@ -255,7 +259,7 @@ wdl_type_inner = ${ | int_type | float_type | object_type - | identifier + | singular_identifier } // NOTE: this rule requires a positive predicate whitespace because there @@ -265,7 +269,8 @@ wdl_type_inner = ${ // // For example, when considering `IntermediateFiles`, `Int` matching the integer // `wdl_type` and `ermediateFiles` matching the `identifier`. -wdl_type = ${ +struct_type = { singular_identifier } +wdl_type = ${ (map_type ~ OPTION ~ &WHITESPACE) | (array_type ~ OPTION ~ &WHITESPACE) | (pair_type ~ OPTION ~ &WHITESPACE) @@ -275,7 +280,7 @@ wdl_type = ${ | (int_type ~ OPTION ~ &WHITESPACE) | (float_type ~ OPTION ~ &WHITESPACE) | (object_type ~ OPTION ~ &WHITESPACE) - | (identifier ~ OPTION ~ &WHITESPACE) + | (struct_type ~ OPTION ~ &WHITESPACE) | (map_type ~ &WHITESPACE) | (array_type ~ &WHITESPACE) | (pair_type ~ &WHITESPACE) @@ -285,32 +290,38 @@ wdl_type = ${ | (int_type ~ &WHITESPACE) | (float_type ~ &WHITESPACE) | (object_type ~ &WHITESPACE) - | (identifier ~ &WHITESPACE) + | (struct_type ~ &WHITESPACE) } -unbound_declaration = { wdl_type ~ identifier } +unbound_declaration_name = { singular_identifier } +unbound_declaration = { wdl_type ~ unbound_declaration_name } -bound_declaration = { wdl_type ~ identifier ~ "=" ~ expression } +bound_declaration_name = { singular_identifier } +bound_declaration = { wdl_type ~ bound_declaration_name ~ "=" ~ expression } declaration = _{ bound_declaration | unbound_declaration } -struct = { "struct" ~ identifier ~ "{" ~ (unbound_declaration)* ~ "}" } +struct_name = { singular_identifier } +struct = { "struct" ~ struct_name ~ "{" ~ (unbound_declaration)* ~ "}" } // Imports. -import_as = @{ "as" ~ (WHITESPACE | COMMENT)+ ~ identifier } -import_alias = @{ - "alias" ~ (WHITESPACE | COMMENT)+ ~ identifier ~ (WHITESPACE | COMMENT)+ ~ "as" ~ (WHITESPACE | COMMENT)+ ~ identifier +import_uri = { string } +import_as = ${ "as" ~ (WHITESPACE | COMMENT)+ ~ singular_identifier } +import_alias_from = { singular_identifier } +import_alias_to = { singular_identifier } +import_alias = ${ + "alias" ~ (WHITESPACE | COMMENT)+ ~ import_alias_from ~ (WHITESPACE | COMMENT)+ ~ "as" ~ (WHITESPACE | COMMENT)+ ~ import_alias_to } -import = ${ - "import" ~ (WHITESPACE | COMMENT)+ ~ string ~ ((WHITESPACE | COMMENT)+ ~ import_as)? ~ ((WHITESPACE | COMMENT)+ ~ import_alias)* +import = ${ + "import" ~ (WHITESPACE | COMMENT)+ ~ import_uri ~ ((WHITESPACE | COMMENT)+ ~ import_as)? ~ ((WHITESPACE | COMMENT)+ ~ import_alias)* } -// ================================// +// ============================== // // Common Workflow/Tasks Elements // -// ================================// +// ============================== // // NOTE: the specification states the following in the workflow section: // @@ -318,68 +329,51 @@ import = ${ // nearly the same usage in workflows as they do in tasks, so we just link to // their earlier descriptions. // -// - Input section. +// - Inputs. // - Private declarations. -// - Output section. +// - Outputs. +// - Metadata. +// - Parameter metadata. // -// As such, I will use a common set of silent rules to define any single rule -// that can be aliased. Note that the cascading rules for metadata sections and -// parameter metadata sections cannot be aliased in this way, as the -// context-specific rules are nested. As such, those sections are duplicated in -// the "Task" and "Workflow" sections below respectively. +// As such, we have used a common set of elements within the two (listed below). // Common input declaration. -common_input = _{ "input" ~ "{" ~ (declaration)* ~ "}" } +input = { "input" ~ "{" ~ (declaration)* ~ "}" } // Common output declaration. -common_output = _{ "output" ~ "{" ~ (bound_declaration)* ~ "}" } +output = { "output" ~ "{" ~ (bound_declaration)* ~ "}" } // Common private declarations. -common_private_declarations = _{ (bound_declaration)+ } +private_declarations = { (bound_declaration)+ } -// Common metadata elements. -// -// DIVERGE: the specification says that this is equal sign (`=`) delimited, but -// the examples show it actually being colon (`:`) delimited. As such, I've used -// colon here. -common_metadata_kv = _{ identifier ~ ":" ~ common_metadata_value } +// Common metadata parameters. +metadata_key = { singular_identifier } +metadata_kv = { metadata_key ~ ":" ~ metadata_value } -common_metadata_value = _{ +metadata_value = { string | number | boolean - | "null" - | common_metadata_object - | common_metadata_array + | null + | metadata_object + | metadata_array } -common_metadata_object = _{ "{}" | "{" ~ common_metadata_kv ~ (COMMA ~ common_metadata_kv)* ~ "}" } -common_metadata_array = _{ "[]" | "[" ~ common_metadata_value ~ (COMMA ~ common_metadata_value)* ~ "]" } +metadata_object = { "{}" | "{" ~ metadata_kv ~ (COMMA ~ metadata_kv)* ~ COMMA? ~ "}" } +metadata_array = { "[]" | "[" ~ metadata_value ~ (COMMA ~ metadata_value)* ~ COMMA? ~ "]" } -// =======// +metadata = { "meta" ~ "{" ~ metadata_kv* ~ "}" } +parameter_metadata = { "parameter_meta" ~ "{" ~ metadata_kv* ~ "}" } + +// ===== // // Tasks // -// =======// +// ===== // // Task runtimes. -// -// DIVERGE: given the below logic concerning optional commas to delimit members -// of `task_metadata_object`s, we determined it would be strange to not also -// allow commas to delimit members of the runtime objects. As -// such, though the specification states that not delimiting is not allowed, we -// allow these keys to be optionally delimited by a comma. -// -// NOTE: the `task_runtime_mapping_inner` and `task_runtime_mapping` are -// structured in this way to avoid consuming whitespace at the end of the line -// by adding an optional comma rule. -task_runtime_mapping_inner = _{ identifier ~ ":" ~ expression } -task_runtime_mapping = { task_runtime_mapping_inner ~ COMMA | task_runtime_mapping_inner } -task_runtime = { "runtime" ~ "{" ~ (task_runtime_mapping)* ~ "}" } - -// Task input. -task_input = { common_input } - -// Task output. -task_output = { common_output } +task_runtime_mapping_value = { expression } +task_runtime_mapping_key = { singular_identifier } +task_runtime_mapping = { task_runtime_mapping_key ~ ":" ~ task_runtime_mapping_value } +task_runtime = { "runtime" ~ "{" ~ (task_runtime_mapping)* ~ "}" } // Expression placeholder options. // @@ -410,8 +404,8 @@ placeholder_option = { placeholder_options = { placeholder_option+ } // Task commands, curly. -command_curly_begin = { "command" ~ "{" } -command_curly_end = { "}" } +command_curly_begin = !{ "command" ~ "{" } +command_curly_end = { "}" } command_curly_expression_placeholder_start = _{ "~{" | "${" } command_curly_expression_placeholder_end = _{ "}" } @@ -422,16 +416,20 @@ command_curly_literal_contents = _{ command_curly_expression_placeholder_expression = { command_curly_placeholder | expression } -command_curly_placeholder = { +command_curly_placeholder = !{ command_curly_expression_placeholder_start ~ placeholder_options* ~ command_curly_expression_placeholder_expression ~ command_curly_expression_placeholder_end } -command_curly = { - command_curly_begin ~ (command_curly_placeholder | command_curly_literal_contents)* ~ command_curly_end +command_curly_contents = { + (command_curly_placeholder | command_curly_literal_contents)* +} + +command_curly = ${ + command_curly_begin ~ command_curly_contents ~ command_curly_end } // Task commands, heredoc. -command_heredoc_begin = { "command" ~ "<<<" } +command_heredoc_begin = !{ "command" ~ "<<<" } command_heredoc_end = { ">>>" } command_heredoc_expression_placeholder_start = _{ "~{" } command_heredoc_expression_placeholder_end = _{ "}" } @@ -442,180 +440,87 @@ command_heredoc_literal_contents = _{ command_heredoc_expression_placeholder_expression = { command_heredoc_placeholder | expression } -command_heredoc_placeholder = { +command_heredoc_placeholder = !{ command_heredoc_expression_placeholder_start ~ placeholder_options* ~ command_heredoc_expression_placeholder_expression ~ command_heredoc_expression_placeholder_end } -command_heredoc = { - command_heredoc_begin ~ (command_heredoc_placeholder | command_heredoc_literal_contents)* ~ command_heredoc_end +command_heredoc_contents = { + (command_heredoc_placeholder | command_heredoc_literal_contents)* } -task_command = { (command_heredoc | command_curly) } - -// Task private declarations. -task_private_declarations = { common_private_declarations } - -// Task metadata and parameter metadata. -task_metadata_kv = { identifier ~ ":" ~ task_metadata_value } - -task_metadata_value = { - string - | number - | boolean - | "null" - | task_metadata_object - | task_metadata_array +command_heredoc = ${ + command_heredoc_begin ~ command_heredoc_contents ~ command_heredoc_end } -// DIVERGE: the specification indicates that the members of both objects and -// arrays should have the same delimiter. From the spec, they specify `,` as the -// delimiter (with no quotes)—this leads to ambiguity as to whether a literal -// comma is needed to delimit these items. -// -// In the case of an array, it seems obvious that a comma is needed to delimit -// items. It is not as obvious whether commas should be required for objects. -// Notably, both of these constructs exist elsewhere in the specification (e.g., -// object literals and array literals), and a comma delimiter is _required_ in -// those cases. -// -// For the object rule below, the comma is designated as optional (`?`) when -// delimiting items. This is following a long discussion within our team -// regarding the inconsistent treatment of required comma delimiters for -// structs, maps, and objects. In short, we have decided to make comma -// delimiters optional when parsing and enforce style rules via a linter built -// on top of this parser. -task_metadata_object = { "{}" | "{" ~ task_metadata_kv ~ (COMMA? ~ task_metadata_kv)* ~ "}" } -task_metadata_array = { "[]" | "[" ~ task_metadata_value ~ (COMMA? ~ task_metadata_value)* ~ "]" } - -// DIVERGE: given the above logic concerning optional commas to delimit members -// of `task_metadata_object`s, we determined it would be strange to not also -// allow commas to delimit members of the the top-level objects themselves. As -// such, though the specification states that not delimiting is not allowed, we -// allow these keys to be optionally delimited by a comma. -// -// Below are the old rules used in case we ever need them. -// -// task_metadata = { "meta" ~ "{" ~ task_metadata_kv* ~ "}" } -// task_parameter_metadata = { "parameter_meta" ~ "{" ~ task_metadata_kv* ~ "}" -task_metadata = { "meta" ~ "{" ~ task_metadata_kv? ~ (COMMA? ~ task_metadata_kv)* ~ "}" } -task_parameter_metadata = { "parameter_meta" ~ "{" ~ task_metadata_kv? ~ (COMMA? ~ task_metadata_kv)* ~ "}" } +task_command = { (command_heredoc | command_curly) } // Task elements. +// +// Note: all task elements except for `private_declarations` can be declared +// only once. This is not trivial to express as part of the grammar—especially +// since these elements can come in any order. As such, ensuring this remains +// true is left to postprocessing. task_element = { - task_input - | task_output + input + | output | task_command | task_runtime - | task_private_declarations - | task_parameter_metadata - | task_metadata + | private_declarations + | parameter_metadata + | metadata } -task = { "task" ~ identifier ~ "{" ~ task_element+ ~ "}" } +task = { "task" ~ singular_identifier ~ "{" ~ task_element+ ~ "}" } -// ===========// +// ========= // // Workflows // -// ===========// - -// Workflow input. -workflow_input = { common_input } - -// Workflow output. -workflow_output = { common_output } - -// Workflow private declarations. -workflow_private_declarations = { common_private_declarations } +// ========= // // Workflow call -qualified_identifier = ${ identifier ~ ("." ~ identifier)+ } -workflow_call_input = { - (identifier ~ "=" ~ expression) - | (identifier ~ "=" ~ identifier) - | identifier -} -// DIVERGE: spec is ambiguous about whether whitespace is +workflow_call_input = { (singular_identifier ~ "=" ~ expression) | singular_identifier } +// DIVERGE: the spec is ambiguous about whether whitespace is // allowed between the opening and closing brackets (`{}`) // when the call body is empty. We have opted to allow whitespace. workflow_call_body = !{ "{" ~ ("input:" ~ workflow_call_input ~ (COMMA ~ workflow_call_input)*)? ~ COMMA? ~ "}" } -workflow_call_as = ${ "as" ~ (WHITESPACE | COMMENT)+ ~ identifier } -workflow_call_after = ${ "after" ~ (WHITESPACE | COMMENT)+ ~ identifier } +workflow_call_as = ${ "as" ~ (WHITESPACE | COMMENT)+ ~ singular_identifier } +workflow_call_after = ${ "after" ~ (WHITESPACE | COMMENT)+ ~ singular_identifier } +workflow_call_name = { qualified_identifier | singular_identifier } workflow_call = ${ - ("call" ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier) ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_as)? ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_after)* ~ ((WHITESPACE | COMMENT)* ~ workflow_call_body)) - | ("call" ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier) ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_as)? ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_after)+) - | ("call" ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier) ~ (WHITESPACE | COMMENT)+ ~ workflow_call_as) - | ("call" ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier)) + ("call" ~ (WHITESPACE | COMMENT)+ ~ workflow_call_name ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_as)? ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_after)* ~ ((WHITESPACE | COMMENT)* ~ workflow_call_body)) + | ("call" ~ (WHITESPACE | COMMENT)+ ~ workflow_call_name ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_as)? ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_after)+) + | ("call" ~ (WHITESPACE | COMMENT)+ ~ workflow_call_name ~ (WHITESPACE | COMMENT)+ ~ workflow_call_as) + | ("call" ~ (WHITESPACE | COMMENT)+ ~ workflow_call_name) } // Workflow scatter -workflow_scatter_iteration_statement = { "(" ~ identifier ~ "in" ~ expression ~ ")" } -workflow_scatter = { +workflow_scatter_iteration_statement_variable = { singular_identifier } +workflow_scatter_iteration_statement_iterable = { expression } +workflow_scatter_iteration_statement = { "(" ~ workflow_scatter_iteration_statement_variable ~ "in" ~ workflow_scatter_iteration_statement_iterable ~ ")" } +workflow_scatter = { "scatter" ~ workflow_scatter_iteration_statement ~ "{" ~ workflow_execution_statement* ~ "}" } // Workflow conditional -workflow_conditional = { "if" ~ "(" ~ expression ~ ")" ~ "{" ~ workflow_execution_statement* ~ "}" } +workflow_conditional_condition = { expression } +workflow_conditional = { "if" ~ "(" ~ workflow_conditional_condition ~ ")" ~ "{" ~ workflow_execution_statement* ~ "}" } // Workflow execution statements workflow_execution_statement = { - (workflow_conditional | workflow_scatter | workflow_call | workflow_private_declarations) + (workflow_conditional | workflow_scatter | workflow_call | private_declarations) } -// Workflow metadata and parameter metadata. -workflow_metadata_kv = { identifier ~ ":" ~ workflow_metadata_value } - -workflow_metadata_value = { - string - | number - | boolean - | "null" - | workflow_metadata_object - | workflow_metadata_array -} - -// DIVERGE: the specification indicates that the members of both objects and -// arrays should have the same delimiter. From the spec, they specify `,` as the -// delimiter (with no quotes)—this leads to ambiguity as to whether a literal -// comma is needed to delimit these items. -// -// In the case of an array, it seems obvious that a comma is needed to delimit -// items. It is not as obvious whether commas should be required for objects. -// Notably, both of these constructs exist elsewhere in the specification (e.g., -// object literals and array literals), and a comma delimiter is _required_ in -// those cases. -// -// For the object rule below, the comma is designated as optional (`?`) when -// delimiting items. This is following a long discussion within our team -// regarding the inconsistent treatment of required comma delimiters for -// structs, maps, and objects. In short, we have decided to make comma -// delimiters optional when parsing and enforce style rules via a linter built -// on top of this parser. -workflow_metadata_object = { "{}" | "{" ~ workflow_metadata_kv ~ (COMMA? ~ workflow_metadata_kv)* ~ "}" } -workflow_metadata_array = { "[]" | "[" ~ workflow_metadata_value ~ (COMMA ~ workflow_metadata_value)* ~ "]" } - -// DIVERGE: given the above logic concerning optional commas to delimit members -// of `workflow_metadata_object`s, we determined it would be strange to not also -// allow commas to delimit members of the the top-level objects themselves. As -// such, though the specification states that not delimiting is not allowed, we -// allow these keys to be optionally delimited by a comma. -// -// Below are the old rules used in case we ever need them. -// -// workflow_metadata = { "meta" ~ "{" ~ workflow_metadata_kv* ~ "}" } -// workflow_parameter_metadata = { "parameter_meta" ~ "{" ~ workflow_metadata_kv* ~ "}" -workflow_metadata = { "meta" ~ "{" ~ workflow_metadata_kv? ~ (COMMA? ~ workflow_metadata_kv)* ~ "}" } -workflow_parameter_metadata = { "parameter_meta" ~ "{" ~ workflow_metadata_kv? ~ (COMMA? ~ workflow_metadata_kv)* ~ "}" } - workflow_element = _{ - workflow_input - | workflow_output + input + | output | workflow_execution_statement - | workflow_parameter_metadata - | workflow_metadata + | parameter_metadata + | metadata } -workflow = { "workflow" ~ identifier ~ "{" ~ workflow_element* ~ "}" } +workflow_name = { singular_identifier } +workflow = { "workflow" ~ workflow_name ~ "{" ~ workflow_element* ~ "}" } // Document elements. //