Current section
Files
Jump to
Current section
Files
src/dream_test@gherkin@step_trie.erl
-module(dream_test@gherkin@step_trie).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/dream_test/gherkin/step_trie.gleam").
-export([new/0, tokenize_step_text/1, lookup/3, parse_step_pattern/1, insert/4]).
-export_type([step_segment/0, captured_value/0, step_trie_node/1, step_trie/1, step_match/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Step Trie - Fast step definition lookup using Cucumber Expressions.\n"
"\n"
" Provides O(word count) step matching instead of O(step definitions) linear search.\n"
" This is the core data structure for efficient Gherkin step matching.\n"
"\n"
" ## Performance Characteristics\n"
"\n"
" - **Insert**: O(words in pattern) - typically 5-10 words\n"
" - **Lookup**: O(words in step text) - independent of total step definitions\n"
" - **Memory**: O(total words across all patterns)\n"
"\n"
" ## Placeholder Syntax\n"
"\n"
" Patterns use Cucumber Expression placeholders:\n"
"\n"
" | Placeholder | Matches | Captures |\n"
" |-------------|---------|----------|\n"
" | `{int}` | Integer like `42`, `-5` | `CapturedInt(Int)` |\n"
" | `{float}` | Decimal like `3.14`, `0.5` | `CapturedFloat(Float)` |\n"
" | `{string}` | Quoted text like `\"hello\"` | `CapturedString(String)` |\n"
" | `{word}` | Single word like `apple` | `CapturedWord(String)` |\n"
" | `{}` | Any single token | `CapturedWord(String)` |\n"
"\n"
" ## Prefix and Suffix Support\n"
"\n"
" Placeholders can have literal prefixes and suffixes:\n"
"\n"
" - `${float}` matches `$3.99` and captures `3.99`\n"
" - `{int}%` matches `50%` and captures `50`\n"
" - `${float}USD` matches `$19.99USD` and captures `19.99`\n"
"\n"
" This works by splitting patterns and text at placeholder/numeric boundaries,\n"
" so `${float}` becomes `[\"$\", \"{float}\"]` in the trie, and `$3.99` becomes\n"
" `[\"$\", \"3.99\"]` during matching.\n"
"\n"
" ## Matching Priority\n"
"\n"
" When multiple patterns could match, the trie uses this priority order:\n"
"\n"
" 1. **Literal words** - exact string match (highest priority)\n"
" 2. **{string}** - quoted string capture\n"
" 3. **{int}** - integer capture\n"
" 4. **{float}** - decimal capture \n"
" 5. **{word}** - single word capture\n"
" 6. **{}** - any word capture (lowest priority)\n"
"\n"
" This ensures the most specific step definition wins.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let trie = new()\n"
" |> insert(\"Given\", \"I have {int} items\", count_handler)\n"
" |> insert(\"Given\", \"I have an empty cart\", empty_handler)\n"
" |> insert(\"Then\", \"the total is ${float}\", total_handler)\n"
"\n"
" // Matches empty_handler (literal \"an\" beats {int})\n"
" lookup(trie, \"Given\", \"I have an empty cart\")\n"
"\n"
" // Matches count_handler, captures [CapturedInt(42)]\n"
" lookup(trie, \"Given\", \"I have 42 items\")\n"
"\n"
" // Matches total_handler, captures [CapturedFloat(19.99)]\n"
" lookup(trie, \"Then\", \"the total is $19.99\")\n"
" ```\n"
).
-type step_segment() :: {literal_word, binary()} |
int_param |
float_param |
string_param |
word_param |
any_param.
-type captured_value() :: {captured_int, integer()} |
{captured_float, float()} |
{captured_string, binary()} |
{captured_word, binary()}.
-opaque step_trie_node(GEO) :: {step_trie_node,
gleam@dict:dict(binary(), GEO),
gleam@dict:dict(binary(), step_trie_node(GEO)),
gleam@option:option(step_trie_node(GEO)),
gleam@option:option(step_trie_node(GEO)),
gleam@option:option(step_trie_node(GEO)),
gleam@option:option(step_trie_node(GEO)),
gleam@option:option(step_trie_node(GEO))}.
-opaque step_trie(GEP) :: {step_trie, step_trie_node(GEP)}.
-type step_match(GEQ) :: {step_match, GEQ, list(captured_value())}.
-file("src/dream_test/gherkin/step_trie.gleam", 163).
?DOC(" Create an empty trie node.\n").
-spec empty_node() -> step_trie_node(any()).
empty_node() ->
{step_trie_node, maps:new(), maps:new(), none, none, none, none, none}.
-file("src/dream_test/gherkin/step_trie.gleam", 216).
?DOC(
" Create a new empty step trie.\n"
"\n"
" Returns a trie with no step definitions.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let trie = new()\n"
" |> insert(\"Given\", \"I have {int} items\", handler)\n"
" ```\n"
).
-spec new() -> step_trie(any()).
new() ->
{step_trie, empty_node()}.
-file("src/dream_test/gherkin/step_trie.gleam", 269).
-spec insert_handler_at_node(step_trie_node(GFC), binary(), GFC) -> step_trie_node(GFC).
insert_handler_at_node(Node, Keyword, Handler) ->
Updated_handlers = gleam@dict:insert(
erlang:element(2, Node),
Keyword,
Handler
),
{step_trie_node,
Updated_handlers,
erlang:element(3, Node),
erlang:element(4, Node),
erlang:element(5, Node),
erlang:element(6, Node),
erlang:element(7, Node),
erlang:element(8, Node)}.
-file("src/dream_test/gherkin/step_trie.gleam", 346).
-spec get_or_create_literal_child(step_trie_node(GGD), binary()) -> step_trie_node(GGD).
get_or_create_literal_child(Node, Word) ->
case gleam_stdlib:map_get(erlang:element(3, Node), Word) of
{ok, Existing} ->
Existing;
{error, _} ->
empty_node()
end.
-file("src/dream_test/gherkin/step_trie.gleam", 411).
-spec is_non_empty(binary()) -> boolean().
is_non_empty(Word) ->
Word /= <<""/utf8>>.
-file("src/dream_test/gherkin/step_trie.gleam", 415).
-spec parse_pattern_word(binary()) -> step_segment().
parse_pattern_word(Word) ->
case Word of
<<"{int}"/utf8>> ->
int_param;
<<"{float}"/utf8>> ->
float_param;
<<"{string}"/utf8>> ->
string_param;
<<"{word}"/utf8>> ->
word_param;
<<"{}"/utf8>> ->
any_param;
_ ->
{literal_word, Word}
end.
-file("src/dream_test/gherkin/step_trie.gleam", 474).
-spec do_split_numeric(binary()) -> list(binary()).
do_split_numeric(Token) ->
case gleam@regexp:from_string(<<"(-?[0-9]+\\.?[0-9]*)"/utf8>>) of
{ok, Re} ->
case gleam@regexp:split(Re, Token) of
[Only] ->
[Only];
Parts ->
_pipe = Parts,
gleam@list:filter(_pipe, fun is_non_empty/1)
end;
{error, _} ->
[Token]
end.
-file("src/dream_test/gherkin/step_trie.gleam", 614).
-spec add_token_if_non_empty(list(binary()), binary()) -> list(binary()).
add_token_if_non_empty(Tokens, Token) ->
case Token of
<<""/utf8>> ->
Tokens;
_ ->
[Token | Tokens]
end.
-file("src/dream_test/gherkin/step_trie.gleam", 609).
-spec finalize_tokens(list(binary()), binary()) -> list(binary()).
finalize_tokens(Tokens, Current) ->
_pipe = add_token_if_non_empty(Tokens, Current),
lists:reverse(_pipe).
-file("src/dream_test/gherkin/step_trie.gleam", 633).
-spec lookup_handler_at_node(
step_trie_node(GHF),
binary(),
list(captured_value())
) -> gleam@option:option(step_match(GHF)).
lookup_handler_at_node(Node, Keyword, Captures) ->
Handler_result = case gleam_stdlib:map_get(erlang:element(2, Node), Keyword) of
{ok, Handler} ->
{some, Handler};
{error, _} ->
_pipe = gleam_stdlib:map_get(erlang:element(2, Node), <<"*"/utf8>>),
gleam@option:from_result(_pipe)
end,
case Handler_result of
{some, Handler@1} ->
{some, {step_match, Handler@1, lists:reverse(Captures)}};
none ->
none
end.
-file("src/dream_test/gherkin/step_trie.gleam", 798).
-spec is_quoted_string(binary()) -> boolean().
is_quoted_string(Word) ->
gleam_stdlib:string_starts_with(Word, <<"\""/utf8>>) andalso gleam_stdlib:string_ends_with(
Word,
<<"\""/utf8>>
).
-file("src/dream_test/gherkin/step_trie.gleam", 467).
?DOC(
" Split a token on boundaries between numeric and non-numeric characters.\n"
" e.g., \"$3.00\" -> [\"$\", \"3.00\"]\n"
" e.g., \"10%\" -> [\"10\", \"%\"]\n"
" e.g., \"hello\" -> [\"hello\"]\n"
" Quoted strings are not split.\n"
).
-spec split_numeric_boundaries(binary()) -> list(binary()).
split_numeric_boundaries(Token) ->
case is_quoted_string(Token) of
true ->
[Token];
false ->
do_split_numeric(Token)
end.
-file("src/dream_test/gherkin/step_trie.gleam", 802).
-spec unquote_string(binary()) -> binary().
unquote_string(Quoted) ->
_pipe = Quoted,
_pipe@1 = gleam@string:drop_start(_pipe, 1),
gleam@string:drop_end(_pipe@1, 1).
-file("src/dream_test/gherkin/step_trie.gleam", 659).
?DOC(
" Match a single word against the node's children.\n"
"\n"
" Priority order:\n"
" 1. Literal match (exact word)\n"
" 2. {string} (quoted string)\n"
" 3. {int} (integer)\n"
" 4. {float} (decimal)\n"
" 5. {word} (unquoted single word)\n"
" 6. {} (any word)\n"
).
-spec lookup_word(
step_trie_node(GHK),
binary(),
binary(),
list(binary()),
list(captured_value())
) -> gleam@option:option(step_match(GHK)).
lookup_word(Node, Keyword, Word, Rest, Captures) ->
case gleam_stdlib:map_get(erlang:element(3, Node), Word) of
{ok, Child} ->
lookup_in_node(Child, Keyword, Rest, Captures);
{error, _} ->
try_param_matches(Node, Keyword, Word, Rest, Captures)
end.
-file("src/dream_test/gherkin/step_trie.gleam", 621).
-spec lookup_in_node(
step_trie_node(GGZ),
binary(),
list(binary()),
list(captured_value())
) -> gleam@option:option(step_match(GGZ)).
lookup_in_node(Node, Keyword, Words, Captures) ->
case Words of
[] ->
lookup_handler_at_node(Node, Keyword, Captures);
[Word | Rest] ->
lookup_word(Node, Keyword, Word, Rest, Captures)
end.
-file("src/dream_test/gherkin/step_trie.gleam", 687).
-spec try_string_match(
step_trie_node(GHW),
binary(),
binary(),
list(binary()),
list(captured_value())
) -> gleam@option:option(step_match(GHW)).
try_string_match(Node, Keyword, Word, Rest, Captures) ->
case {erlang:element(4, Node), is_quoted_string(Word)} of
{{some, Child}, true} ->
Unquoted = unquote_string(Word),
Updated_captures = [{captured_string, Unquoted} | Captures],
lookup_in_node(Child, Keyword, Rest, Updated_captures);
{_, _} ->
none
end.
-file("src/dream_test/gherkin/step_trie.gleam", 718).
-spec try_int_match(
step_trie_node(GII),
binary(),
binary(),
list(binary()),
list(captured_value())
) -> gleam@option:option(step_match(GII)).
try_int_match(Node, Keyword, Word, Rest, Captures) ->
case {erlang:element(5, Node), gleam_stdlib:parse_int(Word)} of
{{some, Child}, {ok, Value}} ->
Updated_captures = [{captured_int, Value} | Captures],
lookup_in_node(Child, Keyword, Rest, Updated_captures);
{_, _} ->
none
end.
-file("src/dream_test/gherkin/step_trie.gleam", 748).
-spec try_float_match(
step_trie_node(GIU),
binary(),
binary(),
list(binary()),
list(captured_value())
) -> gleam@option:option(step_match(GIU)).
try_float_match(Node, Keyword, Word, Rest, Captures) ->
case {erlang:element(6, Node), gleam_stdlib:parse_float(Word)} of
{{some, Child}, {ok, Value}} ->
Updated_captures = [{captured_float, Value} | Captures],
lookup_in_node(Child, Keyword, Rest, Updated_captures);
{_, _} ->
none
end.
-file("src/dream_test/gherkin/step_trie.gleam", 781).
-spec try_any_match(
step_trie_node(GJG),
binary(),
binary(),
list(binary()),
list(captured_value())
) -> gleam@option:option(step_match(GJG)).
try_any_match(Node, Keyword, Word, Rest, Captures) ->
case erlang:element(8, Node) of
{some, Child} ->
Updated_captures = [{captured_word, Word} | Captures],
lookup_in_node(Child, Keyword, Rest, Updated_captures);
none ->
none
end.
-file("src/dream_test/gherkin/step_trie.gleam", 764).
-spec try_word_matches(
step_trie_node(GJA),
binary(),
binary(),
list(binary()),
list(captured_value())
) -> gleam@option:option(step_match(GJA)).
try_word_matches(Node, Keyword, Word, Rest, Captures) ->
case erlang:element(7, Node) of
{some, Child} ->
Updated_captures = [{captured_word, Word} | Captures],
lookup_in_node(Child, Keyword, Rest, Updated_captures);
none ->
try_any_match(Node, Keyword, Word, Rest, Captures)
end.
-file("src/dream_test/gherkin/step_trie.gleam", 734).
-spec try_float_then_word(
step_trie_node(GIO),
binary(),
binary(),
list(binary()),
list(captured_value())
) -> gleam@option:option(step_match(GIO)).
try_float_then_word(Node, Keyword, Word, Rest, Captures) ->
case try_float_match(Node, Keyword, Word, Rest, Captures) of
{some, Result} ->
{some, Result};
none ->
try_word_matches(Node, Keyword, Word, Rest, Captures)
end.
-file("src/dream_test/gherkin/step_trie.gleam", 704).
-spec try_numeric_matches(
step_trie_node(GIC),
binary(),
binary(),
list(binary()),
list(captured_value())
) -> gleam@option:option(step_match(GIC)).
try_numeric_matches(Node, Keyword, Word, Rest, Captures) ->
case try_int_match(Node, Keyword, Word, Rest, Captures) of
{some, Result} ->
{some, Result};
none ->
try_float_then_word(Node, Keyword, Word, Rest, Captures)
end.
-file("src/dream_test/gherkin/step_trie.gleam", 673).
-spec try_param_matches(
step_trie_node(GHQ),
binary(),
binary(),
list(binary()),
list(captured_value())
) -> gleam@option:option(step_match(GHQ)).
try_param_matches(Node, Keyword, Word, Rest, Captures) ->
case try_string_match(Node, Keyword, Word, Rest, Captures) of
{some, Result} ->
{some, Result};
none ->
try_numeric_matches(Node, Keyword, Word, Rest, Captures)
end.
-file("src/dream_test/gherkin/step_trie.gleam", 589).
-spec process_char(binary(), binary(), list(binary()), binary(), boolean()) -> list(binary()).
process_char(Char, Rest, Tokens, Current, In_quotes) ->
case {Char, In_quotes} of
{<<"\""/utf8>>, false} ->
tokenize_preserving_quotes(
Rest,
Tokens,
<<Current/binary, Char/binary>>,
true
);
{<<"\""/utf8>>, true} ->
tokenize_preserving_quotes(
Rest,
Tokens,
<<Current/binary, Char/binary>>,
false
);
{<<" "/utf8>>, false} ->
Updated_tokens = add_token_if_non_empty(Tokens, Current),
tokenize_preserving_quotes(Rest, Updated_tokens, <<""/utf8>>, false);
{_, _} ->
tokenize_preserving_quotes(
Rest,
Tokens,
<<Current/binary, Char/binary>>,
In_quotes
)
end.
-file("src/dream_test/gherkin/step_trie.gleam", 577).
-spec tokenize_preserving_quotes(binary(), list(binary()), binary(), boolean()) -> list(binary()).
tokenize_preserving_quotes(Remaining, Tokens, Current, In_quotes) ->
case gleam_stdlib:string_pop_grapheme(Remaining) of
{error, _} ->
finalize_tokens(Tokens, Current);
{ok, {Char, Rest}} ->
process_char(Char, Rest, Tokens, Current, In_quotes)
end.
-file("src/dream_test/gherkin/step_trie.gleam", 571).
?DOC(
" Tokenize step text for matching.\n"
"\n"
" Prepares step text for trie lookup by splitting it into tokens that align\n"
" with how patterns are parsed. This enables matching patterns with prefixed\n"
" or suffixed placeholders.\n"
"\n"
" ## Tokenization Rules\n"
"\n"
" 1. **Whitespace splitting**: Text is split on spaces\n"
" 2. **Quote preservation**: Quoted strings like `\"Red Widget\"` stay as one token\n"
" 3. **Numeric boundary splitting**: Tokens are split at boundaries between\n"
" numeric and non-numeric characters\n"
"\n"
" The numeric boundary splitting is key for prefix/suffix support. When a\n"
" pattern like `${float}` is parsed, it becomes `[\"$\", \"{float}\"]`. For\n"
" matching to work, the text `$19.99` must also become `[\"$\", \"19.99\"]`.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Basic tokenization\n"
" tokenize_step_text(\"I have 5 items\")\n"
" // [\"I\", \"have\", \"5\", \"items\"]\n"
"\n"
" // Quoted strings preserved\n"
" tokenize_step_text(\"I add \\\"Red Widget\\\" to cart\")\n"
" // [\"I\", \"add\", \"\\\"Red Widget\\\"\", \"to\", \"cart\"]\n"
"\n"
" // Currency prefix split from number\n"
" tokenize_step_text(\"the total is $19.99\")\n"
" // [\"the\", \"total\", \"is\", \"$\", \"19.99\"]\n"
"\n"
" // Percentage suffix split from number\n"
" tokenize_step_text(\"I apply a 15% discount\")\n"
" // [\"I\", \"apply\", \"a\", \"15\", \"%\", \"discount\"]\n"
"\n"
" // Multiple numeric boundaries\n"
" tokenize_step_text(\"price is $99.99USD\")\n"
" // [\"price\", \"is\", \"$\", \"99.99\", \"USD\"]\n"
" ```\n"
).
-spec tokenize_step_text(binary()) -> list(binary()).
tokenize_step_text(Text) ->
_pipe = Text,
_pipe@1 = tokenize_preserving_quotes(_pipe, [], <<""/utf8>>, false),
gleam@list:flat_map(_pipe@1, fun split_numeric_boundaries/1).
-file("src/dream_test/gherkin/step_trie.gleam", 521).
?DOC(
" Look up a step in the trie.\n"
"\n"
" Searches for a handler matching the given keyword and step text.\n"
" Returns the matched handler and captured values, or None if no match.\n"
"\n"
" Lookup is O(word count) - independent of total step definitions.\n"
"\n"
" ## Parameters\n"
"\n"
" - `trie`: The trie to search\n"
" - `keyword`: Step keyword as string (\"Given\", \"When\", \"Then\")\n"
" - `text`: Step text to match (e.g., \"I have 42 items\")\n"
"\n"
" ## Returns\n"
"\n"
" - `Some(StepMatch)`: Contains matched handler and captured values\n"
" - `None`: No step definition matched\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let result = lookup(trie, \"Given\", \"I have 42 items\")\n"
" case result {\n"
" Some(StepMatch(handler, captures)) -> {\n"
" // captures = [CapturedInt(42)]\n"
" }\n"
" None -> // No matching step definition\n"
" }\n"
" ```\n"
).
-spec lookup(step_trie(GGM), binary(), binary()) -> gleam@option:option(step_match(GGM)).
lookup(Trie, Keyword, Text) ->
Words = tokenize_step_text(Text),
lookup_in_node(erlang:element(2, Trie), Keyword, Words, []).
-file("src/dream_test/gherkin/step_trie.gleam", 278).
-spec insert_literal_segment(
step_trie_node(GFF),
binary(),
binary(),
list(step_segment()),
GFF
) -> step_trie_node(GFF).
insert_literal_segment(Node, Word, Keyword, Rest, Handler) ->
Child = get_or_create_literal_child(Node, Word),
Updated_child = insert_into_node(Child, Keyword, Rest, Handler),
Updated_children = gleam@dict:insert(
erlang:element(3, Node),
Word,
Updated_child
),
{step_trie_node,
erlang:element(2, Node),
Updated_children,
erlang:element(4, Node),
erlang:element(5, Node),
erlang:element(6, Node),
erlang:element(7, Node),
erlang:element(8, Node)}.
-file("src/dream_test/gherkin/step_trie.gleam", 251).
-spec insert_into_node(step_trie_node(GEY), binary(), list(step_segment()), GEY) -> step_trie_node(GEY).
insert_into_node(Node, Keyword, Segments, Handler) ->
case Segments of
[] ->
insert_handler_at_node(Node, Keyword, Handler);
[{literal_word, Word} | Rest] ->
insert_literal_segment(Node, Word, Keyword, Rest, Handler);
[int_param | Rest@1] ->
insert_int_segment(Node, Keyword, Rest@1, Handler);
[float_param | Rest@2] ->
insert_float_segment(Node, Keyword, Rest@2, Handler);
[string_param | Rest@3] ->
insert_string_segment(Node, Keyword, Rest@3, Handler);
[word_param | Rest@4] ->
insert_word_segment(Node, Keyword, Rest@4, Handler);
[any_param | Rest@5] ->
insert_any_segment(Node, Keyword, Rest@5, Handler)
end.
-file("src/dream_test/gherkin/step_trie.gleam", 291).
-spec insert_int_segment(
step_trie_node(GFJ),
binary(),
list(step_segment()),
GFJ
) -> step_trie_node(GFJ).
insert_int_segment(Node, Keyword, Rest, Handler) ->
Child = gleam@option:unwrap(erlang:element(5, Node), empty_node()),
Updated_child = insert_into_node(Child, Keyword, Rest, Handler),
{step_trie_node,
erlang:element(2, Node),
erlang:element(3, Node),
erlang:element(4, Node),
{some, Updated_child},
erlang:element(6, Node),
erlang:element(7, Node),
erlang:element(8, Node)}.
-file("src/dream_test/gherkin/step_trie.gleam", 302).
-spec insert_float_segment(
step_trie_node(GFN),
binary(),
list(step_segment()),
GFN
) -> step_trie_node(GFN).
insert_float_segment(Node, Keyword, Rest, Handler) ->
Child = gleam@option:unwrap(erlang:element(6, Node), empty_node()),
Updated_child = insert_into_node(Child, Keyword, Rest, Handler),
{step_trie_node,
erlang:element(2, Node),
erlang:element(3, Node),
erlang:element(4, Node),
erlang:element(5, Node),
{some, Updated_child},
erlang:element(7, Node),
erlang:element(8, Node)}.
-file("src/dream_test/gherkin/step_trie.gleam", 313).
-spec insert_string_segment(
step_trie_node(GFR),
binary(),
list(step_segment()),
GFR
) -> step_trie_node(GFR).
insert_string_segment(Node, Keyword, Rest, Handler) ->
Child = gleam@option:unwrap(erlang:element(4, Node), empty_node()),
Updated_child = insert_into_node(Child, Keyword, Rest, Handler),
{step_trie_node,
erlang:element(2, Node),
erlang:element(3, Node),
{some, Updated_child},
erlang:element(5, Node),
erlang:element(6, Node),
erlang:element(7, Node),
erlang:element(8, Node)}.
-file("src/dream_test/gherkin/step_trie.gleam", 324).
-spec insert_word_segment(
step_trie_node(GFV),
binary(),
list(step_segment()),
GFV
) -> step_trie_node(GFV).
insert_word_segment(Node, Keyword, Rest, Handler) ->
Child = gleam@option:unwrap(erlang:element(7, Node), empty_node()),
Updated_child = insert_into_node(Child, Keyword, Rest, Handler),
{step_trie_node,
erlang:element(2, Node),
erlang:element(3, Node),
erlang:element(4, Node),
erlang:element(5, Node),
erlang:element(6, Node),
{some, Updated_child},
erlang:element(8, Node)}.
-file("src/dream_test/gherkin/step_trie.gleam", 335).
-spec insert_any_segment(
step_trie_node(GFZ),
binary(),
list(step_segment()),
GFZ
) -> step_trie_node(GFZ).
insert_any_segment(Node, Keyword, Rest, Handler) ->
Child = gleam@option:unwrap(erlang:element(8, Node), empty_node()),
Updated_child = insert_into_node(Child, Keyword, Rest, Handler),
{step_trie_node,
erlang:element(2, Node),
erlang:element(3, Node),
erlang:element(4, Node),
erlang:element(5, Node),
erlang:element(6, Node),
erlang:element(7, Node),
{some, Updated_child}}.
-file("src/dream_test/gherkin/step_trie.gleam", 435).
-spec split_on_first_placeholder(binary(), list(binary())) -> list(binary()).
split_on_first_placeholder(Word, Placeholders) ->
case Placeholders of
[] ->
[Word];
[Placeholder | Rest] ->
case gleam@string:split_once(Word, Placeholder) of
{ok, {Before, After}} ->
Parts = [],
Parts@1 = case Before of
<<""/utf8>> ->
Parts;
_ ->
lists:append(Parts, [Before])
end,
Parts@2 = lists:append(Parts@1, [Placeholder]),
Parts@3 = case After of
<<""/utf8>> ->
Parts@2;
_ ->
lists:append(
Parts@2,
split_word_around_placeholder(After)
)
end,
Parts@3;
{error, _} ->
split_on_first_placeholder(Word, Rest)
end
end.
-file("src/dream_test/gherkin/step_trie.gleam", 430).
?DOC(
" Split a word around any placeholder it contains.\n"
" e.g., \"${float}\" -> [\"$\", \"{float}\"]\n"
" e.g., \"{int}%\" -> [\"{int}\", \"%\"]\n"
" e.g., \"hello\" -> [\"hello\"]\n"
).
-spec split_word_around_placeholder(binary()) -> list(binary()).
split_word_around_placeholder(Word) ->
Placeholders = [<<"{int}"/utf8>>,
<<"{float}"/utf8>>,
<<"{string}"/utf8>>,
<<"{word}"/utf8>>,
<<"{}"/utf8>>],
split_on_first_placeholder(Word, Placeholders).
-file("src/dream_test/gherkin/step_trie.gleam", 403).
?DOC(
" Parse a step pattern string into segments.\n"
"\n"
" Splits the pattern by whitespace and converts each token into a `StepSegment`.\n"
" Placeholders like `{int}` become typed parameter segments, while other tokens\n"
" become literal word segments.\n"
"\n"
" ## Placeholder Syntax\n"
"\n"
" | Placeholder | Segment Type | Matches |\n"
" |-------------|--------------|---------|\n"
" | `{int}` | `IntParam` | Integers like `42`, `-5` |\n"
" | `{float}` | `FloatParam` | Decimals like `3.14` |\n"
" | `{string}` | `StringParam` | Quoted strings like `\"hello\"` |\n"
" | `{word}` | `WordParam` | Single unquoted words |\n"
" | `{}` | `AnyParam` | Any single token |\n"
"\n"
" ## Prefix and Suffix Handling\n"
"\n"
" Placeholders can have literal prefixes and/or suffixes attached. The parser\n"
" automatically splits these into separate segments:\n"
"\n"
" - `${float}` → `[LiteralWord(\"$\"), FloatParam]`\n"
" - `{int}%` → `[IntParam, LiteralWord(\"%\")]`\n"
" - `${float}USD` → `[LiteralWord(\"$\"), FloatParam, LiteralWord(\"USD\")]`\n"
"\n"
" This enables patterns like `\"the price is ${float}\"` to match text like\n"
" `\"the price is $19.99\"` and capture `19.99` as the float value.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" parse_step_pattern(\"I have {int} items\")\n"
" // [LiteralWord(\"I\"), LiteralWord(\"have\"), IntParam, LiteralWord(\"items\")]\n"
"\n"
" parse_step_pattern(\"the total is ${float}\")\n"
" // [LiteralWord(\"the\"), LiteralWord(\"total\"), LiteralWord(\"is\"),\n"
" // LiteralWord(\"$\"), FloatParam]\n"
"\n"
" parse_step_pattern(\"I apply a {int}% discount\")\n"
" // [LiteralWord(\"I\"), LiteralWord(\"apply\"), LiteralWord(\"a\"),\n"
" // IntParam, LiteralWord(\"%\"), LiteralWord(\"discount\")]\n"
" ```\n"
).
-spec parse_step_pattern(binary()) -> list(step_segment()).
parse_step_pattern(Pattern) ->
_pipe = Pattern,
_pipe@1 = gleam@string:split(_pipe, <<" "/utf8>>),
_pipe@2 = gleam@list:filter(_pipe@1, fun is_non_empty/1),
_pipe@3 = gleam@list:flat_map(_pipe@2, fun split_word_around_placeholder/1),
gleam@list:map(_pipe@3, fun parse_pattern_word/1).
-file("src/dream_test/gherkin/step_trie.gleam", 240).
?DOC(
" Insert a step pattern into the trie.\n"
"\n"
" Adds a handler at the path specified by the pattern. If a handler already\n"
" exists for the same keyword and pattern, it is replaced.\n"
"\n"
" ## Parameters\n"
"\n"
" - `trie`: The trie to insert into\n"
" - `keyword`: Step keyword as string (\"Given\", \"When\", \"Then\", or \"*\" for any)\n"
" - `pattern`: Step pattern with placeholders (e.g., \"I have {int} items\")\n"
" - `handler`: The handler to store\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let trie = new()\n"
" |> insert(\"Given\", \"I have {int} items\", have_items)\n"
" |> insert(\"When\", \"I add {int} more\", add_items)\n"
" ```\n"
).
-spec insert(step_trie(GEV), binary(), binary(), GEV) -> step_trie(GEV).
insert(Trie, Keyword, Pattern, Handler) ->
Segments = parse_step_pattern(Pattern),
Updated_root = insert_into_node(
erlang:element(2, Trie),
Keyword,
Segments,
Handler
),
{step_trie, Updated_root}.