lib/cond_parser.ex

defmodule CondParser do
  @moduledoc """
  `CondParser` reads conditions like
  `(':foo' or ("toto" == true AND (12 != 13 OR false))) and true` and transform them
  either in an ast with `parse/1`, or in json with `to_json/1`.

  Ast looks like this:

  ```
  {:and_op,
    {:or_op, ":foo",
      {:and_op,
        {:eq_op, "toto", true},
        {:or_op,
          {:not_eq_op, 12, 13},
          false
        }
      }
    },
    true
  }
  ```

  Json looks like this:

  ```json
  {
    "left": {
      "left": ":foo",
      "op": "or",
      "right": {
        "left": {"left": "toto", "op": "eq", "right": true},
        "op": "and",
        "right": {
          "left": {"left": 12, "op": "not_eq", "right": 13},
          "op": "or",
          "right": false
        }
      }
    },
    "op": "and",
    "right": true
  }
  ```
  """

  @type ast() :: any()
  @type json() :: map()

  @spec parse(binary) :: {:ok, ast()} | {:error, any()}
  def parse(str) do
    with {:ok, tokens, _end_line} <- str |> to_charlist() |> :cond_lexer.string(),
         {:ok, ast} <- :cond_parser.parse(tokens) do
      {:ok, ast}
    else
      {_, reason, _} ->
        {:error, reason}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @spec parse!(binary) :: ast
  def parse!(str) do
    case parse(str) do
      {:ok, ast} -> ast
      {:error, err} -> throw(err)
    end
  end

  @spec to_json(binary) :: {:error, any()} | {:ok, json()}
  def to_json(str) do
    case parse(str) do
      {:ok, ast} -> {:ok, JsonBuilder.build(ast)}
      {:error, reason} -> {:error, reason}
    end
  end

  @spec to_json!(binary) :: json()
  def to_json!(str) do
    case to_json(str) do
      {:ok, json} -> json
      {:error, err} -> throw(err)
    end
  end

  @spec is_valid?(binary) :: boolean()
  def is_valid?(str) do
    case parse(str) do
      {:ok, _ast} -> true
      {:error, _err} -> false
    end
  end
end