defmodule AbnfParsec do alias AbnfParsec.{Parser, Generator} @moduledoc """ Generates a parser from ABNF definition - text (`:abnf`) or file path (`:abnf_file`) An entry rule can be defined by `:parse`. If defined, a `parse/1` function and a `parse!/1` function will be generated with the entry rule. By default, every chunk defined by a rule is wrapped (in list) and tagged by the rulename. Use the options to `:untag`, `:unwrap` or both (`:unbox`). Parsed chunks (rules) can be discarded `:ignore`. Transformations (`:map`, `:reduce`, `:replace`) can be applied by passing in a `:transform` map with keys being rulenames and values being 2-tuples of transformation type (`:map`, `:reduce`, `:replace`), and mfa tuple (for `:map` and `reduce`) or a literal value (for `:replace`) Example usage: defmodule JsonParser do use AbnfParsec, abnf_file: "test/fixture/json.abnf", parse: :json_text, transform: %{ "string" => {:reduce, {List, :to_string, []}}, "int" => [{:reduce, {List, :to_string, []}}, {:map, {String, :to_integer, []}}], "frac" => {:reduce, {List, :to_string, []}}, "null" => {:replace, nil}, "true" => {:replace, true}, "false" => {:replace, false} }, untag: ["member"], unwrap: ["int", "frac"], unbox: [ "JSON-text", "null", "true", "false", "digit1-9", "decimal-point", "escape", "unescaped", "char" ], ignore: [ "name-separator", "value-separator", "quotation-mark", "begin-object", "end-object", "begin-array", "end-array" ] end json = ~s| {"a": {"b": 1.2, "c": [true]}, "d": null, "e": "e\\te"} | JsonParser.json_text(json) # or JsonParser.parse(json) # => {:ok, ...} JsonParser.parse!(json) # => [ object: [ [ string: ["a"], value: [ object: [ [string: ["b"], value: [number: [int: 1, frac: ".2"]]], [string: ["c"], value: [array: [value: [true]]]] ] ] ], [string: ["d"], value: [nil]], [string: ["e"], value: [string: ["e\\te"]]] ] ] """ @type rulename :: :binary @type rulenames :: [rulename()] @type transformation :: {:replace | :reduce | :map, mfa :: {module, atom, [term]}} @type transformations :: %{optional(rulename) => transformation | [transformation]} @doc """ All rules by default are wrapped and tagged. See NimbleParsec for more details. Options: - `:abnf` (`binary`) - ABNF directly in string data - `:abnf_file` (`binary`) - ABNF file path - `:debug` (`boolean`) - whether to output generated parser code - `:skip` (`rulenames`) - rules to be skipped when generating parser code - so user can define their own parsec definition - `:ignore` (`rulenames`) - rules to be discarded after being parsed - `:untag` (`rulenames`) - rules to be wrapped only after being parsed - `:unwrap` (`rulenames`) - rules to be tagged only after being parsed - needs to be sure that there is only singular parsed entry - `:unbox` (`rulenames`) - made up term, to both untag and unwrap - `:transform` (`transformations`) - a map of `rulenames` to transformations - `{:map, mfa}` - `{:reduce, mfa}` - `{:replace, val}` - `{:post_traverse, mfa}` - `{:pre_traverse, mfa}` - `:parse` (`atom`) - described in module doc, needs to be an atom that is in normalized form of its original string rulename """ defmacro __using__(opts) do abnf = case Keyword.fetch(opts, :abnf_file) do {:ok, filepath} -> File.read!(filepath) :error -> Keyword.fetch!(opts, :abnf) end debug? = Keyword.get(opts, :debug, false) parse = Keyword.get(opts, :parse) opts = opts |> Enum.into(%{}) |> Map.update(:transform, %{}, &elem(Code.eval_quoted(&1), 0)) code = abnf |> Parser.parse!() |> Generator.generate(opts) if debug? do code |> Macro.to_string() |> Code.format_string!() |> IO.puts() end quote do import NimbleParsec unquote(Generator.core()) unquote(code) if unquote(parse) do def parse(text) do text |> unquote({parse, [], []}) end def parse!(text) do case parse(text) do {:ok, syntax, "", _, _, _} -> syntax {:ok, _, leftover, _, _, _} -> raise AbnfParsec.LeftoverTokenError, "Leftover: #{leftover}" {:error, error, _, _, _, _} -> raise AbnfParsec.UnexpectedTokenError, error end end end end end end