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 | task_def | scenario_def }

// --- Task Definitions (Reusable Drivers) ---
task_def                          = { "task" ~ identifier ~ "(" ~ task_param_list? ~ ")" ~ "{" ~ task_body_item+ ~ "}" }
task_param_list                   = { identifier ~ ("," ~ identifier)* }
task_body_item                    = { action | condition }

// --- Task Calls (in given/when/then blocks) ---
task_call                         = { identifier ~ "(" ~ task_arg_list? ~ ")" }
task_arg_list                     = { task_arg ~ ("," ~ task_arg)* }
task_arg                          = { wait_marker | string | number | variable_ref | identifier }

// 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" ~ "{" ~ (task_call | 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:" ~ (task_call | action | condition)* }
when_block                        = { "when:"  ~ (task_call | action)+ }
then_block                        = { "then:"  ~ (task_call | 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_condition |
    state_condition |
    system_condition |
    filesystem_condition |
    terminal_condition |
    web_condition
}

// --- System Conditions ---
system_condition = { "System" ~ (
    service_is_running_condition |
    service_is_stopped_condition |
    service_is_installed_condition |
    port_is_listening_condition |
    port_is_closed_condition
) }

service_is_running_condition   = { "service_is_running" ~ non_empty_string }
service_is_stopped_condition   = { "service_is_stopped" ~ non_empty_string }
service_is_installed_condition = { "service_is_installed" ~ non_empty_string }
port_is_listening_condition    = { "port_is_listening" ~ number }
port_is_closed_condition       = { "port_is_closed" ~ number }

action = {
    system_action |
    filesystem_action |
    set_cwd_action |
    run_action |
    web_action
}

// Condition Types
wait_condition                    = { "wait" ~ WHITESPACE* ~ comparison_op ~ WHITESPACE* ~ wait_marker }
state_condition                   = { "Test" ~ (state_has_succeeded | state_can_start) }
state_has_succeeded               = { "has_succeeded" ~ identifier }
state_can_start                   = { "can_start" }

// --- Filesystem Conditions ---
file_is_not_empty_condition       = { "file" ~ non_empty_string ~ "is_not_empty" }
file_is_empty_condition           = { "file" ~ non_empty_string ~ "is_empty" }
filesystem_condition_keyword      = @{ "file_exists" | "file_does_not_exist" | "dir_exists" | "dir_does_not_exist" | "file_contains" }
filesystem_condition              = { "FileSystem" ~ (
    (filesystem_condition_keyword ~ non_empty_string ~ "with_content" ~ string) |
    (filesystem_condition_keyword ~ non_empty_string) |
    file_is_not_empty_condition |
    file_is_empty_condition
) }

// --- Terminal Conditions ---
terminal_condition                = { "Terminal" ~ (
    output_not_contains_condition |
    output_contains_condition |
    output_matches_condition |
    last_command_succeeded_cond |
    last_command_failed_cond |
    last_command_exit_code_is_cond |
    stdout_is_empty_condition |
    stderr_is_empty_condition |
    stderr_contains_condition |
    output_starts_with_condition |
    output_ends_with_condition |
    output_equals_condition |
    output_is_valid_json_condition |
    json_output_has_path_condition |
    json_output_at_equals_condition |
    json_output_at_includes_condition |
    json_output_at_has_item_count_condition
) }

// --- Web Conditions ---
web_condition                     = { "Web" ~ (
    response_status_is_condition |
    response_status_is_success_condition |
    response_status_is_error_condition |
    response_status_is_in_condition |
    response_time_is_below_condition |
    response_body_contains_condition |
    response_body_matches_condition |
    response_body_equals_json |
    json_body_has_path_condition |
    json_path_equals_condition |
    json_path_capture_condition |
    json_value_is_string_condition |
    json_value_is_number_condition |
    json_value_is_array_condition |
    json_value_is_object_condition |
    json_value_has_size_condition |
    output_is_valid_json_condition |
    json_output_has_path_condition |
    json_output_at_equals_condition |
    json_output_at_includes_condition |
    json_output_at_has_item_count_condition
) }

// Json-specific condition parts
json_value_is_string_condition = { "json_response" ~ "at" ~ non_empty_string ~ "is_a_string" }
json_value_is_number_condition = { "json_response" ~ "at" ~ non_empty_string ~ "is_a_number" }
json_value_is_array_condition = { "json_response" ~ "at" ~ non_empty_string ~ "is_an_array" }
json_value_is_object_condition = { "json_response" ~ "at" ~ non_empty_string ~ "is_an_object" }
json_value_has_size_condition = { "json_response" ~ "at" ~ non_empty_string ~ "has_size" ~ number }
output_is_valid_json_condition    = { "output_is_valid_json" }
json_output_has_path_condition    = { "json_output" ~ "has_path" ~ non_empty_string }
json_output_at_equals_condition   = { "json_output" ~ "at" ~ non_empty_string ~ "equals" ~ value }
json_output_at_includes_condition = { "json_output" ~ "at" ~ non_empty_string ~ "includes" ~ value }
json_output_at_has_item_count_condition = { "json_output" ~ "at" ~ non_empty_string ~ "has_item_count" ~ number }
json_body_has_path_condition      = { "json_body" ~ "has_path" ~ non_empty_string }
json_path_equals_condition        = { "json_path" ~ "at" ~ non_empty_string ~ "equals" ~ (value | binary_op) }
json_path_capture_condition       = { "json_path" ~ "at" ~ non_empty_string ~ "as" ~ identifier }

// Terminal-specific condition parts
output_not_contains_condition     = { "output_not_contains" ~ non_empty_string }
output_contains_condition         = { "output_contains" ~ non_empty_string }
output_matches_condition          = { "output_matches" ~ non_empty_string ~ ("as" ~ identifier)? }
last_command_succeeded_cond       = { "last_command" ~ "succeeded" }
last_command_failed_cond          = { "last_command" ~ "failed" }
last_command_exit_code_is_cond    = { "last_command" ~ "exit_code_is" ~ number }
stdout_is_empty_condition         = { "stdout_is_empty" }
stderr_is_empty_condition         = { "stderr_is_empty" }
stderr_contains_condition         = { "stderr_contains" ~ non_empty_string }
output_starts_with_condition      = { "output_starts_with" ~ non_empty_string }
output_ends_with_condition        = { "output_ends_with" ~ non_empty_string }
output_equals_condition           = { "output_equals" ~ non_empty_string }

// Web-specific condition parts
response_status_is_condition         = { "response_status_is" ~ ( number | string ) }
response_status_is_success_condition = { "response_status" ~ "is_success" }
response_status_is_error_condition   = { "response_status" ~ "is_error" }
response_status_is_in_condition      = { "response_status" ~ "is_in" ~ "[" ~ number ~ ("," ~ number)* ~ "]" }
response_time_is_below_condition     = { "response_time" ~ "is_below" ~ wait_marker }
response_body_contains_condition     = { "response_body_contains" ~ non_empty_string }
response_body_matches_condition      = { "response_body_matches" ~ string ~ ("as" ~ identifier)? }
response_body_equals_json            = { "response_body_equals_json" ~ non_empty_string ~ ("ignore_fields" ~ "[" ~ (string ~ ("," ~ string)*)? ~ "]")? }

// Action Types
set_cwd_action                    = { "Terminal" ~ "set_cwd" ~ non_empty_string }
run_action                        = { "Terminal" ~ "run" ~ non_empty_string }
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)
}

filesystem_action                 = { "FileSystem" ~ filesystem_action_keyword ~ string ~ ("with_content" ~ string)? ~ ("as" ~ identifier)? }
filesystem_action_keyword         = @{ "file" | "create_dir" | "delete_file" | "delete_dir" | "create_file" | "read_file" }

// --- System Actor ---
system_action = { "System" ~ system_action_type }
system_action_type = {
    system_pause |
    system_log |
    system_timestamp |
    system_uuid
}

system_pause = { "pause" ~ wait_marker }
system_log = { "log" ~ string }
system_timestamp = { "timestamp" ~ "as" ~ (string | identifier) }
system_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 }