lib/ewebmachine/builder.resources.ex

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
  ```

  ## Common Plugs macro helper

  As the most common use case is to match resources, run the webmachine
  automate, then set a 404 if no resource match, then handle error code, then
  send the response, the `resources_plugs/1` macro allows you to do that.

  For example, if you want to convert all HTTP errors as Exceptions, and
  consider that all path must be handled and so any non matching path should
  return a 404 :

      resources_plugs error_as_exception: true, nomatch_404: true

  is equivalent to

      plug :resource_match
      plug Ewebmachine.Plug.Run
      plug :wm_notset_404
      plug Ewebmachine.Plug.ErrorAsException
      plug Ewebmachine.Plug.Send

      defp wm_notset_404(%{state: :unset}=conn,_), do: resp(conn,404,"")
      defp wm_notset_404(conn,_), do: conn

  Another example, following plugs must handle non matching paths and errors
  should be converted into `GET /error/:status` that must be handled by
  following plugs :

      resources_plugs error_forwarding: "/error/:status"

  is equivalent to

      plug :resource_match
      plug Ewebmachine.Plug.Run
      plug Ewebmachine.Plug.ErrorAsForward, forward_pattern: "/error/:status"
      plug Ewebmachine.Plug.Send
  """
  defmacro __using__(opts) do
    quote location: :keep do
      @before_compile Ewebmachine.Builder.Resources
      use Plug.Router
      import Plug.Router, only: []
      import Ewebmachine.Builder.Resources

      Module.register_attribute(__MODULE__, :wm_routes, accumulate: true)

      if unquote(opts[:default_plugs]) do
        plug :resource_match
        plug Ewebmachine.Plug.Run
        plug Ewebmachine.Plug.Send
      end

      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(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.match, 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

  resource ShortenedRouteName, "/my/route/that/would/generate/a/long/module/name/:commaid" do
    id = String.split(commaid,",")
    %{foo: id}
  after
    plug SomeAdditionnalPlug
    resource_exists do: state.id == ["hello"]
  end
  ```

  """
  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

  alias Ewebmachine.Plug.ErrorAsException
  alias Ewebmachine.Plug.ErrorAsForward
  defmacro resources_plugs(opts \\ []) do
    {errorplug,errorplug_params} = cond do
      opts[:error_as_exception]->{ErrorAsException,[]}
      (forward_pattern=opts[:error_forwarding])->{ErrorAsForward,[forward_pattern: forward_pattern]}
      true -> {false,[]}
    end
    quote do
      plug :resource_match
      plug Ewebmachine.Plug.Run
      if unquote(opts[:nomatch_404]), do: plug :wm_notset_404
      if unquote(errorplug), do: plug(unquote(errorplug),unquote(errorplug_params))
      plug Ewebmachine.Plug.Send

      defp wm_notset_404(%{state: :unset}=conn,_), do: resp(conn,404,"")
      defp wm_notset_404(conn,_), do: conn
    end
  end
end