# `Cucumber.Expression`
[🔗](https://github.com/huddlz-hq/cucumber/blob/main/lib/cucumber/expression.ex#L1)

Parser and matcher for Cucumber Expressions used in step definitions.

Cucumber Expressions are a human-friendly alternative to regular expressions,
allowing you to define step patterns with typed parameters. This module handles
compiling these expressions and extracting/converting parameters using NimbleParsec.

This implementation follows the official Cucumber Expression syntax from
https://github.com/cucumber/cucumber-expressions

## Parameter Types

The following parameter types are supported:

* `{string}` - Matches quoted strings ("example") and converts to string
* `{int}` - Matches integers (42, -5) and converts to integer
* `{float}` - Matches floating point numbers (3.14, -0.5) and converts to float
* `{word}` - Matches a single word (no whitespace) and converts to string
* `{atom}` - Matches a word and converts to atom (pending -> :pending)

## Advanced Features

* **Optional text**: Use `(text)` for optional text that may or may not be present
* **Alternation**: Use `word1/word2` to match alternative words (not captured)
* **Optional parameters**: Use `{int?}` for optional matching (returns nil if absent)
* **Escape sequences**: Use `\(`, `\)`, `\/`, `\{`, `\}`, `\\` for literal characters

## Examples

    # Matching a step with a string parameter
    "I click {string} button" matches "I click \"Submit\" button"

    # Matching a step with multiple parameters
    "I add {int} items to my {word} list" matches "I add 5 items to my shopping list"

    # Optional text (for pluralization)
    "I have {int} cucumber(s)" matches "I have 1 cucumber" and "I have 5 cucumbers"

    # Alternation
    "I click/tap the button" matches "I click the button" or "I tap the button"

    # Optional parameter
    "I have {int?} items" matches both "I have 5 items" and "I have items"

# `compiled`

```elixir
@type compiled() :: [
  {:literal, String.t()}
  | {:parameter, String.t(), parameter_matcher(), :required | :optional}
  | {:optional, String.t()}
  | {:alternation, [String.t()]}
]
```

# `custom_types`

```elixir
@type custom_types() :: %{
  required(String.t()) =&gt; %{:regexp =&gt; Regex.t(), optional(atom()) =&gt; term()}
}
```

Custom parameter type definitions, keyed by type name — see
`Cucumber.ParameterTypes`.

# `parameter_matcher`

```elixir
@type parameter_matcher() ::
  function() | {:custom, Regex.t(), non_neg_integer(), function() | nil}
```

A parameter matcher: either a NimbleParsec parser function (built-in
types) or a custom-type matcher built from a user-registered regex.

# `compile`

```elixir
@spec compile(String.t()) :: compiled()
```

Compiles a Cucumber Expression pattern into a matchable AST.

This function transforms a human-readable pattern with typed parameters
into an AST that can be used for matching step text. Custom parameter
types (see `Cucumber.ParameterTypes`) are passed as the second argument;
`compile/1` compiles with the built-in types only.

Raises `Cucumber.UndefinedParameterTypeError` if the pattern references a
parameter type that is neither built-in nor in `custom_types`.

## Examples

    iex> compiled = Cucumber.Expression.compile("I have {int} items")
    iex> is_list(compiled)
    true

# `compile`

```elixir
@spec compile(String.t(), custom_types()) :: compiled()
```

# `match`

```elixir
@spec match(String.t(), compiled()) :: {:match, [term()]} | :no_match
```

Matches a step text against a compiled Cucumber Expression.

This function attempts to match step text against a compiled Cucumber Expression
and extracts/converts any parameters if there's a match.

## Parameters

* `text` - The step text to match against the pattern
* `compiled` - A compiled Cucumber Expression from `compile/1`

## Returns

Returns one of:
* `{:match, args}` - If the text matches, where `args` is a list of converted parameter values
* `:no_match` - If the text doesn't match the expression

## Examples

    iex> compiled = Cucumber.Expression.compile("I have {int} items")
    iex> Cucumber.Expression.match("I have 42 items", compiled)
    {:match, [42]}
    iex> Cucumber.Expression.match("I have no items", compiled)
    :no_match

# `parse_atom_param`

```elixir
@spec parse_atom_param(binary(), keyword()) ::
  {:ok, [term()], rest, context, line, byte_offset}
  | {:error, reason, rest, context, line, byte_offset}
when line: {pos_integer(), byte_offset},
     byte_offset: non_neg_integer(),
     rest: binary(),
     reason: String.t(),
     context: map()
```

Parses the given `binary` as parse_atom_param.

Returns `{:ok, [token], rest, context, position, byte_offset}` or
`{:error, reason, rest, context, line, byte_offset}` where `position`
describes the location of the parse_atom_param (start position) as `{line, offset_to_start_of_line}`.

To column where the error occurred can be inferred from `byte_offset - offset_to_start_of_line`.

## Options

  * `:byte_offset` - the byte offset for the whole binary, defaults to 0
  * `:line` - the line and the byte offset into that line, defaults to `{1, byte_offset}`
  * `:context` - the initial context value. It will be converted to a map

# `parse_float_param`

```elixir
@spec parse_float_param(binary(), keyword()) ::
  {:ok, [term()], rest, context, line, byte_offset}
  | {:error, reason, rest, context, line, byte_offset}
when line: {pos_integer(), byte_offset},
     byte_offset: non_neg_integer(),
     rest: binary(),
     reason: String.t(),
     context: map()
```

Parses the given `binary` as parse_float_param.

Returns `{:ok, [token], rest, context, position, byte_offset}` or
`{:error, reason, rest, context, line, byte_offset}` where `position`
describes the location of the parse_float_param (start position) as `{line, offset_to_start_of_line}`.

To column where the error occurred can be inferred from `byte_offset - offset_to_start_of_line`.

## Options

  * `:byte_offset` - the byte offset for the whole binary, defaults to 0
  * `:line` - the line and the byte offset into that line, defaults to `{1, byte_offset}`
  * `:context` - the initial context value. It will be converted to a map

# `parse_int_param`

```elixir
@spec parse_int_param(binary(), keyword()) ::
  {:ok, [term()], rest, context, line, byte_offset}
  | {:error, reason, rest, context, line, byte_offset}
when line: {pos_integer(), byte_offset},
     byte_offset: non_neg_integer(),
     rest: binary(),
     reason: String.t(),
     context: map()
```

Parses the given `binary` as parse_int_param.

Returns `{:ok, [token], rest, context, position, byte_offset}` or
`{:error, reason, rest, context, line, byte_offset}` where `position`
describes the location of the parse_int_param (start position) as `{line, offset_to_start_of_line}`.

To column where the error occurred can be inferred from `byte_offset - offset_to_start_of_line`.

## Options

  * `:byte_offset` - the byte offset for the whole binary, defaults to 0
  * `:line` - the line and the byte offset into that line, defaults to `{1, byte_offset}`
  * `:context` - the initial context value. It will be converted to a map

# `parse_string_param`

```elixir
@spec parse_string_param(binary(), keyword()) ::
  {:ok, [term()], rest, context, line, byte_offset}
  | {:error, reason, rest, context, line, byte_offset}
when line: {pos_integer(), byte_offset},
     byte_offset: non_neg_integer(),
     rest: binary(),
     reason: String.t(),
     context: map()
```

Parses the given `binary` as parse_string_param.

Returns `{:ok, [token], rest, context, position, byte_offset}` or
`{:error, reason, rest, context, line, byte_offset}` where `position`
describes the location of the parse_string_param (start position) as `{line, offset_to_start_of_line}`.

To column where the error occurred can be inferred from `byte_offset - offset_to_start_of_line`.

## Options

  * `:byte_offset` - the byte offset for the whole binary, defaults to 0
  * `:line` - the line and the byte offset into that line, defaults to `{1, byte_offset}`
  * `:context` - the initial context value. It will be converted to a map

# `parse_word_param`

```elixir
@spec parse_word_param(binary(), keyword()) ::
  {:ok, [term()], rest, context, line, byte_offset}
  | {:error, reason, rest, context, line, byte_offset}
when line: {pos_integer(), byte_offset},
     byte_offset: non_neg_integer(),
     rest: binary(),
     reason: String.t(),
     context: map()
```

Parses the given `binary` as parse_word_param.

Returns `{:ok, [token], rest, context, position, byte_offset}` or
`{:error, reason, rest, context, line, byte_offset}` where `position`
describes the location of the parse_word_param (start position) as `{line, offset_to_start_of_line}`.

To column where the error occurred can be inferred from `byte_offset - offset_to_start_of_line`.

## Options

  * `:byte_offset` - the byte offset for the whole binary, defaults to 0
  * `:line` - the line and the byte offset into that line, defaults to `{1, byte_offset}`
  * `:context` - the initial context value. It will be converted to a map

---

*Consult [api-reference.md](api-reference.md) for complete listing*
