choreo 0.13.0

DSL for BDD type testing.
Documentation
// choreo - A DSL for defining interactive test scenarios.
// This grammar defines the DSL structure and syntax.

// Ignore whitespace and comments.
WHITESPACE                        = _{ " " | "\t" | NEWLINE }
COMMENT                           = _{ "#" ~ (!NEWLINE ~ ANY)* }

// choreo DSL is a series of statements.
grammar                           = { SOI ~ statement* ~ EOI }
statement                         = { feature_def | settings_def | env_def | var_def | actors_def | background_def | scenario_def }

// Setting up the test suite.
settings_def = {
    ("setting:" ~ setting ~ ("," ~ setting)*) |
    ("settings" ~ "{" ~ setting+ ~ "}")
}
setting                           = { identifier ~ WHITESPACE* ~ "=" ~ WHITESPACE* ~ (value | binary_op) }
background_def                    = { "background" ~ "{" ~ (action | condition)+ ~ "}" }
env_def = {
    ("env:" ~ identifier ~ ("," ~ identifier)*) |
    ("env" ~ "{" ~ identifier+ ~ "}") |
    ("env" ~ identifier)
}

var_def = {
    ("var" ~ identifier ~ WHITESPACE* ~ "=" ~ WHITESPACE* ~ value) |
    ("var" ~ identifier ~ WHITESPACE* ~ "=" ~ WHITESPACE* ~ array) |
    ("var:" ~ identifier ~ ("," ~ identifier)*)
}

actors_def = {
    ("actors:" ~ identifier ~ ("," ~ identifier)*) |
    ("actors" ~ "{" ~ identifier+ ~ "}") |
    ("actor" ~ identifier)
}

feature_def                       = { "feature" ~ string }
parallel_keyword                  = { "parallel" }

scenario_body_item = { test | foreach_block }
scenario_body = { scenario_body_item* }
scenario_def = {  parallel_keyword? ~ "scenario" ~ string ~ "{" ~ scenario_body ~ after_block? ~ "}" }
after_block                       = { "after" ~ "{" ~ action+ ~ "}" }

foreach_block = { "foreach" ~ identifier ~ "in" ~ (variable_ref | identifier) ~ "{" ~ test+ ~ "}" }
variable_ref = { "${" ~ identifier ~ ( ("[" ~ number ~ "]") | ("." ~ identifier) )* ~ "}" }

// Defining a test case.
test                              = { "test" ~ (identifier | string | variable_ref) ~ string ~ "{" ~ given_block ~ when_block ~ then_block ~ "}" }
given_block                       = { "given:" ~ (action | condition)* }
when_block                        = { "when:"  ~ action+ }
then_block                        = { "then:"  ~ condition+ }

// --- Generic Rule Block ---
rule                              = { "rule" ~ string ~ "{" ~ when_block_rule ~ then_block_rule ~ "}" }
when_block_rule                   = { "when:" ~ condition+ }
then_block_rule                   = { "then:" ~ action+ }

// --- Conditions & Actions ---
condition = {
    wait_for_condition |
    check_state |
    check_filesystem |
    check_terminal |
    check_web
}

action = {
    perform_system_action |
    perform_filesystem_action |
    perform_terminal_run |
    perform_web_action
}

// Condition Types (Imperative Verb-Subject)
wait_for_condition                = { "wait" ~ WHITESPACE* ~ comparison_op ~ WHITESPACE* ~ wait_marker }
check_state                       = { "Test" ~ (expect_success | expect_startable) }
expect_success                    = { "expect_success" ~ identifier }
expect_startable                  = { "expect_startable" }

// --- Filesystem Conditions ---
check_file_not_empty              = { "file" ~ non_empty_string ~ "is_not_empty" }
check_file_empty                  = { "file" ~ non_empty_string ~ "is_empty" }
filesystem_condition_keyword      = @{ "expect_file" | "expect_no_file" | "expect_dir" | "expect_no_dir" | "expect_content" }
check_filesystem                  = { "FileSystem" ~ (
    (filesystem_condition_keyword ~ non_empty_string ~ "with_content" ~ string) |
    (filesystem_condition_keyword ~ non_empty_string) |
    check_file_not_empty |
    check_file_empty
) }

// --- Terminal Conditions ---
check_terminal                    = { "Terminal" ~ (
    expect_output_contains |
    expect_output_matches |
    expect_command_success |
    expect_command_failure |
    expect_exit_code |
    expect_stdout_empty |
    expect_stderr_empty |
    expect_stderr_contains |
    expect_output_prefix |
    expect_output_suffix |
    expect_output_equals |
    expect_valid_json |
    expect_json_path |
    expect_json_equals |
    expect_json_includes |
    expect_json_item_count
) }

// --- Web Conditions ---
check_web                         = { "Web" ~ (
    expect_status_code |
    expect_status_success |
    expect_status_error |
    expect_status_in_range |
    expect_response_time_below |
    expect_body_contains |
    expect_body_matches |
    expect_body_json_equals |
    expect_json_body_path |
    expect_json_path_value |
    capture_json_path |
    expect_json_string |
    expect_json_number |
    expect_json_array |
    expect_json_object |
    expect_json_size |
    expect_valid_json |
    expect_json_output_path |
    expect_json_output_equals |
    expect_json_output_includes |
    expect_json_output_count
) }

// Json-specific condition parts (Imperative)
expect_json_string             = { "json_response" ~ "at" ~ non_empty_string ~ "is_string" }
expect_json_number             = { "json_response" ~ "at" ~ non_empty_string ~ "is_number" }
expect_json_array              = { "json_response" ~ "at" ~ non_empty_string ~ "is_array" }
expect_json_object             = { "json_response" ~ "at" ~ non_empty_string ~ "is_object" }
expect_json_size               = { "json_response" ~ "at" ~ non_empty_string ~ "has_size" ~ number }
expect_valid_json              = { "check_valid_json" }
expect_json_output_path        = { "json_output" ~ "has_path" ~ non_empty_string }
expect_json_output_equals      = { "json_output" ~ "at" ~ non_empty_string ~ "equals" ~ value }
expect_json_output_includes    = { "json_output" ~ "at" ~ non_empty_string ~ "includes" ~ value }
expect_json_output_count       = { "json_output" ~ "at" ~ non_empty_string ~ "has_count" ~ number }
expect_json_body_path          = { "json_body" ~ "has_path" ~ non_empty_string }
expect_json_path_value         = { "json_path" ~ "at" ~ non_empty_string ~ "equals" ~ (value | binary_op) }
capture_json_path              = { "json_path" ~ "at" ~ non_empty_string ~ "capture_as" ~ identifier }

// Terminal-specific condition parts (Imperative)
expect_output_contains         = { "expect_contains" ~ non_empty_string }
expect_output_matches          = { "expect_matches" ~ non_empty_string ~ ("as" ~ identifier)? }
expect_command_success         = { "expect_success" }
expect_command_failure         = { "expect_failure" }
expect_exit_code               = { "expect_exit_code" ~ number }
expect_stdout_empty            = { "expect_stdout_empty" }
expect_stderr_empty            = { "expect_stderr_empty" }
expect_stderr_contains         = { "expect_stderr_contains" ~ non_empty_string }
expect_output_prefix           = { "expect_starts_with" ~ non_empty_string }
expect_output_suffix           = { "expect_ends_with" ~ non_empty_string }
expect_output_equals           = { "expect_equals" ~ non_empty_string }

// Web-specific condition parts (Imperative)
expect_status_code             = { "expect_status" ~ ( number | string ) }
expect_status_success          = { "expect_status_success" }
expect_status_error            = { "expect_status_error" }
expect_status_in_range         = { "expect_status_in" ~ "[" ~ number ~ ("," ~ number)* ~ "]" }
expect_response_time_below     = { "expect_response_below" ~ wait_marker }
expect_body_contains           = { "expect_body_contains" ~ non_empty_string }
expect_body_matches            = { "expect_body_matches" ~ string ~ ("as" ~ identifier)? }
expect_body_json_equals        = { "expect_body_json" ~ non_empty_string ~ ("ignore_fields" ~ "[" ~ (string ~ ("," ~ string)*)? ~ "]")? }

// Action Types (Imperative)
perform_terminal_run           = { "Terminal" ~ "run" ~ non_empty_string }
perform_web_action             = { "Web" ~ web_action_type }
web_action_type = {
    ("set_header" ~ string ~ string) |
    ("clear_header" ~ string) |
    ("clear_headers") |
    ("set_cookie" ~ string ~ string) |
    ("clear_cookie" ~ string) |
    ("clear_cookies") |
    ("http_get" ~ non_empty_string) |
    ("http_post" ~ non_empty_string ~ "with_body" ~ string) |
    ("http_put" ~ non_empty_string ~ "with_body" ~ string) |
    ("http_patch" ~ non_empty_string ~ "with_body" ~ string) |
    ("http_delete" ~ non_empty_string)
}

perform_filesystem_action      = { "FileSystem" ~ filesystem_action_keyword ~ string ~ ("with_content" ~ string)? ~ ("as" ~ identifier)? }
filesystem_action_keyword      = @{ "write_file" | "create_directory" | "delete_file" | "delete_directory" | "create_file" | "read_file" }

// --- System Actor (Imperative) ---
perform_system_action          = { "System" ~ system_action_type }
system_action_type = {
    pause_system |
    log_message |
    capture_timestamp |
    generate_uuid
}

pause_system                   = { "pause" ~ wait_marker }
log_message                    = { "log" ~ string }
capture_timestamp              = { "timestamp" ~ "as" ~ (string | identifier) }
generate_uuid                  = { "uuid" ~ "as" ~ identifier }

// The basic building blocks of the choreo language.
identifier                     = @{ ( "_" | 'a'..'z' | 'A'..'Z' ) ~ ( "_" | 'a'..'z' | 'A'..'Z' | '0'..'9' )* }
string                         = ${ "\"" ~ within_double ~ "\"" | "'" ~ within_single ~ "'" }
within_double                  = @{ ( "\\" ~ ANY | !"\"" ~ ANY )* }
within_single                  = @{ ( "\\" ~ ANY | !"'" ~ ANY )* }
non_empty_string               = ${ "\"" ~ within_double_non_empty ~ "\"" | "'" ~ within_single_non_empty ~ "'"  }
within_double_non_empty        = @{ ( "\\" ~ ANY | !"\"" ~ ANY )+ }
within_single_non_empty        = @{ ( "\\" ~ ANY | !"'" ~ ANY )+ }
number                         = @{ ASCII_DIGIT+ }
float                          = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }
duration                       = @{ ASCII_DIGIT+ ~ ("ms" | "s") }
wait_unit                      = @{ "s" | "ms" }
wait_marker                    = @{ float ~ wait_unit }
value                          = { string | number | array | object | binary_op }
comparison_op                  = @{ ">=" | "<=" | "==" | ">" | "<" }
binary_op                      = { "true" | "false" }
array                          = { "[" ~ WHITESPACE* ~ (value ~ ("," ~ value)*)? ~ WHITESPACE* ~ "]" }
object                         = { "{" ~ WHITESPACE* ~ (pair ~ ("," ~ pair)*)? ~ WHITESPACE* ~ "}" }
pair                           = { (identifier | string) ~ WHITESPACE* ~ ":" ~ WHITESPACE* ~ value }