From f095b2e42c56628daea51f8bac0bc84b6d0c9107 Mon Sep 17 00:00:00 2001 From: Shane Logsdon Date: Wed, 14 May 2014 23:45:13 -0400 Subject: [PATCH 1/4] getting there --- lib/mix/tasks/sugar/scaffold.ex | 2 - lib/sugar/controller.ex | 1 + lib/sugar/router.ex | 2 +- lib/sugar/router/filters.ex | 29 ++++++++++++++ lib/sugar/router/hooks.ex | 63 +++++++++++++++++++++++++++++++ test/sugar/router/hooks_test.exs | 65 ++++++++++++++++++++++++++++++++ test/sugar/router_test.exs | 16 ++++---- 7 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 lib/sugar/router/filters.ex create mode 100644 lib/sugar/router/hooks.ex create mode 100644 test/sugar/router/hooks_test.exs diff --git a/lib/mix/tasks/sugar/scaffold.ex b/lib/mix/tasks/sugar/scaffold.ex index bf3c992..7f38f60 100644 --- a/lib/mix/tasks/sugar/scaffold.ex +++ b/lib/mix/tasks/sugar/scaffold.ex @@ -26,8 +26,6 @@ defmodule Mix.Tasks.Sugar.Scaffold do end defp do_scaffold(name, opts) do - module = camelize atom_to_binary(Mix.project[:app]) - assigns = [ app: Mix.project[:app], module: name, diff --git a/lib/sugar/controller.ex b/lib/sugar/controller.ex index 6237cf2..02c197e 100644 --- a/lib/sugar/controller.ex +++ b/lib/sugar/controller.ex @@ -48,6 +48,7 @@ defmodule Sugar.Controller do quote do import unquote(__MODULE__) import unquote(Plug.Conn) + use unquote(Sugar.Router.Hooks) end end diff --git a/lib/sugar/router.ex b/lib/sugar/router.ex index 59f1bfe..dcfa0f0 100644 --- a/lib/sugar/router.ex +++ b/lib/sugar/router.ex @@ -62,7 +62,7 @@ defmodule Sugar.Router do end defp call_controller_action(%Plug.Conn{state: :unset} = conn, controller, action, binding) do - apply controller, action, [conn, Keyword.delete(binding, :conn)] + apply controller, :call_action, [action, conn, Keyword.delete(binding, :conn)] end defp call_controller_action(conn, _, _, _) do conn diff --git a/lib/sugar/router/filters.ex b/lib/sugar/router/filters.ex new file mode 100644 index 0000000..d6d2dfa --- /dev/null +++ b/lib/sugar/router/filters.ex @@ -0,0 +1,29 @@ +defmodule Sugar.Router.Filters do + ## Macros + + @doc """ + Macro used to add necessary items to a router. + """ + defmacro __using__(_opts) do + quote do + import unquote(__MODULE__) + @filters [] + @before_compile unquote(__MODULE__) + end + end + + @doc """ + Defines a default route to catch all unmatched routes. + """ + defmacro __before_compile__(env) do + module = env.module + filters = Enum.reverse(Module.get_attribute(module, :filters)) + _escaped = Macro.escape(filters) + end + + defmacro filter(spec) do + quote do + @filters [unquote(spec)|@filters] + end + end +end diff --git a/lib/sugar/router/hooks.ex b/lib/sugar/router/hooks.ex new file mode 100644 index 0000000..d99dfff --- /dev/null +++ b/lib/sugar/router/hooks.ex @@ -0,0 +1,63 @@ +defmodule Sugar.Router.Hooks do + @doc """ + Macro used to add necessary items to a router. + """ + defmacro __using__(_opts) do + quote do + import unquote(__MODULE__) + @hooks [] + @all_hooks_key :__all_hooks__ + @before_compile unquote(__MODULE__) + end + end + + defmacro __before_compile__(env) do + module = env.module + hooks = Module.get_attribute(module, :hooks) + + quote do + def call_action(action, conn, args) do + conn = call_before_hooks(unquote(hooks), unquote(module), action, conn) + conn = apply(unquote(module), action, [conn, args]) + call_after_hooks(unquote(hooks), unquote(module), action, conn) + end + end + end + + def call_before_hooks(hooks, module, action, conn) do + call_hooks(:before, hooks, module, action, conn) + end + + def call_after_hooks(hooks, module, action, conn) do + call_hooks(:after, hooks, module, action, conn) + end + + defp call_hooks(type, hooks, module, action, conn) do + hooks + |> Enum.filter(fn ({t, _}) -> t === type end) + |> Enum.filter(fn ({_, {act, _}}) -> act === action || act === :__all_hooks__ end) + |> Enum.reduce(conn, fn({_, {_, fun}}, conn) -> + apply module, fun, [conn] + end) + end + + defmacro before_hook(function) when is_atom(function) do + quote do + @hooks @hooks++[{:before, {@all_hooks_key, unquote(function)}}] + end + end + defmacro before_hook(function, _opts) when is_atom(function) do + quote do + end + end + + defmacro after_hook(function) when is_atom(function) do + quote do + @hooks @hooks++[{:after, {@all_hooks_key, unquote(function)}}] + end + end + defmacro after_hook(function, _opts) when is_atom(function) do + quote do + end + end +end diff --git a/test/sugar/router/hooks_test.exs b/test/sugar/router/hooks_test.exs new file mode 100644 index 0000000..0541069 --- /dev/null +++ b/test/sugar/router/hooks_test.exs @@ -0,0 +1,65 @@ +defmodule Sugar.Router.HooksTest do + use ExUnit.Case, async: true + use Plug.Test + + test "before_hook/1 all" do + conn = conn(:get, "/") + |> Sugar.Router.HooksTest.Router.call([]) + + assert get_resp_header(conn, "content-type") === ["application/json; charset=utf-8"] + end + + test "before_hook/2 all + only show" do + conn = conn(:get, "/show") + |> Sugar.Router.HooksTest.Router.call([]) + + assert get_resp_header(conn, "content-type") === ["application/json; charset=utf-8"] + assert conn.assigns[:id] === 1 + end + + test "after_hook/1 all" do + conn = conn(:get, "/") + |> Sugar.Router.HooksTest.Router.call([]) + + assert conn.state === :sent + end + + defmodule Router do + use Sugar.Router + alias Sugar.Router.HooksTest.Controller + + get "/", Controller, :index + get "/show", Controller, :show + end + + defmodule Controller do + use Sugar.Controller + + before_hook :json + before_hook :set_assign, only: [:show] + + after_hook :send + + def index(conn, _args) do + conn |> resp(200, "[]") + end + + def show(conn, _args) do + conn |> resp(200, "[]") + end + + ## Hooks + + def json(conn) do + conn |> put_resp_header("content-type", "application/json; charset=utf-8") + end + + def set_assign(conn) do + conn |> assign(:id, 1) + end + + def send(conn) do + conn |> send_resp + end + end +end diff --git a/test/sugar/router_test.exs b/test/sugar/router_test.exs index 7a1be37..4bbe224 100644 --- a/test/sugar/router_test.exs +++ b/test/sugar/router_test.exs @@ -5,7 +5,7 @@ defmodule Sugar.RouterTest do test "get/3" do conn = conn(:get, "/get") - conn = Sugar.RouterTest.Router.call(conn, []) + |> Sugar.RouterTest.Router.call([]) assert conn.state === :sent assert conn.status === 200 @@ -13,7 +13,7 @@ defmodule Sugar.RouterTest do test "post/3" do conn = conn(:post, "/post") - conn = Sugar.RouterTest.Router.call(conn, []) + |> Sugar.RouterTest.Router.call([]) assert conn.state === :sent assert conn.status === 200 @@ -21,7 +21,7 @@ defmodule Sugar.RouterTest do test "put/3" do conn = conn(:put, "/put") - conn = Sugar.RouterTest.Router.call(conn, []) + |> Sugar.RouterTest.Router.call([]) assert conn.state === :sent assert conn.status === 200 @@ -29,7 +29,7 @@ defmodule Sugar.RouterTest do test "patch/3" do conn = conn(:patch, "/patch") - conn = Sugar.RouterTest.Router.call(conn, []) + |> Sugar.RouterTest.Router.call([]) assert conn.state === :sent assert conn.status === 200 @@ -37,7 +37,7 @@ defmodule Sugar.RouterTest do test "delete/3" do conn = conn(:delete, "/delete") - conn = Sugar.RouterTest.Router.call(conn, []) + |> Sugar.RouterTest.Router.call([]) assert conn.state === :sent assert conn.status === 200 @@ -45,7 +45,7 @@ defmodule Sugar.RouterTest do test "options/3" do conn = conn(:options, "/options") - conn = Sugar.RouterTest.Router.call(conn, []) + |> Sugar.RouterTest.Router.call([]) assert conn.state === :sent assert conn.status === 200 @@ -53,7 +53,7 @@ defmodule Sugar.RouterTest do test "any/3 any" do conn = conn(:any, "/any") - conn = Sugar.RouterTest.Router.call(conn, []) + |> Sugar.RouterTest.Router.call([]) assert conn.state === :sent assert conn.status === 200 @@ -61,7 +61,7 @@ defmodule Sugar.RouterTest do test "any/3 get" do conn = conn(:get, "/any") - conn = Sugar.RouterTest.Router.call(conn, []) + |> Sugar.RouterTest.Router.call([]) assert conn.state === :sent assert conn.status === 200 From 937e1d3aa08d334ad21c376638899080fc7bfa00 Mon Sep 17 00:00:00 2001 From: Shane Logsdon Date: Thu, 15 May 2014 09:56:24 -0400 Subject: [PATCH 2/4] request hooks fully working moved into sugar.controller namespace. at 100% coverage --- lib/sugar/controller.ex | 2 +- lib/sugar/{router => controller}/hooks.ex | 26 ++++++++++-- .../{router => controller}/hooks_test.exs | 41 +++++++++++++++---- test/sugar/controller_test.exs | 5 --- 4 files changed, 57 insertions(+), 17 deletions(-) rename lib/sugar/{router => controller}/hooks.ex (68%) rename test/sugar/{router => controller}/hooks_test.exs (52%) diff --git a/lib/sugar/controller.ex b/lib/sugar/controller.ex index 02c197e..6792a61 100644 --- a/lib/sugar/controller.ex +++ b/lib/sugar/controller.ex @@ -48,7 +48,7 @@ defmodule Sugar.Controller do quote do import unquote(__MODULE__) import unquote(Plug.Conn) - use unquote(Sugar.Router.Hooks) + use unquote(Sugar.Controller.Hooks) end end diff --git a/lib/sugar/router/hooks.ex b/lib/sugar/controller/hooks.ex similarity index 68% rename from lib/sugar/router/hooks.ex rename to lib/sugar/controller/hooks.ex index d99dfff..43a4a5d 100644 --- a/lib/sugar/router/hooks.ex +++ b/lib/sugar/controller/hooks.ex @@ -1,4 +1,4 @@ -defmodule Sugar.Router.Hooks do +defmodule Sugar.Controller.Hooks do @doc """ Macro used to add necessary items to a router. """ @@ -46,8 +46,18 @@ defmodule Sugar.Router.Hooks do @hooks @hooks++[{:before, {@all_hooks_key, unquote(function)}}] end end - defmacro before_hook(function, _opts) when is_atom(function) do + defmacro before_hook(function, opts) when is_atom(function) do quote do + opts = unquote(opts) + function = unquote(function) + cond do + opts[:only] -> + @hooks Enum.reduce(opts[:only], @hooks, fn (method, acc) -> + acc++[{:before, {method, function}}] + end) + true -> + @hooks + end end end @@ -56,8 +66,18 @@ defmodule Sugar.Router.Hooks do @hooks @hooks++[{:after, {@all_hooks_key, unquote(function)}}] end end - defmacro after_hook(function, _opts) when is_atom(function) do + defmacro after_hook(function, opts) when is_atom(function) do quote do + opts = unquote(opts) + function = unquote(function) + cond do + opts[:only] -> + @hooks Enum.reduce(opts[:only], @hooks, fn (method, acc) -> + acc++[{:after, {method, function}}] + end) + true -> + @hooks + end end end end diff --git a/test/sugar/router/hooks_test.exs b/test/sugar/controller/hooks_test.exs similarity index 52% rename from test/sugar/router/hooks_test.exs rename to test/sugar/controller/hooks_test.exs index 0541069..93a5c41 100644 --- a/test/sugar/router/hooks_test.exs +++ b/test/sugar/controller/hooks_test.exs @@ -1,32 +1,52 @@ -defmodule Sugar.Router.HooksTest do +defmodule Sugar.Controller.HooksTest do use ExUnit.Case, async: true use Plug.Test test "before_hook/1 all" do conn = conn(:get, "/") - |> Sugar.Router.HooksTest.Router.call([]) + |> Sugar.Controller.HooksTest.Router.call([]) assert get_resp_header(conn, "content-type") === ["application/json; charset=utf-8"] end test "before_hook/2 all + only show" do conn = conn(:get, "/show") - |> Sugar.Router.HooksTest.Router.call([]) + |> Sugar.Controller.HooksTest.Router.call([]) assert get_resp_header(conn, "content-type") === ["application/json; charset=utf-8"] assert conn.assigns[:id] === 1 + + conn = conn(:get, "/") + |> Sugar.Controller.HooksTest.Router.call([]) + + assert get_resp_header(conn, "content-type") === ["application/json; charset=utf-8"] + refute conn.assigns[:id] === 1 end test "after_hook/1 all" do conn = conn(:get, "/") - |> Sugar.Router.HooksTest.Router.call([]) + |> Sugar.Controller.HooksTest.Router.call([]) + + assert conn.private[:id] === 2 + end + + test "after_hook/2 all + only show" do + conn = conn(:get, "/show") + |> Sugar.Controller.HooksTest.Router.call([]) assert conn.state === :sent + assert conn.private[:id] === 2 + + conn = conn(:get, "/") + |> Sugar.Controller.HooksTest.Router.call([]) + + refute conn.state === :sent + assert conn.private[:id] === 2 end defmodule Router do use Sugar.Router - alias Sugar.Router.HooksTest.Controller + alias Sugar.Controller.HooksTest.Controller get "/", Controller, :index get "/show", Controller, :show @@ -35,10 +55,11 @@ defmodule Sugar.Router.HooksTest do defmodule Controller do use Sugar.Controller - before_hook :json + before_hook :set_json before_hook :set_assign, only: [:show] - after_hook :send + after_hook :send, only: [:show] + after_hook :clear_assign def index(conn, _args) do conn |> resp(200, "[]") @@ -50,7 +71,7 @@ defmodule Sugar.Router.HooksTest do ## Hooks - def json(conn) do + def set_json(conn) do conn |> put_resp_header("content-type", "application/json; charset=utf-8") end @@ -61,5 +82,9 @@ defmodule Sugar.Router.HooksTest do def send(conn) do conn |> send_resp end + + def clear_assign(conn) do + conn |> assign_private(:id, 2) + end end end diff --git a/test/sugar/controller_test.exs b/test/sugar/controller_test.exs index 5b8758d..98838d5 100644 --- a/test/sugar/controller_test.exs +++ b/test/sugar/controller_test.exs @@ -2,11 +2,6 @@ defmodule Sugar.ControllerTest do use ExUnit.Case, async: true use Plug.Test - test "__using__/1" do - use Sugar.Controller - assert Keyword.has_key? __ENV__.functions, Sugar.Controller - end - import Sugar.Controller import Plug.Conn From 18e42fe4ee9d788ce43fc6c67feabe3bd559af76 Mon Sep 17 00:00:00 2001 From: Shane Logsdon Date: Thu, 15 May 2014 11:03:22 -0400 Subject: [PATCH 3/4] request filters in place for router 100% coverage. slight tweaks elsewhere --- lib/sugar/router.ex | 13 +++++-- lib/sugar/router/filters.ex | 37 ++++++++++++------- test/sugar/controller/hooks_test.exs | 4 +-- test/sugar/router/filters_test.exs | 53 ++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 test/sugar/router/filters_test.exs diff --git a/lib/sugar/router.ex b/lib/sugar/router.ex index dcfa0f0..4d0508a 100644 --- a/lib/sugar/router.ex +++ b/lib/sugar/router.ex @@ -35,6 +35,7 @@ defmodule Sugar.Router do import unquote(__MODULE__) import Plug.Conn use Plug.Router + use Sugar.Router.Filters @before_compile unquote(__MODULE__) plug Plug.Parsers, parsers: [:urlencoded, :multipart] @@ -54,15 +55,23 @@ defmodule Sugar.Router do @doc """ Defines a default route to catch all unmatched routes. """ - defmacro __before_compile__(_env) do + defmacro __before_compile__(env) do + module = env.module + # From Sugar.Router.Filters + filters = Module.get_attribute(module, :filters) + quote do + # Our default match so Plug doesn't fall on its face + # when accessing an undefined route Plug.Router.match _ do conn = var!(conn) Sugar.Controller.not_found conn end defp call_controller_action(%Plug.Conn{state: :unset} = conn, controller, action, binding) do - apply controller, :call_action, [action, conn, Keyword.delete(binding, :conn)] + conn = call_before_filters(unquote(filters), action, conn) + conn = apply controller, :call_action, [action, conn, Keyword.delete(binding, :conn)] + call_after_filters(unquote(filters), action, conn) end defp call_controller_action(conn, _, _, _) do conn diff --git a/lib/sugar/router/filters.ex b/lib/sugar/router/filters.ex index d6d2dfa..4c8551d 100644 --- a/lib/sugar/router/filters.ex +++ b/lib/sugar/router/filters.ex @@ -1,6 +1,4 @@ defmodule Sugar.Router.Filters do - ## Macros - @doc """ Macro used to add necessary items to a router. """ @@ -8,22 +6,37 @@ defmodule Sugar.Router.Filters do quote do import unquote(__MODULE__) @filters [] - @before_compile unquote(__MODULE__) + @all_filters_key :__all_filters__ + @before_compile unquote(Sugar.Router) end end - @doc """ - Defines a default route to catch all unmatched routes. - """ - defmacro __before_compile__(env) do - module = env.module - filters = Enum.reverse(Module.get_attribute(module, :filters)) - _escaped = Macro.escape(filters) + def call_before_filters(filters, action, conn) do + call_filters(:before, filters, action, conn) + end + + def call_after_filters(filters, action, conn) do + call_filters(:after, filters, action, conn) + end + + defp call_filters(type, filters, action, conn) do + filters + |> Enum.filter(fn ({t, _}) -> t === type end) + |> Enum.filter(fn ({_, {act, _}}) -> act === action || act === :__all_filters__ end) + |> Enum.reduce(conn, fn({_, {_, {module, fun}}}, conn) -> + apply module, fun, [conn] + end) + end + + defmacro before_filter(module, function) do + quote do + @filters @filters++[{:before, {@all_filters_key, {unquote(module), unquote(function)}}}] + end end - defmacro filter(spec) do + defmacro after_filter(module, function) do quote do - @filters [unquote(spec)|@filters] + @filters @filters++[{:after, {@all_filters_key, {unquote(module), unquote(function)}}}] end end end diff --git a/test/sugar/controller/hooks_test.exs b/test/sugar/controller/hooks_test.exs index 93a5c41..12ec90c 100644 --- a/test/sugar/controller/hooks_test.exs +++ b/test/sugar/controller/hooks_test.exs @@ -59,7 +59,7 @@ defmodule Sugar.Controller.HooksTest do before_hook :set_assign, only: [:show] after_hook :send, only: [:show] - after_hook :clear_assign + after_hook :set_private def index(conn, _args) do conn |> resp(200, "[]") @@ -83,7 +83,7 @@ defmodule Sugar.Controller.HooksTest do conn |> send_resp end - def clear_assign(conn) do + def set_private(conn) do conn |> assign_private(:id, 2) end end diff --git a/test/sugar/router/filters_test.exs b/test/sugar/router/filters_test.exs new file mode 100644 index 0000000..061173a --- /dev/null +++ b/test/sugar/router/filters_test.exs @@ -0,0 +1,53 @@ +defmodule Sugar.Router.FiltersTest do + use ExUnit.Case, async: true + use Plug.Test + + test "before_filter/1 all" do + conn = conn(:get, "/") + |> Sugar.Router.FiltersTest.Router.call([]) + + assert get_resp_header(conn, "content-type") === ["application/json; charset=utf-8"] + end + + test "after_filter/1 all" do + conn = conn(:get, "/") + |> Sugar.Router.FiltersTest.Router.call([]) + + refute conn.assigns[:id] === 1 + end + + defmodule Router do + use Sugar.Router + alias Sugar.Router.FiltersTest.Controller + + before_filter Sugar.Router.FiltersTest.Filters, :set_json + after_filter Sugar.Router.FiltersTest.Filters, :clear_assigns + + get "/", Controller, :index + get "/show", Controller, :show + end + + defmodule Controller do + use Sugar.Controller + + def index(conn, _args) do + conn |> assign(:id, 1) |> resp(200, "[]") + end + + def show(conn, _args) do + conn |> resp(200, "[]") + end + end + + defmodule Filters do + import Plug.Conn + + def set_json(conn) do + conn |> put_resp_header("content-type", "application/json; charset=utf-8") + end + + def clear_assigns(conn) do + %{ conn | assigns: %{} } + end + end +end From b0294bd2fdc765b5e94d5a4e3f4cbf16f0f44321 Mon Sep 17 00:00:00 2001 From: Shane Logsdon Date: Thu, 15 May 2014 13:50:45 -0400 Subject: [PATCH 4/4] cleanup and docs --- CHANGELOG.md | 5 +- README.md | 3 +- lib/sugar/controller/hooks.ex | 82 ++++++++++++++++++++++++------ lib/sugar/router/filters.ex | 46 +++++++++++++++++ test/sugar/router/filters_test.exs | 7 +-- 5 files changed, 121 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d76d493..7bebf84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ - [Enhancement] Upgrade to Elixir v0.13.2 - [Dependency] Adding [sugar-framework/plugs](https://github.com/sugar-framework/plugs) back in -- [Enhancement] Removed mix.lock from repository to improve dependency management +- [Enhancement] Removed mix.lock from repository to improve dependency management in projects +- [Enhancement] Added Unit tests to increase coverage from ~5% to ~96% +- [Enhancement] Added Controller hooks for before/after actions +- [Enhancement] Added Router filters for before/after calling matched route ### [v0.2.0](https://github.com/sugar-framework/sugar/tree/v0.2.0) diff --git a/README.md b/README.md index 47ba23e..54ab4ac 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,7 @@ end ## Todo Items -- request before/after hooks -- basic authentication +- basic authentication (?) - Templating - EEx - [ErlyDTL](https://github.com/erlydtl/erlydtl) diff --git a/lib/sugar/controller/hooks.ex b/lib/sugar/controller/hooks.ex index 43a4a5d..45bbf62 100644 --- a/lib/sugar/controller/hooks.ex +++ b/lib/sugar/controller/hooks.ex @@ -1,6 +1,34 @@ defmodule Sugar.Controller.Hooks do + @moduledoc """ + Allows for before and after hooks to a controller. Each hook has the + opportunity to modify and/or read the request's `conn` in each stage. + + #### Example + + defmodule MyController do + use Sugar.Controller + + before_hook :set_headers + after_hook :send + + def index(conn, _args) do + conn |> resp(200, "[]") + end + + ## Hooks + + def set_headers(conn) do + conn |> put_resp_header("content-type", "application/json; charset=utf-8") + end + + def send(conn) do + conn |> send_resp + end + end + """ + @doc """ - Macro used to add necessary items to a router. + Macro used to add necessary items to a controller. """ defmacro __using__(_opts) do quote do @@ -21,26 +49,37 @@ defmodule Sugar.Controller.Hooks do conn = apply(unquote(module), action, [conn, args]) call_after_hooks(unquote(hooks), unquote(module), action, conn) end - end - end - def call_before_hooks(hooks, module, action, conn) do - call_hooks(:before, hooks, module, action, conn) - end + defp call_before_hooks(hooks, module, action, conn) do + call_hooks(:before, hooks, module, action, conn) + end - def call_after_hooks(hooks, module, action, conn) do - call_hooks(:after, hooks, module, action, conn) - end + defp call_after_hooks(hooks, module, action, conn) do + call_hooks(:after, hooks, module, action, conn) + end - defp call_hooks(type, hooks, module, action, conn) do - hooks - |> Enum.filter(fn ({t, _}) -> t === type end) - |> Enum.filter(fn ({_, {act, _}}) -> act === action || act === :__all_hooks__ end) - |> Enum.reduce(conn, fn({_, {_, fun}}, conn) -> - apply module, fun, [conn] - end) + defp call_hooks(type, hooks, module, action, conn) do + hooks + |> Stream.filter(fn ({t, _}) -> t === type end) + |> Stream.filter(fn ({_, {act, _}}) -> act === action || act === :__all_hooks__ end) + |> Enum.reduce(conn, fn({_, {_, fun}}, conn) -> + apply module, fun, [conn] + end) + end + end end + @doc """ + Adds a before hook to a controller. + + ## Arguments + + - `function` - `atom` - name of the function to be called within the hook + - `opts` - `Keyword` - optional - used to target specific actions. Possible + options include: + - `only` - `List` - a list of atoms representing the actions on which the + hook should be applied + """ defmacro before_hook(function) when is_atom(function) do quote do @hooks @hooks++[{:before, {@all_hooks_key, unquote(function)}}] @@ -61,6 +100,17 @@ defmodule Sugar.Controller.Hooks do end end + @doc """ + Adds an after hook to a controller. + + ## Arguments + + - `function` - `atom` - name of the function to be called within the hook + - `opts` - `Keyword` - optional - used to target specific actions. Possible + options include: + - `only` - `List` - a list of atoms representing the actions on which the + hook should be applied + """ defmacro after_hook(function) when is_atom(function) do quote do @hooks @hooks++[{:after, {@all_hooks_key, unquote(function)}}] diff --git a/lib/sugar/router/filters.ex b/lib/sugar/router/filters.ex index 4c8551d..51b71a2 100644 --- a/lib/sugar/router/filters.ex +++ b/lib/sugar/router/filters.ex @@ -1,4 +1,32 @@ defmodule Sugar.Router.Filters do + @moduledoc """ + Allows for before and after hooks to a controller. Each hook has the + opportunity to modify and/or read the request's `conn` in each stage. + + #### Example + + defmodule MyController do + use Sugar.Controller + + before_hook :set_headers + after_hook :send + + def index(conn, _args) do + conn |> resp(200, "[]") + end + + ## Hooks + + def set_headers(conn) do + conn |> put_resp_header("content-type", "application/json; charset=utf-8") + end + + def send(conn) do + conn |> send_resp + end + end + """ + @doc """ Macro used to add necessary items to a router. """ @@ -11,10 +39,12 @@ defmodule Sugar.Router.Filters do end end + @doc false def call_before_filters(filters, action, conn) do call_filters(:before, filters, action, conn) end + @doc false def call_after_filters(filters, action, conn) do call_filters(:after, filters, action, conn) end @@ -28,12 +58,28 @@ defmodule Sugar.Router.Filters do end) end + @doc """ + Adds a before hook to a controller. + + ## Arguments + + - `module` - `atom` - name of the module that contains the filter function + - `function` - `atom` - name of the function to be called within the hook + """ defmacro before_filter(module, function) do quote do @filters @filters++[{:before, {@all_filters_key, {unquote(module), unquote(function)}}}] end end + @doc """ + Adds a after hook to a controller. + + ## Arguments + + - `module` - `atom` - name of the module that contains the filter function + - `function` - `atom` - name of the function to be called within the hook + """ defmacro after_filter(module, function) do quote do @filters @filters++[{:after, {@all_filters_key, {unquote(module), unquote(function)}}}] diff --git a/test/sugar/router/filters_test.exs b/test/sugar/router/filters_test.exs index 061173a..25f54ed 100644 --- a/test/sugar/router/filters_test.exs +++ b/test/sugar/router/filters_test.exs @@ -19,9 +19,10 @@ defmodule Sugar.Router.FiltersTest do defmodule Router do use Sugar.Router alias Sugar.Router.FiltersTest.Controller - - before_filter Sugar.Router.FiltersTest.Filters, :set_json - after_filter Sugar.Router.FiltersTest.Filters, :clear_assigns + alias Sugar.Router.FiltersTest.Filters + + before_filter Filters, :set_json + after_filter Filters, :clear_assigns get "/", Controller, :index get "/show", Controller, :show