Skip to content

Instantly share code, notes, and snippets.

@erikreedstrom
Created September 4, 2019 13:41
Show Gist options
  • Save erikreedstrom/47574baea5f712e24ef1e5ca6a23a5ee to your computer and use it in GitHub Desktop.
Save erikreedstrom/47574baea5f712e24ef1e5ca6a23a5ee to your computer and use it in GitHub Desktop.
Assertions for validating JSON responses in Elixir.
defmodule ExUnit.Assertions.JSON do
@moduledoc """
Assertions for validating JSON responses.
This code was cribbed from [Voorhees](https://github.com/danmcclain/voorhees)
and adapted to follow the modern ExUnit `lhs == rhs` assertion syntax,
as well as simplify matching logic.
"""
import ExUnit.Assertions
@doc """
Asserts the payload matches the format of the expected keys matched in.
"""
@spec assert_json_schema(map | list | String.t(), list) :: list
def assert_json_schema(payload, expected_keys) when is_binary(payload),
do: assert_json_schema(Jason.decode!(payload), expected_keys)
def assert_json_schema(payload, expected_keys) do
expected_keys = normalize_key_list(expected_keys)
content_keys =
payload
|> Map.keys()
|> extract_subkeys(payload)
|> normalize_key_list
assert content_keys == expected_keys
content_keys
end
@doc """
Asserts the payload matches the values from expected payload.
"""
@spec assert_json_payload(map | String.t(), list | map) :: map
def assert_json_payload(payload, expected_payload) when is_binary(payload),
do: assert_json_payload(Jason.decode!(payload), expected_payload)
def assert_json_payload(payload, expected_payload) do
expected_payload = normalize_map(expected_payload)
parsed_payload = payload |> normalize_map() |> filter_out_extra_keys(expected_payload)
assert parsed_payload == expected_payload
parsed_payload
end
## PRIVATE FUNCTIONS
# REVIEW: Needs clean up
defp filter_out_extra_keys(payload, expected_payload) when is_list(payload) do
payload
|> Enum.with_index()
|> Enum.map(fn {value, index} ->
filter_out_extra_keys(value, Enum.at(expected_payload, index))
end)
end
defp filter_out_extra_keys(payload, nil) when is_map(payload), do: payload
defp filter_out_extra_keys(payload, expected_payload) when is_map(payload) do
payload
|> Enum.filter(fn {key, _value} -> expected_payload |> Map.keys() |> Enum.member?(key) end)
|> Enum.map(fn
{key, value} when is_map(value) or is_list(value) ->
{key, filter_out_extra_keys(value, expected_payload[key])}
entry ->
entry
end)
|> Enum.into(%{})
end
defp filter_out_extra_keys(payload, _expected_payload), do: payload
defp normalize_map(map) when is_map(map) do
map
|> Enum.map(&normalize_map_entry(&1))
|> Enum.into(%{})
end
defp normalize_map(list) when is_list(list), do: Enum.map(list, &normalize_map(&1))
defp normalize_map(value), do: value
defp normalize_map_entry({_key, %{__struct__: _} = value}), do: flunk("cannot normalize struct: #{inspect(value)}")
defp normalize_map_entry({key, value}) when is_map(value) or is_list(value) do
{normalize_key(key), normalize_map(value)}
end
defp normalize_map_entry({key, value}) do
{normalize_key(key), value}
end
defp normalize_key_list([]), do: []
defp normalize_key_list([{key, subkeys} | rest]) when is_list(subkeys) do
[{normalize_key(key), Enum.sort(normalize_key_list(subkeys))} | normalize_key_list(rest)]
end
defp normalize_key_list([key | rest]) do
if is_list(key) do
# Unwrap lists of keys
[normalize_key_list(key) | normalize_key_list(rest)] |> List.first()
else
[normalize_key(key) | normalize_key_list(rest)]
end
end
defp normalize_key(key) when is_binary(key), do: String.to_atom(key)
defp normalize_key(key), do: key
defp extract_subkeys([], _map), do: []
defp extract_subkeys([key | rest], map) do
case map[key] do
value when is_map(value) ->
[{key, extract_subkeys(Map.keys(value), value)} | extract_subkeys(rest, map)]
value when is_list(value) ->
[{key, subkey_arrays(value)} | extract_subkeys(rest, map)]
_ ->
[key | extract_subkeys(rest, map)]
end
end
defp subkey_arrays(value) do
value
|> Enum.map(fn
element when is_map(element) -> extract_subkeys(Map.keys(element), element)
_ -> []
end)
|> Enum.uniq()
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment