Packages

Parser for nested params from Json Api document with `included` object.

Current section

Files

Jump to
ja_nested_params lib ja_nested_params.ex
Raw

lib/ja_nested_params.ex

defmodule JaNestedParams do
@moduledoc """
Parse `Json Api v1.0` parameters. Additionally, deviate from the specification
by allowing the building of nested parameters, just like those consumed by
Ecto's changeset at initialization.
Make use of the `included` array of the Json Api specification. This does not
conform to spec beacuse, as of v1.0, it does not expect compound documents
traveling from client side back to server.
More over, leverage the `meta` object, within `resource linkage` and `resource
object`, to allow the inclusion of new resources in the `included` array.
These new resources won't have an `id`, but still need to be distinguishable
among other new resources of the same type.
The `meta` object is expected to contain a member called `new-resource-tag`
whenever an object doesn't have an id yet. The value of `new-resource-tag`
must be unique within a group of new resources of the same type.
If the primary data is not an array, but a single resource, no `id` member is
needed if said resource is new, as provided by the specification. Nor anything
needs to be said about it in the `meta` object.
"""
import Enum, only: [with_index: 1]
@doc """
Accept an Json Api-ish document that may have an `included` array, which may
also contain new resources that conform to the clauses explained above.
"""
def to_attributes(%{"data" => data, "included" => included}) when is_list(included) do
as_attributes(data, included)
end
def to_attributes(%{"data" => data, "included" => _}), do: as_attributes(data, [])
def to_attributes(%{"data" => data}), do: as_attributes(data, [])
def to_attributes(data) when is_map(data), do: as_attributes(data, [])
defp primary_linkage_key(%{"type" => _, "id" => _} = resource) do
resource
|> Map.take(["type", "id"])
|> linkage_key()
end
defp primary_linkage_key(%{"type" => type} ), do: {type,"",1}
defp linkage_key(%{"meta" => %{"new-resource-tag" => newTag },
"type" => type} = resource) when not is_nil(newTag) do
case resource do
%{"id" => nil} -> nil
%{"id" => ""} -> nil
%{"id" => id} -> {type,id,0}
_ -> {type,"",newTag}
end
end
defp linkage_key(%{"type" => _, "id" => nil}), do: nil
defp linkage_key(%{"type" => type, "id" => id}), do: {type,id,0}
defp linkage_key(_), do: nil
defp add_id_member(struct, %{"id" => id}), do: Map.put_new(struct, "id", id)
defp add_id_member(struct, _), do: struct
defp as_attributes(data, included) do
id_key = primary_linkage_key data
includedSet = Map.new included, &({linkage_key(&1), &1})
{ object , _ } = repeat id_key, data , MapSet.new(), includedSet
object
end
defp follow_many(name,linkages,accum,included) do
{ branches, idList, newAccum } =
Enum.reduce linkages, {[], [], accum}, fn(identifier, {many,ids,acc})->
with {_, keyId, _} = newKey <- linkage_key(identifier)
do
case included do
%{^newKey => something } ->
{newBranch, oneMore} =
repeat(newKey, something, acc, included)
{ [newBranch | many] ,
(if keyId == "", do: ids, else: [keyId | ids]) ,
oneMore
}
_ ->
{ many,
(if keyId == "", do: ids, else: [keyId | ids]) ,
MapSet.put(acc, newKey)
}
end
else
_ -> { many, ids, acc }
end
end
{ [ {name, Map.new(for {a, b} <- with_index(branches), do: {b, a})},
{"#{name}_ids", idList}
] ,
newAccum
}
end
defp follow_one(name,identifier,accum,included) do
with {_, keyId, _} = newKey <- linkage_key(identifier)
do
case included do
%{^newKey => something} ->
{newBranch, oneMore} =
repeat(newKey, something, accum, included)
{ [ {name, newBranch} ] ++
(if keyId == "", do: [], else: [{"#{name}_id", keyId}]),
oneMore
}
_ ->
{ (if keyId == "", do: [], else: [{"#{name}_id", keyId}]),
MapSet.put(accum,newKey)
}
end
else
_ -> { [], accum }
end
end
defp repeat(key, branch, visited, included) do
visitedAndMe = MapSet.put(visited, key)
{ params, increasedVisits } =
case {MapSet.member?(visited, key), branch} do
{false, %{"relationships" => rels} } ->
{propertyList, newVisited} =
Enum.flat_map_reduce rels, visitedAndMe, fn
({name, %{"data" => nil}}, accum) ->
{ [ {"#{name}_id", nil} ] , accum }
({name, %{"data" => linkages}}, accum) when is_list(linkages) ->
follow_many(name, linkages, accum, included)
({name, %{"data" => identifier} }, accum) ->
follow_one(name, identifier, accum, included)
(_any, accum) ->
{[], accum}
end
{Map.new(propertyList), newVisited}
_ ->
{%{}, visitedAndMe}
end
{ Map.merge(params, branch["attributes"] || %{})
|> add_id_member(branch) ,
increasedVisits
}
end
end