Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,56 @@ def is_authorized(body, %{args: [conn, _params]}) do
end
```

### Simple reflection for all decorated functions
A "hidden" function `__decorated_functions__/0` is added to any module that decorates functions and returns a list of
all decorated functions within that module.

This can be useful for testing purposes since you don't have to assert the decorated behaviors of the decorated
functions. So long as your decorator function is well tested, you can just assert the expected decorators are present
for the given function.

The function returns a map with the function name and arguments as the key and a list of tuples with the decorator.
This permits multiple decorators to be asserted with a single function call so that adding new decorators will cause
the assertion to fail.

For example, given the following module:
```elixir
defmodule NewModule do
use Decorator.Define, [new_decorator: 1, another_decorator: 2]

@decorate new_decorator("one")
def func(%StructOne{} = msg) do
msg
end

@decorate new_decorator("two")
@decorate another_decorator("a", "b")
@decorate new_decorator("b")
def func(%StructTwo{c: 2} = _msg) do
:ok
end
end
```

You can assert the decorated functions like so:
```elixir
test "Module with decorated functions are returned by `__decorated_functions__()" do
assert %{
{:func, ["%StructOne{} = msg"]} => [
{DecoratorTest.Fixture.NewDecorator, :new_decorator, ["one"]}
],
{:func, ["%StructTwo{c: 2} = _msg"]} => [
{DecoratorTest.Fixture.NewDecorator, :new_decorator, ["two"]},
{DecoratorTest.Fixture.AnotherDecorator, :another_decorator, ["a", "b"]},
{DecoratorTest.Fixture.NewDecorator, :new_decorator, ["b"]}
]
} == NewModule.__decorated_functions__()
end
```

Obviously, any changes to the parameters of the decorated function will cause the simple assertion to fail, but that's
intentional, as the decorator may be dependent on the parameters of the decorated function.

## Copyright and License

Copyright (c) 2016 Arjan Scherpenisse
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.4.0
1.4.1
40 changes: 40 additions & 0 deletions lib/decorator/decorate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,40 @@ defmodule Decorator.Decorate do
defstruct name: nil, arity: nil, module: nil, args: nil, kind: nil
end

defmacro add_decorated_functions(env) do
decorated = Module.get_attribute(env.module, :decorated_functions) |> Enum.reverse()
Module.delete_attribute(env.module, :decorated_functions)

funcs =
Enum.reduce(decorated, %{}, fn {func, params, decorators}, acc ->
Map.put(acc, {func, params}, decorators)
end)
|> Macro.escape()

quote do
@doc ~S"""
Returns a map of all decorated functions in the module.

For example, given the following module:
defmodule NewModule do
use Decorator.Define, [new_decorator: 1]

@decorate new_decorator("one")
def func(%StructOne{} = msg), do: msg
end

It will return:
iex> NewModule.__decorated_functions__()
%{
{:func, ["%StructOne{} = msg"]} => [
{DecoratorTest.Fixture.NewDecorator, :new_decorator, ["one"]}
]
}
"""
def __decorated_functions__(), do: unquote(funcs)
end
end

def on_definition(env, kind, fun, args, guards, body) do
decorators =
Module.get_attribute(env.module, :decorate) ++
Expand All @@ -17,6 +51,12 @@ defmodule Decorator.Decorate do
attrs = extract_attributes(env.module, body)
decorated = {kind, fun, args, guards, body, decorators, attrs}

unless Enum.empty?(decorators) do
args = Enum.map(args, fn arg -> Macro.to_string(arg) end)
info = {fun, args, decorators |> Enum.reverse()}
Module.put_attribute(env.module, :decorated_functions, info)
end

Module.put_attribute(env.module, :decorated, decorated)
Module.delete_attribute(env.module, :decorate)
end
Expand Down
2 changes: 2 additions & 0 deletions lib/decorator/define.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ defmodule Decorator.Define do
Module.register_attribute(__MODULE__, :decorate_all, accumulate: true)
Module.register_attribute(__MODULE__, :decorate, accumulate: true)
Module.register_attribute(__MODULE__, :decorated, accumulate: true)
Module.register_attribute(__MODULE__, :decorated_functions, accumulate: true)

@on_definition {Decorator.Decorate, :on_definition}
@before_compile {Decorator.Decorate, :before_compile}
@before_compile {Decorator.Decorate, :add_decorated_functions}
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Decorator.Mixfile do
use Mix.Project

@source_url "https://github.com/arjan/decorator"
@version File.read!("VERSION")
@version File.read!("VERSION") |> String.trim()

def project do
[
Expand Down
45 changes: 45 additions & 0 deletions test/decorated_functions_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule DecoratorTest.Fixture.NewDecorator do
use Decorator.Define, new_decorator: 1

def new_decorator(_arg, body, _context), do: body
end

defmodule DecoratorTest.Fixture.AnotherDecorator do
use Decorator.Define, another_decorator: 2

def another_decorator(_arg1, _arg2, body, _context), do: body
end

defmodule StructOne, do: defstruct([:a, :b])
defmodule StructTwo, do: defstruct([:c, :d])

defmodule DecoratorTest.Fixture.NewModule do
use DecoratorTest.Fixture.NewDecorator
use DecoratorTest.Fixture.AnotherDecorator

@decorate new_decorator("one")
def func(%StructOne{} = msg), do: msg.a

@decorate new_decorator("two")
@decorate another_decorator("a", "b")
@decorate new_decorator("b")
def func(%StructTwo{c: 2} = _msg), do: :ok
end

defmodule DecoratorTest.MyTest do
use ExUnit.Case
alias DecoratorTest.Fixture.NewModule

test "Module with decorated functions are returned by `__decorated_functions__()" do
assert %{
{:func, ["%StructOne{} = msg"]} => [
{DecoratorTest.Fixture.NewDecorator, :new_decorator, ["one"]}
],
{:func, ["%StructTwo{c: 2} = _msg"]} => [
{DecoratorTest.Fixture.NewDecorator, :new_decorator, ["two"]},
{DecoratorTest.Fixture.AnotherDecorator, :another_decorator, ["a", "b"]},
{DecoratorTest.Fixture.NewDecorator, :new_decorator, ["b"]}
]
} == NewModule.__decorated_functions__()
end
end