├── .formatter.exs ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── VERSION ├── lib └── decorator │ ├── decorate.ex │ └── define.ex ├── mix.exs ├── mix.lock └── test ├── basic_test.exs ├── context_test.exs ├── decorate_all_test.exs ├── decorate_exunit_test.exs ├── default_argument_test.exs ├── defdelegate_test.exs ├── function_decorator_test.exs ├── multiple_clauses_test.exs ├── multiple_decorator_modules_used_in_one_module.exs ├── precondition_test.exs ├── test_helper.exs ├── try_wrapper_test.exs └── two_argument_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | name: 4 | test 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} 10 | env: 11 | MIX_ENV: test 12 | strategy: 13 | matrix: 14 | otp: ['21.0', '23.0'] 15 | elixir: ['1.7', '1.11'] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: erlef/setup-elixir@v1 19 | with: 20 | otp-version: ${{matrix.otp}} 21 | elixir-version: ${{matrix.elixir}} 22 | - run: mix deps.get 23 | - run: mix compile --warnings-as-errors 24 | - run: mix test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | decorator-*.tar 24 | 25 | # Temporary files for e.g. tests. 26 | /tmp 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Arjan Scherpenisse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir function decorators 2 | 3 | [![Build Status](https://github.com/arjan/decorator/workflows/test/badge.svg)](https://github.com/arjan/decorator) 4 | [![Module Version](https://img.shields.io/hexpm/v/decorator.svg)](https://hex.pm/packages/decorator) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/decorator/) 6 | [![Total Download](https://img.shields.io/hexpm/dt/decorator.svg)](https://hex.pm/packages/decorator) 7 | [![License](https://img.shields.io/hexpm/l/decorator.svg)](https://github.com/arjan/decorator/blob/master/LICENSE) 8 | [![Last Updated](https://img.shields.io/github/last-commit/arjan/decorator.svg)](https://github.com/arjan/decorator/commits/master) 9 | 10 | A function decorator is a "`@decorate`" annotation that is put just 11 | before a function definition. It can be used to add extra 12 | functionality to Elixir functions. The runtime overhead of a function 13 | decorator is zero, as it is executed on compile time. 14 | 15 | Examples of function decorators include: loggers, instrumentation 16 | (timing), precondition checks, et cetera. 17 | 18 | 19 | ## Some remarks in advance 20 | 21 | Some people think function decorators are a bad idea, as they can 22 | perform magic stuff on your functions (side effects!). Personally, I 23 | think they are just another form of metaprogramming, one of Elixir's 24 | selling points. But use decorators wisely, and always study the 25 | decorator code itself, so you know what it is doing. 26 | 27 | Decorators are always marked with the `@decorate` literal, so that 28 | it's clear in the code that decorators are being used. 29 | 30 | 31 | ## Installation 32 | 33 | Add `:decorator` to your list of dependencies in `mix.exs`: 34 | 35 | ```elixir 36 | def deps do 37 | [ 38 | {:decorator, "~> 1.2"} 39 | ] 40 | end 41 | ``` 42 | 43 | You can now define your function decorators. 44 | 45 | ## Usage 46 | 47 | Function decorators are macros which you put just before defining a 48 | function. It looks like this: 49 | 50 | ```elixir 51 | defmodule MyModule do 52 | use PrintDecorator 53 | 54 | @decorate print() 55 | def square(a) do 56 | a * a 57 | end 58 | end 59 | ``` 60 | 61 | Now whenever you call `MyModule.square()`, you'll see the message: `Function 62 | called: square` in the console. 63 | 64 | Defining the decorator is pretty easy. Create a module in which you 65 | *use* the `Decorator.Define` module, passing in the decorator name and 66 | arity, or more than one if you want. 67 | 68 | The following declares the above `@print` decorator which prints a 69 | message every time the decorated function is called: 70 | 71 | ```elixir 72 | defmodule PrintDecorator do 73 | use Decorator.Define, [print: 0] 74 | 75 | def print(body, context) do 76 | quote do 77 | IO.puts("Function called: " <> Atom.to_string(unquote(context.name))) 78 | unquote(body) 79 | end 80 | end 81 | 82 | end 83 | ``` 84 | 85 | The arguments to the decorator function (the `def print(...)`) are the 86 | function's body (the abstract syntax tree (AST)), as well as a `context` 87 | argument which holds information like the function's name, defining module, 88 | arity and the arguments AST. 89 | 90 | 91 | ### Compile-time arguments 92 | 93 | Decorators can have compile-time arguments passed into the decorator 94 | macros. 95 | 96 | For instance, you could let the print function only print when a 97 | certain logging level has been set: 98 | 99 | ```elixir 100 | @decorate print(:debug) 101 | def foo() do 102 | ... 103 | ``` 104 | 105 | In this case, you specify the arity 1 for the decorator: 106 | 107 | ```elixir 108 | defmodule PrintDecorator do 109 | use Decorator.Define, [print: 1] 110 | ``` 111 | 112 | And then your `print/3` decorator function gets the level passed in as 113 | the first argument: 114 | 115 | ```elixir 116 | def print(level, body, context) do 117 | # ... 118 | end 119 | ``` 120 | 121 | ### Decorating all functions in a module 122 | 123 | A shortcut to decorate all functions in a module is to use the `@decorate_all` attribute, as shown below. It is 124 | important to note that the `@decorate_all` attribute only 125 | affects the function clauses below its definition. 126 | 127 | ```elixir 128 | defmodule MyApp.APIController 129 | use MyBackend.LoggerDecorator 130 | 131 | @decorate_all log_request() 132 | 133 | def index(_conn, params) do 134 | # ... 135 | end 136 | 137 | def detail(_conn, params) do 138 | # ... 139 | end 140 | ``` 141 | 142 | In this example, the `log_request()` decorator is applied to both 143 | `index/2` and `detail/2`. 144 | 145 | 146 | ### Functions with multiple clauses 147 | 148 | If you have a function with multiple clauses, and only decorate one 149 | clause, you will notice that you get compiler warnings about unused 150 | variables and other things. For functions with multiple clauses the 151 | general advice is this: You should create an empty function head, and 152 | call the decorator on that head, like this: 153 | 154 | ```elixir 155 | defmodule DecoratorFunctionHead do 156 | use DecoratorFunctionHead.PrintDecorator 157 | 158 | @decorate print() 159 | def hello(first_name, last_name \\ nil) 160 | 161 | def hello(:world, _last_name) do 162 | :world 163 | end 164 | 165 | def hello(first_name, last_name) do 166 | "Hello #{first_name} #{last_name}" 167 | end 168 | end 169 | ``` 170 | 171 | 172 | ### Decorator context 173 | 174 | Besides the function body AST, the decorator function also gets a 175 | *context* argument passed in. This context holds information about the 176 | function being decorated, namely its module, function name, arity, function 177 | kind, and arguments as a list of AST nodes. 178 | 179 | The print decorator can print its function name like this: 180 | 181 | ```elixir 182 | def print(body, context) do 183 | Logger.debug("Function #{context.name}/#{context.arity} with kind #{context.kind} called in module #{context.module}!") 184 | end 185 | ``` 186 | 187 | Even more advanced, you can use the function arguments in the 188 | decorator. To create an `is_authorized` decorator which performs some 189 | checks on the Phoenix %Conn{} structure, you can create a decorator 190 | function like this: 191 | 192 | ```elixir 193 | def is_authorized(body, %{args: [conn, _params]}) do 194 | quote do 195 | if unquote(conn).assigns.user do 196 | unquote(body) 197 | else 198 | unquote(conn) 199 | |> send_resp(401, "unauthorized") 200 | |> halt() 201 | end 202 | end 203 | end 204 | ``` 205 | 206 | ## Copyright and License 207 | 208 | Copyright (c) 2016 Arjan Scherpenisse 209 | 210 | This library is MIT licensed. See the 211 | [LICENSE](https://github.com/arjan/decorator/blob/master/LICENSE) for details. 212 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.4.0 -------------------------------------------------------------------------------- /lib/decorator/decorate.ex: -------------------------------------------------------------------------------- 1 | defmodule Decorator.Decorate do 2 | @moduledoc false 3 | 4 | defmodule Context do 5 | @moduledoc """ 6 | Struct with information about the function that is being decorated. 7 | """ 8 | 9 | defstruct name: nil, arity: nil, module: nil, args: nil, kind: nil 10 | end 11 | 12 | def on_definition(env, kind, fun, args, guards, body) do 13 | decorators = 14 | Module.get_attribute(env.module, :decorate) ++ 15 | Module.get_attribute(env.module, :decorate_all) 16 | 17 | attrs = extract_attributes(env.module, body) 18 | decorated = {kind, fun, args, guards, body, decorators, attrs} 19 | 20 | Module.put_attribute(env.module, :decorated, decorated) 21 | Module.delete_attribute(env.module, :decorate) 22 | end 23 | 24 | defp extract_attributes(module, body) do 25 | Macro.postwalk(body, %{}, fn 26 | {:@, _, [{attr, _, nil}]} = n, attrs -> 27 | attrs = Map.put(attrs, attr, Module.get_attribute(module, attr)) 28 | {n, attrs} 29 | 30 | n, acc -> 31 | {n, acc} 32 | end) 33 | |> elem(1) 34 | end 35 | 36 | defmacro before_compile(env) do 37 | decorated = Module.get_attribute(env.module, :decorated) |> Enum.reverse() 38 | Module.delete_attribute(env.module, :decorated) 39 | 40 | decorated_functions = decorated_functions(decorated) 41 | 42 | decorated 43 | |> filter_undecorated(decorated_functions) 44 | |> reject_empty_clauses() 45 | |> Enum.reduce({[], []}, fn d, acc -> 46 | decorate(env, d, decorated_functions, acc) 47 | end) 48 | |> elem(1) 49 | |> Enum.reverse() 50 | end 51 | 52 | defp decorated_functions(all) do 53 | Enum.group_by( 54 | all, 55 | fn {_kind, fun, args, _guard, _body, _decorators, _attrs} -> 56 | {fun, Enum.count(args)} 57 | end, 58 | fn {_kind, _fun, _args, _guard, _body, decorators, _attrs} -> 59 | decorators 60 | end 61 | ) 62 | |> Enum.filter(fn {_k, decorators_list} -> 63 | List.flatten(decorators_list) != [] 64 | end) 65 | |> Enum.into(%{}) 66 | end 67 | 68 | # Remove all defs which are not decorated -- these doesn't need to be redefined. 69 | defp filter_undecorated(all, decorated_functions) do 70 | all 71 | |> Enum.filter(fn {_kind, fun, args, _guard, _body, _decorators, _attrs} -> 72 | Map.has_key?(decorated_functions, {fun, Enum.count(args)}) 73 | end) 74 | end 75 | 76 | defp reject_empty_clauses(all) do 77 | Enum.reject(all, fn {_kind, _fun, _args, _guards, body, _decorators, _attrs} -> 78 | body == nil 79 | end) 80 | end 81 | 82 | defp implied_arities(args) do 83 | arity = Enum.count(args) 84 | 85 | default_count = 86 | args 87 | |> Enum.filter(fn 88 | {:\\, _, _} -> true 89 | _ -> false 90 | end) 91 | |> Enum.count() 92 | 93 | :lists.seq(arity, arity - default_count, -1) 94 | end 95 | 96 | defp decorate( 97 | env, 98 | {kind, fun, args, guard, body, decorators, attrs}, 99 | decorated_functions, 100 | {prev_funs, all} 101 | ) do 102 | override_clause = 103 | implied_arities(args) 104 | |> Enum.map( 105 | "e do 106 | defoverridable [{unquote(fun), unquote(&1)}] 107 | end 108 | ) 109 | 110 | attrs = 111 | attrs 112 | |> Enum.map(fn {attr, value} -> 113 | {:@, [], [{attr, [], [Macro.escape(value)]}]} 114 | end) 115 | 116 | arity = Enum.count(args || []) 117 | context = %Context{name: fun, arity: arity, args: args, module: env.module, kind: kind} 118 | 119 | applicable_decorators = 120 | case decorators do 121 | [] -> Map.get(decorated_functions, {fun, arity}) |> hd() 122 | _ -> decorators 123 | end 124 | 125 | body = 126 | applicable_decorators 127 | |> Enum.reverse() 128 | |> Enum.reduce(body, fn decorator, body -> 129 | apply_decorator(context, decorator, body) 130 | end) 131 | |> ensure_do() 132 | 133 | def_clause = 134 | case guard do 135 | [] -> 136 | quote do 137 | Kernel.unquote(kind)(unquote(fun)(unquote_splicing(args)), unquote(body)) 138 | end 139 | 140 | _ -> 141 | quote do 142 | Kernel.unquote(kind)( 143 | unquote(fun)(unquote_splicing(args)) when unquote_splicing(guard), 144 | unquote(body) 145 | ) 146 | end 147 | end 148 | 149 | fun_and_arity = {fun, arity} 150 | 151 | if not Enum.member?(prev_funs, fun_and_arity) do 152 | {[fun_and_arity | prev_funs], [def_clause, override_clause] ++ attrs ++ all} 153 | else 154 | {prev_funs, [def_clause] ++ attrs ++ all} 155 | end 156 | end 157 | 158 | defp ensure_do([{:do, _} | _] = body), do: body 159 | defp ensure_do(body), do: [do: body] 160 | 161 | # a do-block will automatically be put in a `try do` by Elixir when one of the keywords 162 | # `rescue`, `catch` or `after` is present. hence `try_clauses`. 163 | defp apply_decorator(context, mfa, [{:do, body} | try_clauses]) do 164 | [do: apply_decorator(context, mfa, body)] ++ 165 | apply_decorator_try_clauses(context, mfa, try_clauses) 166 | end 167 | 168 | defp apply_decorator(context, {module, fun, args}, body) do 169 | if Enum.member?(module.__info__(:functions), {fun, Enum.count(args) + 2}) do 170 | Kernel.apply(module, fun, (args || []) ++ [body, context]) 171 | else 172 | raise ArgumentError, "Unknown decorator function: #{fun}/#{Enum.count(args)}" 173 | end 174 | end 175 | 176 | defp apply_decorator(_context, decorator, _body) do 177 | raise ArgumentError, "Invalid decorator: #{inspect(decorator)}" 178 | end 179 | 180 | defp apply_decorator_try_clauses(_, _, []), do: [] 181 | 182 | defp apply_decorator_try_clauses(context, mfa, [{:after, after_block} | try_clauses]) do 183 | [after: apply_decorator(context, mfa, after_block)] ++ 184 | apply_decorator_try_clauses(context, mfa, try_clauses) 185 | end 186 | 187 | defp apply_decorator_try_clauses(context, mfa, [{try_clause, try_match_block} | try_clauses]) 188 | when try_clause in [:rescue, :catch] do 189 | [{try_clause, apply_decorator_to_try_clause_block(context, mfa, try_match_block)}] ++ 190 | apply_decorator_try_clauses(context, mfa, try_clauses) 191 | end 192 | 193 | defp apply_decorator_to_try_clause_block(context, mfa, try_match_block) do 194 | try_match_block 195 | |> Enum.map(fn {:->, meta, [match, body]} -> 196 | {:->, meta, [match, apply_decorator(context, mfa, body)]} 197 | end) 198 | end 199 | 200 | def generate_args(0, _caller), do: [] 201 | def generate_args(n, caller), do: for(i <- 1..n, do: Macro.var(:"var#{i}", caller)) 202 | end 203 | -------------------------------------------------------------------------------- /lib/decorator/define.ex: -------------------------------------------------------------------------------- 1 | defmodule Decorator.Define do 2 | @moduledoc false 3 | 4 | defmacro __using__(decorators) do 5 | decorator_module = __CALLER__.module 6 | 7 | decorator_macros = 8 | for {decorator, arity} <- decorators do 9 | arglist = Decorator.Decorate.generate_args(arity, decorator_module) 10 | 11 | quote do 12 | defmacro unquote(decorator)(unquote_splicing(arglist)) do 13 | if Module.get_attribute(__CALLER__.module, :decorate) != nil do 14 | raise ArgumentError, "Decorator #{unquote(decorator)} used without @decorate" 15 | end 16 | 17 | Macro.escape({unquote(decorator_module), unquote(decorator), unquote(arglist)}) 18 | end 19 | end 20 | end 21 | 22 | quote do 23 | @decorator_module unquote(decorator_module) 24 | @decorator_defs unquote(decorators) 25 | 26 | unquote_splicing(decorator_macros) 27 | 28 | defmacro __using__(_) do 29 | imports = 30 | for {decorator, arity} <- @decorator_defs do 31 | quote do 32 | {unquote(decorator), unquote(arity)} 33 | end 34 | end 35 | 36 | quote do 37 | import unquote(@decorator_module), only: unquote(imports) 38 | 39 | if is_nil(Module.get_attribute(__MODULE__, :has_been_decorated)) do 40 | Module.register_attribute(__MODULE__, :has_been_decorated, accumulate: false) 41 | Module.put_attribute(__MODULE__, :has_been_decorated, true) 42 | 43 | Module.register_attribute(__MODULE__, :decorate_all, accumulate: true) 44 | Module.register_attribute(__MODULE__, :decorate, accumulate: true) 45 | Module.register_attribute(__MODULE__, :decorated, accumulate: true) 46 | 47 | @on_definition {Decorator.Decorate, :on_definition} 48 | @before_compile {Decorator.Decorate, :before_compile} 49 | end 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Decorator.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/arjan/decorator" 5 | @version File.read!("VERSION") 6 | 7 | def project do 8 | [ 9 | app: :decorator, 10 | version: @version, 11 | elixir: "~> 1.5", 12 | elixirc_options: elixirrc_options(Mix.env()), 13 | build_embedded: Mix.env() == :prod, 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps(), 16 | docs: docs(), 17 | package: package() 18 | ] 19 | end 20 | 21 | def application do 22 | [ 23 | extra_applications: [:logger] 24 | ] 25 | end 26 | 27 | defp elixirrc_options(:test), do: [] 28 | defp elixirrc_options(_), do: [warnings_as_errors: true] 29 | 30 | defp deps do 31 | [ 32 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 33 | ] 34 | end 35 | 36 | defp docs do 37 | [ 38 | extras: ["README.md"], 39 | main: "readme", 40 | formatter_opts: [gfm: true], 41 | homepage_url: @source_url, 42 | source_url: @source_url, 43 | api_reference: false 44 | ] 45 | end 46 | 47 | defp package do 48 | [ 49 | description: description(), 50 | files: ["lib", "mix.exs", "*.md", "LICENSE", "VERSION"], 51 | maintainers: ["Arjan Scherpenisse"], 52 | licenses: ["MIT"], 53 | links: %{"GitHub" => @source_url} 54 | ] 55 | end 56 | 57 | defp description do 58 | "Function decorators for Elixir" 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, 4 | "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, 5 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/basic_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DecoratorTest.Fixture.MyDecorator do 2 | use Decorator.Define, some_decorator: 0 3 | 4 | def some_decorator(body, _context) do 5 | body 6 | end 7 | end 8 | 9 | defmodule DecoratorTest.Fixture.MyModule do 10 | use DecoratorTest.Fixture.MyDecorator 11 | 12 | @decorate some_decorator() 13 | def square(a) do 14 | a * a 15 | end 16 | 17 | @decorate some_decorator() 18 | def answer, do: 24 19 | 20 | @value 123 21 | @decorate some_decorator() 22 | def value123, do: @value 23 | 24 | @value 666 25 | @decorate some_decorator() 26 | def value666 do 27 | {:ok, trunc(2 * @value * 0.5)} 28 | end 29 | end 30 | 31 | defmodule DecoratorTest.Basic do 32 | use ExUnit.Case 33 | alias DecoratorTest.Fixture.MyModule 34 | 35 | test "basic function decoration" do 36 | assert 4 == MyModule.square(2) 37 | assert 16 == MyModule.square(4) 38 | end 39 | 40 | test "decorate function with no argument list" do 41 | assert 24 == MyModule.answer() 42 | end 43 | 44 | test "normal module attributes should still work" do 45 | assert 123 == MyModule.value123() 46 | assert {:ok, 666} == MyModule.value666() 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/context_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DecoratorTest.Fixture.ContextDecorator do 2 | use Decorator.Define, expose: 0 3 | alias Decorator.Decorate.Context 4 | 5 | def expose(body, %Context{arity: arity, module: module, name: name, kind: kind}) do 6 | quote do 7 | {%{ 8 | arity: unquote(arity), 9 | module: unquote(module), 10 | name: unquote(name), 11 | kind: unquote(kind) 12 | }, unquote(body)} 13 | end 14 | end 15 | end 16 | 17 | defmodule DecoratorTest.Fixture.ContextModule do 18 | use DecoratorTest.Fixture.ContextDecorator 19 | 20 | @decorate expose() 21 | def result(a) do 22 | a 23 | end 24 | 25 | def public_sum(a, b) do 26 | private_sum(a, b) 27 | end 28 | 29 | @decorate expose() 30 | defp private_sum(a, b) do 31 | a + b 32 | end 33 | end 34 | 35 | defmodule DecoratorTest.Context do 36 | use ExUnit.Case, async: true 37 | alias DecoratorTest.Fixture.ContextModule 38 | 39 | test "exposes public function context" do 40 | assert {context, 3} = ContextModule.result(3) 41 | assert context == %{arity: 1, kind: :def, module: ContextModule, name: :result} 42 | end 43 | 44 | test "exposes private function context" do 45 | assert {context, 3} = ContextModule.public_sum(1, 2) 46 | assert context == %{arity: 2, kind: :defp, module: ContextModule, name: :private_sum} 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/decorate_all_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DecoratorDecorateAllTest.Fixture.MyDecorator do 2 | use Decorator.Define, some_decorator: 0 3 | 4 | def some_decorator(body, _context) do 5 | {:ok, body} 6 | end 7 | end 8 | 9 | defmodule DecoratorDecorateAllTest.Fixture.MyModule do 10 | use DecoratorDecorateAllTest.Fixture.MyDecorator 11 | 12 | @decorate_all some_decorator() 13 | 14 | def square(a) do 15 | a * a 16 | end 17 | 18 | def answer, do: 24 19 | 20 | def value123, do: 123 21 | 22 | def value666, do: 666 23 | 24 | def empty_body(a) 25 | 26 | def empty_body(10), do: 11 27 | def empty_body(n), do: n + 2 28 | end 29 | 30 | defmodule DecoratorDecorateAllTest.Fixture.MyModuleWithAttribute do 31 | use DecoratorDecorateAllTest.Fixture.MyDecorator 32 | 33 | @decorate_all some_decorator() 34 | 35 | @custom_attr 15 36 | @custom_attr_map %{some_val: 3, other_val: 10} 37 | 38 | def fun1(x), do: x + 2 39 | 40 | def fun2(x), do: x + @custom_attr 41 | 42 | def fun3(x), do: x + @custom_attr_map[:some_val] 43 | end 44 | 45 | defmodule DecoratorDecorateAllTest.Fixture.MyModuleWithSeparatedClauses do 46 | use DecoratorDecorateAllTest.Fixture.MyDecorator 47 | 48 | @decorate_all some_decorator() 49 | 50 | def fun1(0), do: :zero 51 | 52 | def fun2(x), do: x + 2 53 | 54 | def fun1(x), do: x 55 | end 56 | 57 | defmodule DecoratorDecorateAllTest do 58 | use ExUnit.Case 59 | alias DecoratorDecorateAllTest.Fixture.MyModule 60 | alias DecoratorDecorateAllTest.Fixture.MyModuleWithAttribute 61 | alias DecoratorDecorateAllTest.Fixture.MyModuleWithSeparatedClauses 62 | 63 | test "decorate_all" do 64 | assert {:ok, 4} == MyModule.square(2) 65 | assert {:ok, 16} == MyModule.square(4) 66 | assert {:ok, 24} == MyModule.answer() 67 | assert {:ok, 123} == MyModule.value123() 68 | assert {:ok, 666} == MyModule.value666() 69 | assert {:ok, 11} == MyModule.empty_body(10) 70 | assert {:ok, 8} == MyModule.empty_body(6) 71 | assert {:ok, 10} == MyModuleWithAttribute.fun1(8) 72 | assert {:ok, 20} == MyModuleWithAttribute.fun2(5) 73 | assert {:ok, 8} == MyModuleWithAttribute.fun3(5) 74 | assert {:ok, :zero} == MyModuleWithSeparatedClauses.fun1(0) 75 | assert {:ok, 4} == MyModuleWithSeparatedClauses.fun2(2) 76 | assert {:ok, 2} == MyModuleWithSeparatedClauses.fun1(2) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/decorate_exunit_test.exs: -------------------------------------------------------------------------------- 1 | # A pretty meta-test that shows that decorators can be used inside 2 | # ExUnit. 3 | 4 | defmodule Decorator.TestFixture do 5 | use Decorator.Define, test_fixture: 0 6 | 7 | def test_fixture(body, _context) do 8 | quote do 9 | IO.puts("- before -") 10 | unquote(body) 11 | IO.puts("- after -") 12 | end 13 | end 14 | end 15 | 16 | defmodule DecorateExunitTests do 17 | use ExUnit.Case 18 | 19 | test "test decorating an exunit test" do 20 | # Let's compile a test module on the fly. We do this on the fly 21 | # because otherwise ExUnit runs the test case immediately. We need 22 | # to run it on-demand so we can that the decoration happened. 23 | 24 | Module.create( 25 | TheRealTestCase, 26 | quote do 27 | use ExUnit.Case 28 | use Decorator.TestFixture 29 | 30 | @decorate test_fixture() 31 | test "hello" do 32 | IO.puts("hi") 33 | end 34 | end, 35 | file: "nofile" 36 | ) 37 | 38 | # ExUnit creates a function for each test case named "test " + the test case label. 39 | # The IO is captured, as the decorator (in this case) prints some stuff. 40 | 41 | output = 42 | ExUnit.CaptureIO.capture_io(fn -> 43 | assert :ok = apply(TheRealTestCase, :"test hello", [nil]) 44 | end) 45 | 46 | # Verify that the decoration happened 47 | assert output == """ 48 | - before - 49 | hi 50 | - after - 51 | """ 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/default_argument_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DecoratorTest.Fixture.OptionalArgsTestDecorator do 2 | use Decorator.Define, test: 0 3 | 4 | def test(body, _context) do 5 | body 6 | end 7 | end 8 | 9 | defmodule DecoratorTest.Fixture.OptionalArgsTestModule do 10 | use DecoratorTest.Fixture.OptionalArgsTestDecorator 11 | 12 | @decorate test() 13 | def result(_aa, arg \\ nil) do 14 | {:ok, arg} 15 | end 16 | end 17 | 18 | defmodule DecoratorTest.DefaultArguments do 19 | use ExUnit.Case 20 | alias DecoratorTest.Fixture.OptionalArgsTestModule 21 | 22 | test "decorated function with optional args" do 23 | assert {:ok, nil} == OptionalArgsTestModule.result(1) 24 | assert {:ok, 1} == OptionalArgsTestModule.result(2, 1) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/defdelegate_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DefdelegateTests.Fixture.MyDecorator do 2 | use Decorator.Define, plus_one: 0 3 | 4 | def plus_one(body, _context) do 5 | quote do 6 | unquote(body) + 1 7 | end 8 | end 9 | end 10 | 11 | defmodule DefdelegateTests.A do 12 | def value(x) do 13 | x 14 | end 15 | end 16 | 17 | defmodule DefdelegateTests.B do 18 | use DefdelegateTests.Fixture.MyDecorator 19 | 20 | @decorate plus_one() 21 | defdelegate value(x), to: DefdelegateTests.A 22 | end 23 | 24 | defmodule DefdelegateTests do 25 | use ExUnit.Case 26 | 27 | alias DefdelegateTests.B 28 | 29 | test "defdelegate should be decorated" do 30 | assert B.value(2) == 3 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/function_decorator_test.exs: -------------------------------------------------------------------------------- 1 | # Example decorator which modifies the return value of the function 2 | # by wrapping it in a tuple. 3 | defmodule DecoratorTest.Fixture.FunctionResultDecorator do 4 | use Decorator.Define, function_result: 1 5 | 6 | def function_result(add, body, _context) do 7 | quote do 8 | {unquote(add), unquote(body)} 9 | end 10 | end 11 | end 12 | 13 | defmodule DecoratorTest.Fixture.MyFunctionResultModule do 14 | use DecoratorTest.Fixture.FunctionResultDecorator 15 | 16 | @decorate function_result(:ok) 17 | def square(a) do 18 | a * a 19 | end 20 | 21 | @decorate function_result(:error) 22 | def square_error(a) do 23 | a * a 24 | end 25 | 26 | @decorate function_result(:a) 27 | @decorate function_result("b") 28 | def square_multiple(a) do 29 | a * a 30 | end 31 | end 32 | 33 | defmodule DecoratorTest.Fixture.DecoratedFunctionClauses do 34 | use DecoratorTest.Fixture.FunctionResultDecorator 35 | 36 | @decorate function_result(:ok) 37 | def foo(n) when is_number(n), do: n 38 | 39 | @decorate function_result(:error) 40 | def foo(x), do: x 41 | end 42 | 43 | defmodule DecoratorTest.Fixture.DecoratedFunctionWithDifferentArities do 44 | use DecoratorTest.Fixture.FunctionResultDecorator 45 | 46 | @decorate function_result(:ok) 47 | def testfun(a, b) do 48 | a + b 49 | end 50 | 51 | @decorate function_result(:ok) 52 | def testfun(a) do 53 | a 54 | end 55 | end 56 | 57 | defmodule DecoratorTest.Fixture.PrivateDecorated do 58 | use DecoratorTest.Fixture.FunctionResultDecorator 59 | 60 | def pub(x), do: foo(x) 61 | 62 | @decorate function_result(:foo) 63 | defp foo(x), do: x 64 | end 65 | 66 | defmodule DecoratorTest.Fixture.DecoratedFunctionWithEmptyClause do 67 | use DecoratorTest.Fixture.FunctionResultDecorator 68 | 69 | @decorate function_result(:ok) 70 | def multiply(x, y \\ 1) 71 | 72 | def multiply(1, y) do 73 | y 74 | end 75 | 76 | def multiply(x, y) do 77 | x * y 78 | end 79 | end 80 | 81 | # Tests itself 82 | defmodule DecoratorTest.FunctionDecorator do 83 | use ExUnit.Case 84 | 85 | alias DecoratorTest.Fixture.{ 86 | MyFunctionResultModule, 87 | DecoratedFunctionClauses, 88 | DecoratedFunctionWithDifferentArities, 89 | DecoratedFunctionWithEmptyClause, 90 | FunctionResultDecorator, 91 | PrivateDecorated 92 | } 93 | 94 | test "test function decoration with argument, modify return value, multiple decorators" do 95 | assert {:ok, 4} == MyFunctionResultModule.square(2) 96 | assert {:error, 4} == MyFunctionResultModule.square_error(2) 97 | assert {"b", {:a, 4}} == MyFunctionResultModule.square_multiple(2) 98 | end 99 | 100 | test "decorating a function with many clauses" do 101 | assert {:ok, 22} == DecoratedFunctionClauses.foo(22) 102 | assert {:error, "string"} == DecoratedFunctionClauses.foo("string") 103 | end 104 | 105 | test "decorating a function with different arity heads" do 106 | assert {:ok, 3} == DecoratedFunctionWithDifferentArities.testfun(1, 2) 107 | assert {:ok, 5} == DecoratedFunctionWithDifferentArities.testfun(5) 108 | end 109 | 110 | test "decorating a function with an empty clause" do 111 | assert {:ok, 11} == DecoratedFunctionWithEmptyClause.multiply(11) 112 | assert {:ok, 24} == DecoratedFunctionWithEmptyClause.multiply(6, 4) 113 | assert {:ok, 5} == DecoratedFunctionWithEmptyClause.multiply(1, 5) 114 | end 115 | 116 | test "should throw error when defining an unknown decorator" do 117 | definition = 118 | quote do 119 | use FunctionResultDecorator 120 | 121 | @decorate nonexisting() 122 | def foo do 123 | end 124 | end 125 | 126 | try do 127 | Module.create(NonExistingDecoratorModule, definition, file: "test.ex") 128 | flunk("Should have thrown an error") 129 | rescue 130 | CompileError -> :ok 131 | ArgumentError -> :ok 132 | end 133 | end 134 | 135 | test "should throw error when defining an invalid decorator" do 136 | assert_raise ArgumentError, fn -> 137 | defmodule InvalidDecoratorModule do 138 | use FunctionResultDecorator 139 | 140 | @bar 33 141 | 142 | @decorate 1_111_111_111_111 143 | def foo do 144 | end 145 | end 146 | end 147 | end 148 | 149 | test "should throw error when using decorator macro outside @decorate" do 150 | assert_raise ArgumentError, fn -> 151 | defmodule InvalidDecoratorUseModule do 152 | use FunctionResultDecorator 153 | 154 | def foo do 155 | function_result(:bar) 156 | end 157 | end 158 | end 159 | end 160 | 161 | test "should throw error when using decorator wrong arity" do 162 | try do 163 | defmodule InvalidDecoratorArityUseModule do 164 | use FunctionResultDecorator 165 | 166 | @decorate function_result() 167 | def foo do 168 | end 169 | end 170 | 171 | flunk("Should have thrown an error") 172 | rescue 173 | CompileError -> :ok 174 | ArgumentError -> :ok 175 | end 176 | end 177 | 178 | test "private functions can be decorated" do 179 | assert {:foo, :bar} == PrivateDecorated.pub(:bar) 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /test/multiple_clauses_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DecoratorTest.Fixture.MultipleClausesTestDecorator do 2 | use Decorator.Define, test: 0 3 | 4 | def test(body, _context) do 5 | {:ok, body} 6 | end 7 | end 8 | 9 | defmodule DecoratorTest.Fixture.MultipleClausesTestModule do 10 | use DecoratorTest.Fixture.MultipleClausesTestDecorator 11 | 12 | @decorate test() 13 | def result(1) do 14 | 1 15 | end 16 | 17 | def result(2) do 18 | 2 19 | end 20 | 21 | def result(n) do 22 | n * n 23 | end 24 | end 25 | 26 | defmodule DecoratorTest.MultipleClauses do 27 | use ExUnit.Case 28 | alias DecoratorTest.Fixture.MultipleClausesTestModule 29 | 30 | test "decorated function with multiple clauses" do 31 | assert {:ok, 1} == MultipleClausesTestModule.result(1) 32 | assert {:ok, 2} == MultipleClausesTestModule.result(2) 33 | assert {:ok, 25} == MultipleClausesTestModule.result(5) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/multiple_decorator_modules_used_in_one_module.exs: -------------------------------------------------------------------------------- 1 | defmodule DecoratorTest.Fixture.MonitoringDecorator do 2 | use Decorator.Define, some_decorator: 0 3 | 4 | def some_decorator(body, _context) do 5 | {:ok, body} 6 | end 7 | end 8 | 9 | defmodule DecoratorTest.Fixture.LoggingDecorator do 10 | use Decorator.Define, test_log: 0 11 | 12 | def test_log(body, _context) do 13 | {:ok, body} 14 | end 15 | end 16 | 17 | defmodule DecoratorTest.Fixture.TwoDecoratorsUsed do 18 | use DecoratorTest.Fixture.LoggingDecorator 19 | use DecoratorTest.Fixture.MonitoringDecorator 20 | 21 | @decorate some_decorator() 22 | def result(default \\ nil) do 23 | default 24 | end 25 | end 26 | 27 | defmodule DecoratorTest.MultipleDecoratorModules do 28 | use ExUnit.Case 29 | alias DecoratorTest.Fixture.TwoDecoratorsUsed 30 | 31 | test "module compiles and is not redefined" do 32 | assert {:ok, "tested"} == TwoDecoratorsUsed.result("tested") 33 | assert {:ok, nil} == TwoDecoratorsUsed.result() 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/precondition_test.exs: -------------------------------------------------------------------------------- 1 | # Example decorator which uses one of the function arguments to 2 | # perform a precondition check. 3 | defmodule DecoratorTest.Fixture.PreconditionDecorator do 4 | use Decorator.Define, is_authorized: 0 5 | 6 | def is_authorized(body, %{args: [conn]}) do 7 | quote do 8 | if unquote(conn).assigns.user do 9 | unquote(body) 10 | else 11 | raise RuntimeError, "Not authorized!" 12 | end 13 | end 14 | end 15 | end 16 | 17 | defmodule DecoratorTest.Fixture.MyIsAuthorizedModule do 18 | use DecoratorTest.Fixture.PreconditionDecorator 19 | 20 | @decorate is_authorized() 21 | def perform(conn) do 22 | {:ok, conn} 23 | end 24 | end 25 | 26 | defmodule DecoratorTest.Precondition do 27 | use ExUnit.Case 28 | alias DecoratorTest.Fixture.MyIsAuthorizedModule 29 | 30 | test "precondition decorator" do 31 | assert {:ok, _} = MyIsAuthorizedModule.perform(%{assigns: %{user: true}}) 32 | 33 | assert_raise RuntimeError, fn -> 34 | MyIsAuthorizedModule.perform(%{assigns: %{user: false}}) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/try_wrapper_test.exs: -------------------------------------------------------------------------------- 1 | # two arguments 2 | defmodule DecoratorTest.Fixture.TryWrapperDecorator do 3 | use Decorator.Define, test: 0 4 | 5 | def test(body, _context) do 6 | {:ok, body} 7 | end 8 | end 9 | 10 | defmodule DecoratorTest.Fixture.TryWrapperTestModule do 11 | use DecoratorTest.Fixture.TryWrapperDecorator 12 | 13 | @decorate test() 14 | def rescued(a) do 15 | if a == :raise do 16 | raise RuntimeError, "text" 17 | end 18 | 19 | a 20 | rescue 21 | _ in RuntimeError -> :error 22 | end 23 | 24 | @decorate test() 25 | def catched(a) do 26 | if a == :throw do 27 | throw(a) 28 | end 29 | 30 | a 31 | catch 32 | _ -> :thrown 33 | end 34 | 35 | @decorate test() 36 | def try_after(a) do 37 | a 38 | after 39 | IO.write("after") 40 | end 41 | 42 | @decorate test() 43 | def rescued_and_catched(a) do 44 | case a do 45 | :throw -> throw(a) 46 | :raise -> raise RuntimeError, "text" 47 | a -> a 48 | end 49 | rescue 50 | _ in RuntimeError -> :error 51 | catch 52 | _ -> :thrown 53 | end 54 | end 55 | 56 | defmodule DecoratorTest.TryWrapper do 57 | use ExUnit.Case 58 | import ExUnit.CaptureIO 59 | 60 | alias DecoratorTest.Fixture.TryWrapperTestModule 61 | 62 | test "Functions which have a 'rescue' clause" do 63 | assert {:ok, 3} = TryWrapperTestModule.rescued(3) 64 | assert {:ok, :error} = TryWrapperTestModule.rescued(:raise) 65 | end 66 | 67 | test "Functions which have a 'catch' clause" do 68 | assert {:ok, 3} = TryWrapperTestModule.catched(3) 69 | assert {:ok, :thrown} = TryWrapperTestModule.catched(:throw) 70 | end 71 | 72 | test "Functions which have an 'after' clause" do 73 | assert capture_io(fn -> 74 | send(self(), TryWrapperTestModule.try_after(3)) 75 | end) == "after" 76 | 77 | assert_received {:ok, 3} 78 | end 79 | 80 | test "Functions which have a 'rescue' and a 'catch' clause" do 81 | assert {:ok, 3} = TryWrapperTestModule.rescued_and_catched(3) 82 | assert {:ok, :thrown} = TryWrapperTestModule.rescued_and_catched(:throw) 83 | assert {:ok, :error} = TryWrapperTestModule.rescued_and_catched(:raise) 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/two_argument_test.exs: -------------------------------------------------------------------------------- 1 | # two arguments 2 | defmodule DecoratorTest.Fixture.TwoArgumentDecorator do 3 | use Decorator.Define, two: 2 4 | 5 | def two(one, two, body, _context) do 6 | quote do 7 | {unquote(one), unquote(two), unquote(body)} 8 | end 9 | end 10 | end 11 | 12 | defmodule DecoratorTest.Fixture.MyTwoFunctionTestModule do 13 | use DecoratorTest.Fixture.TwoArgumentDecorator 14 | 15 | @decorate two(1, 2) 16 | def result(a) do 17 | a 18 | end 19 | end 20 | 21 | defmodule DecoratorTest.Fixture.FunctionDocTestModule do 22 | use DecoratorTest.Fixture.TwoArgumentDecorator 23 | 24 | @doc "result function which does some things" 25 | def result(a) do 26 | a 27 | end 28 | 29 | @doc "result function which does some things" 30 | @decorate two(1, 2) 31 | def result2(a) do 32 | a 33 | end 34 | end 35 | 36 | defmodule DecoratorTest.TwoArgument do 37 | use ExUnit.Case 38 | 39 | alias DecoratorTest.Fixture.{ 40 | MyTwoFunctionTestModule, 41 | FunctionDocTestModule 42 | } 43 | 44 | test "Two arguments" do 45 | assert {1, 2, 3} == MyTwoFunctionTestModule.result(3) 46 | end 47 | 48 | test "Functions with module doc" do 49 | assert 3 == FunctionDocTestModule.result(3) 50 | assert {1, 2, 3} == FunctionDocTestModule.result2(3) 51 | end 52 | end 53 | --------------------------------------------------------------------------------