diff --git a/.gitignore b/.gitignore index 2689f0d..5a97981 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ /ebin /deps /doc/* -!/doc/*.png erl_crash.dump *.ez *~ diff --git a/README.md b/README.md index 05099af..44dd016 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ based on Webmachine from basho. This version is not backward compatible with the previous one that was only a thin wrapper around webmachine, use the branch 1.0-legacy to use the old one. -The principle is to go through the [HTTP decision tree](https://raw.githubusercontent.com/kbrw/ewebmachine/master/doc/http_diagram.png) +The principle is to go through the [HTTP decision tree](./assets/http_diagram.png) and make decisions according to response of some callbacks called "handlers". To do that, the library gives you 5 plugs and 2 plug pipeline builders : @@ -96,7 +96,7 @@ Go to `/wm_debug` to see precedent requests and debug there HTTP decision path. The debug UI can be updated automatically on the requests. -![Debug UI example](https://raw.githubusercontent.com/kbrw/ewebmachine/master/doc/debug_ui.png) +![Debug UI example](./assets/debug_ui.png) ## Use Cowboy to serve the plug diff --git a/doc/debug_ui.png b/assets/debug_ui.png similarity index 100% rename from doc/debug_ui.png rename to assets/debug_ui.png diff --git a/doc/http_diagram.png b/assets/http_diagram.png similarity index 100% rename from doc/http_diagram.png rename to assets/http_diagram.png diff --git a/lib/ewebmachine/core.utils.ex b/lib/ewebmachine/core.utils.ex index 258d0a4..f94f7d4 100644 --- a/lib/ewebmachine/core.utils.ex +++ b/lib/ewebmachine/core.utils.ex @@ -82,7 +82,7 @@ defmodule Ewebmachine.Core.Utils do @doc """ Get the string list from a comma separated list of HTTP quoted strings """ - @spec split_quoted_strings([String.t]) :: [String.t] + @spec split_quoted_strings(String.t) :: [String.t] def split_quoted_strings(str) do str |> Plug.Conn.Utils.list() |> diff --git a/lib/ewebmachine/handlers.ex b/lib/ewebmachine/handlers.ex index 241d7e1..007cca4 100644 --- a/lib/ewebmachine/handlers.ex +++ b/lib/ewebmachine/handlers.ex @@ -5,7 +5,7 @@ defmodule Ewebmachine.Handlers do @moduledoc """ Implement the functions described below to make decisions in the - [HTTP decision tree](http_diagram.png) : + [HTTP decision tree](./assets/http_diagram.png) : - `service_available/2` - `resource_exists/2` diff --git a/lib/ewebmachine/plug.debug.ex b/lib/ewebmachine/plug.debug.ex index 54c16b7..02e1414 100644 --- a/lib/ewebmachine/plug.debug.ex +++ b/lib/ewebmachine/plug.debug.ex @@ -28,7 +28,7 @@ defmodule Ewebmachine.Plug.Debug do browser will navigate to the debugging UI of the new request (you can still use back/next to navigate through requests) - ![Debug UI example](debug_ui.png) + ![Debug UI example](./assets/debug_ui.png) """ use Plug.Router diff --git a/lib/ewebmachine/plug.run.ex b/lib/ewebmachine/plug.run.ex index 7cde75f..806751f 100644 --- a/lib/ewebmachine/plug.run.ex +++ b/lib/ewebmachine/plug.run.ex @@ -1,7 +1,7 @@ defmodule Ewebmachine.Plug.Run do @moduledoc ~S""" - Plug passing your `conn` through the [HTTP decision tree](http_diagram.png) - to fill its status and response. + Plug passing your `conn` through the [HTTP decision + tree](./assets/http_diagram.png) to fill its status and response. This plug does not send the HTTP result, instead the `conn` result of this plug must be sent with the plug diff --git a/mix.exs b/mix.exs index 1fd3472..2a7b1dd 100644 --- a/mix.exs +++ b/mix.exs @@ -32,9 +32,11 @@ defmodule Ewebmachine.Mixfile do defp docs do [ + assets: "assets", extras: [ "CHANGELOG.md": [title: "Changelog"], - "README.md": [title: "Overview"] + "README.md": [title: "Overview"], + "pages/demystify_dsl.md": [title: "Demystify Ewebmachine DSL"], ], main: "readme", source_url: git_repository(), diff --git a/pages/demystify_dsl.md b/pages/demystify_dsl.md new file mode 100644 index 0000000..3032980 --- /dev/null +++ b/pages/demystify_dsl.md @@ -0,0 +1,316 @@ +# Demystify Ewebmachine DSL + +It's very likely, as a reader of this documentation, that you already wrote a a +route with `Ewebmachine` (or at least copy and pasted one), but did you ever +wonder once how does it works under the hood? Maybe you did start looking into +it and were repelled by the heavy use of macro. + +This document aims to go through some of `Ewebmachine`'s internals, in order to +explain how, from a bunch of macros, we end up with a whole Plug pipeline. + +--- + +Let's start with this small module: +```elixir +defmodule MyApi do + use Ewebmachine.Builder.Resources + + resource "/api/path" do after + allowed_methods do: ["GET"] + + defh(to_html, do: "

HTML

") + end +end +``` + +It imports the macro `Ewebmachine.Builder.Resources.resource/[3-4]` into the +scope, that we can then use to make the `/api/path` route. + +From this point on, the macro's magic starts :). + +## How do handlers (`allowed_methods` and friends) work? + +The [`resource` macro creates a module from the given +body](https://github.com/kbrw/ewebmachine/blob/b7659b9f5068cb188409d016d13635c6c4b74d6b/lib/ewebmachine/builder.resources.ex#L157-L174). + +```elixir +defmodule Ewebmachine.Builder.Resources do + defmacro resource({:__aliases__, _, route_aliases},route,do: init_block, after: body) do + resource_quote(Module.concat([__CALLER__.module|route_aliases]),route,init_block,body) + end + defmacro resource(route,do: init_block, after: body) do + resource_quote(Module.concat(__CALLER__.module,"EWM"<>route_as_mod(route)),route,init_block,body) + end + + def resource_quote(wm_module,route,init_block,body) do + quote do + @wm_routes {unquote(route), unquote(wm_module), unquote(Macro.escape(init_block))} + + defmodule unquote(wm_module) do + use Ewebmachine.Builder.Handlers + unquote(body) + plug :add_handlers + end + end + end + + # [...] +end +``` + +> #### Dynamic module {: .neutral} +> +> Dynamically named module aren't nested under their parent module. That's why +> the `resource` macro concatenates it with the caller's module. + +In this module each handler will become a function. As is, each handler is a +macro. + +The created module uses the `Ewebmachine.Builder.Handler` module. This module +defines the [list of +handlers](https://github.com/kbrw/ewebmachine/blob/b7659b9f5068cb188409d016d13635c6c4b74d6b/lib/ewebmachine/builder.handlers.ex#L91-L97) +(`allowed_methods`, etc...). For [each handler defined in this list, a +macro](https://github.com/kbrw/ewebmachine/blob/b7659b9f5068cb188409d016d13635c6c4b74d6b/lib/ewebmachine/builder.handlers.ex#L144-L152) +is created: + +```elixir +defmodule Ewebmachine.Builder.Handlers do + @resource_fun_names [ + :allowed_methods, + # [...] + ] + + for resource_fun_name<-@resource_fun_names do + Module.eval_quoted(Ewebmachine.Builder.Handlers, quote do + @doc "see `Ewebmachine.Handlers.#{unquote(resource_fun_name)}/2`" + defmacro unquote(resource_fun_name)(do_block) do + name = unquote(resource_fun_name) + handler_quote(name,do_block[:do]) + end + end) + end + + # [...] +end +``` + +Inside this macro, the called [function +`handler_quote`](https://github.com/kbrw/ewebmachine/blob/b7659b9f5068cb188409d016d13635c6c4b74d6b/lib/ewebmachine/builder.handlers.ex#L102-L110) +takes care of adding the `{name, __MODULE__}` (where `name` is the handler's +name) to the module attribute `@resource_handlers` and defining a function. + +```elixir +defmodule Ewebmachine.Builder.Handlers do + defp handler_quote(name,body,guard,conn_match,state_match) do + quote do + @resource_handlers Map.put(@resource_handlers,unquote(name),__MODULE__) + def unquote(name)(unquote(conn_match)=var!(conn),unquote(state_match)=var!(state)) when unquote(guard) do + res = unquote(body) + wrap_response(res,var!(conn),var!(state)) + end + end + end + + # [...] +end +``` + +> #### defh macro {: .info} +> +> [`defh`](https://github.com/kbrw/ewebmachine/blob/b7659b9f5068cb188409d016d13635c6c4b74d6b/lib/ewebmachine/builder.handlers.ex#L139-L142) +> macro which allows you to pass guard, works the same way underneath and calls +> `handler_quote` too. + +Great we now know how handlers are transformed into functions. + +--- + +## But how are handlers called? + +### Adding custom handlers to the connection + +The `:add_handlers` plug used by the created module takes care of adding +handler names saved into the module's attribute to the connection's private +field `:resource_handlers`. + +`use Ewebmachine.Builder.Handler` defines a `@before_compile +Ewebmachine.Builder.Handler` attributes in which the [`add_handlers` plug +function](https://github.com/kbrw/ewebmachine/blob/b7659b9f5068cb188409d016d13635c6c4b74d6b/lib/ewebmachine/builder.handlers.ex#L71-L78) +is defined: + +```elixir +defmodule Ewebmachine.Builder.Handlers do + defmacro __before_compile__(_env) do + quote do + defp add_handlers(conn, opts) do + # [ ... ] + Plug.Conn.put_private(conn, :resource_handlers, + Enum.into(@resource_handlers, conn.private[:resource_handlers] || %{})) + end + end + end +end +``` + +### Internal usage of custom handlers + +Ewebmachine [decision +tree](https://github.com/kbrw/ewebmachine/blob/b7659b9f5068cb188409d016d13635c6c4b74d6b/lib/ewebmachine/core.ex) +calls handlers when going through the tree. For instance, the +[`allowed_methods` is +call](https://github.com/kbrw/ewebmachine/blob/b7659b9f5068cb188409d016d13635c6c4b74d6b/lib/ewebmachine/core.ex#L61) +as such: + +```elixir +{methods, conn, state} = resource_call(conn, state, :allowed_methods) +``` + +To use a custom handler, `Ewebmachine` simply looks up with the handler's name, +into its private connection field `:resource_handlers` (added by the +`:add_handlers` plug), which contains a map where keys are handler's names and +values are the handler's module. If you did not define a handler it falls back +to the default one inside the `Ewebmachine.Handlers` module. + +```elixir +defmodule Ewebmachine.Core.DSL do + def resource_call(conn, state, fun) do + handler = conn.private[:resource_handlers][fun] || Ewebmachine.Handlers + {reply, conn, state} = term = apply(handler, fun, [conn, state]) + # [ ... ] + end + + # [ ... ] +end +``` + +--- + +Here is what the code would look like if we expand explained macros until now: + +```elixir +defmodule MyApi do + @before_compile Ewebmachine.Builder.Resources + use Plug.Router + import Plug.Router, only: [] + import Ewebmachine.Builder.Resources + + defp resource_match(conn, _opts) do + conn |> match(nil) |> dispatch(nil) + end + + @wm_routes [{"/api/path", MyApi.EWMApiPath, []}] +end + +defmodule MyApi.EWMApiPath do + use Plug.Builder + + @resource_handlers %{ + allowed_methods: __MODULE__, + to_html: __MODULE__ + } + + def allowed_methods(conn, state) do + res = ["GET"] + {res, conn, state} + end + + def to_html(conn, state) do + res = "

HTML

" + {res, conn, state} + end + + defp add_handlers(conn, _opts) do + # [...] + Plug.Conn.put_private(conn, :resource_handlers, + Enum.into(@resource_handlers, conn.private[:resource_handlers] || %{})) + end + + plug :add_handlers +end +``` + +### How does Ewebmachine call all of this? + +The missing piece of the puzzle is now, how does Ewebmachine call our plug +module `MyApi.EWMApiPath`. + +From the macros' expansion above, we can see that it uses the `Plug.Router`. +Moreover, the line `@before_compile Ewebmachine.Builder.Resources` isn't +expanded, let's look into it. `Ewebmachine.Builder.Resources` calls the +[`__before_compile__` +macro](https://github.com/kbrw/ewebmachine/blob/b7659b9f5068cb188409d016d13635c6c4b74d6b/lib/ewebmachine/builder.resources.ex#L102-L119) +does the following: + +```elixir +defmacro __before_compile__(_env) do + wm_routes = Module.get_attribute __CALLER__.module, :wm_routes + route_matches = for {route,wm_module,init_block}<-Enum.reverse(wm_routes) do + quote do + Plug.Router.match unquote(route) do + init = unquote(init_block) + var!(conn) = put_private(var!(conn),:machine_init,init) + unquote(wm_module).call(var!(conn),[]) + end + end + end + final_match = if !match?({"/*"<>_,_,_},hd(wm_routes)), + do: quote(do: Plug.Router.match _ do var!(conn) end) + quote do + unquote_splicing(route_matches) + unquote(final_match) + end +end +``` + +which produces a [`Plug.Router`'s +match](https://hexdocs.pm/plug/Plug.Router.html#match/3), giving us the +following once expanded: + +```elixir +defmodule MyApi do + use Plug.Router + import Plug.Router, only: [] + import Ewebmachine.Builder.Resources + + defp resource_match(conn, _opts) do + conn |> match(nil) |> dispatch(nil) + end + + @wm_routes [{"/api/path", MyApi.EWMApiPath, :irrelevant_stuff}] + + Plug.Router.match "/api/path" do + init = :irrelevant_stuff + conn = put_private(conn, :machine_init, init) + MyApiEWMApiPath.call(conn, []) + end + + Plug.Router.match _ do conn +end +``` + +The only thing left to make the whole thing work is to add a few plugs. That's +what the `Ewebmachine.Builder.Resources.resources_plugs` macro usually does, +but let's use only the required bits: + +```elixir +defmodule MyApi do + # [...] + Plug.Router.match _ do conn + + plug :resource_match + plug Ewebmachine.Plug.Run + plug Ewebmachine.Plug.Send +end +``` + +The `:resource_match` function plug finds a matching route (`match(nil)`) and +calls it if matching (`dispatch(nil)`). Once found the connection `conn` is +returned by the plug module (for instance here `MyApiEWMApiPath`), and now +contains our resource custom handlers. + +Then the `Ewebmachine.Plug.Run` plug, which contains the `Ewebmachine`'s +decision tree, is called, and its behaviour will change based on our custom +handlers. + +Finally, the `Ewebmachine.Plug.Send` plug is called and sends the response if +the connection wasn't halted before.