How to force NimbleParsec to use labels?

I’m using NimbleParsec to parse a protocol and I would like to know if there is anyway to force a combinator to use labels.

For example, if I have this integer(min: 1) |> label("protocol"), the library will return an random error and not the protocol one in case I don’t send an integer. I would like to known if there is anyway to encapsulate a logic to guarantee that in case it fails, I can get a label to then pattern match later on the exact part it failed.

Thanks!

Using your example parser, I see:

iex> defmodule Parser do
...>  import NimbleParsec
...>
...>  defparsec :number,
...>    integer(min: 1) 
...>    |> label("protocol")
...> end
{:module, Parser, <<70, 79, 82, 49, 0, 0, 19, ...>>,.....

iex> Parser.number "3"
{:ok, [3], "", %{}, {1, 0}, 1

iex> Parser.number "a"
{:error, "expected protocol", "a", %{}, {1, 0}, 0}

Which seems pretty reasonable to me. I do find returning meaningful parsing errors a bit tricky at times - but typically not an issue in this simple kind of case.

What are you expecting, and what are you seeing?

1 Like

Sorry about the noise, I found the problem. I’m building a long chain of combinators, and the error was coming from the next one in the chain. I posted just a small snippet to make it easier to frame the question, but it masked out the problem.

This is the combinator now:

  device_id =
    integer(min: 1) |> unwrap_and_tag(:device_id) |> lookahead(string(",")) |> label("device id")

And it matches this message:

"*HQ,1234567890,V1,...

The *HQ, is being match by a previous combinator, and then the next part is the device ID, which is now correctly being parsed.

NimbleParsec is really good, but it can get quite tricky sometimes.

Glad you’re up and running. I typically try to make the grammar as explicit as possible and avoid lookahead unless really necessary. Perhaps thats just my preference. For example:

defmodule Parser do
  import NimbleParsec

  preamble =
    string("*HQ")
    |> label("preamble")
  
  device_id =
    integer(min: 1) 
    |> label("device id")

  version =
    string("V")
    |> integer(1)
    |> reduce({Enum, :join, []})
    |> label("version")

  separator =
    string(",")
    |> label("separator")
  
  defparsec :number,
    preamble
    |> ignore(separator)
    |> concat(device_id)
    |> ignore(separator)
    |> concat(version)
  
end

Examples

iex> Parser.number "*HQ,1234567890,V1"
{:ok, ["*HQ", 1234567890, "V1"], "", %{}, {1, 0}, 17}

iex> Parser.number "*HQ,A1234567890,V1"
{:error, "expected device id", "A1234567890,V1", %{}, {1, 0}, 4}

Which still gives an appropriate error message and makes it easy to expand the grammar imo.

2 Likes