Giter Club home page Giter Club logo

decorator's Introduction

Elixir function decorators

Build Status Module Version Hex Docs Total Download License Last Updated

A function decorator is a "@decorate" annotation that is put just before a function definition. It can be used to add extra functionality to Elixir functions. The runtime overhead of a function decorator is zero, as it is executed on compile time.

Examples of function decorators include: loggers, instrumentation (timing), precondition checks, et cetera.

Some remarks in advance

Some people think function decorators are a bad idea, as they can perform magic stuff on your functions (side effects!). Personally, I think they are just another form of metaprogramming, one of Elixir's selling points. But use decorators wisely, and always study the decorator code itself, so you know what it is doing.

Decorators are always marked with the @decorate literal, so that it's clear in the code that decorators are being used.

Installation

Add :decorator to your list of dependencies in mix.exs:

def deps do
  [
    {:decorator, "~> 1.2"}
  ]
end

You can now define your function decorators.

Usage

Function decorators are macros which you put just before defining a function. It looks like this:

defmodule MyModule do
  use PrintDecorator

  @decorate print()
  def square(a) do
    a * a
  end
end

Now whenever you call MyModule.square(), you'll see the message: Function called: square in the console.

Defining the decorator is pretty easy. Create a module in which you use the Decorator.Define module, passing in the decorator name and arity, or more than one if you want.

The following declares the above @print decorator which prints a message every time the decorated function is called:

defmodule PrintDecorator do
  use Decorator.Define, [print: 0]

  def print(body, context) do
    quote do
      IO.puts("Function called: " <> Atom.to_string(unquote(context.name)))
      unquote(body)
    end
  end

end

The arguments to the decorator function (the def print(...)) are the function's body (the abstract syntax tree (AST)), as well as a context argument which holds information like the function's name, defining module, arity and the arguments AST.

Compile-time arguments

Decorators can have compile-time arguments passed into the decorator macros.

For instance, you could let the print function only print when a certain logging level has been set:

@decorate print(:debug)
def foo() do
...

In this case, you specify the arity 1 for the decorator:

defmodule PrintDecorator do
  use Decorator.Define, [print: 1]

And then your print/3 decorator function gets the level passed in as the first argument:

def print(level, body, context) do
# ...
end

Decorating all functions in a module

A shortcut to decorate all functions in a module is to use the @decorate_all attribute, as shown below. It is important to note that the @decorate_all attribute only affects the function clauses below its definition.

defmodule MyApp.APIController
  use MyBackend.LoggerDecorator

  @decorate_all log_request()

  def index(_conn, params) do
    # ...
  end

  def detail(_conn, params) do
    # ...
  end

In this example, the log_request() decorator is applied to both index/2 and detail/2.

Functions with multiple clauses

If you have a function with multiple clauses, and only decorate one clause, you will notice that you get compiler warnings about unused variables and other things. For functions with multiple clauses the general advice is this: You should create an empty function head, and call the decorator on that head, like this:

defmodule DecoratorFunctionHead do
  use DecoratorFunctionHead.PrintDecorator

  @decorate print()
  def hello(first_name, last_name \\ nil)

  def hello(:world, _last_name) do
    :world
  end

  def hello(first_name, last_name) do
    "Hello #{first_name} #{last_name}"
  end
end

Decorator context

Besides the function body AST, the decorator function also gets a context argument passed in. This context holds information about the function being decorated, namely its module, function name, arity, function kind, and arguments as a list of AST nodes.

The print decorator can print its function name like this:

def print(body, context) do
  Logger.debug("Function #{context.name}/#{context.arity} with kind #{context.kind} called in module #{context.module}!")
end

Even more advanced, you can use the function arguments in the decorator. To create an is_authorized decorator which performs some checks on the Phoenix %Conn{} structure, you can create a decorator function like this:

def is_authorized(body, %{args: [conn, _params]}) do
  quote do
    if unquote(conn).assigns.user do
      unquote(body)
    else
      unquote(conn)
      |> send_resp(401, "unauthorized")
      |> halt()
    end
  end
end

Copyright and License

Copyright (c) 2016 Arjan Scherpenisse

This library is MIT licensed. See the LICENSE for details.

decorator's People

Contributors

antedeguemon avatar demoj1 avatar jeffkreeftmeijer avatar kianmeng avatar ktec avatar ludwikbukowski avatar polvalente avatar ryanbjones avatar sobolevn avatar tsuyoshi84 avatar tverlaan avatar vic avatar whilefalse avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

decorator's Issues

Not working correctly when used with function head without body

In Elixir, there is a convention that we use empty function head, which we use when we want to document the function and to define default arguments. I think decorator does not work correctly with such an empty head.

Example:

use Decorator.Define, [myfun: 0]

  def myfun(body, _context) do
    quote do
      unquote(body)
    end
  end


  def myfun(test)

  @decorate myfun()
  def myfun(_test) do
    IO.puts "test"
  end

Will generate warning:

warning: this clause cannot match because a previous clause at line XX always matches

Is it possible that it creates new head with body, that just overmatch following heads?
I mean something like:

  def myfun(_test) do
    nil
  end
  def myfun(_test) do
    IO.puts "test"
  end

OTP 20 / Elixir 1.5.0-rc.0 Rescue Emits Compiler Warning

Hello! I suspect this is similar to #11 but when a function is decorated and has a function body with a rescue, and the rescue reraises System.stacktrace the new OTP 20 builds emit warnings.

This works:

  @decorate decorated()
  @spec fetch!(module, binary) :: any | no_return
  def fetch!(model, pkey) do
    try do
     Repo.get!(model, pkey)
    rescue
      _exception in Ecto.NoResultsError ->
        reraise Errors.NotFound, System.stacktrace
      _exception in Ecto.Query.CastError ->
        reraise Errors.BadRequest.InvalidID, [id: pkey], System.stacktrace
    end
  end

This doesn't:

  @decorate decorated()
  @spec fetch!(module, binary) :: any | no_return
  def fetch!(model, pkey) do
   Repo.get!(model, pkey)
  rescue
    _exception in Ecto.NoResultsError ->
      reraise Errors.NotFound, System.stacktrace
    _exception in Ecto.Query.CastError ->
      reraise Errors.BadRequest.InvalidID, [id: pkey], System.stacktrace
  end

This emits:

warning: erlang:get_stacktrace/0 used following a 'try' expression may stop working in a future release. (Use it inside 'try'.)
  web/plugs/load_resource.ex:54

warning: erlang:get_stacktrace/0 used in the wrong part of 'try' expression. (Use it in the block between 'catch' and 'end'.)
  web/plugs/load_resource.ex:54

warning: erlang:get_stacktrace/0 used following a 'try' expression may stop working in a future release. (Use it inside 'try'.)
  web/plugs/load_resource.ex:56

warning: erlang:get_stacktrace/0 used in the wrong part of 'try' expression. (Use it in the block between 'catch' and 'end'.)
  web/plugs/load_resource.ex:56

Looking through the OTP commits, it looks like https://github.com/erlang/otp/pull/1453/files is the offending one, however both of those should probably emit the same code. Please let me know if I can provide any further details.

Thank you

Release master

Hey. Just wondering if there was something in particular holding you back from releasing what's currently in master to hex? Cheers!

Decorator used with Ecto

Let's say I'd like to decorate_all functions that are injected by Ecto.Repo. I am struggling with this:

defmodule App.Repo do
  use Ecto.Repo, otp_app: :app
  use Decorator.Define
  @decorate_all function_result(:test)

  def function_result(add, body, _context) do
    quote do
      {unquote(add), unquote(body)}
    end
  end

(...)

Firstly, it seems not to work due to warning:

== Compilation error in file lib/app/repo.ex ==
** (ArgumentError) cannot make function __pool__/0 overridable because it was not defined
    (elixir) lib/module.ex:913: anonymous fn/2 in Module.make_overridable/2
    (stdlib) lists.erl:1338: :lists.foreach/2
    (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
    (stdlib) erl_eval.erl:228: :erl_eval.expr/5
    (stdlib) erl_eval.erl:229: :erl_eval.expr/5
    /lib/app/repo.ex:1: Decorator.Decorate.before_compile/1

Once I force filter out __pool__ function from decoration, it compiles but only few functions are decorated. Most of them are not

Imperative form?

Looking at the Erlang counterpart, I see they use the @decorate annotation, instead of @decorator that we use. @decorate is maybe a bit more clear, given that's an imperative, instead of a noun.

@decorator maybe implies that we are defining a decorator, while in fact we are saying that we want to apply the decorator to the function.

@vic what do you think?

Can't issue good compile-time errors re bad arguments

I'm raising because my decorator arguments are bad, and the stack trace isn't giving as much help as I like to provide:

== Compilation error in file lib/my_app/my_module.ex ==
** (ArgumentError) event_name must be made of atoms
    lib/telemetry_decorator.ex:47: anonymous fn/2 in TelemetryDecorator.telemetry/3
    (elixir 1.10.4) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
    lib/telemetry_decorator.ex:47: TelemetryDecorator.telemetry/3
    lib/decorator/decorate.ex:164: Decorator.Decorate.apply_decorator/3
    (elixir 1.10.4) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
    lib/decorator/decorate.ex:128: Decorator.Decorate.decorate/4
    (elixir 1.10.4) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
    expanding macro: Decorator.Decorate.before_compile/1

That not bad as a clue, but it'd be great to put the line number of the decoration in there. I can scrape some from context.args, if there are any; more from body, but I can't get the filename anywhere.

Possible fixes:

  • Pass the filename and line number in Decorator.Decorate.Context

  • Pass the full __CALLER__ in Decorator.Decorate.Context

  • try around calling the decorator; rescue; insert the decorator into the stack trace; reraise

I'm happy to raise a PR for either, or some way that you think would be cleaner; what would you feel OK merging?

Can't Mock code used within the decorators

I use Mock to, well mock, entire modules for tests. Mock duplicates entire modules with given functions to simulate original functions.

I try to mock one specific module used by my decorator, and it doesn't seem to be working.
I'm assuming decorator does its magic in compile time, locking me out of the mocking in runtime.

Do you know if I can test for example, decorated controller functions bypassing the decorator?

separate tests and helper modules

This is such a cool library! Thanks for it.

When I have started looking through tests, one thing caught my eye: mixing tests and helper modules together. In general they are divided. What do you think of it?

If that fine by you, I will send a PR.

Decorator is not compatible with `@impl` directive introduced in Elixir 1.5

Elixir 1.5 introduces the @impl true directive to declare functions as callback implementations of a behaviour.

These declarations don't work with the @decorate macro. Example:

defmodule MyModule
  @behaviour MyBehaviour
  @behaviour AnotherBehaviour

  @impl MyBehaviour
  @decorate my_callback() # <== causes the @impl to emit a warning
  def my_callback do
  end
 
  @impl AnotherBehaviour
  def another_callback do
  end
end

Function result is `[]` when decorating a function head

When decorating a function head, the result is always [] if you return the result of the body:

If you have this:

defmodule DecoratorFunctionHead do
  use DecoratorFunctionHead.PrintDecorator

  @decorate print()
  def hello(first_name, last_name \\ nil)

  def hello(:world, _last_name) do
    :world
  end

  def hello(:earth, _last_name) do
    :earth
  end

  def hello(first_name, last_name) do
    "Hello #{first_name} #{last_name}"
  end
end

Then tests will fail as:

1) test greets the world (DecoratorFunctionHeadTest)
     test/decorator_function_head_test.exs:4
     Assertion with == failed
     code:  assert DecoratorFunctionHead.hello(:world) == :world
     left:  []
     right: :world
     stacktrace:
       test/decorator_function_head_test.exs:5: (test)

I did a little digging and it seems to be because body for a function head is [] (empty AST), so that's what it returns. I didn't have time to dig into it deeper though, I might be able to do it in the following days/weeks. For now we managed to have a workaround and not use a decorator when we have a function head.

I've created this repo to reproduce the issue: https://github.com/uesteibar/decorator_function_head

Using two different decorator modules in the same file creates duplicate functions

I'm trying to implement a custom decorator to apply the logger_metadata to spawned batch processes from Absinthe's batch. I have the following module

defmodule LoggerDecorator do
  @moduledoc """
  Makes it so that async functions that have been invoked can find the parent caller and
  put the request_id on the logger, which will enhance debugging
  """
  use Decorator.Define, [apply_request_id_to_logger: 0]
  require Logger

  def apply_request_id_to_logger(body, _context) do
    quote do
      parent_pid =
        :"$callers"
        |> Process.get()
        |> List.last()

      {_, metadata} = Process.info(parent_pid)[:dictionary][:logger_metadata]
      request_id = Keyword.get(metadata, :request_id, nil)
      Logger.metadata([request_id: request_id])

      unquote(body)
    end
  end
end

We use Appsignal, so at the top of our resolver file I have

defmodule MyApp.Resolver.Template do 
  use Appsignal.Instrumentation.Decorators
  use LoggerDecorator

 @decorate apply_request_id_to_logger()
  def all(location_uids, organization_uid, user_uid, types \\ Template.visible_template_types()) do 
  end

  @decorate transaction_event()
  def update(params) do
  end
end

which will throw the error

== Compilation error in file lib/black_mamba/templates.ex ==
** (CompileError) lib/black_mamba/templates.ex:1: def all/4 defines defaults multiple times. Elixir allows defaults to be declared once per definition. Instead of:

    def foo(:first_clause, b \\ :default) do ... end
    def foo(:second_clause, b \\ :default) do ... end

In other files where a function does not have a default, but both decorator modules are used, I get a slew of

warning: this clause cannot match because a previous clause at line 1 always matches
  web/resolver/location.ex:1

On Elixir 1.9, decorator 1.2.4

Multiple decorators order

Hey guys!
Thank you for the great tool!
I'm experimenting a bit with multiple decorators like:

@decorate foo()
@decorate bar()
@decorate baz()
def hello() do
  :world
end

Intuitively I expect baz to decorate hello, then bar decorates baz and hello, then, finally foo decorates all of them bar, baz, hello.

But for some reason, there is Enum.reverse here

|> Enum.reverse()

so the execution is the opposite: baz decorates bar, foo, hello.

What is the intention of reversing the order?
And do you recommend applying several decorators?
Thank you!

[Question] How to decorate all functions in a module?

I'm trying to use this library to decorate every function in a module. My current strategy for doing this is using an on_definition hook, and put_attribute'ing the required parameters. This appears to be working nearly. It has some trouble with multi-clause functions; especially with function headers.

After banging my head against this for a few hours, I thought it might just be better to ask if there's an easier way to decorate every function. Thanks!

Can't use other modules macros while using decorator

While using decorator with Spandex i'm not able to add another use in sequence:

defmodule Fetcher.Info do
  use Spandex.Decorators
  use Interceptor.Annotation

 @intercept true
 @decorate span(type: :backend)
 def retrieve_information(resource_uuid) do
      #mycode
 end
end

Using this code I get the following warning:

warning: this clause cannot match because a previous clause at line 1 always matches
  lib/fetcher/info.ex:1

Could it be useful to insert something like this?

Module.register_attribute __MODULE__, :decorate, accumulate: true

To allow the macros to be used as a list and not overwrite the previous.
I think this would also help the #29, what do you think?

[Question] What to do about used/unused variable warnings?

Forgive me for using this as a venue to get a recommendation, but I'm having a really hard time even forming my question in other venues, so I thought I'd try it out here since this library is what spurred this question.

My goal here is to explore adding permission checks to my context functions.

@decorate permit(:create_thing)
def create_thing(_user, params), do:
   %Thing{} |> Thing.changset(params) |> Repo.insert()

I have setup permit to use the first argument provided to the decorated function (the user):

def permit(action, body, %{args: [user, _params]}) do
   with :ok -> Permissions.can(user, action) do
     unquote(body)
   end
end

Note that in the above use-case, the user is actually only used in the decorated code, NOT the function being decorated. This confuses the compiler and gives warnings (if it's underscored, I'm told that it's being used even though it's underscored. If it's NOT underscored, I get warnings saying it's unused).

I have to note that this entire thing smells bad because my best case scenario here is that a seemingly unused arg is actually being used (or vice-versa). This makes me seriously question my API choices, but I think I'm onto a very powerful use of this library.

SO, my question is:

  1. Is what I'm running into something that feels like a bug somewhere? Is it a common issue?
  2. Seeing what I'm attempting to accomplish, am I missing anything obvious that might be a better API approach?

Thanks for a great library - appreciate all the work that went into it!

Elixir 1.5 Errors

Hi there! On Elixir 1.5-rc.0, this library seems to be misbehaving on rescue blocks when a function is @decorated. It can be pretty simply reproduced with the following code, most of which is from the README.

defmodule PrintDecorator do
  use Decorator.Define, [print: 0]

  def print(body, context) do
    quote do
      IO.puts("Function called: " <> Atom.to_string(unquote(context.name)))
      unquote(body)
    end
  end

end

defmodule Testing do
  use PrintDecorator

  @decorate print()
  def hello do
    :world
  rescue
    e -> IO.inspect e
  end

end

When you try to compile, it errors with:

== Compilation error in file lib/testing.ex ==
** (CompileError) lib/testing.ex:24: unhandled operator ->
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (stdlib) lists.erl:1355: :lists.mapfoldl/3
    /Users/asummers/Code/testing/lib/testing.ex:13: Decorator.Decorate.before_compile/1

I'm looking around to see if I see anything immediate but wanted to open this issue for visibility / to see if you had any ideas off the top of your head of what might be causing this. It's also entirely possible this is a regression in Elixir itself.

Unquoting default params results in elixir_locals.ensure_no_undefined_local/3

Definition

defmodule ArgsValidator do
  use Decorator.Define, validate: 1

  def validate(_, _, %{args: function_args}) do
    quote do: unquote(function_args)
  end
end

Usage

use ArgsValidator

@decorate validate([&Ecto.UUID.cast/1])
  defdelegate resource(uuid, opts \\ []),
    to: ResourceByUUID,
    as: :call
    (elixir) src/elixir_locals.erl:108: :elixir_locals."-ensure_no_undefined_local/3-lc$^0/1-0-"/2
    (elixir) src/elixir_locals.erl:108: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3
    (stdlib) erl_eval.erl:680: :erl_eval.do_apply/6
** (exit) shutdown: 1
    (mix) lib/mix/tasks/compile.all.ex:59: Mix.Tasks.Compile.All.do_compile/4
    (mix) lib/mix/tasks/compile.all.ex:24: anonymous fn/1 in Mix.Tasks.Compile.All.run/1
    (mix) lib/mix/tasks/compile.all.ex:40: Mix.Tasks.Compile.All.with_logger_app/1
    (mix) lib/mix/task.ex:331: Mix.Task.run_task/3
    (mix) lib/mix/tasks/compile.ex:96: Mix.Tasks.Compile.run/1
    (mix) lib/mix/task.ex:331: Mix.Task.run_task/3
    (iex) lib/iex/helpers.ex:104: IEx.Helpers.recompile/1

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.