-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(GIY) :: {step_trie_node, gleam@dict:dict(binary(), GIY), gleam@dict:dict(binary(), step_trie_node(GIY)), gleam@option:option(step_trie_node(GIY)), gleam@option:option(step_trie_node(GIY)), gleam@option:option(step_trie_node(GIY)), gleam@option:option(step_trie_node(GIY)), gleam@option:option(step_trie_node(GIY))}. -opaque step_trie(GIZ) :: {step_trie, step_trie_node(GIZ)}. -type step_match(GJA) :: {step_match, GJA, 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(GJM), binary(), GJM) -> step_trie_node(GJM). 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(GKN), binary()) -> step_trie_node(GKN). 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(GLP), binary(), list(captured_value()) ) -> gleam@option:option(step_match(GLP)). 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(GLU), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(GLU)). 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(GLJ), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(GLJ)). 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(GMG), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(GMG)). 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(GMS), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(GMS)). 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(GNE), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(GNE)). 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(GNQ), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(GNQ)). 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(GNK), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(GNK)). 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(GMY), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(GMY)). 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(GMM), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(GMM)). 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(GMA), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(GMA)). 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, <>, true ); {<<"\""/utf8>>, true} -> tokenize_preserving_quotes( Rest, Tokens, <>, 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, <>, 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(GKW), binary(), binary()) -> gleam@option:option(step_match(GKW)). 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(GJP), binary(), binary(), list(step_segment()), GJP ) -> step_trie_node(GJP). 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(GJI), binary(), list(step_segment()), GJI) -> step_trie_node(GJI). 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(GJT), binary(), list(step_segment()), GJT ) -> step_trie_node(GJT). 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(GJX), binary(), list(step_segment()), GJX ) -> step_trie_node(GJX). 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(GKB), binary(), list(step_segment()), GKB ) -> step_trie_node(GKB). 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(GKF), binary(), list(step_segment()), GKF ) -> step_trie_node(GKF). 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(GKJ), binary(), list(step_segment()), GKJ ) -> step_trie_node(GKJ). 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(GJF), binary(), binary(), GJF) -> step_trie(GJF). 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}.