lib/ex_aws/dynamo/decoder.ex

defmodule ExAws.Dynamo.Decoder do
  @moduledoc """
  Decodes a Dynamo response into a struct.

  If Dynamo.Decodable is implemented for the struct, it will be called
  after the completion of the coercion.

  This is important for handling nested maps if you wanted the nested maps
  to have atom keys.
  """

  alias ExAws.Dynamo.Decodable

  def decode(item, as: struct_module) do
    item
    |> decode
    |> binary_map_to_struct(struct_module)
    |> Decodable.decode()
  end

  @doc """
  Convert Dynamo format to Elixir

  Functions which convert the Dynamo-style values into normal Elixir values.
  Use these if you just want the Dynamo result to look more like Elixir without
  coercing it into a particular struct.
  """
  def decode(%{"BOOL" => true}), do: true
  def decode(%{"BOOL" => false}), do: false
  def decode(%{"BOOL" => "true"}), do: true
  def decode(%{"BOOL" => "false"}), do: false
  def decode(%{"NULL" => true}), do: nil
  def decode(%{"NULL" => "true"}), do: nil
  def decode(%{"B" => value}), do: Base.decode64!(value)
  def decode(%{"S" => value}), do: value
  def decode(%{"M" => value}), do: value |> decode

  def decode(%{"BS" => values}), do: MapSet.new(values)
  def decode(%{"SS" => values}), do: MapSet.new(values)

  def decode(%{"NS" => values}) do
    values
    |> Stream.map(&binary_to_number/1)
    |> Enum.into(MapSet.new())
  end

  def decode(%{"L" => values}) do
    Enum.map(values, &decode/1)
  end

  def decode(%{"N" => value}) when is_binary(value), do: binary_to_number(value)
  def decode(%{"N" => value}) when value |> is_integer or value |> is_float, do: value

  def decode(%{} = item) do
    item
    |> Enum.reduce(%{}, fn {k, v}, map ->
      Map.put(map, k, decode(v))
    end)
  end

  @doc "Attempts to convert a number to a float, and then an integer"
  def binary_to_number(binary) when is_binary(binary) do
    String.to_float(binary)
  rescue
    ArgumentError -> String.to_integer(binary)
  end

  def binary_to_number(binary), do: binary

  @doc "Converts a map with binary keys to the specified struct"
  def binary_map_to_struct(bmap, module) do
    module.__struct__
    |> Map.from_struct()
    |> Enum.reduce(%{}, fn {k, v}, map ->
      Map.put(map, k, Map.get(bmap, Atom.to_string(k), v))
    end)
    |> Map.put(:__struct__, module)
  end
end