-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( " Fast step-definition lookup using Cucumber-Expression-style placeholders.\n" "\n" " This is the data structure behind `dream_test/gherkin/steps`. It matches step\n" " text in time proportional to the **number of tokens in the step text**, not\n" " the number of registered step definitions.\n" "\n" " ## Placeholder syntax\n" "\n" " - `{int}`: integers like `42`, `-5`\n" " - `{float}`: decimals like `3.14`, `-0.5`\n" " - `{string}`: quoted strings like `\"hello world\"`\n" " - `{word}` / `{}`: a single token\n" "\n" " Prefix/suffix text can be attached to placeholders. For example,\n" " `${float}USD` matches `$19.99USD` and captures `19.99` as a float.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " step_trie.lookup(trie, \"Then\", \"the total is $19.99USD\")\n" " |> should\n" " |> be_equal(\n" " Some(\n" " step_trie.StepMatch(handler: \"total_usd\", captures: [\n" " step_trie.CapturedFloat(19.99),\n" " ]),\n" " ),\n" " )\n" " |> or_fail_with(\"expected float capture for $19.99USD\")\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(FRU) :: {step_trie_node, gleam@dict:dict(binary(), FRU), gleam@dict:dict(binary(), step_trie_node(FRU)), gleam@option:option(step_trie_node(FRU)), gleam@option:option(step_trie_node(FRU)), gleam@option:option(step_trie_node(FRU)), gleam@option:option(step_trie_node(FRU)), gleam@option:option(step_trie_node(FRU))}. -opaque step_trie(FRV) :: {step_trie, step_trie_node(FRV)}. -type step_match(FRW) :: {step_match, FRW, list(captured_value())}. -file("src/dream_test/gherkin/step_trie.gleam", 128). ?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", 186). ?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 =\n" " step_trie.new()\n" " |> step_trie.insert(\n" " keyword: \"Given\",\n" " pattern: \"I have an empty cart\",\n" " handler: \"empty\",\n" " )\n" " ```\n" ). -spec new() -> step_trie(any()). new() -> {step_trie, empty_node()}. -file("src/dream_test/gherkin/step_trie.gleam", 241). -spec insert_handler_at_node(step_trie_node(FSI), binary(), FSI) -> step_trie_node(FSI). 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", 318). -spec get_or_create_literal_child(step_trie_node(FTJ), binary()) -> step_trie_node(FTJ). 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", 386). -spec is_non_empty(binary()) -> boolean(). is_non_empty(Word) -> Word /= <<""/utf8>>. -file("src/dream_test/gherkin/step_trie.gleam", 390). -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", 446). -spec append_if_non_empty(list(binary()), binary()) -> list(binary()). append_if_non_empty(Parts, Value) -> case Value of <<""/utf8>> -> Parts; _ -> lists:append(Parts, [Value]) end. -file("src/dream_test/gherkin/step_trie.gleam", 460). -spec append_list(list(binary()), list(binary())) -> list(binary()). append_list(Parts, Extra) -> lists:append(Parts, Extra). -file("src/dream_test/gherkin/step_trie.gleam", 483). -spec split_numeric_with_regex(binary(), gleam@regexp:regexp()) -> list(binary()). split_numeric_with_regex(Token, Re) -> case gleam@regexp:split(Re, Token) of [Only] -> [Only]; Parts -> _pipe = Parts, gleam@list:filter(_pipe, fun is_non_empty/1) end. -file("src/dream_test/gherkin/step_trie.gleam", 476). -spec do_split_numeric(binary()) -> list(binary()). do_split_numeric(Token) -> case gleam@regexp:from_string(<<"(-?[0-9]+\\.?[0-9]*)"/utf8>>) of {ok, Re} -> split_numeric_with_regex(Token, Re); {error, _} -> [Token] end. -file("src/dream_test/gherkin/step_trie.gleam", 617). -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", 612). -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", 636). -spec lookup_handler_at_node( step_trie_node(FUV), binary(), list(captured_value()) ) -> gleam@option:option(step_match(FUV)). 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", 801). -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", 469). ?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", 805). -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", 662). ?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(FVA), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(FVA)). 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", 676). -spec try_param_matches( step_trie_node(FVG), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(FVG)). 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", 707). -spec try_numeric_matches( step_trie_node(FVS), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(FVS)). 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", 737). -spec try_float_then_word( step_trie_node(FWE), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(FWE)). 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", 767). -spec try_word_matches( step_trie_node(FWQ), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(FWQ)). 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", 784). -spec try_any_match( step_trie_node(FWW), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(FWW)). 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", 624). -spec lookup_in_node( step_trie_node(FUP), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(FUP)). 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", 690). -spec try_string_match( step_trie_node(FVM), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(FVM)). 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", 721). -spec try_int_match( step_trie_node(FVY), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(FVY)). 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", 751). -spec try_float_match( step_trie_node(FWK), binary(), binary(), list(binary()), list(captured_value()) ) -> gleam@option:option(step_match(FWK)). 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", 592). -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", 580). -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", 574). ?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" " ## Example\n" "\n" " ```gleam\n" " step_trie.tokenize_step_text(\"I add \\\"Red Widget\\\" and pay $19.99USD\")\n" " |> should\n" " |> be_equal([\n" " \"I\",\n" " \"add\",\n" " \"\\\"Red Widget\\\"\",\n" " \"and\",\n" " \"pay\",\n" " \"$\",\n" " \"19.99\",\n" " \"USD\",\n" " ])\n" " |> or_fail_with(\n" " \"expected tokenization to preserve quotes and split $19.99USD\",\n" " )\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", 528). ?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" " step_trie.lookup(trie, \"Then\", \"the total is $19.99USD\")\n" " |> should\n" " |> be_equal(\n" " Some(\n" " step_trie.StepMatch(handler: \"total_usd\", captures: [\n" " step_trie.CapturedFloat(19.99),\n" " ]),\n" " ),\n" " )\n" " |> or_fail_with(\"expected float capture for $19.99USD\")\n" " ```\n" ). -spec lookup(step_trie(FUC), binary(), binary()) -> gleam@option:option(step_match(FUC)). 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", 250). -spec insert_literal_segment( step_trie_node(FSL), binary(), binary(), list(step_segment()), FSL ) -> step_trie_node(FSL). 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", 223). -spec insert_into_node(step_trie_node(FSE), binary(), list(step_segment()), FSE) -> step_trie_node(FSE). 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", 263). -spec insert_int_segment( step_trie_node(FSP), binary(), list(step_segment()), FSP ) -> step_trie_node(FSP). 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", 274). -spec insert_float_segment( step_trie_node(FST), binary(), list(step_segment()), FST ) -> step_trie_node(FST). 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", 285). -spec insert_string_segment( step_trie_node(FSX), binary(), list(step_segment()), FSX ) -> step_trie_node(FSX). 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", 296). -spec insert_word_segment( step_trie_node(FTB), binary(), list(step_segment()), FTB ) -> step_trie_node(FTB). 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", 307). -spec insert_any_segment( step_trie_node(FTF), binary(), list(step_segment()), FTF ) -> step_trie_node(FTF). 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", 453). -spec split_after(binary()) -> list(binary()). split_after(After) -> case After of <<""/utf8>> -> []; _ -> split_word_around_placeholder(After) end. -file("src/dream_test/gherkin/step_trie.gleam", 405). ?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", 410). -spec split_on_first_placeholder(binary(), list(binary())) -> list(binary()). split_on_first_placeholder(Word, Placeholders) -> case Placeholders of [] -> [Word]; [Placeholder | Rest] -> case try_split_on_placeholder(Word, Placeholder) of {some, Parts} -> Parts; none -> split_on_first_placeholder(Word, Rest) end end. -file("src/dream_test/gherkin/step_trie.gleam", 424). -spec try_split_on_placeholder(binary(), binary()) -> gleam@option:option(list(binary())). try_split_on_placeholder(Word, Placeholder) -> case gleam@string:split_once(Word, Placeholder) of {ok, {Before, After}} -> {some, append_placeholder_parts(Before, Placeholder, After)}; {error, _} -> none end. -file("src/dream_test/gherkin/step_trie.gleam", 435). -spec append_placeholder_parts(binary(), binary(), binary()) -> list(binary()). append_placeholder_parts(Before, Placeholder, After) -> _pipe = [], _pipe@1 = append_if_non_empty(_pipe, Before), _pipe@2 = lists:append(_pipe@1, [Placeholder]), append_list(_pipe@2, split_after(After)). -file("src/dream_test/gherkin/step_trie.gleam", 378). ?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" " ## Example\n" "\n" " ```gleam\n" " step_trie.parse_step_pattern(\"the total is ${float}USD\")\n" " |> should\n" " |> be_equal([\n" " step_trie.LiteralWord(\"the\"),\n" " step_trie.LiteralWord(\"total\"),\n" " step_trie.LiteralWord(\"is\"),\n" " step_trie.LiteralWord(\"$\"),\n" " step_trie.FloatParam,\n" " step_trie.LiteralWord(\"USD\"),\n" " ])\n" " |> or_fail_with(\n" " \"expected ${float}USD to split into literal + FloatParam segments\",\n" " )\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", 212). ?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" " |> step_trie.insert(\n" " keyword: \"Given\",\n" " pattern: \"I have {int} items\",\n" " handler: \"count\",\n" " )\n" " ```\n" ). -spec insert(step_trie(FSB), binary(), binary(), FSB) -> step_trie(FSB). 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}.