defmodule ExpressionParser do import NimbleParsec whitespace = ignore(optional(ascii_string([?\s, ?\t, ?\n, ?\r], min: 1))) identifier = ascii_string([?a..?z, ?A..?Z, ?_, ?0..?9], min: 1) |> label("identifier") |> unwrap_and_tag(:field) quoted_string = ignore(string("\"")) |> repeat(utf8_char(not: ?")) |> reduce({List, :to_string, []}) |> ignore(string("\"")) number = ascii_string([?0..?9], min: 1) |> map({String, :to_integer, []}) atom_value = ignore(string("'")) |> ascii_string([?a..?z, ?A..?Z], min: 1) |> ignore(string("'")) |> map({__MODULE__, :identity, []}) list_item = choice([ number, atom_value, quoted_string |> unwrap_and_tag(:string) ]) atom_list = ignore(string("[")) |> optional(whitespace) |> repeat( list_item |> optional(ignore(string(","))) |> optional(whitespace) ) |> ignore(string("]")) |> tag(:list) operator = choice([ string("ilike") |> replace(:ilike), string(">=") |> replace(:>=), string("<=") |> replace(:<=), string("!=") |> replace(:!=), string(">") |> replace(:>), string("<") |> replace(:<), string("=") |> replace(:==), string("in") |> replace(:in) ]) value = choice([ quoted_string |> unwrap_and_tag(:string), number |> unwrap_and_tag(:number), atom_list ]) comparison = identifier |> ignore(whitespace) |> concat(operator) |> ignore(whitespace) |> concat(value) |> tag(:comparison) defcombinatorp :expr, parsec(:logic_expr) paren_expr = ignore(string("(")) |> ignore(whitespace) |> parsec(:expr) |> ignore(whitespace) |> ignore(string(")")) logic_term = choice([ paren_expr, comparison ]) logical_op = ignore(whitespace) |> choice([ string("and") |> replace(:and), string("or") |> replace(:or) ]) |> ignore(whitespace) logic_expr = logic_term |> repeat( logical_op |> concat(logic_term) ) |> reduce({__MODULE__, :reduce_logic, []}) defparsec :logic_expr, logic_expr def parse(input) do case logic_expr(input) do {:ok, result, "", _, _, _} -> {:ok, result} {:ok, _, rest, _, _, _} -> {:error, "Unexpected remaining input: #{inspect(rest)}"} {:error, reason, _, _, _, _} -> {:error, reason} end end def reduce_logic([head | tail]) do Enum.chunk_every(tail, 2) |> Enum.reduce(head, fn [op, right], acc -> {op, acc, right} end) end def identity(value), do: value end