lib/aliyun_oss/client/response.ex

defmodule Aliyun.Oss.Client.Response do
  defstruct [:data, :headers]

  @value_casting_rules %{
    "Prefix" => :string,
    "Marker" => :string,
    "IsTruncated" => :boolean,
    "MaxKeys" => :integer,
    "KeyCount" => :integer,
    "Delimiter" => :string,
    "RuleNumber" => :integer,
    "HttpErrorCodeReturnedEquals" => :integer,
    "PassQueryString" => :boolean,
    "MirrorPassQueryString" => :boolean,
    "MirrorFollowRedirect" => :boolean,
    "MirrorCheckMd5" => :boolean,
    "PassAll" => :boolean,
    "HttpRedirectCode" => :integer
  }

  def parse(body, headers \\ []) do
    %__MODULE__{
      data: parse_body(body, parse_content_type(headers)),
      headers: headers
    }
  end

  defp parse_content_type(headers) do
    headers
    |> Enum.find(fn {key, _value} -> String.downcase(key) == "content-type" end)
    |> case do
      {_, "application/json"} -> :json
      {_, "application/xml"} -> :xml
      _ -> nil
    end
  end

  defp parse_body(body, :xml), do: body |> XmlToMap.naive_map() |> cast_data()
  defp parse_body(body, :json), do: body |> Jason.decode!() |> cast_data()
  defp parse_body(body, _), do: body

  defp cast_data(map) do
    map
    |> Enum.reduce(map, fn
      {key, inner_map = %{}}, new_map when map_size(inner_map) > 0 ->
        case matched_rules(@value_casting_rules, Map.keys(inner_map)) do
          empty = %{} when map_size(empty) == 0 -> %{new_map | key => cast_data(inner_map)}
          # NOTE: assume only need to deal with the first level where the target keys appear
          rules -> %{new_map | key => cast_map_values(inner_map, rules)}
        end

      {key, value}, new_map ->
        %{new_map | key => cast_value(value, Map.get(@value_casting_rules, key))}
    end)
  end

  defp matched_rules(rules, keys) do
    rules
    |> Stream.filter(fn {k, _} -> k in keys end)
    |> Enum.into(%{})
  end

  defp cast_map_values(map = %{}, rules) do
    map
    |> Enum.reduce(map, fn {key, value}, new_map ->
      %{new_map | key => cast_value(value, Map.get(rules, key))}
    end)
  end

  defp cast_value("true", :boolean), do: true
  defp cast_value(_, :boolean), do: false
  defp cast_value(m = %{}, :map), do: m
  defp cast_value(m = %{}, _) when map_size(m) == 0, do: nil

  defp cast_value(value, :float) do
    case Float.parse(value) do
      {n, _} -> n
      _ -> nil
    end
  end

  defp cast_value(value, :integer) do
    case Integer.parse(value) do
      {n, _} -> n
      _ -> nil
    end
  end

  defp cast_value(value, _), do: value
end