lib/logfmt_ex/encoder.ex

defmodule LogfmtEx.Encoder do
  @moduledoc """
  Encodes key/value pairs.
  """

  alias LogfmtEx.ValueEncoder

  @type key :: String.t() | atom()

  @delimiter ?=

  @spec encode(key(), term(), keyword()) :: iodata()
  def encode(key, value, opts \\ [])

  def encode(:domain, domain, opts) do
    delimiter = opts |> Keyword.get(:delimiter, @delimiter)

    [encode_key(:domain), delimiter, inspect(domain)]
  end

  def encode(:mfa, {m, f, a}, opts) do
    delimiter = opts |> Keyword.get(:delimiter, @delimiter)

    [encode_key(:mfa), delimiter, Exception.format_mfa(m, f, a)]
  end

  def encode(key, value, opts) do
    delimiter = opts |> Keyword.get(:delimiter, @delimiter)

    [encode_key(key), delimiter, encode_value(value)]
  rescue
    error -> "there was an error: #{inspect(error)}"
  end

  defp encode_value(""), do: ~s("")

  defp encode_value(value) do
    value = value |> ValueEncoder.encode()

    case infer_quotes(value) do
      :unquoted -> value
      :quoted -> ["\"", value, "\""]
      :escaped -> ["\"", escape(value), "\""]
    end
  end

  defp infer_quotes(value), do: infer_quotes(value, :unquoted)
  defp infer_quotes(<<>>, acc), do: acc
  defp infer_quotes(<<front, _rest::binary>>, _acc) when front <= 0x1F, do: :escaped
  defp infer_quotes(<<" ", rest::binary>>, _acc), do: infer_quotes(rest, :quoted)
  defp infer_quotes(<<"\"", _rest::binary>>, _acc), do: :escaped
  defp infer_quotes(<<"=", rest::binary>>, _acc), do: infer_quotes(rest, :quoted)
  defp infer_quotes(<<"\\", rest::binary>>, :unquoted), do: infer_quotes(rest, :unquoted)
  defp infer_quotes(<<"\\", rest::binary>>, :quoted), do: infer_quotes(rest, :escaped)
  defp infer_quotes(<<_front, rest::binary>>, acc), do: infer_quotes(rest, acc)

  defp encode_key(key) when is_atom(key), do: Atom.to_string(key)
  defp encode_key(key) when is_bitstring(key), do: key

  defp escape(string),
    do: escape(string, "")

  defp escape("", acc), do: acc

  defp escape(<<"\t", rest::binary>>, acc),
    do: escape(rest, <<acc::binary, "\\t">>)

  defp escape(<<"\n", rest::binary>>, acc),
    do: escape(rest, <<acc::binary, "\\n">>)

  defp escape(<<"\r", rest::binary>>, acc),
    do: escape(rest, <<acc::binary, "\\r">>)

  defp escape(<<"\"", rest::binary>>, acc),
    do: escape(rest, <<acc::binary, "\\\"">>)

  defp escape(<<"\\", rest::binary>>, acc),
    do: escape(rest, <<acc::binary, "\\\\">>)

  defp escape(<<c, rest::binary>>, acc),
    do: escape(rest, <<acc::binary, c>>)
end