Packages

A series of macros, and utilities to help hadling your elixir tonics.

Current section

Files

Jump to
rubbergloves lib handler handler.ex
Raw

lib/handler/handler.ex

defmodule Rubbergloves.Handler do
@moduledoc"""
A series of macros to determine how to authorize a specific action for a principle.
# Usage
## 1. Define what your rubber gloves can handle
```
defmodule MyApp.Gloves do
use Rubbergloves.Handler, wearer: MyApp.User
# wizzards can handle any poison
can_handle!(%MyApp.User{type: "wizzard"}, :pickup_poison, %{poison: _any})
# apprentice need to be qualified
can_handle?(%MyApp.User{type: "apprentice"} = apprentice, :pickup_poison, %{poison: poison}) do
Apprentice.is_qualified_to_handle?(apprentice, poison)
end
# Can use multipule phase checks, so if previosu phase fails we can fallback to other checks
phase :secondary_check do
can_handle?(%MyApp.User{race: "human"} = apprentice, :pickup_poison, %{poison: poison}) do
ImmunityDatabase.is_immune_to(apprentice, poison)
end
end
end
```
## 2. Check if wearer can handle
```
defmodule MyApp.SomeController do
def index(conn, params) do
user = get_principle_somehow()
with :ok <- MyApp.Gloves.handle(user, :read_secret_recipe, params. [:default, :secondary_check]) do
fetch_recipe(params["recipe_id"])
end
end
end
```
## 3. Providing Insights
```
defmodule MyApp.Gloves do
use Rubbergloves, wearer: MyApp.User
# Return boolean to provide no isights
can_handle?(user, :read_secret_recipe) do
false
end
# Optionally return {:error, reason} tuple to give better feedback
can_handle?(user, :read_secret_recipe) do
{:error, :novice_warlock}
end
end
```
"""
defmodule Match do
defstruct [:phase, :reason, :success?]
end
defmodule MatchInfo do
defstruct [:wearer, :conditions, :action, :handler, matches: []]
end
alias Rubbergloves.Handler.Match
alias Rubbergloves.Handler.MatchInfo
defmacro __using__(opts) do
module = Keyword.get(opts, :wearer)
quote do
import Rubbergloves.Handler
alias Rubbergloves.Handler.Match
alias Rubbergloves.Handler.MatchInfo
@before_compile Rubbergloves.Handler
@module unquote(module)
Module.register_attribute(__MODULE__, :phases, accumulate: true, persist: true)
@phases :default
@phase :default
end
end
@doc"""
Macro to confirm that the priniciple can handle a given action with speciic conditions
i.e. allow everyone to do anything
> can_handle!(_any_principle, _any_action, _any_conditions)
"""
defmacro can_handle!(principle, action, conditions \\ nil) do
quote do
defp handle_check(@phase, @module, unquote(principle), unquote(action), unquote(conditions)) do
:ok
end
end
end
@doc"""
Macro to check if the priniciple can handle a given action with speciic conditions.
i.e. allow everyone to do anything
> can_handle?(_any_principle, :action, _any_conditions) do
> true
> end
"""
defmacro can_handle?(principle, action, conditions \\ nil, do: block) do
quote do
defp handle_check(
@phase,
@module,
unquote(principle) = principle,
unquote(action) = action,
unquote(conditions) = conditions
) do
process_check(unquote(block), [@phase, @module, principle, action, conditions])
end
end
end
defmacro phase(name, [handle_by: handler]) do
quote do
@phase unquote(name)
@phases unquote(name)
defp handle_check(@phase, @module, principle, action, conditions) do
{_, result} = unquote(handler).handle(principle, action, conditions)
result
end
@phase :default
end
end
defmacro phase(name, do: block) do
quote do
@phase unquote(name)
@phases unquote(name)
unquote(block)
@phase :default
end
end
defmacro __before_compile__(_env) do
quote do
defp handle_check(phase, type, principle, action, conditions),
do: process_check({:error, :no_matching_handler}, [@phase, @module, principle, action, conditions])
def handle(principle, action, conditions \\ nil, phases \\ @phases) when is_map(principle) do
struct = Map.get(principle, :__struct__)
matches = phases
|> Enum.reverse()
|> Enum.reduce([], fn phase, acum ->
if(is_success(acum)) do
acum
else
[handle_check(phase, struct, principle, action, conditions)] ++ acum
end
end)
info = %MatchInfo{wearer: principle, matches: matches, handler: __MODULE__, action: action, conditions: conditions,}
if(is_success(info)) do
{:ok, info}
else
{:error, info}
end
end
def is_success(%Match{success?: success}), do: success
def is_success(matches) when is_list(matches), do: Enum.any?(matches, &is_success/1)
def is_success(%MatchInfo{matches: matches}), do: is_success(matches)
defp merge_errors(_, :ok), do: :ok
defp merge_errors(error, failed_match) do
Map.merge(error, :matches, Map.get(error, :matches) ++ failed_match)
end
defp process_check(:ok, meta), do: success(meta)
defp process_check(true, meta), do: success(meta)
defp process_check(false, meta), do: process_check({:error, :match_failed}, meta)
defp process_check({:error, reason}, [phase, handler, _principle, action, conditions]), do: %Match{phase: phase, reason: reason, success?: false}
defp success([phase, handler, _principle, action, conditions]), do: %Match{phase: phase, success?: true}
end
end
end