defmodule Poison.EncodeError do @type t :: %__MODULE__{message: String.t, value: any} defexception message: nil, value: nil def message(%{message: nil, value: value}) do "unable to encode value: #{inspect(value)}" end def message(%{message: message}) do message end end defmodule Poison.Encode do @moduledoc false alias Poison.{Encoder, EncodeError} defmacro __using__(_) do quote do alias String.Chars alias Poison.EncodeError @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 EncodeError, value: value, message: "expected a String.Chars encodable value, got: #{inspect(value)}" impl -> impl.to_string(value) end end end end end defmodule Poison.Pretty 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) 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 Poison.Encoder 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 Poison.Encoder, for: Atom do alias Poison.Encoder def encode(nil, _), do: "null" def encode(true, _), do: "true" def encode(false, _), do: "false" def encode(atom, options) do Encoder.BitString.encode(Atom.to_string(atom), options) end end defimpl Poison.Encoder, for: BitString do alias Poison.EncodeError use Bitwise def encode("", _), do: "\"\"" def encode(string, options) do [?", escape(string, Map.get(options, :escape)), ?"] end defp escape("", _), do: [] for {char, seq} <- Enum.zip('"\\\n\t\r\f\b', '"\\ntrfb') do defp escape(<> <> rest, mode) do [unquote("\\" <> <>) | escape(rest, mode)] end end # http://en.wikipedia.org/wiki/Unicode_control_characters defp escape(<> <> rest, mode) when char <= 0x1F or char == 0x7F do [seq(char) | escape(rest, mode)] end defp escape(<> <> rest, mode) when char in 0x80..0x9F do [seq(char) | escape(rest, mode)] end defp escape(<> <> 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#utf-16-surrogate-pairs defp escape(<> <> rest, :unicode) when char > 0xFFFF do code = char - 0x10000 [seq(0xD800 ||| (code >>> 10)), seq(0xDC00 ||| (code &&& 0x3FF)) | escape(rest, :unicode)] end defp escape(<> <> rest, mode) when mode in [:html_safe, :javascript] and char in [0x2028, 0x2029] do [seq(char) | escape(rest, mode)] end defp escape(<> <> rest, :html_safe) do ["\\/" | escape(rest, :html_safe)] end defp escape(<> <> rest, :html_safe) do [char | escape(rest, :html_safe)] end defp escape(string, mode) do size = chunk_size(string, mode, 0) <> = string [chunk | escape(rest, mode)] end defp chunk_size(<> <> _, _mode, acc) when char <= 0x1F or char in '"\\' do acc end defp chunk_size(<> <> 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(<> <> _, mode, acc) when mode in [:html_safe, :javascript] and char in [0x2028, 0x2029] do acc end defp chunk_size(<> <> 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 EncodeError, 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 Poison.Encoder, for: Integer do def encode(integer, _options) do Integer.to_string(integer) end end defimpl Poison.Encoder, for: Float do def encode(float, _options) do :io_lib_format.fwrite_g(float) end end defimpl Poison.Encoder, for: Map do @compile :inline_list_funcs alias Poison.{Encoder, EncodeError} use Poison.{Encode, Pretty} 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), Encoder.BitString.encode(encode_name(&1), options), ": ", Encoder.encode(:maps.get(&1, map), options) | &2] ["{\n", tl(:lists.foldl(fun, [], :maps.keys(map))), ?\n, spaces(offset - indent), ?}] end def encode(map, _, options) do fun = &[?,, Encoder.BitString.encode(encode_name(&1), options), ?:, Encoder.encode(:maps.get(&1, map), options) | &2] [?{, tl(:lists.foldl(fun, [], :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 EncodeError, value: name, message: "duplicate key found: #{inspect(key)}" end end) map end end defimpl Poison.Encoder, for: List do alias Poison.Encoder use Poison.Pretty @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 = &[?,, Encoder.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), Encoder.encode(&1, options) | &2] ["[\n", tl(:lists.foldr(fun, [], list)), ?\n, spaces(offset - indent), ?]] end end defimpl Poison.Encoder, for: [Range, Stream, MapSet, HashSet] do alias Poison.Encoder use Poison.Pretty def encode(collection, options) do encode(collection, pretty(options), options) end def encode(collection, false, options) do fun = &[?,, Encoder.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), Encoder.encode(&1, options)] case Enum.flat_map(collection, fun) do [] -> "[]" [_ | tail] -> ["[\n", tail, ?\n, spaces(offset - indent), ?]] end end end defimpl Poison.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do alias Poison.Encoder def encode(value, options) do Encoder.BitString.encode(@for.to_iso8601(value), options) end end defimpl Poison.Encoder, for: Any do alias Poison.{Encoder, EncodeError} 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 Encoder, for: unquote(module) do def encode(struct, options) do Encoder.Map.encode(unquote(extractor), options) end end end end def encode(%{__struct__: _} = struct, options) do Encoder.Map.encode(Map.from_struct(struct), options) end def encode(value, _options) do raise EncodeError, value: value end end