Current section
Files
Jump to
Current section
Files
lib/condiment.ex
defmodule Condiment do
@moduledoc """
Add flavors to your context APIs easily!
No need to create different functions to cater to different use cases, instead you can have one single public function and add flavors conditionally, with Condiment.
"""
@type t :: %__MODULE__{
token: any(),
opts: list(),
resolvers: list(),
condiment_opts: list()
}
defstruct [:token, :opts, :resolvers, :condiment_opts]
defmodule NotImplementedError do
defexception [:message]
end
defimpl Inspect do
import Inspect.Algebra
def inspect(condiment, _opts) do
data = condiment |> Map.from_struct() |> Enum.into([])
concat(["#Condiment<", inspect(data), ">"])
end
end
@doc """
To use `Condiment`, you start with the `Condiment.new/2,3` interface.
The currently available options for `condiment_opts` is:
- `:on_unknown_fields
one of `:nothing`, `:error`, or `:raise` (default). This option specify what to do when user supplies a field that's not resolvable.
"""
@spec new(any, list(), list()) :: Condiment.t()
def new(token, opts, condiment_opts \\ []) do
%__MODULE__{
token: token,
opts: opts,
resolvers: [],
condiment_opts: condiment_opts
}
end
@doc """
`field` is what you allow users to query for. The resolver is how to resolve that query.
The resolver has to be 2-arity, the first argument is the the result of the previously ran resolver (the first resolver gets `token` instead).
"""
@spec add(Condiment.t(), atom, (any, map() -> any)) :: Condiment.t()
def add(%__MODULE__{} = condiment, key, resolver)
when is_function(resolver, 2) and is_atom(key) do
%{condiment | resolvers: [{key, resolver} | condiment.resolvers]}
end
@doc """
Runs all of the resolvers conditionally based on what user requested, it runs in the order that you defined (not the order the user supplied).
"""
@spec run(Condiment.t()) :: any
def run(%__MODULE__{
token: token,
opts: opts,
resolvers: resolvers,
condiment_opts: condiment_opts
}) do
resolvable_fields = Keyword.keys(resolvers)
requested_fields = Keyword.keys(opts)
validation = validate_opts(opts, resolvers)
unknown_fields_strategy = Keyword.get(condiment_opts, :on_unknown_fields, :raise)
case {validation, unknown_fields_strategy} do
{{:error, field}, :error} ->
{:error, build_message(field, resolvable_fields)}
{{:error, field}, :raise} ->
raise NotImplementedError, build_message(field, resolvable_fields)
_ ->
map_opts = opts |> Enum.into(%{})
resolvers
|> Enum.reverse()
|> Enum.filter(fn {field, _resolver} ->
field in requested_fields
end)
|> Enum.reduce(token, fn {_field, resolver}, acc ->
resolver.(acc, map_opts)
end)
end
end
defp validate_opts(opts, resolvers) do
resolvable_fields = Keyword.keys(resolvers)
requested_fields = Keyword.keys(opts)
requested_fields
|> Enum.reduce_while(:ok, fn f, _acc ->
case f in resolvable_fields do
true ->
{:cont, :ok}
false ->
{:halt, {:error, f}}
end
end)
end
defp build_message(field, resolvable_fields) do
"Don't know how to resolve #{inspect(field)}. You can add to #{__MODULE__} with `#{__MODULE__}.add/3`. Current known resolvables: #{
inspect(resolvable_fields)
}"
end
end