lib/ex_kdl.ex

defmodule ExKdl do
  @moduledoc """
  A robust and efficient decoder and encoder for the KDL Document Language.
  """

  alias ExKdl.{DecodeError, EncodeError, Encoder, Lexer, Node, Parser}

  @doc """
  Decodes a KDL-encoded document from the `input` binary.

  ## Examples

      iex> ExKdl.decode("node 10")
      {:ok,
       [
         %ExKdl.Node{
           name: "node",
           type: nil,
           values: [%ExKdl.Value{type: nil, value: %Decimal{coef: 10}}],
           properties: %{},
           children: []
         }
       ]}

      iex> ExKdl.decode(~s|node "unterminated string|)
      {:error,
       %ExKdl.DecodeError{
         line: 1,
         message: "unterminated string meets end of file"
       }}
  """
  @spec decode(binary) :: {:ok, [Node.t()]} | {:error, DecodeError.t()}
  def decode(input) when is_binary(input) do
    with {:ok, tokens} <- Lexer.lex(input) do
      Parser.parse(tokens)
    end
  end

  def decode(_) do
    {:error, %DecodeError{message: "Argument to decode/1 must be a KDL-encoded binary"}}
  end

  @doc """
  Decodes a KDL-encoded document from the `input` binary.

  Similar to `decode/1` except it will raise in the event of an error.

  ## Examples

      iex> ExKdl.decode!("node 10")
      [
        %ExKdl.Node{
          name: "node",
          type: nil,
          values: [%ExKdl.Value{type: nil, value: %Decimal{coef: 10}}],
          properties: %{},
          children: []
        }
      ]

      iex> ExKdl.decode!(~s|node "unterminated string|)
      ** (ExKdl.DecodeError) Line 1: unterminated string meets end of file
  """
  @spec decode!(binary) :: [Node.t()]
  def decode!(input) do
    case decode(input) do
      {:ok, nodes} ->
        nodes

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

  @doc """
  Encodes a list of `ExKdl.Node` structs into a KDL-encoded binary.

  ## Examples

      iex> ExKdl.encode([%ExKdl.Node{name: "node", values: [%ExKdl.Value{value: %Decimal{coef: 10}}]}])
      {:ok, "node 10\\n"}

      iex> ExKdl.encode(nil)
      {:error, %ExKdl.EncodeError{message: "Argument to encode/1 must be a list of KDL nodes"}}
  """
  @spec encode([Node.t()]) :: {:ok, binary} | {:error, EncodeError.t()}
  def encode(input) when is_list(input) do
    Encoder.encode(input)
  end

  def encode(_) do
    {:error, %EncodeError{message: "Argument to encode/1 must be a list of KDL nodes"}}
  end

  @doc """
  Encodes a list of `ExKdl.Node` structs into a KDL-encoded binary.

  Similar to `encode/1` except it will raise in the event of an error.

  ## Examples

      iex> ExKdl.encode!([%ExKdl.Node{name: "node", values: [%ExKdl.Value{value: %Decimal{coef: 10}}]}])
      "node 10\\n"

      iex> ExKdl.encode!(nil)
      ** (ExKdl.EncodeError) Argument to encode/1 must be a list of KDL nodes
  """
  @spec encode!([Node.t()]) :: binary
  def encode!(input) do
    case encode(input) do
      {:ok, encoded} ->
        encoded

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