Packages

Ewebmachine contains macros and plugs to allow you to compose HTTP decision handlers and run the HTTP decision tree to get your HTTP response. This project is a rewrite for Elixir and Plug of basho webmachine.

Current section

Files

Jump to
ewebmachine lib builders.ex
Raw

lib/builders.ex

defmodule Ewebmachine.Builder.Handlers do
@moduledoc """
`use` this module will `use Plug.Builder` (so a plug pipeline
described with the `plug module_or_function_plug` macro), but gives
you an `:add_handler` local function plug which adds to the conn
the locally defined ewebmachine handlers (see `Ewebmachine.Handlers`).
So :
- Construct your automate decision handler through multiple `:add_handler` plugs
- Pipe the plug `Ewebmachine.Plug.Run` to run the HTTP automate which
will call these handlers to take decisions.
- Pipe the plug `Ewebmachine.Plug.Send` to send and halt any conn previsously passed
through an automate run.
To define handlers, use the following helpers :
- the handler specific macros (like `Ewebmachine.Builder.Handlers.resource_exists/1`)
- the macro `defh/2` to define any helpers, usefull for body
producing handlers or to have multiple function clauses
- in handler implementation `conn` and `state` binding are available
- the response of the handler implementation is wrapped, so that
returning `:my_response` is the same as returning `{:my_response,conn,state}`
Below a full example :
```
defmodule MyJSONApi do
use Ewebmachine.Builder.Handlers
plug :cors
plug :add_handlers, init: %{}
content_types_provided do: ["application/json": :to_json]
defh to_json, do: Poison.encode!(state[:json_obj])
defp cors(conn,_), do:
put_resp_header(conn,"Access-Control-Allow-Origin","*")
end
defmodule GetUser do
use Ewebmachine.Builder.Handlers
plug MyJSONApi
plug :add_handlers
plug Ewebmachine.Plug.Run
plug Ewebmachine.Plug.Send
resource_exists do:
pass( !is_nil(user=DB.User.get(conn.params["q"])), json_obj: user)
end
defmodule GetOrder do
use Ewebmachine.Builder.Handlers
plug MyJSONApi
plug :add_handlers
plug Ewebmachine.Plug.Run
plug Ewebmachine.Plug.Send
resource_exists do:
pass(!is_nil(order=DB.Order.get(conn.params["q"])), json_obj: order)
end
defmodule API do
use Plug.Router
plug :match
plug :dispatch
get "/get/user", do: GetUser.call(conn,[])
get "/get/order", do: GetOrder.call(conn,[])
end
```
"""
defmacro __before_compile__(_env) do
quote do
defp add_handlers(conn, opts) do
if opts && (init=opts[:init]), do:
conn = put_private(conn,:machine_init,init)
Plug.Conn.put_private(conn,:resource_handlers,
Enum.into(@resource_handlers,conn.private[:resource_handlers] || %{}))
end
end
end
defmacro __using__(_opts) do
quote location: :keep do
use Plug.Builder
import Ewebmachine.Builder.Handlers
@before_compile Ewebmachine.Builder.Handlers
@resource_handlers %{}
ping do: :pong
end
end
@resource_fun_names [
:resource_exists,:service_available,:is_authorized,:forbidden,:allow_missing_post,:malformed_request,:known_methods,
:base_uri,:uri_too_long,:known_content_type,:valid_content_headers,:valid_entity_length,:options,:allowed_methods,
:delete_resource,:delete_completed,:post_is_create,:create_path,:process_post,:content_types_provided,
:content_types_accepted,:charsets_provided,:encodings_provided,:variances,:is_conflict,:multiple_choices,
:previously_existed,:moved_permanently,:moved_temporarily,:last_modified,:expires,:generate_etag, :ping
]
defp sig_to_sigwhen({:when,_,[{name,_,params},guard]}), do: {name,params,guard}
defp sig_to_sigwhen({name,_,params}) when is_list(params), do: {name,params,true}
defp sig_to_sigwhen({name,_,_}), do: {name,[quote(do: _),quote(do: _)],true}
defp handler_quote(name,body,guard,conn_match,state_match) do
quote do
@resource_handlers Dict.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
defp handler_quote(name,body) do
handler_quote(name,body,true,quote(do: _),quote(do: _))
end
@doc """
define a resource handler function as described at
`Ewebmachine.Handlers`.
Since there is a specific macro in this module for each handler,
this macro is useful :
- to define body producing and body processing handlers (the one
referenced in the response of `Ewebmachine.Handlers.content_types_provided/2` or
`Ewebmachine.Handlers.content_types_accepted/2`)
- to explicitly take the `conn` and the `state` parameter, which
allows you to add guards and pattern matching for instance to
define multiple clauses for the handler
```
defh to_html, do: "hello you"
defh from_json, do: pass(:ok, json: Poison.decode!(read_body conn))
```
```
defh resources_exists(conn,%{obj: obj}) when obj !== nil, do: true
defh resources_exists(conn,_), do: false
```
"""
defmacro defh(signature,do_block) do
{name,[conn_match,state_match],guard} = sig_to_sigwhen(signature)
handler_quote(name,do_block[:do],guard,conn_match,state_match)
end
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
@doc false
def wrap_response({_,%Plug.Conn{},_}=tuple,_,_), do: tuple
def wrap_response(r,conn,state), do: {r,conn,state}
@doc """
Shortcut macro for :
{response,var!(conn),Enum.into(update_state,var!(state))}
use it if your handler wants to add some value to a collectable
state (a map for instance), but using default "conn" current
binding.
for instance a resources_exists implementation "caching" the result
in the state could be :
pass (user=DB.get(state.id)) != nil, current_user: user
# same as returning :
{true,conn,%{id: "arnaud", current_user: %User{id: "arnaud"}}}
"""
defmacro pass(response,update_state) do
quote do
{unquote(response),var!(conn),Enum.into(unquote(update_state),var!(state))}
end
end
end
defmodule Ewebmachine.Builder.Resources do
@moduledoc ~S"""
`use` this module will `use Plug.Builder` (so a plug pipeline
described with the `plug module_or_function_plug` macro), but gives
you a `:resource_match` local function plug which matches routes declared
with the `resource/2` macro and execute the plug defined by its body.
See `Ewebmachine.Builder.Handlers` documentation to see how to
contruct these modules (in the `after` block)
Below a full example :
```
defmodule FullApi do
use Ewebmachine.Builder.Resources
if Mix.env == :dev, do: plug Ewebmachine.Plug.Debug
# pre plug, for instance you can put plugs defining common handlers
plug :resource_match
plug Ewebmachine.Plug.Run
# customize ewebmachine result, for instance make an error page handler plug
plug Ewebmachine.Plug.Send
# plug after that will be executed only if no ewebmachine resources has matched
resource "/hello/:name" do %{name: name} after
plug SomeAdditionnalPlug
content_types_provided do: ['application/xml': :to_xml]
defh to_xml, do: "<Person><name>#{state.name}</name>"
end
resource "/*path" do %{path: Enum.join(path,"/")} after
resource_exists do:
File.regular?(path state.path)
content_types_provided do:
[{state.path|>Plug.MIME.path|>default_plain,:to_content}]
defh to_content, do:
File.stream!(path(state.path),[],300_000_000)
defp path(relative), do: "#{:code.priv_dir :ewebmachine_example}/web/#{relative}"
defp default_plain("application/octet-stream"), do: "text/plain"
defp default_plain(type), do: type
end
end
```
"""
defmacro __using__(_) do
quote location: :keep do
use Plug.Router
import Plug.Router, only: []
import Ewebmachine.Builder.Resources
@before_compile Ewebmachine.Builder.Resources
defp resource_match(conn, _opts) do
conn |> match(nil) |> dispatch(nil)
end
end
end
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
defp remove_first(":"<>e), do: e
defp remove_first("*"<>e), do: e
defp remove_first(e), do: e
defp route_as_mod("/"), do: Root
defp route_as_mod(route), do:
(route |> String.split("/") |> Enum.map(& &1 |> remove_first |> String.capitalize) |> Enum.join("."))
@doc ~S"""
Create a webmachine handler plug and use it on `:resource_match` when path matches
- the route will be the matching spec (see Plug.Router.matc, string spec only)
- do_block will be called on match (so matching bindings will be
available) and should return the initial state
- after_block will be the webmachine handler plug module body
(wrapped with `use Ewebmachine.Builder.Handlers` and `plug
:add_handlers` to clean the declaration.
```
resource "/my/route/:commaid" do
id = String.split(commaid,",")
%{foo: id}
after
plug SomeAdditionnalPlug
resource_exists do: state.id == ["hello"]
end
```
"""
defmacro resource(route,do: init_block, after: body) do
wm_module = Module.concat(__CALLER__.module,route_as_mod(route))
old_wm_routes = Module.get_attribute(__CALLER__.module, :wm_routes) || []
Module.put_attribute __CALLER__.module, :wm_routes, [{route,wm_module,init_block}|old_wm_routes]
quote do
defmodule unquote(wm_module) do
use Ewebmachine.Builder.Handlers
unquote(body)
plug :add_handlers
end
end
end
end