lib/jose/poison/lexical_encoder.ex

if Code.ensure_loaded?(Poison) do
  defmodule JOSE.Poison.LexicalEncodeError do
    @type t :: %__MODULE__{message: String.t(), value: any}

    defexception message: nil, value: nil

    def exception(args) when is_list(args) do
      if Code.ensure_loaded?(Poison) and Code.ensure_loaded?(Poison.EncodeError) do
        Poison.EncodeError.exception(args)
      else
        struct = __struct__()
        {valid, invalid} = Enum.split_with(args, fn {k, _} -> Map.has_key?(struct, k) end)

        case invalid do
          [] ->
            :ok

          _ ->
            IO.warn(
              "the following fields are unknown when raising " <>
                "#{inspect(__MODULE__)}: #{inspect(invalid)}. " <>
                "Please make sure to only give known fields when raising " <>
                "or redefine #{inspect(__MODULE__)}.exception/1 to " <>
                "discard unknown fields. Future Elixir versions will raise on " <> "unknown fields given to raise/2"
            )
        end

        Kernel.struct!(struct, valid)
      end
    end

    def message(%{message: nil, value: value}) do
      "unable to encode value: #{inspect(value)}"
    end

    def message(%{message: message}) do
      message
    end
  end

  defmodule JOSE.Poison.LexicalEncode do
    @moduledoc false

    alias JOSE.Poison.{LexicalEncodeError, LexicalEncoder}

    defmacro __using__(_) do
      quote do
        alias JOSE.Poison.LexicalEncodeError
        alias String.Chars

        @compile {:inline, encode_name: 1}

        # Fast path encoding string keys
        defp encode_name(value) when is_binary(value) do
          value
        end

        defp encode_name(value) do
          case Chars.impl_for(value) do
            nil ->
              raise LexicalEncodeError,
                value: value,
                message: "expected a String.Chars encodable value, got: #{inspect(value)}"

            impl ->
              impl.to_string(value)
          end
        end
      end
    end
  end

  defmodule JOSE.Poison.LexicalPretty do
    @moduledoc false

    defmacro __using__(_) do
      quote do
        @default_indent 2
        @default_offset 0

        @compile {:inline, pretty: 1, indent: 1, offset: 1, offset: 2, spaces: 1}

        defp pretty(options) do
          Map.get(options, :pretty) == true
        end

        defp indent(options) do
          Map.get(options, :indent, @default_indent)
        end

        defp offset(options) do
          Map.get(options, :offset, @default_offset)
        end

        defp offset(options, value) do
          Map.put(options, :offset, value)
        end

        defp spaces(count) do
          :binary.copy(" ", count)
        end
      end
    end
  end

  defprotocol JOSE.Poison.LexicalEncoder do
    @fallback_to_any true

    @typep escape :: :unicode | :javascript | :html_safe
    @typep pretty :: boolean
    @typep indent :: non_neg_integer
    @typep offset :: non_neg_integer
    @typep strict_keys :: boolean

    @type options :: %{
            optional(:escape) => escape,
            optional(:pretty) => pretty,
            optional(:indent) => indent,
            optional(:offset) => offset,
            optional(:strict_keys) => strict_keys
          }

    @spec encode(t, options) :: iodata
    def encode(value, options)
  end

  defimpl JOSE.Poison.LexicalEncoder, for: Atom do
    alias JOSE.Poison.LexicalEncoder

    def encode(nil, _), do: "null"
    def encode(true, _), do: "true"
    def encode(false, _), do: "false"

    def encode(atom, options) do
      LexicalEncoder.BitString.encode(Atom.to_string(atom), options)
    end
  end

  defimpl JOSE.Poison.LexicalEncoder, for: BitString do
    alias JOSE.Poison.LexicalEncodeError

    import Bitwise

    def encode("", _), do: "\"\""

    def encode(string, options) do
      [?", escape(string, Map.get(options, :escape)), ?"]
    end

    defp escape("", _), do: []

    for {char, seq} <- Enum.zip(~c"\"\\\n\t\r\f\b", ~c"\"\\ntrfb") do
      defp escape(<<unquote(char)>> <> rest, mode) do
        [unquote("\\" <> <<seq>>) | escape(rest, mode)]
      end
    end

    # http://en.wikipedia.org/wiki/Unicode_control_characters
    defp escape(<<char>> <> rest, mode) when char <= 0x1F or char == 0x7F do
      [seq(char) | escape(rest, mode)]
    end

    defp escape(<<char::utf8>> <> rest, mode) when char in 0x80..0x9F do
      [seq(char) | escape(rest, mode)]
    end

    defp escape(<<char::utf8>> <> rest, :unicode) when char in 0xA0..0xFFFF do
      [seq(char) | escape(rest, :unicode)]
    end

    # http://en.wikipedia.org/wiki/UTF-16#Example_UTF-16_encoding_procedure
    # http://unicodebook.readthedocs.org/unicode_encodings.html
    defp escape(<<char::utf8>> <> rest, :unicode) when char > 0xFFFF do
      code = char - 0x10000

      [
        seq(0xD800 ||| code >>> 10),
        seq(0xDC00 ||| (code &&& 0x3FF))
        | escape(rest, :unicode)
      ]
    end

    defp escape(<<char::utf8>> <> rest, mode)
         when mode in [:html_safe, :javascript] and char in [0x2028, 0x2029] do
      [seq(char) | escape(rest, mode)]
    end

    defp escape(<<?/::utf8>> <> rest, :html_safe) do
      ["\\/" | escape(rest, :html_safe)]
    end

    defp escape(string, mode) do
      size = chunk_size(string, mode, 0)
      <<chunk::binary-size(size), rest::binary>> = string
      [chunk | escape(rest, mode)]
    end

    defp chunk_size(<<char>> <> _, _mode, acc)
         when char <= 0x1F or char in ~c"\"\\" do
      acc
    end

    defp chunk_size(<<?/::utf8>> <> _, :html_safe, acc) do
      acc
    end

    defp chunk_size(<<char>> <> rest, mode, acc) when char < 0x80 do
      chunk_size(rest, mode, acc + 1)
    end

    defp chunk_size(<<_::utf8>> <> _, :unicode, acc) do
      acc
    end

    defp chunk_size(<<char::utf8>> <> _, mode, acc)
         when mode in [:html_safe, :javascript] and char in [0x2028, 0x2029] do
      acc
    end

    defp chunk_size(<<codepoint::utf8>> <> rest, mode, acc) do
      size =
        cond do
          codepoint < 0x800 -> 2
          codepoint < 0x10000 -> 3
          true -> 4
        end

      chunk_size(rest, mode, acc + size)
    end

    defp chunk_size("", _, acc), do: acc

    defp chunk_size(other, _, _) do
      raise LexicalEncodeError, value: other
    end

    @compile {:inline, seq: 1}
    defp seq(char) do
      case Integer.to_charlist(char, 16) do
        s when length(s) < 2 -> ["\\u000" | s]
        s when length(s) < 3 -> ["\\u00" | s]
        s when length(s) < 4 -> ["\\u0" | s]
        s -> ["\\u" | s]
      end
    end
  end

  defimpl JOSE.Poison.LexicalEncoder, for: Integer do
    def encode(integer, _options) do
      Integer.to_string(integer)
    end
  end

  defimpl JOSE.Poison.LexicalEncoder, for: Float do
    def encode(float, _options) do
      :io_lib_format.fwrite_g(float)
    end
  end

  defimpl JOSE.Poison.LexicalEncoder, for: Map do
    @compile :inline_list_funcs

    alias JOSE.Poison.{LexicalEncoder, LexicalEncodeError}

    use JOSE.Poison.{LexicalEncode, LexicalPretty}

    def encode(map, _) when map_size(map) < 1, do: "{}"

    def encode(map, options) do
      map
      |> strict_keys(Map.get(options, :strict_keys, false))
      |> encode(pretty(options), options)
    end

    def encode(map, true, options) do
      indent = indent(options)
      offset = offset(options) + indent
      options = offset(options, offset)

      fun =
        &[
          ",\n",
          spaces(offset),
          LexicalEncoder.BitString.encode(encode_name(&1), options),
          ": ",
          LexicalEncoder.encode(:maps.get(&1, map), options) | &2
        ]

      [
        "{\n",
        tl(:lists.foldr(fun, [], :lists.sort(:maps.keys(map)))),
        ?\n,
        spaces(offset - indent),
        ?}
      ]
    end

    def encode(map, _, options) do
      fun =
        &[
          ?,,
          LexicalEncoder.BitString.encode(encode_name(&1), options),
          ?:,
          LexicalEncoder.encode(:maps.get(&1, map), options) | &2
        ]

      [?{, tl(:lists.foldr(fun, [], :lists.sort(:maps.keys(map)))), ?}]
    end

    defp strict_keys(map, false), do: map

    defp strict_keys(map, true) do
      map
      |> Map.keys()
      |> Enum.each(fn key ->
        name = encode_name(key)

        if Map.has_key?(map, name) do
          raise LexicalEncodeError,
            value: name,
            message: "duplicate key found: #{inspect(key)}"
        end
      end)

      map
    end
  end

  defimpl JOSE.Poison.LexicalEncoder, for: List do
    alias JOSE.Poison.LexicalEncoder

    use JOSE.Poison.LexicalPretty

    @compile :inline_list_funcs

    def encode([], _), do: "[]"

    def encode(list, options) do
      encode(list, pretty(options), options)
    end

    def encode(list, false, options) do
      fun = &[?,, LexicalEncoder.encode(&1, options) | &2]
      [?[, tl(:lists.foldr(fun, [], list)), ?]]
    end

    def encode(list, true, options) do
      indent = indent(options)
      offset = offset(options) + indent
      options = offset(options, offset)

      fun = &[",\n", spaces(offset), LexicalEncoder.encode(&1, options) | &2]
      ["[\n", tl(:lists.foldr(fun, [], list)), ?\n, spaces(offset - indent), ?]]
    end
  end

  defimpl JOSE.Poison.LexicalEncoder, for: [Range, Stream, MapSet, HashSet] do
    alias JOSE.Poison.LexicalEncoder

    use JOSE.Poison.LexicalPretty

    def encode(collection, options) do
      encode(collection, pretty(options), options)
    end

    def encode(collection, false, options) do
      fun = &[?,, LexicalEncoder.encode(&1, options)]

      case Enum.flat_map(collection, fun) do
        [] -> "[]"
        [_ | tail] -> [?[, tail, ?]]
      end
    end

    def encode(collection, true, options) do
      indent = indent(options)
      offset = offset(options) + indent
      options = offset(options, offset)

      fun = &[",\n", spaces(offset), LexicalEncoder.encode(&1, options)]

      case Enum.flat_map(collection, fun) do
        [] -> "[]"
        [_ | tail] -> ["[\n", tail, ?\n, spaces(offset - indent), ?]]
      end
    end
  end

  defimpl JOSE.Poison.LexicalEncoder, for: [Date, Time, NaiveDateTime, DateTime] do
    alias JOSE.Poison.LexicalEncoder

    def encode(value, options) do
      LexicalEncoder.BitString.encode(@for.to_iso8601(value), options)
    end
  end

  defimpl JOSE.Poison.LexicalEncoder, for: Any do
    alias JOSE.Poison.{LexicalEncoder, LexicalEncodeError}

    defmacro __deriving__(module, struct, options) do
      deriving(module, struct, options)
    end

    def deriving(module, _struct, options) do
      only = options[:only]
      except = options[:except]

      extractor =
        cond do
          only ->
            quote(do: Map.take(struct, unquote(only)))

          except ->
            except = [:__struct__ | except]
            quote(do: Map.drop(struct, unquote(except)))

          true ->
            quote(do: :maps.remove(:__struct__, struct))
        end

      quote do
        defimpl LexicalEncoder, for: unquote(module) do
          def encode(struct, options) do
            LexicalEncoder.Map.encode(unquote(extractor), options)
          end
        end
      end
    end

    def encode(%{__struct__: _} = struct, options) do
      LexicalEncoder.Map.encode(Map.from_struct(struct), options)
    end

    def encode(value, _options) do
      raise LexicalEncodeError, value: value
    end
  end
end