diff --git a/.formatter.exs b/.formatter.exs
new file mode 100644
index 0000000..bca9e1d
--- /dev/null
+++ b/.formatter.exs
@@ -0,0 +1,52 @@
+locals_without_parens = [
+ resource: 4,
+ resource: 3,
+ defh: 2,
+ pass: 2,
+
+ # Handlers
+ allowed_methods: 1,
+ allow_missing_post: 1,
+ base_uri: 1,
+ charsets_provided: 1,
+ content_types_accepted: 1,
+ content_types_provided: 1,
+ create_path: 1,
+ delete_completed: 1,
+ delete_resource: 1,
+ encodings_provided: 1,
+ expires: 1,
+ forbidden: 1,
+ generate_etag: 1,
+ is_authorized: 1,
+ is_conflict: 1,
+ known_content_type: 1,
+ known_methods: 1,
+ language_available: 1,
+ last_modified: 1,
+ malformed_request: 1,
+ moved_permanently: 1,
+ moved_temporarily: 1,
+ multiple_choices: 1,
+ options: 1,
+ ping: 1,
+ post_is_create: 1,
+ previously_existed: 1,
+ process_post: 1,
+ resource_exists: 1,
+ service_available: 1,
+ uri_too_long: 1,
+ validate_content_checksum: 1,
+ valid_content_headers: 1,
+ valid_entity_length: 1,
+ variances: 1
+]
+
+[
+ import_deps: [
+ :plug
+ ],
+ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
+ locals_without_parens: locals_without_parens ++ [decision: 2],
+ export: [locals_without_parens: locals_without_parens]
+]
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 0000000..51086f7
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1 @@
+ad87df770d2be55ab1becbc05de5d9edf961bad2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fc9c2a..5b84bbf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,37 @@
# Changelog
+## Unreleased
+
+## [2.4.0] (2025-12-08)
+
+### Added
+
+- documentation for the handler given to the handlers `content_types_provided`
+ and `content_types_accepted`
+
+### Changed
+
+- Project documentation was rewrote
+- `Ewebmachine.Plug.Debug`, `Ewebmachine.Plug.ErrorAsException`,
+ `Ewebmachine.Plug.ErrorAsForward`, `Ewebmachine.Plug.Run`, and
+ `Ewebmachine.Plug.Send` now implement the `Plug` behaviour. Nothing changed in
+ their implementation.
+- `Ewebmachine.Builder.Resources.resource_quote/4` is now a private function
+- Default `Ewebmachine.Handlers.to_html/2` handler now respond with an empty
+ string instead of with `"
Hello World
"`
+
+### Removed
+
+- Publicly exposed function and macro intended for testing purposes:
+ - `Ewebmachine.Handlers.ping/2` (function)
+ - `Ewebmachine.Builder.Handlers.ping/1` (macro)
+- Ewebmachine no longer depends on `plug_cowboy`
+
+### Fixed
+
+- Crash when creating an exception `Ewebmachine.Plug.ErrorAsException` with a
+ `resp_body` which isn't a `bitstring`.
+
## [2.3.3] (2025-09-22)
### Changed
diff --git a/README.md b/README.md
index 44dd016..bffa6dd 100644
--- a/README.md
+++ b/README.md
@@ -1,129 +1,159 @@
# Ewebmachine [](https://github.com/kbrw/ewebmachine/actions/workflows/build-and-test.yml) [](https://hex.pm/packages/ewebmachine) [](https://hexdocs.pm/ewebmachine) 
Ewebmachine is a full rewrite with clean DSL and plug integration
-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](./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 :
-
-- `Ewebmachine.Plug.Run` go through the HTTP decision tree and fill
- the `conn` response according to it
-- `Ewebmachine.Plug.Send` is used to send a conn set with `Ewebmachine.Plug.Run`
-- `Ewebmachine.Plug.Debug` gives you a debugging web UI to see the
- HTTP decision path taken by each request.
-- `Ewebmachine.Plug.ErrorAsException` take a conn with a response set but not
- send, and throw an exception is the status code is an exception
-- `Ewebmachine.Plug.ErrorAsForward` take a conn with a response set but not
- send, and forward it changing the request to `GET /error/pattern/:status`
-- `Ewebmachine.Builder.Handlers` gives you helpers macros and a
- `:add_handler` plug to add `handlers` as defined in
- `Ewebmachine.Handlers` to your conn, and set the initial user state.
-- `Ewebmachine.Builder.Resources` gives you a `resource` macro to
- define at the same time an `Ewebmachine.Builder.Handlers` and the
- matching spec to use it, and a plug `:resource_match` to do the
- match and execute the associated plug. The macro `resources_plugs` helps you
- to define common plug pipeline
-
-## Example usage
+based on [Webmachine from basho](https://github.com/webmachine/webmachine).
+
+[Documentation for Ewebmachine is available on
+hexdocs](https://hexdocs.pm/ewebmachine/readme.html).
+
+## Installation
```elixir
-defmodule MyJSONApi do
- use Ewebmachine.Builder.Handlers
- plug :cors
- plug :add_handlers, init: %{}
+ def deps do
+ [
+ {:ewebmachine, "~> 2.3"}
+ ]
+ end
+```
- content_types_provided do: ["application/json": :to_json]
- defh to_json, do: Poison.encode!(state[:json_obj])
+> [!WARNING]
+> 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.
- defp cors(conn,_), do:
- put_resp_header(conn,"Access-Control-Allow-Origin","*")
-end
+## Overview
+Ewebmachine is an application layer that adds HTTP semantic awareness on top of
+the excellent `Plug` library. Ewebmachine's DSL provides an easy way to work
+with your application's data.
-defmodule ErrorRoutes do
- use Ewebmachine.Builder.Resources ; resources_plugs
- resource "/error/:status" do %{s: elem(Integer.parse(status),0)} after
- content_types_provided do: ['text/html': :to_html, 'application/json': :to_json]
- defh to_html, do: " Error ! : '#{Ewebmachine.Core.Utils.http_label(state.s)}'
"
- defh to_json, do: ~s/{"error": #{state.s}, "label": "#{Ewebmachine.Core.Utils.http_label(state.s)}"}/
- finish_request do: {:halt,state.s}
- end
-end
+## Core concepts
+
+Ewebmachine comes with a few core concepts:
+* **Resource** (a.k.a. any information that can be named): is declared via the
+ `Ewebmachine.Builder.Resources.resource/2` macro which expands into a plug
+ module inside of which handlers are declared.
-defmodule FullApi do
+* **Handler**: an handler is a function which return value is used by the
+ decision tree. Each handler has its own purpose and helps you make the
+ appropriate response.
+
+* **(HTTP) Decision tree**: process an HTTP connection by routing it through the
+ decision tree where decisions are taken based on a resource's handlers. Its
+ processing flow is described on [this schema](./assets/http_diagram.png)[^1].
+
+With the core concepts in mind let's go through a simple example:
+```elixir
+defmodule App.Api do
use Ewebmachine.Builder.Resources
- if Mix.env == :dev, do: plug Ewebmachine.Plug.Debug
- resources_plugs error_forwarding: "/error/:status", nomatch_404: true
- plug ErrorRoutes
- resource "/hello/:name" do %{name: name} after
- content_types_provided do: ['application/xml': :to_xml]
- defh to_xml, do: "#{state.name}"
- end
+ plug :resource_match
+ plug Ewebmachine.Plug.Run
+ plug Ewebmachine.Plug.Send
+
+ resource "/api/users/:id" do
+ %{user_id: id}
+ after
+ plug App.Api.CommonHandlers
- resource "/hello/json/:name" do %{name: name} after
- plug MyJSONApi #this is also a plug pipeline
- allowed_methods do: ["GET","DELETE"]
- delete_resource do: DB.delete(state.name)
+ allowed_methods do: ["GET"]
- defh resource_exists do
- user = DB.get(state.name)
- pass(user !== nil, json_obj: user)
+ resource_exists do
+ case App.User.fetch(state.user_id) do
+ {:ok, user} -> pass(true, json_obj: user)
+ {:error, :not_found} -> false
+ end
end
+
+ defh to_json, do: JSON.encode!(state.user)
end
- resource "/static/*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
+ resource "/api/orders/:id" do
+ %{order_id: id}
+ after
+ plug App.Api.CommonHandlers
+
+ allowed_methods do: ["GET"]
+
+ resource_exists do
+ case App.Order.fetch(state.order_id) do
+ {:ok, order} -> pass(true, json_obj: order)
+ {:error, :not_found} -> false
+ end
+ end
end
end
+
+defmodule App.Api.CommonHandlers do
+ use Ewebmachine.Builder.Handlers
+
+ plug :add_handlers, init: %{}
+
+ content_types_provided do: ["application/json": :to_json]
+
+ defh to_json, do: JSON.encode!(state.json_obj)
+end
```
-## Debug UI
+Let's go through this example by calling the `App.Api` plug module with a fake
+connection:
+```elixir
+conn = Plug.Test.conn("GET", "/api/users/1")
+conn = App.Api.call(conn, [])
+```
-Go to `/wm_debug` to see precedent requests and debug there HTTP
-decision path. The debug UI can be updated automatically on the
-requests.
+The `App.Api` module is a `Plug.Router` composed of the routes `/api/users/:id`
+and `/api/orders/:id` which are defined via the `resource` macro. When our
+connection enter this plug pipeline it first goes through the `:resource_match`
+plug which calls the plugs `:match` and `:dispatch` defined by `Plug.Router`.
+The `/api/users/:id` path being matched, the connection is dispatched to this
+`resource`.
-
+*The resource macro underneath makes a plug module into which the
+`:add_handlers` plug is added*.
+
+The connection goes through the resource's plug module which contains the
+`App.Api.CommonHandlers` and `:add_handlers` plug.
-## Use Cowboy to serve the plug
+*The `:add_handlers` function plug sets inside the connection the module
+implementing each handler. In this example the `allowed_methods` and
+`resource_exists` handlers will be those of the resource module whereas the
+module used for the definition of the `content_types_provided` handler will be
+`App.Api.CommonHandlers`.*
-Create a simple supervision tree with only the Cowboy server adapter spec.
+The connection continues through the plug pipeline and then enters the
+`Ewebmachine.Plug.Run` plug which executes the decision tree with the set
+handlers. The response is then sent by the `Ewebmachine.Plug.Send` plug.
+## Serve the plug
+
+Ewebmachine exposes plugs therefore it can be served as any other. As referenced
+by [`Plug` itself](https://hexdocs.pm/plug/readme.html#installation) there is
+two options either `plug_cowboy` or `bandit`.
+
+For instance to serve the previous `App.Api` plug with the `plug_cowboy`:
```elixir
-defmodule MyApp do
+defmodule App do
use Application
- def start(_type, _args), do:
- Supervisor.start_link([
- Plug.Cowboy.child_spec(:http,FullApi,[], port: 4000)
- ], strategy: :one_for_one)
-end
-```
-And add it as your application entry point in your `mix.exs`
+ def start(_type, _args) do
+ children = [Plug.Cowboy.child_spec(:http, App.Api, [], port: 80)]
-```elixir
-def application do
- [applications: [:logger,:ewebmachine,:cowboy], mod: {MyApp,[]}]
+ Supervisor.start_link(children, strategy: :one_for_one)
+ end
end
-defp deps, do:
- [{:ewebmachine, "2.3.2"}, {:cowboy, "~> 1.0"}]
```
+## Debug UI
+
+By adding the `Ewebmachine.Plug.Debug` plug to the plug pipeline of your
+application you can have access to a debug UI exposed on the `/wm_debug` route.
+
+
+
# CONTRIBUTING
Hi, and thank you for wanting to contribute.
Please refer to the centralized informations available at: https://github.com/kbrw#contributing
+[^1]: All credits for this schema goes to Alan Dean and Justin Sheehy. See
+ [here](https://github.com/webmachine/webmachine/wiki/Diagram).
diff --git a/examples/hello/.gitignore b/examples/hello/.gitignore
deleted file mode 100644
index cbc15fd..0000000
--- a/examples/hello/.gitignore
+++ /dev/null
@@ -1,19 +0,0 @@
-# The directory Mix will write compiled artifacts to.
-/_build
-
-# If you run "mix test --cover", coverage assets end up here.
-/cover
-
-# The directory Mix downloads your dependencies sources to.
-/deps
-
-# Where 3rd-party dependencies like ExDoc output generated docs.
-/doc
-
-# If the VM crashes, it generates a dump, let's ignore it too.
-erl_crash.dump
-
-# Also ignore archive artifacts (built via "mix archive.build").
-*.ez
-
-/mix.lock
\ No newline at end of file
diff --git a/examples/hello/README.md b/examples/hello/README.md
deleted file mode 100644
index 89c6d67..0000000
--- a/examples/hello/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# Example application for ewebmachine
-
-## URLs
-
-* `/hello/:name`
-* `/hello/json/:name`
-* `/static/*path`
-
-
diff --git a/examples/hello/config/config.exs b/examples/hello/config/config.exs
deleted file mode 100644
index d92d716..0000000
--- a/examples/hello/config/config.exs
+++ /dev/null
@@ -1,30 +0,0 @@
-# This file is responsible for configuring your application
-# and its dependencies with the aid of the Mix.Config module.
-import Config
-
-# This configuration is loaded before any dependency and is restricted
-# to this project. If another project depends on this project, this
-# file won't be loaded nor affect the parent project. For this reason,
-# if you want to provide default values for your application for
-# 3rd-party users, it should be done in your "mix.exs" file.
-
-# You can configure for your application as:
-#
-# config :hello, key: :value
-#
-# And access this configuration in your application as:
-#
-# Application.get_env(:hello, :key)
-#
-# Or configure a 3rd-party app:
-#
-# config :logger, level: :info
-#
-
-# It is also possible to import configuration files, relative to this
-# directory. For example, you can emulate configuration per environment
-# by uncommenting the line below and defining dev.exs, test.exs and such.
-# Configuration from the imported file will override the ones defined
-# here (which is why it is important to import them last).
-#
-# import_config "#{Mix.env}.exs"
diff --git a/examples/hello/lib/hello.ex b/examples/hello/lib/hello.ex
deleted file mode 100644
index f418055..0000000
--- a/examples/hello/lib/hello.ex
+++ /dev/null
@@ -1,123 +0,0 @@
-defmodule Hello do
- defmodule App do
- use Application
-
- def start(_type, _args) do
- Supervisor.start_link([
- Plug.Cowboy.child_spec(scheme: :http, plug: Hello.Api, options: [port: 4000]),
- Hello.Db
- ], strategy: :one_for_one)
- end
- end
-
- defmodule Db do
- def child_spec(_), do: %{
- id: __MODULE__,
- start: {__MODULE__, :start_link, []},
- type: :worker,
- restart: :permanent,
- shutdown: 500
- }
- def start_link, do: Agent.start_link(&Map.new/0, name: __MODULE__)
- def get(id), do: Agent.get(__MODULE__, &(Map.get(&1, id, nil)))
- def put(id, val), do: Agent.update(__MODULE__, &(Map.put(&1, id, val)))
- def delete(id), do: Agent.update(__MODULE__, &(Map.delete(&1, id)))
- end
-
- defmodule ApiCommon do
- use Ewebmachine.Builder.Handlers
- plug :cors
- plug :add_handlers
-
- content_types_provided do: ["application/json": :to_json]
- defh to_json(conn, state), do: {Poison.encode!(state[:json_obj]), conn, state}
-
- defp cors(conn, _) do
- put_resp_header(conn, "Access-Control-Allow-Origin", "*")
- end
- end
-
- defmodule Api do
- use Ewebmachine.Builder.Resources
- plug Ewebmachine.Plug.Debug
-
- resources_plugs nomatch_404: true
-
- resource "/hello/:name" do %{name: name} after
- content_types_provided do: ['application/xml': :to_xml]
- defh to_xml(conn, state), do: {"#{state.name}", conn, state}
- end
-
- resource "/hello/json/:name" do %{name: name} after
- plug ApiCommon # this is also a plug pipeline
-
- allowed_methods do: ["GET", "PUT", "DELETE"]
- content_types_accepted do: ['application/json': :from_json]
-
- defh resource_exists(conn, state) do
- case Hello.Db.get(state.name) do
- nil -> {false, conn, state}
- user -> {true, conn, Map.put(state, :json_obj, user)}
- end
- end
-
- defh delete_resource(conn, state), do: {Hello.Db.delete(state.name), conn, state}
-
- defh from_json(conn, state) do
- value = conn |> Ewebmachine.fetch_req_body([]) |> Ewebmachine.req_body |> Poison.decode!
- _ = Hello.Db.put(state.name, value)
- {true, conn, state}
- end
- end
-
- resource "/new" do %{} after
- plug ApiCommon #this is also a plug pipeline
-
- allowed_methods do: ["POST"]
- content_types_accepted do: ['application/json': :from_json]
- post_is_create do: true
-
- defh create_path(conn, state), do: {state.newpath, conn, state}
-
- defh from_json(conn, state) do
- value = conn |> Ewebmachine.fetch_req_body([]) |> Ewebmachine.req_body() |> Poison.decode!()
- newpath = "#{:io_lib.format("~9..0b", [:rand.uniform(999999999)])}"
- _ = Hello.Db.put(value["id"], value)
- {true, conn, Map.put(state, :newpath, newpath)}
- end
- end
-
- resource "/new_with_redirect" do %{} after
- plug ApiCommon #this is also a plug pipeline
-
- allowed_methods do: ["POST"]
- content_types_accepted do: ['application/json': :from_json]
- post_is_create do: true
-
- defh create_path(conn, state), do: {state.newpath, conn, state}
-
- defh from_json(conn, state) do
- value = conn |> Ewebmachine.fetch_req_body([]) |> Ewebmachine.req_body() |> Poison.decode!()
- newpath = "#{:io_lib.format("~9..0b", [:rand.uniform(999999999)])}"
- _ = Hello.Db.put(value["id"], value)
- conn = Plug.Conn.put_private(conn, :resp_redirect, true)
- {true, conn, Map.put(state, :newpath, newpath)}
- end
- end
-
- resource "/static/*path" do %{path: Enum.join(path, "/")} after
- resource_exists do: File.regular?(path(state.path))
- content_types_provided do: [ {state.path |> MIME.from_path() |> default_plain, :to_content} ]
-
- defh to_content(conn, state) do
- body = File.stream!( path(state.path), [], 300_000_000)
- {body, conn, state}
- end
-
- defp path(relative), do: "#{:code.priv_dir(:ewebmachine)}/static/#{relative}"
-
- defp default_plain("application/octet-stream"), do: "text/plain"
- defp default_plain(type), do: type
- end
- end
-end
diff --git a/examples/hello/mix.exs b/examples/hello/mix.exs
deleted file mode 100644
index 463144e..0000000
--- a/examples/hello/mix.exs
+++ /dev/null
@@ -1,27 +0,0 @@
-defmodule Hello.Mixfile do
- use Mix.Project
-
- def project do
- [app: :hello,
- version: "0.1.0",
- elixir: "~> 1.3",
- build_embedded: Mix.env == :prod,
- start_permanent: Mix.env == :prod,
- deps: deps()]
- end
-
- def application do
- [
- applications: [:logger, :ewebmachine, :cowboy, :poison],
- mod: {Hello.App, []}
- ]
- end
-
- defp deps do
- [
- {:ewebmachine, path: "../.."},
- {:cowboy, ">= 1.0.4"},
- {:poison, "~> 3.0.0"}
- ]
- end
-end
diff --git a/lib/ewebmachine.ex b/lib/ewebmachine.ex
index c4b998b..bcfab33 100644
--- a/lib/ewebmachine.ex
+++ b/lib/ewebmachine.ex
@@ -1,35 +1,38 @@
defmodule Ewebmachine do
- @moduledoc (File.read!("README.md") |>
- String.replace(~r/^See the \[generated.*$/m, "") |>
- String.replace(~r/^.*Build Status.*$/m, "") |>
- String.replace("https://raw.githubusercontent.com/kbrw/ewebmachine/master/doc/", ""))
+ @moduledoc """
+ Contains utility functions to work with Ewebmachine private fields set inside `conn`
+ """
alias Plug.Conn
@doc """
- Set :resp_redirect to `true`
+ Sets `:resp_redirect` to `true`
"""
- @spec do_redirect(Plug.Conn.t) :: Plug.Conn.t
+ @spec do_redirect(Plug.Conn.t()) :: Plug.Conn.t()
def do_redirect(conn) do
Conn.put_private(conn, :resp_redirect, true)
end
@doc """
- Returns request body from request (requires fetching body first)
+ Returns request body from request (requires fetching body first).
+
+ See `fetch_req_body/2` to fetch the body.
"""
- @spec req_body(Plug.Conn.t) :: binary
+ @spec req_body(Plug.Conn.t()) :: binary
def req_body(conn), do: conn.private[:req_body]
@doc """
- Fetch request body
+ Fetches the request body.
- Options:
- * max_length: maximum bytes to fetch (default: 1_000_000)
+ Options:
+ * `max_length` - maximum bytes to fetch. Defaults to: `1_000_000`.
"""
- @spec fetch_req_body(Plug.Conn.t, Enumerable.t) :: Plug.Conn.t
+ @spec fetch_req_body(Plug.Conn.t(), Enumerable.t()) :: Plug.Conn.t()
def fetch_req_body(conn, opts) do
- if conn.private[:req_body] do conn else
- {:ok, body, conn} = Conn.read_body(conn, length: (opts[:max_length] || 1_000_000))
+ if conn.private[:req_body] do
+ conn
+ else
+ {:ok, body, conn} = Conn.read_body(conn, length: opts[:max_length] || 1_000_000)
Conn.put_private(conn, :req_body, body)
end
end
diff --git a/lib/ewebmachine/app.ex b/lib/ewebmachine/app.ex
index 9d6e7dc..bd63fac 100644
--- a/lib/ewebmachine/app.ex
+++ b/lib/ewebmachine/app.ex
@@ -1,10 +1,14 @@
defmodule Ewebmachine.App do
- @moduledoc false
+ @moduledoc false
use Application
- def start(_,_) do
- Supervisor.start_link([
- Ewebmachine.Log,
- Ewebmachine.Events
- ], strategy: :one_for_one)
+
+ def start(_, _) do
+ Supervisor.start_link(
+ [
+ Ewebmachine.Log,
+ Ewebmachine.Events
+ ],
+ strategy: :one_for_one
+ )
end
end
diff --git a/lib/ewebmachine/builder.handlers.ex b/lib/ewebmachine/builder.handlers.ex
index 833ad0b..e419a1d 100644
--- a/lib/ewebmachine/builder.handlers.ex
+++ b/lib/ewebmachine/builder.handlers.ex
@@ -1,125 +1,138 @@
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
- ```
+ Provides macros to define your handlers, usually inside a
+ `Ewebmachine.Builder.Resources.resource/2`.
+
+ This module defines:
+ * a specific macro for each handler defined in `Ewebmachine.Builder.Handlers`
+ see the [Handlers](#handlers) section.
+ * the `defh/2` macro to define handler
+ * the `pass/2` macro to ease handler response
+
+ > #### `use Ewebmachine.Builder.Handlers` {: .info}
+ >
+ > When you `use Ewebmachine.Builder.Handlers`, it will `use Plug.Builder`
+ > and define a function plug `:add_handlers`. This plug will register every
+ > defined handlers inside the calling module into `conn`. Those registered
+ > handler will then be used by the `Ewebmachine.Plug.Run` plug.
+
+ > #### Declare and use common handlers {: .tip}
+ >
+ > If you want to add common handlers that should be used for several
+ > `Ewebmachine.Builder.Resources.resource/2`s, you can do so by making a plug
+ > module with your common handlers and use the `:add_handlers` plug.
+ > The module implementing an handler is saved inside the `conn` and is then
+ > used by the `Ewebmachine.Plug.Run` plug to call the appropriate one.
+ > For instance if you define the following plug module:
+ > ```elixir
+ > defmodule CommonHandlers
+ > use Ewebmachine.Builder.Handlers
+ > plug :add_handlers
+ >
+ > content_types_provided ["application/json": :to_json]
+ > defh to_json do
+ > JSON.encode!(state.response)
+ > end
+ > end
+ > ```
+ > and add it to your application's plug pipeline then the module implementing
+ > the handler `content_types_provided` will be `CommonHandlers` instead of
+ > the default `Ewebmachine.Handlers`.
"""
defmacro __before_compile__(_env) do
quote do
defp add_handlers(conn, opts) do
- conn = case Access.fetch(opts, :init) do
- {:ok, init} when not (init in [false, nil]) -> put_private(conn, :machine_init, init)
- _ -> conn
- end
- Plug.Conn.put_private(conn, :resource_handlers,
- Enum.into(@resource_handlers, conn.private[:resource_handlers] || %{}))
+ conn =
+ case Access.fetch(opts, :init) do
+ {:ok, init} when init not in [false, nil] -> put_private(conn, :machine_init, init)
+ _ -> conn
+ end
+
+ 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
+
+ if Mix.env() == :test do
+ defh ping(_, _), do: :pong
+ end
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, :finish_request
+ :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,
+ :finish_request
]
- 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
- conn_match = case var_in_patterns?(conn_match, :conn) do
- true -> conn_match
- false -> quote(do: unquote(conn_match) = var!(conn))
- end
+ 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
+ conn_match =
+ case var_in_patterns?(conn_match, :conn) do
+ true -> conn_match
+ false -> quote(do: unquote(conn_match) = var!(conn))
+ end
- state_match = case var_in_patterns?(state_match, :state) do
- true -> state_match
- false -> quote(do: unquote(state_match) = var!(state))
- end
+ state_match =
+ case var_in_patterns?(state_match, :state) do
+ true -> state_match
+ false -> quote(do: unquote(state_match) = var!(state))
+ end
quote do
- @resource_handlers Map.put(@resource_handlers,unquote(name),__MODULE__)
+ @resource_handlers Map.put(@resource_handlers, unquote(name), __MODULE__)
def unquote(name)(unquote(conn_match), unquote(state_match)) when unquote(guard) do
res = unquote(body)
- wrap_response(res,var!(conn),var!(state))
+ 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: _))
+
+ defp handler_quote(name, body) do
+ handler_quote(name, body, true, quote(do: _), quote(do: _))
end
defp var_in_patterns?(ast, name) do
@@ -135,67 +148,99 @@ defmodule Ewebmachine.Builder.Handlers do
end
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))
- ```
+ @doc false
+ def wrap_response({_, %Plug.Conn{}, _} = tuple, _, _), do: tuple
+ def wrap_response(r, conn, state), do: {r, conn, state}
- ```
- defh resources_exists(conn,%{obj: obj}) when obj !== nil, do: true
- defh resources_exists(conn,_), do: false
- ```
+ @doc """
+ Defines a resource handler function as described by the `Ewebmachine.Handlers`
+ module.
+
+ Handlers defined with this macro can either return the complete tuple
+ `{response, conn, state}` or only the `response`. The returned value is then
+ wrapped if needed.
+
+ Use this macro when you want to:
+ * define a handler which processes and produces body (the one given to
+ `Ewebmachine.Handlers.content_types_provided/2` or
+ `Ewebmachine.Handlers.content_types_accepted/2`)
+
+ ```elixir
+ content_types_accepted do: ["text/heml": :to_html]
+ content_types_provided do: ["application/json": :from_json]
+
+ defh to_html, do: "hello you"
+ defh from_json, do: pass(:ok, json: Poison.decode!(read_body conn))
+ ```
+
+ * use pattern matching via the function's head or a guard
+ ```elixir
+ defh resources_exists(conn, %{obj: obj}) when obj != nil, do: true
+ defh resources_exists(conn, _), do: false
+ ```
+
+ > #### Predefined handler {: .info}
+ >
+ > If you don't need pattern matching you can directly use the predefined
+ > handlers from this module. For instance instead of `defh
+ > resource_exists(conn, state) do ... end` use `resource_exist do ... end`.
"""
defmacro defh(signature, do_block) do
- {name, [conn_match,state_match], guard} = sig_to_sigwhen(signature)
+ {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
- Code.eval_quoted(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, [], __ENV__)
+ for resource_fun_name <- @resource_fun_names do
+ Code.eval_quoted(
+ quote do
+ @doc group: :handler
+ @doc """
+ Same as `defh #{unquote(resource_fun_name)}(conn, state) do ... end`
+
+ See `Ewebmachine.Handlers.#{unquote(resource_fun_name)}/2` for more
+ information on the handler's purpose.
+ """
+ defmacro unquote(resource_fun_name)(do_block) do
+ name = unquote(resource_fun_name)
+ handler_quote(name, do_block[:do])
+ end
+ end,
+ [],
+ __ENV__
+ )
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))}
+ Passes the handler response as well as updating the state.
+
+ It expands to
+ ```elixir
+ {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.
+ Use it when you need to add some value to an `t:Enumerable.t/0` state.
- for instance a resources_exists implementation "caching" the result
- in the state could be :
+ For instance for a `Ewebmachine.Handlers.resource_exists/2` handler which
+ caches the result:
+ ```elixir
+ resource_exists do
+ user = DB.get(state.id)
- pass (user=DB.get(state.id)) != nil, current_user: user
- # same as returning :
- {true,conn,%{id: "arnaud", current_user: %User{id: "arnaud"}}}
+ pass is_map(user), current_user: user
+ end
+ ```
+ instead of defining the whole response tuple of the handler
+ ```elixir
+ resource_exists do
+ user = DB.get(state.id)
+
+ {is_map(user), conn, Map.put(state, :current_user, user)}
+ end
+ ```
"""
- defmacro pass(response,update_state) do
+ defmacro pass(response, update_state) do
quote do
- {unquote(response),var!(conn),Enum.into(unquote(update_state),var!(state))}
+ {unquote(response), var!(conn), Enum.into(unquote(update_state), var!(state))}
end
end
end
diff --git a/lib/ewebmachine/builder.resources.ex b/lib/ewebmachine/builder.resources.ex
index 24b8adc..b736c00 100644
--- a/lib/ewebmachine/builder.resources.ex
+++ b/lib/ewebmachine/builder.resources.ex
@@ -1,26 +1,33 @@
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.
+ Provides macros to make endpoints for resources.
- See `Ewebmachine.Builder.Handlers` documentation to see how to
- contruct these modules (in the `after` block)
+ > #### `use Ewebmachine.Builder.Resources` {: .info}
+ >
+ > When you `use Ewebmachine.Builder.Resources`, it will `use Plug.Router`,
+ > `import Ewebmachine.Builder.Resources`, declare a private function plug
+ > `resource_match` which passes the connection through the `:match` and
+ > `:dispatch` plugs from `Plug`.
- Below a full example :
+ `use Ewebmachine.Builder.Resources` also accept the following option:
+ * `:default_plugs` - which when set to `true`, also add the plugs
+ `:resource_match`, `Ewebmachine.Plug.Run` and `Ewebmachine.Plug.Send`
- ```
+ ## Example
+ Multiple resources can be declared inside a single module and will be matched
+ accordingly.
+ ```elixir
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
+ # 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
+ # 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
+ # Plug after that will be executed only if no ewebmachine resources has matched
resource "/hello/:name" do %{name: name} after
plug SomeAdditionnalPlug
@@ -28,55 +35,22 @@ defmodule Ewebmachine.Builder.Resources do
defh to_xml, do: "#{state.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
+ resource "/hello/foo/:bar" do %{bar: bar} after
+ content_types_provided do: ['text/plain': :to_text]
+ defh to_text, do: "Foo #{state.bar}"
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
+ > #### Increasing compilation time {: .warning}
+ >
+ > Using many `resource/2` macros inside the same module can significantly
+ > raise the compilation time since it makes a module underneath and if one
+ > depends on another module at compilte time it will need to recompile the
+ > whole file, which the compiler does in a linear maner.
+ >
+ > Splitting a module which uses many `resource/2` will decrease the
+ > compilation time.
"""
defmacro __using__(opts) do
quote location: :keep do
@@ -100,68 +74,93 @@ defmodule Ewebmachine.Builder.Resources do
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),[])
+ 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
- end
- final_match = if !match?({"/*"<>_,_,_},hd(wm_routes)),
- do: quote(do: Plug.Router.match _ do var!(conn) end)
+
+ final_match =
+ if !match?({"/*" <> _, _, _}, hd(wm_routes)) do
+ quote(
+ do:
+ Plug.Router.match _ do
+ var!(conn)
+ end
+ )
+ 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 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
+ defp route_as_mod(route) do
+ route
+ |> String.split("/")
+ |> Enum.map(&(&1 |> remove_first |> String.capitalize()))
+ |> Enum.join()
+ end
- - 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.
+ @doc """
+ Same as `resource/2` but you can set the module's name.
+ ```elixir
+ resource ShortenedRouteName, "/my/route/that/would/generate/a/long/module/name/:id" do
+ # ...
+ end
```
- resource "/my/route/:commaid" do
- id = string.split(commaid,",")
- %{foo: id}
- after
- plug someadditionnalplug
- resource_exists do: state.id == ["hello"]
+ """
+ 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
- resource ShortenedRouteName, "/my/route/that/would/generate/a/long/module/name/:commaid" do
- id = String.split(commaid,",")
+ @doc ~S"""
+ Creates a plug module which is executed on path match.
+
+ The created module will use the `Ewebmachine.Builder.Handlers` module and add
+ the plug `:add_handlers` at the end of it.
+
+ See `Plug.Router` for the path specification.
+
+ The macro takes a `do` and an `after`. Both are executed on match. The `do`
+ block is executed to make the initial state. In its scope path bindings are
+ available. The handlers declared in the `after` block are executed by the
+ `Ewebmachine.Plug.Run` plug.
+
+ ## Example
+
+ ```elixir
+ resource "/my/route/: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)
+ 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
+ defp resource_quote(wm_module, route, init_block, body) do
quote do
@wm_routes {unquote(route), unquote(wm_module), unquote(Macro.escape(init_block))}
@@ -173,23 +172,55 @@ defmodule Ewebmachine.Builder.Resources do
end
end
- alias Ewebmachine.Plug.ErrorAsException
- alias Ewebmachine.Plug.ErrorAsForward
+ @doc """
+ Adds common plugs to match resources with Ewebmachine.
+
+ The macro's intent is to cover the most common use case to match resources. To
+ do so it adds the following plug by default:
+ * `:resource_match` which passes the connection through the `:match` and
+ `:dispatch` plugs of `Plug`.
+ * `Ewebmachine.Plug.Run`
+ * `Ewebmachine.Plug.Send`
+
+ Additional processing can be provided for connection which did not match any
+ route or which returns an erroneous http code via the following options:
+ * `:nomatch_404` - if sets to `true` adds a function plug `wm_notset_404`
+ which is case of an `:unset` state respond with a `404` http code. Defaults
+ to `false`.
+ * `:error_as_exception` - if sets to `true` adds the
+ `Ewebmachine.Plug.ErrorAsException` plug. Defaults to `false`.
+ * `:error_forwarding` - when a pattern is given and `:error_as_exception` is
+ not set, it adds the `Ewebmachine.Plug.ErrorAsForward` plug with as
+ `:forward_pattern` the given pattern. Defaults to `nil`.
+ """
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
+ alias Ewebmachine.Plug.ErrorAsException
+ alias Ewebmachine.Plug.ErrorAsForward
+
+ {error_as_exception?, opts} = Keyword.pop(opts, :error_as_exception, false)
+ {forward_pattern, opts} = Keyword.pop(opts, :error_as_exception)
+ {nomatch_404?, _} = Keyword.pop(opts, :nomatch_404, false)
+
+ true = is_boolean(error_as_exception?)
+ true = is_binary(forward_pattern) || is_nil(forward_pattern)
+ true = is_boolean(nomatch_404?)
+
+ {errorplug, errorplug_params} =
+ cond do
+ error_as_exception? -> {ErrorAsException, []}
+ is_binary(forward_pattern) -> {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))
+ if unquote(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
+ defp wm_notset_404(%{state: :unset} = conn, _), do: resp(conn, 404, "")
+ defp wm_notset_404(conn, _), do: conn
end
end
end
diff --git a/lib/ewebmachine/compat.ex b/lib/ewebmachine/compat.ex
deleted file mode 100644
index 1d41d75..0000000
--- a/lib/ewebmachine/compat.ex
+++ /dev/null
@@ -1,14 +0,0 @@
-defmodule Ewebmachine.Compat do
- @moduledoc false
-end
-
-defmodule Ewebmachine.Compat.Enum do
- @moduledoc false
-
- case Version.compare(System.version(), "1.4.0") do
- :gt ->
- defdelegate split_with(arg0, arg1), to: Enum
- _ ->
- defdelegate split_with(arg0, arg1), to: Enum, as: :partition
- end
-end
diff --git a/lib/ewebmachine/core.dsl.ex b/lib/ewebmachine/core.dsl.ex
index 139d432..a721802 100644
--- a/lib/ewebmachine/core.dsl.ex
+++ b/lib/ewebmachine/core.dsl.ex
@@ -1,34 +1,19 @@
defmodule Ewebmachine.Core.DSL do
+ @moduledoc false
+
## Macros and helpers defining the DSL for the Ewebmachine decision
## core : for legacy reasons, the module is called 'DSL' while they are
## are mostly helper functions.
- ##
- ## Changes:
- ## Macros hiding `conn` and `user_state` variables have been removed
- ## as they can produce unsafe use of these variables if used in
- ## structures like if/cond/... which is deprecated in elixir 1.3
-
- ## Usage :
-
- ## decision mydecision(conn, user_state, args...) do # def mydecision(conn, user_state, arg...)
- ## ...debug_decision
- ## ...exec body
- ## end
- @moduledoc false
alias Plug.Conn
- defmacro __using__(_opts) do quote do
- import Ewebmachine.Core.DSL
- import Ewebmachine.Core.Utils
- end end
-
- def sig_to_sigwhen({:when, _, [{name,_,params}, guard]}), do: {name, params, guard}
+ def sig_to_sigwhen({:when, _, [{name, _, params}, guard]}), do: {name, params, guard}
def sig_to_sigwhen({name, _, params}) when is_list(params), do: {name, params, true}
def sig_to_sigwhen({name, _, _}), do: {name, [], true}
defmacro decision(sig, do: body) do
{name, [conn, state], guard} = sig_to_sigwhen(sig)
+
quote do
def unquote(name)(unquote(conn), unquote(state)) when unquote(guard) do
var!(conn) = Ewebmachine.Log.debug_decision(unquote(conn), unquote(name))
@@ -41,11 +26,13 @@ defmodule Ewebmachine.Core.DSL do
handler = conn.private[:resource_handlers][fun] || Ewebmachine.Handlers
{reply, conn, state} = term = apply(handler, fun, [conn, state])
conn = Ewebmachine.Log.debug_call(conn, handler, fun, [conn, state], term)
+
case reply do
{:halt, code} ->
- throw {:halt, set_response_code(conn, code)}
+ throw({:halt, set_response_code(conn, code)})
+
_ ->
- {reply, conn, state}
+ {reply, conn, state}
end
end
@@ -60,50 +47,54 @@ defmodule Ewebmachine.Core.DSL do
def get_header_val(conn, name), do: first_or_nil(Conn.get_req_header(conn, name))
def set_response_code(conn, code) do
- conn = conn # halt machine when set response code, on respond
- |> Conn.put_status(code)
- |> Ewebmachine.Log.debug_enddecision
+ # halt machine when set response code, on respond
+ conn =
+ conn
+ |> Conn.put_status(code)
+ |> Ewebmachine.Log.debug_enddecision()
+
conn = if !conn.resp_body, do: %{conn | resp_body: ""}, else: conn
%{conn | state: :set}
end
def set_resp_header(conn, k, v), do: Conn.put_resp_header(conn, k, v)
-
+
def set_resp_headers(conn, kvs) do
- Enum.reduce(kvs, conn,
- fn {k,v}, acc ->
- Conn.put_resp_header(acc, k, v)
- end)
+ Enum.reduce(kvs, conn, fn {k, v}, acc ->
+ Conn.put_resp_header(acc, k, v)
+ end)
end
def remove_resp_header(conn, k) do
Conn.delete_resp_header(conn, k)
end
- def set_disp_path(conn, path), do: %{conn | script_name: String.split("#{path}","/")}
+ def set_disp_path(conn, path), do: %{conn | script_name: String.split("#{path}", "/")}
def resp_body(conn), do: conn.private[:machine_body_stream] || conn.resp_body
def set_resp_body(conn, body) when is_binary(body) or is_list(body) do
%{conn | resp_body: body}
end
- def set_resp_body(conn, body) do #if not an IO List, then it should be an enumerable
+
+ # if not an IO List, then it should be an enumerable
+ def set_resp_body(conn, body) do
Conn.put_private(conn, :machine_body_stream, body)
end
def has_resp_body(conn) do
- (!is_nil(conn.resp_body) or !is_nil(conn.private[:machine_body_stream]))
+ !is_nil(conn.resp_body) or !is_nil(conn.private[:machine_body_stream])
end
-
+
def get_metadata(conn, key), do: conn.private[key]
def set_metadata(conn, k, v), do: Conn.put_private(conn, k, v)
-
+
def compute_body_md5(conn) do
conn = Ewebmachine.fetch_req_body(conn, [])
:crypto.hash(:md5, Ewebmachine.req_body(conn))
end
- def first_or_nil([v|_]), do: v
+ def first_or_nil([v | _]), do: v
def first_or_nil(_), do: nil
end
diff --git a/lib/ewebmachine/core.ex b/lib/ewebmachine/core.ex
index 5682fbd..f8418cd 100644
--- a/lib/ewebmachine/core.ex
+++ b/lib/ewebmachine/core.ex
@@ -1,34 +1,42 @@
defmodule Ewebmachine.Core do
- use Ewebmachine.Core.DSL
+ @moduledoc false
## Basho webmachine Core rewrited in a systematic way to make the
## conversion as reliable as possible. The Ewebmachine.Core.DSL
## allows this conversion to be clean imitating the DSL of Basho.
- @moduledoc false
- @spec v3(Plug.Conn.t, any) :: Plug.Conn.t
+ import Ewebmachine.Core.DSL
+ import Ewebmachine.Core.Utils
+
+ initial_decision =
+ if Mix.env() == :test,
+ do: :v3b13_test,
+ else: :v3b13
+
+ @spec v3(Plug.Conn.t(), any) :: Plug.Conn.t()
def v3(conn, user_state) do
try do
- {_, conn, _} = v3b13(Ewebmachine.Log.debug_init(conn), user_state)
+ {_, conn, _} = unquote(initial_decision)(Ewebmachine.Log.debug_init(conn), user_state)
conn
catch
- :throw, {:halt,conn} -> conn
+ :throw, {:halt, conn} -> conn
end
end
- ## "Service Available"
- decision v3b13(conn, state) do
+ decision v3b13_test(conn, state) do
case resource_call(conn, state, :ping) do
{:pong, conn, state} ->
- v3b13b(conn, state);
+ v3b13(conn, state)
+
{_, conn, state} ->
- respond(conn, state, 503)
+ respond(conn, state, 503)
end
end
- ## "see `v3b13/2`"
- decision v3b13b(conn, state) do
+ ## "Service Available"
+ decision v3b13(conn, state) do
{reply, conn, state} = resource_call(conn, state, :service_available)
+
if reply do
v3b12(conn, state)
else
@@ -39,6 +47,7 @@ defmodule Ewebmachine.Core do
## "Known method?"
decision v3b12(conn, state) do
{methods, conn, state} = resource_call(conn, state, :known_methods)
+
if method(conn) in methods do
v3b11(conn, state)
else
@@ -49,7 +58,8 @@ defmodule Ewebmachine.Core do
## "URI too long?"
decision v3b11(conn, state) do
{reply, conn, state} = resource_call(conn, state, :uri_too_long)
- if reply do
+
+ if reply do
respond(conn, state, 414)
else
v3b10(conn, state)
@@ -59,10 +69,11 @@ defmodule Ewebmachine.Core do
## "Method allowed?"
decision v3b10(conn, state) do
{methods, conn, state} = resource_call(conn, state, :allowed_methods)
+
if method(conn) in methods do
v3b9(conn, state)
else
- conn = set_resp_headers(conn, %{"allow" => Enum.join(methods,",")})
+ conn = set_resp_headers(conn, %{"allow" => Enum.join(methods, ",")})
respond(conn, state, 405)
end
end
@@ -82,19 +93,23 @@ defmodule Ewebmachine.Core do
{:not_validated, conn, state} ->
case Base.decode64(get_header_val(conn, "content-md5")) do
{:ok, checksum} ->
- case compute_body_md5(conn) do
- ^checksum ->
- v3b9b(conn, state);
- _ ->
- respond(conn, state, 400)
- end
+ case compute_body_md5(conn) do
+ ^checksum ->
+ v3b9b(conn, state)
+
+ _ ->
+ respond(conn, state, 400)
+ end
+
_ ->
- respond(conn, state, 400)
+ respond(conn, state, 400)
end
+
{false, conn, state} ->
- respond(conn, state, 400)
+ respond(conn, state, 400)
+
{_, conn, state} ->
- v3b9b(conn, state)
+ v3b9b(conn, state)
end
end
@@ -102,9 +117,10 @@ defmodule Ewebmachine.Core do
decision v3b9b(conn, state) do
case resource_call(conn, state, :malformed_request) do
{true, conn, state} ->
- respond(conn, state, 400)
+ respond(conn, state, 400)
+
{false, conn, state} ->
- v3b8(conn, state)
+ v3b8(conn, state)
end
end
@@ -112,10 +128,11 @@ defmodule Ewebmachine.Core do
decision v3b8(conn, state) do
case resource_call(conn, state, :is_authorized) do
{true, conn, state} ->
- v3b7(conn, state)
+ v3b7(conn, state)
+
{auth_head, conn, state} ->
conn = set_resp_header(conn, "www-authenticate", to_string(auth_head))
- respond(conn, state, 401)
+ respond(conn, state, 401)
end
end
@@ -123,15 +140,17 @@ defmodule Ewebmachine.Core do
decision v3b7(conn, state) do
case resource_call(conn, state, :forbidden) do
{true, conn, state} ->
- respond(conn, state, 403);
+ respond(conn, state, 403)
+
{false, conn, state} ->
- v3b6(conn, state)
+ v3b6(conn, state)
end
end
## "Okay Content-* Headers?"
decision v3b6(conn, state) do
{reply, conn, state} = resource_call(conn, state, :valid_content_headers)
+
if reply do
v3b5(conn, state)
else
@@ -142,6 +161,7 @@ defmodule Ewebmachine.Core do
## "Known Content-Type?"
decision v3b5(conn, state) do
{reply, conn, state} = resource_call(conn, state, :known_content_type)
+
if reply do
v3b4(conn, state)
else
@@ -152,6 +172,7 @@ defmodule Ewebmachine.Core do
## "Req Entity Too Large?"
decision v3b4(conn, state) do
{reply, conn, state} = resource_call(conn, state, :valid_entity_length)
+
if reply do
v3b3(conn, state)
else
@@ -162,38 +183,43 @@ defmodule Ewebmachine.Core do
## "OPTIONS?"
decision v3b3(conn, state) do
case method(conn) do
- "OPTIONS"->
+ "OPTIONS" ->
{hdrs, conn, state} = resource_call(conn, state, :options)
conn = set_resp_headers(conn, hdrs)
respond(conn, state, 200)
+
_ ->
- v3c3(conn, state)
+ v3c3(conn, state)
end
end
## "Accept exists?"
decision v3c3(conn, state) do
{ct_provided, conn, state} = resource_call(conn, state, :content_types_provided)
- p_types = for {type,_fun} <- ct_provided, do: normalize_mtype(type)
+ p_types = for {type, _fun} <- ct_provided, do: normalize_mtype(type)
+
case get_header_val(conn, "accept") do
nil ->
- conn = set_metadata(conn, :'content-type', hd(p_types))
- v3d4(conn, state)
+ conn = set_metadata(conn, :"content-type", hd(p_types))
+ v3d4(conn, state)
+
_ ->
- v3c4(conn, state)
+ v3c4(conn, state)
end
end
## "Acceptable media type available?"
decision v3c4(conn, state) do
{ct_provided, conn, state} = resource_call(conn, state, :content_types_provided)
- p_types = for {type,_fun} <- ct_provided, do: normalize_mtype(type)
+ p_types = for {type, _fun} <- ct_provided, do: normalize_mtype(type)
+
case choose_media_type(p_types, get_header_val(conn, "accept")) do
nil ->
- respond(conn, state, 406)
+ respond(conn, state, 406)
+
type ->
- conn = set_metadata(conn, :'content-type', type)
- v3d4(conn, state)
+ conn = set_metadata(conn, :"content-type", type)
+ v3d4(conn, state)
end
end
@@ -209,6 +235,7 @@ defmodule Ewebmachine.Core do
## "Acceptable Language available? %% WMACH-46 (do this as proper conneg)"
decision v3d5(conn, state) do
{reply, conn, state} = resource_call(conn, state, :language_available)
+
if reply do
v3e5(conn, state)
else
@@ -220,12 +247,15 @@ defmodule Ewebmachine.Core do
decision v3e5(conn, state) do
case get_header_val(conn, "accept-charset") do
nil ->
- {charset, conn, state} = choose_charset(conn, state, "*")
- case charset do
- nil -> respond(conn, state, 406);
- _ -> v3f6(conn, state)
- end
- _ -> v3e6(conn, state)
+ {charset, conn, state} = choose_charset(conn, state, "*")
+
+ case charset do
+ nil -> respond(conn, state, 406)
+ _ -> v3f6(conn, state)
+ end
+
+ _ ->
+ v3e6(conn, state)
end
end
@@ -233,27 +263,32 @@ defmodule Ewebmachine.Core do
decision v3e6(conn, state) do
accept = get_header_val(conn, "accept-charset")
{charset, conn, state} = choose_charset(conn, state, accept)
+
case charset do
- nil -> respond(conn, state, 406);
- _ -> v3f6(conn, state)
+ nil -> respond(conn, state, 406)
+ _ -> v3f6(conn, state)
end
end
## Accept-Encoding exists?
## also, set content-type header here, now that charset is chosen)
decision v3f6(conn, state) do
- {type, subtype, params} = get_metadata(conn, :'content-type')
- char = get_metadata(conn, :'chosen-charset')
- params = char && Map.put(params, :charset, char) || params
- conn = set_resp_header(conn, "content-type", format_mtype({type,subtype,params}))
+ {type, subtype, params} = get_metadata(conn, :"content-type")
+ char = get_metadata(conn, :"chosen-charset")
+ params = (char && Map.put(params, :charset, char)) || params
+ conn = set_resp_header(conn, "content-type", format_mtype({type, subtype, params}))
+
case get_header_val(conn, "accept-encoding") do
nil ->
- {encoding, conn, state} = choose_encoding(conn, state, "identity;q=1.0,*;q=0.5")
- case encoding do
- nil -> respond(conn, state, 406);
- _ -> v3g7(conn, state)
- end
- _ -> v3f7(conn, state)
+ {encoding, conn, state} = choose_encoding(conn, state, "identity;q=1.0,*;q=0.5")
+
+ case encoding do
+ nil -> respond(conn, state, 406)
+ _ -> v3g7(conn, state)
+ end
+
+ _ ->
+ v3f7(conn, state)
end
end
@@ -261,9 +296,10 @@ defmodule Ewebmachine.Core do
decision v3f7(conn, state) do
accept = get_header_val(conn, "accept-encoding")
{encoding, conn, state} = choose_encoding(conn, state, accept)
+
case encoding do
- nil -> respond(conn, state, 406);
- _ -> v3g7(conn, state)
+ nil -> respond(conn, state, 406)
+ _ -> v3g7(conn, state)
end
end
@@ -271,13 +307,16 @@ defmodule Ewebmachine.Core do
decision v3g7(conn, state) do
## his is the first place after all conneg, so set Vary here
{vars, conn, state} = variances(conn, state)
- conn = if length(vars) > 0 do
- set_resp_header(conn, "vary", Enum.join(vars, ","))
- else
- conn
- end
+
+ conn =
+ if length(vars) > 0 do
+ set_resp_header(conn, "vary", Enum.join(vars, ","))
+ else
+ conn
+ end
{reply, conn, state} = resource_call(conn, state, :resource_exists)
+
if reply do
v3g8(conn, state)
else
@@ -307,6 +346,7 @@ defmodule Ewebmachine.Core do
decision v3g11(conn, state) do
etags = split_quoted_strings(get_header_val(conn, "if-match"))
{reply, conn, state} = resource_call(conn, state, :generate_etag)
+
if reply in etags do
v3h10(conn, state)
else
@@ -335,6 +375,7 @@ defmodule Ewebmachine.Core do
## "I-UM-S is valid date?"
decision v3h11(conn, state) do
iums_date = get_header_val(conn, "if-unmodified-since")
+
if convert_request_date(iums_date) == :bad_date do
v3i12(conn, state)
else
@@ -347,6 +388,7 @@ defmodule Ewebmachine.Core do
req_date = get_header_val(conn, "if-unmodified-since")
req_erl_date = convert_request_date(req_date)
{res_erl_date, conn, state} = resource_call(conn, state, :last_modified)
+
if res_erl_date > req_erl_date do
respond(conn, state, 412)
else
@@ -357,12 +399,14 @@ defmodule Ewebmachine.Core do
## "Moved permanently? (apply PUT to different URI)"
decision v3i4(conn, state) do
{reply, conn, state} = resource_call(conn, state, :moved_permanently)
+
case reply do
{true, moved_uri} ->
- conn = set_resp_header(conn, "location", moved_uri)
- respond(conn, state, 301)
+ conn = set_resp_header(conn, "location", moved_uri)
+ respond(conn, state, 301)
+
false ->
- v3p3(conn, state)
+ v3p3(conn, state)
end
end
@@ -395,7 +439,7 @@ defmodule Ewebmachine.Core do
## "GET or HEAD?"
decision v3j18(conn, state) do
- if method(conn) in ["GET","HEAD"] do
+ if method(conn) in ["GET", "HEAD"] do
respond(conn, state, 304)
else
respond(conn, state, 412)
@@ -406,16 +450,18 @@ defmodule Ewebmachine.Core do
decision v3k5(conn, state) do
case resource_call(conn, state, :moved_permanently) do
{{true, moved_uri}, conn, state} ->
- conn = set_resp_header(conn, "location", moved_uri)
- respond(conn, state, 301)
+ conn = set_resp_header(conn, "location", moved_uri)
+ respond(conn, state, 301)
+
{false, conn, state} ->
- v3l5(conn, state)
+ v3l5(conn, state)
end
end
## "Previously existed?"
decision v3k7(conn, state) do
{reply, conn, state} = resource_call(conn, state, :previously_existed)
+
if reply do
v3k5(conn, state)
else
@@ -430,6 +476,7 @@ defmodule Ewebmachine.Core do
## provided ETag is a member, we follow the error case out
## via v3j18.
{etag, conn, state} = resource_call(conn, state, :generate_etag)
+
if etag in etags do
v3j18(conn, state)
else
@@ -441,10 +488,11 @@ defmodule Ewebmachine.Core do
decision v3l5(conn, state) do
case resource_call(conn, state, :moved_temporarily) do
{{true, moved_uri}, conn, state} ->
- conn = set_resp_header(conn, "location", moved_uri)
- respond(conn, state, 307)
+ conn = set_resp_header(conn, "location", moved_uri)
+ respond(conn, state, 307)
+
{false, conn, state} ->
- v3m5(conn, state)
+ v3m5(conn, state)
end
end
@@ -469,6 +517,7 @@ defmodule Ewebmachine.Core do
## "IMS is valid date?"
decision v3l14(conn, state) do
ims_date = get_header_val(conn, "if-modified-since")
+
if convert_request_date(ims_date) == :bad_date do
v3m16(conn, state)
else
@@ -478,9 +527,10 @@ defmodule Ewebmachine.Core do
## "IMS > Now?"
decision v3l15(conn, state) do
- now_date_time = :calendar.universal_time
+ now_date_time = :calendar.universal_time()
req_date = get_header_val(conn, "if-modified-since")
req_erl_date = convert_request_date(req_date)
+
if req_erl_date > now_date_time do
v3m16(conn, state)
else
@@ -493,6 +543,7 @@ defmodule Ewebmachine.Core do
req_date = get_header_val(conn, "if-modified-since")
req_erl_date = convert_request_date(req_date)
{res_erl_date, conn, state} = resource_call(conn, state, :last_modified)
+
if !res_erl_date or res_erl_date > req_erl_date do
v3m16(conn, state)
else
@@ -512,6 +563,7 @@ defmodule Ewebmachine.Core do
## "Server allows POST to missing resource?"
decision v3m7(conn, state) do
{amp, conn, state} = resource_call(conn, state, :allow_missing_post)
+
if amp do
v3n11(conn, state)
else
@@ -543,6 +595,7 @@ defmodule Ewebmachine.Core do
decision v3m20b(conn, state) do
{reply, conn, state} = resource_call(conn, state, :delete_completed)
+
if reply do
v3o20(conn, state)
else
@@ -553,6 +606,7 @@ defmodule Ewebmachine.Core do
## "Server allows POST to missing resource?"
decision v3n5(conn, state) do
{reply, conn, state} = resource_call(conn, state, :allow_missing_post)
+
if reply do
v3n11(conn, state)
else
@@ -563,30 +617,37 @@ defmodule Ewebmachine.Core do
## "Redirect?"
decision v3n11(conn, state) do
{reply, conn, state} = resource_call(conn, state, :post_is_create)
+
if reply do
{_, conn, state} = accept_helper(conn, state)
{new_path, conn, state} = resource_call(conn, state, :create_path)
- if is_nil(new_path), do: raise "post_is_create w/o create_path"
- if !is_binary(new_path), do: raise "create_path not a string (#{inspect new_path})"
+ if is_nil(new_path), do: raise("post_is_create w/o create_path")
+ if !is_binary(new_path), do: raise("create_path not a string (#{inspect(new_path)})")
{base_uri, conn, state} = resource_call(conn, state, :base_uri)
- base_uri = if String.last(base_uri) == "/" do
- String.slice(base_uri, 0..-2//1)
- else
- base_uri
- end
- new_path = if !match?("/"<>_, new_path) do
- "#{path(conn)}/#{new_path}"
- else
- new_path
- end
- conn = if !get_resp_header(conn, "location") do
- set_resp_header(conn, "location", base_uri <> new_path)
- else
- conn
- end
+ base_uri =
+ if String.last(base_uri) == "/" do
+ String.slice(base_uri, 0..-2//1)
+ else
+ base_uri
+ end
+
+ new_path =
+ if !match?("/" <> _, new_path) do
+ "#{path(conn)}/#{new_path}"
+ else
+ new_path
+ end
+
+ conn =
+ if !get_resp_header(conn, "location") do
+ set_resp_header(conn, "location", base_uri <> new_path)
+ else
+ conn
+ end
+
redirect_helper(conn, state)
else
{true, conn, state} = resource_call(conn, state, :process_post)
@@ -608,7 +669,8 @@ defmodule Ewebmachine.Core do
decision v3o14(conn, state) do
case resource_call(conn, state, :is_conflict) do
{true, conn, state} ->
- respond(conn, state, 409)
+ respond(conn, state, 409)
+
{_, conn, state} ->
{_, conn, state} = accept_helper(conn, state)
v3p11(conn, state)
@@ -627,18 +689,18 @@ defmodule Ewebmachine.Core do
## Multiple representations?
## also where body generation for GET and HEAD is done)
decision v3o18(conn, state) do
- if method(conn) in ["GET","HEAD"] do
+ if method(conn) in ["GET", "HEAD"] do
{etag, conn, state} = resource_call(conn, state, :generate_etag)
conn = if etag, do: set_resp_header(conn, "etag", quoted_string(etag)), else: conn
- ct = get_metadata(conn, :'content-type')
+ ct = get_metadata(conn, :"content-type")
{lm, conn, state} = resource_call(conn, state, :last_modified)
conn = if lm, do: set_resp_header(conn, "last-modified", rfc1123_date(lm)), else: conn
{exp, conn, state} = resource_call(conn, state, :expires)
conn = if exp, do: set_resp_header(conn, "expires", rfc1123_date(exp)), else: conn
{ct_provided, conn, state} = resource_call(conn, state, :content_types_provided)
- f = Enum.find_value(ct_provided, fn {t,f} -> normalize_mtype(t) == ct && f end)
+ f = Enum.find_value(ct_provided, fn {t, f} -> normalize_mtype(t) == ct && f end)
{body, conn, state} = resource_call(conn, state, f)
{body, conn, state} = encode_body(conn, state, body)
conn = set_resp_body(conn, body)
@@ -650,6 +712,7 @@ defmodule Ewebmachine.Core do
decision v3o18b(conn, state) do
{mc, conn, state} = resource_call(conn, state, :multiple_choices)
+
if mc do
respond(conn, state, 300)
else
@@ -669,6 +732,7 @@ defmodule Ewebmachine.Core do
## "Conflict?"
decision v3p3(conn, state) do
{reply, conn, state} = resource_call(conn, state, :is_conflict)
+
if reply do
respond(conn, state, 409)
else
@@ -694,16 +758,20 @@ defmodule Ewebmachine.Core do
{enc_provided, conn, state} = resource_call(conn, state, :encodings_provided)
accept = if length(ct_provided) < 2, do: [], else: ["Accept"]
accept_enc = if length(enc_provided) < 2, do: [], else: ["Accept-Encoding"]
- {accept_char, conn, state} = case resource_call(conn, state, :charsets_provided) do
- {:no_charset, c, s} ->
- {[], c, s}
- {charset, c, s} ->
- if length(charset) < 2 do
- {[], c, s}
- else
- {["Accept-Charset"], c, s}
- end
- end
+
+ {accept_char, conn, state} =
+ case resource_call(conn, state, :charsets_provided) do
+ {:no_charset, c, s} ->
+ {[], c, s}
+
+ {charset, c, s} ->
+ if length(charset) < 2 do
+ {[], c, s}
+ else
+ {["Accept-Charset"], c, s}
+ end
+ end
+
{variances, conn, state} = resource_call(conn, state, :variances)
{accept ++ accept_enc ++ accept_char ++ variances, conn, state}
end
@@ -714,16 +782,17 @@ defmodule Ewebmachine.Core do
conn = set_metadata(conn, :mediaparams, h_params)
{ct_accepted, conn, state} = resource_call(conn, state, :content_types_accepted)
- mtfun = Enum.find_value(ct_accepted, fn {accept,f} ->
- fuzzy_mt_match(ct,normalize_mtype(accept)) && f
- end)
+ mtfun =
+ Enum.find_value(ct_accepted, fn {accept, f} ->
+ fuzzy_mt_match(ct, normalize_mtype(accept)) && f
+ end)
if mtfun do
{_reply, conn, state} = resource_call(conn, state, mtfun)
encode_body_if_set(conn, state)
else
{_, conn, _} = respond(conn, state, 415)
- throw {:halt, conn}
+ throw({:halt, conn})
end
end
@@ -739,26 +808,42 @@ defmodule Ewebmachine.Core do
end
def encode_body(conn, state, body) do
- chosen_cset = get_metadata(conn, :'chosen-charset')
- {charsetter, conn, state} = case resource_call(conn, state, :charsets_provided) do
- {:no_charset, c, s} ->
- {&(&1), c, s}
- {cp, c, s} ->
- cs = Enum.find_value(cp, fn {c, f} ->
- (to_string(c) == chosen_cset) && f
- end) || &(&1)
- {cs, c, s}
- end
- chosen_enc = get_metadata(conn, :'content-encoding')
+ chosen_cset = get_metadata(conn, :"chosen-charset")
+
+ {charsetter, conn, state} =
+ case resource_call(conn, state, :charsets_provided) do
+ {:no_charset, c, s} ->
+ {& &1, c, s}
+
+ {cp, c, s} ->
+ cs =
+ Enum.find_value(cp, &Function.identity/1, fn {c, f} ->
+ to_string(c) == chosen_cset && f
+ end)
+
+ {cs, c, s}
+ end
+
+ chosen_enc = get_metadata(conn, :"content-encoding")
{enc_provided, conn, state} = resource_call(conn, state, :encodings_provided)
- encoder = Enum.find_value(enc_provided,
- fn {enc,f} -> (to_string(enc) == chosen_enc) && f end) || &(&1)
- body = case body do
- body when is_binary(body) or is_list(body) ->
- body |> IO.iodata_to_binary |> charsetter.() |> encoder.()
- _->
- body |> Stream.map(&IO.iodata_to_binary/1) |> Stream.map(charsetter) |> Stream.map(encoder)
- end
+
+ encoder =
+ Enum.find_value(enc_provided, &Function.identity/1, fn {enc, f} ->
+ to_string(enc) == chosen_enc && f
+ end)
+
+ body =
+ case body do
+ body when is_binary(body) or is_list(body) ->
+ body |> IO.iodata_to_binary() |> charsetter.() |> encoder.()
+
+ _ ->
+ body
+ |> Stream.map(&IO.iodata_to_binary/1)
+ |> Stream.map(charsetter)
+ |> Stream.map(encoder)
+ end
+
{body, conn, state}
end
@@ -766,28 +851,35 @@ defmodule Ewebmachine.Core do
{enc_provided, conn, state} = resource_call(conn, state, :encodings_provided)
encs = for {enc, _} <- enc_provided, do: to_string(enc)
chosen_enc = choose_encoding(encs, acc_enc_hdr)
- conn = if chosen_enc !== "identity" do
- set_resp_header(conn, "content-encoding", chosen_enc)
- else
- conn
- end
- conn = set_metadata(conn, :'content-encoding', chosen_enc)
+
+ conn =
+ if chosen_enc !== "identity" do
+ set_resp_header(conn, "content-encoding", chosen_enc)
+ else
+ conn
+ end
+
+ conn = set_metadata(conn, :"content-encoding", chosen_enc)
{chosen_enc, conn, state}
end
def choose_charset(conn, state, acc_char_hdr) do
case resource_call(conn, state, :charsets_provided) do
{:no_charset, conn, state} ->
- {:no_charset, conn, state}
+ {:no_charset, conn, state}
+
{cl, conn, state} ->
- charsets = for {cset,_f} <- cl, do: to_string(cset)
- charset = choose_charset(charsets, acc_char_hdr)
- conn = if (charset) do
- set_metadata(conn, :'chosen-charset', charset)
- else
- conn
- end
- {charset, conn, state}
+ charsets = for {cset, _f} <- cl, do: to_string(cset)
+ charset = choose_charset(charsets, acc_char_hdr)
+
+ conn =
+ if charset do
+ set_metadata(conn, :"chosen-charset", charset)
+ else
+ conn
+ end
+
+ {charset, conn, state}
end
end
@@ -796,25 +888,27 @@ defmodule Ewebmachine.Core do
if !get_resp_header(conn, "location") do
raise "Response had do_redirect but no Location"
else
- respond(conn, state, 303)
+ respond(conn, state, 303)
end
else
- v3p11(conn, state)
+ v3p11(conn, state)
end
end
def respond(conn, state, code) do
- {conn, state} = if (code == 304) do
- conn = remove_resp_header(conn, "content-type")
- {etag, conn, state} = resource_call(conn, state, :generate_etag)
- conn = if etag, do: set_resp_header(conn, "etag", quoted_string(etag)), else: conn
+ {conn, state} =
+ if code == 304 do
+ conn = remove_resp_header(conn, "content-type")
+ {etag, conn, state} = resource_call(conn, state, :generate_etag)
+ conn = if etag, do: set_resp_header(conn, "etag", quoted_string(etag)), else: conn
+
+ {exp, conn, state} = resource_call(conn, state, :expires)
+ conn = if exp, do: set_resp_header(conn, "expires", rfc1123_date(exp)), else: conn
+ {conn, state}
+ else
+ {conn, state}
+ end
- {exp, conn, state} = resource_call(conn, state, :expires)
- conn = if exp, do: set_resp_header(conn, "expires", rfc1123_date(exp)), else: conn
- {conn, state}
- else
- {conn, state}
- end
conn = set_response_code(conn, code)
resource_call(conn, state, :finish_request)
end
diff --git a/lib/ewebmachine/core.utils.ex b/lib/ewebmachine/core.utils.ex
index b8fa260..0201178 100644
--- a/lib/ewebmachine/core.utils.ex
+++ b/lib/ewebmachine/core.utils.ex
@@ -3,19 +3,25 @@ defmodule Ewebmachine.Core.Utils do
HTTP utility module
"""
- @type norm_content_type :: {type :: String.t, subtype :: String.t, params :: map}
+ @type norm_content_type :: {type :: String.t(), subtype :: String.t(), params :: map}
@doc """
Convert any content type representation (see spec) into a `norm_content_type`
"""
- @spec normalize_mtype({type :: String.t, params :: map} | (type :: String.t) | norm_content_type) :: norm_content_type
+ @spec normalize_mtype(
+ {type :: String.t(), params :: map}
+ | (type :: String.t())
+ | norm_content_type
+ ) :: norm_content_type
def normalize_mtype({type, params}) do
case String.split(type, "/") do
[type, subtype] -> {type, subtype, params}
_ -> {"application", "octet-stream", %{}}
end
end
- def normalize_mtype({_, _, %{}}=mtype), do: mtype
+
+ def normalize_mtype({_, _, %{}} = mtype), do: mtype
+
def normalize_mtype(type) do
case Plug.Conn.Utils.media_type(to_string(type)) do
{:ok, type, subtype, params} -> {type, subtype, params}
@@ -29,17 +35,17 @@ defmodule Ewebmachine.Core.Utils do
"""
@spec fuzzy_mt_match(norm_content_type, norm_content_type) :: boolean
def fuzzy_mt_match({h_type, h_subtype, h_params}, {a_type, a_subtype, a_params}) do
- (a_type == h_type or a_type == "*" ) and
- (a_subtype == h_subtype or a_subtype=="*") and
+ (a_type == h_type or a_type == "*") and
+ (a_subtype == h_subtype or a_subtype == "*") and
Enum.all?(a_params, fn {k, v} -> h_params[k] == v end)
end
@doc """
Format a `norm_content_type` into an HTTP content type header
"""
- @spec format_mtype(norm_content_type) :: String.t
+ @spec format_mtype(norm_content_type) :: String.t()
def format_mtype({type, subtype, params}) do
- params = params |> Enum.map(fn {k, v}->"; #{k}=#{v}" end) |> Enum.join()
+ params = params |> Enum.map(fn {k, v} -> "; #{k}=#{v}" end) |> Enum.join()
"#{type}/#{subtype}#{params}"
end
@@ -49,26 +55,34 @@ defmodule Ewebmachine.Core.Utils do
- `accept_header`, the HTTP header `Accept`
- `ct_provided`, the list of provided content types
"""
- @spec choose_media_type([norm_content_type], String.t) :: norm_content_type | nil
+ @spec choose_media_type([norm_content_type], String.t()) :: norm_content_type | nil
def choose_media_type(ct_provided, accept_header) do
- accepts = accept_header |>
- Plug.Conn.Utils.list() |>
- Enum.map(fn
- "*" -> "*/*"
- e -> e
- end) |>
- Enum.map(&Plug.Conn.Utils.media_type/1)
- accepts = for {:ok, type, subtype, params} <- accepts do
- q = case Float.parse(params["q"] || "1") do
- {q, _} -> q
- _ -> 1
- end
- {q, type, subtype, Map.delete(params, "q")}
- end |>
- Enum.sort() |> Enum.reverse()
+ accepts =
+ accept_header
+ |> Plug.Conn.Utils.list()
+ |> Enum.map(fn
+ "*" -> "*/*"
+ e -> e
+ end)
+ |> Enum.map(&Plug.Conn.Utils.media_type/1)
+
+ accepts =
+ for {:ok, type, subtype, params} <- accepts do
+ q =
+ case Float.parse(params["q"] || "1") do
+ {q, _} -> q
+ _ -> 1
+ end
+
+ {q, type, subtype, Map.delete(params, "q")}
+ end
+ |> Enum.sort()
+ |> Enum.reverse()
+
Enum.find_value(accepts, fn {_, atype, asubtype, aparams} ->
Enum.find(ct_provided, fn {type, subtype, params} ->
- (atype=="*" or atype==type) and (asubtype=="*" or asubtype==subtype) and aparams==params
+ (atype == "*" or atype == type) and (asubtype == "*" or asubtype == subtype) and
+ aparams == params
end)
end)
end
@@ -76,37 +90,46 @@ defmodule Ewebmachine.Core.Utils do
@doc """
Remove quotes from HTTP quoted string
"""
- @spec quoted_string(String.t) :: String.t
+ @spec quoted_string(String.t()) :: String.t()
def quoted_string(value), do: Plug.Conn.Utils.token(value)
@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() |>
- Enum.map(&Plug.Conn.Utils.token/1)
+ str
+ |> Plug.Conn.Utils.list()
+ |> Enum.map(&Plug.Conn.Utils.token/1)
end
@doc """
Convert a calendar date to a rfc1123 date string
"""
- @spec rfc1123_date({{year :: integer, month :: integer, day :: integer}, {h :: integer, min :: integer, sec :: integer}}) :: String.t
+ @spec rfc1123_date(
+ {{year :: integer, month :: integer, day :: integer},
+ {h :: integer, min :: integer, sec :: integer}}
+ ) :: String.t()
def rfc1123_date({{yyyy, mm, dd}, {hour, min, sec}}) do
day_number = :calendar.day_of_the_week({yyyy, mm, dd})
args = [:httpd_util.day(day_number), dd, :httpd_util.month(mm), yyyy, hour, min, sec]
- :io_lib.format(~c"~s, ~2.2.0w ~3.s ~4.4.0w ~2.2.0w:~2.2.0w:~2.2.0w GMT", args) |> IO.iodata_to_binary()
+
+ :io_lib.format(~c"~s, ~2.2.0w ~3.s ~4.4.0w ~2.2.0w:~2.2.0w:~2.2.0w GMT", args)
+ |> IO.iodata_to_binary()
end
@doc """
Convert rfc1123 or rfc850 to :calendar dates
"""
- @spec convert_request_date(String.t) :: {{year :: integer, month :: integer, day :: integer}, {h :: integer, min :: integer, sec :: integer}} | :bad_date
+ @spec convert_request_date(String.t()) ::
+ {{year :: integer, month :: integer, day :: integer},
+ {h :: integer, min :: integer, sec :: integer}}
+ | :bad_date
def convert_request_date(date) do
try do
:httpd_util.convert_request_date(~c"#{date}")
- catch _, _ -> :bad_date
+ catch
+ _, _ -> :bad_date
end
end
@@ -116,7 +139,7 @@ defmodule Ewebmachine.Core.Utils do
- `acc_enc_hdr`, the HTTP header `Accept-Encoding`
- `encs`, the list of supported encoding
"""
- @spec choose_encoding([String.t], String.t) :: String.t | nil
+ @spec choose_encoding([String.t()], String.t()) :: String.t() | nil
def choose_encoding(encs, acc_enc_hdr) do
choose(encs, acc_enc_hdr, "identity")
end
@@ -127,7 +150,7 @@ defmodule Ewebmachine.Core.Utils do
- `acc_char_hdr`, the HTTP header `Accept-Charset`
- `charsets`, the list of supported charsets
"""
- @spec choose_charset([String.t], String.t) :: String.t | nil
+ @spec choose_charset([String.t()], String.t()) :: String.t() | nil
def choose_charset(charsets, acc_char_hdr) do
choose(charsets, acc_char_hdr, "utf8")
end
@@ -135,7 +158,7 @@ defmodule Ewebmachine.Core.Utils do
@doc """
Get HTTP status label from HTTP code
"""
- @spec http_label(code :: integer) :: String.t
+ @spec http_label(code :: integer) :: String.t()
def http_label(100), do: "Continue"
def http_label(101), do: "Switching Protocol"
def http_label(200), do: "OK"
@@ -179,38 +202,38 @@ defmodule Ewebmachine.Core.Utils do
def http_label(504), do: "Gateway Timeout"
def http_label(505), do: "HTTP Version Not Supported"
- ###
- ### Priv
- ###
- alias Ewebmachine.Compat
-
defp choose(choices, header, default) do
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding#weighted_accept-encoding_values
## sorted set of {prio,value}
prios = prioritized_values(header)
# determine if default is ok or any is ok if no match
default_prio = Enum.find_value(prios, fn {prio, v} -> v == default && prio end)
start_prio = Enum.find_value(prios, fn {prio, v} -> v == "*" && prio end)
- default_ok = case default_prio do
- nil -> start_prio !== +0.0
- +0.0 -> false
- _ -> true
- end
+
+ default_ok =
+ case default_prio do
+ nil -> start_prio !== +0.0
+ +0.0 -> false
+ _ -> true
+ end
+
any_ok = start_prio not in [nil, +0.0]
# remove choices where prio == 0.0
- {zero_prios, prios} = Compat.Enum.split_with(prios, fn {prio, _} -> prio == +0.0 end)
+ {zero_prios, prios} = Enum.split_with(prios, fn {prio, _} -> prio == +0.0 end)
+
choices_to_remove = Enum.map(zero_prios, &elem(&1, 1))
- choices = Enum.filter(choices, &!(String.downcase(&1) in choices_to_remove))
+ choices = Enum.filter(choices, &(!(String.downcase(&1) in choices_to_remove)))
if choices !== [] do
# find first match, if not found and any_ok, then first choice, else if default_ok, take it
Enum.find_value(prios, fn {_, val} ->
Enum.find(choices, &(val == String.downcase(&1)))
- end)
- || (any_ok && hd(choices)
- || (default_ok && Enum.find(choices, &(&1 == default))
- || nil))
+ end) ||
+ ((any_ok && hd(choices)) ||
+ ((default_ok && Enum.find(choices, &(&1 == default))) ||
+ nil))
else
# No proposed choice is acceptable by client
nil
@@ -219,19 +242,23 @@ defmodule Ewebmachine.Core.Utils do
defp prioritized_values(header) do
header
- |> Plug.Conn.Utils.list
- |> Enum.map(fn e->
- {q,v} = case String.split(e,~r"\s;\s", parts: 2) do
- [value,params] ->
- case Float.parse(Plug.Conn.Utils.params(params)["q"] || "1.0") do
- {q,_}->{q,value}
- :error -> {1.0,value}
- end
- [value] -> {1.0,value}
+ |> Plug.Conn.Utils.list()
+ |> Enum.map(fn e ->
+ {q, v} =
+ case String.split(e, ~r"\s;\s", parts: 2) do
+ [value, params] ->
+ case Float.parse(Plug.Conn.Utils.params(params)["q"] || "1.0") do
+ {q, _} -> {q, value}
+ :error -> {1.0, value}
+ end
+
+ [value] ->
+ {1.0, value}
end
- {q,String.downcase(v)}
- end)
- |> Enum.sort
- |> Enum.reverse
+
+ {q, String.downcase(v)}
+ end)
+ |> Enum.sort()
+ |> Enum.reverse()
end
end
diff --git a/lib/ewebmachine/events.ex b/lib/ewebmachine/events.ex
index 90fe854..e2c8343 100644
--- a/lib/ewebmachine/events.ex
+++ b/lib/ewebmachine/events.ex
@@ -1,4 +1,6 @@
defmodule Ewebmachine.Events do
+ @moduledoc false
+
def child_spec(_) do
Registry.child_spec(keys: :duplicate, name: __MODULE__)
end
@@ -6,21 +8,35 @@ defmodule Ewebmachine.Events do
@dispatch_key :events
def dispatch(log) do
Registry.dispatch(__MODULE__, @dispatch_key, fn entries ->
- for {pid, nil} <- entries, do: send(pid,{:log,log})
+ for {pid, nil} <- entries, do: send(pid, {:log, log})
end)
end
import Plug.Conn
+
def stream_chunks(conn) do
- conn = conn |>
- put_resp_header("content-type", "text/event-stream") |>
- send_chunked(200)
- {:ok, _} = Registry.register(__MODULE__,@dispatch_key,nil)
- conn = Stream.repeatedly(fn-> receive do {:log,log}-> log end end)
+ conn =
+ conn
+ |> put_resp_header("content-type", "text/event-stream")
+ |> send_chunked(200)
+
+ {:ok, _} = Registry.register(__MODULE__, @dispatch_key, nil)
+
+ conn =
+ Stream.repeatedly(fn ->
+ receive do
+ {:log, log} -> log
+ end
+ end)
|> Enum.reduce_while(conn, fn log, conn ->
io = "event: new_query\ndata: #{log}\n\n"
- case chunk(conn,io) do {:ok,conn}->{:cont,conn};{:error,:closed}->{:halt,conn} end
+
+ case chunk(conn, io) do
+ {:ok, conn} -> {:cont, conn}
+ {:error, :closed} -> {:halt, conn}
+ end
end)
+
halt(conn)
end
end
diff --git a/lib/ewebmachine/handlers.ex b/lib/ewebmachine/handlers.ex
index 007cca4..182221d 100644
--- a/lib/ewebmachine/handlers.ex
+++ b/lib/ewebmachine/handlers.ex
@@ -1,98 +1,61 @@
defmodule Ewebmachine.Handlers do
- @type conn :: Plug.Conn.t
- @type state :: any
- @type halt :: {:halt, 200..599}
-
@moduledoc """
- Implement the functions described below to make decisions in the
- [HTTP decision tree](./assets/http_diagram.png) :
-
- - `service_available/2`
- - `resource_exists/2`
- - `is_authorized/2`
- - `forbidden/2`
- - `allow_missing_post/2`
- - `malformed_request/2`
- - `uri_too_long/2`
- - `known_content_type/2`
- - `valid_content_headers/2`
- - `valid_entity_length/2`
- - `options/2`
- - `allowed_methods/2`
- - `known_methods/2`
- - `content_types_provided/2`
- - `content_types_accepted/2`
- - `delete_resource/2`
- - `delete_completed/2`
- - `post_is_create/2`
- - `create_path/2`
- - `base_uri/2`
- - `process_post/2`
- - `language_available/2`
- - `charsets_provided/2`
- - `encodings_provided/2`
- - `variances/2`
- - `is_conflict/2`
- - `multiple_choices/2`
- - `previously_existed/2`
- - `moved_permanently/2`
- - `moved_temporarily/2`
- - `last_modified/2`
- - `expires/2`
- - `generate_etag/2`
- - `validate_content_checksum/2`
- - `ping/2`
- - Body-producing function, see `to_html/2` (but any function name can be
- used, as referenced by `content_types_provided/2`
- - POST/PUT processing function, see `from_json/2` (but any function name can be
- used, as referenced by `content_types_accepted/2`
-
- All the handlers have the same signature :
+ Provides the default implementation for each resource handler.
+ An handler is a function with the following signature:
+ ```elixir
+ @spec handler(conn, state) :: {response :: any | {:halt, 200..599}, conn, state}
+ when conn: Plug.Conn.t(), state: var()
```
- (conn :: Plug.Conn.t,state :: any)->{response :: any | {:halt,200..599},conn :: Plug.Conn.t, state :: any}
- ```
- where every handler implementation :
+ An handler:
+ * can update the `conn`
+
+ > ##### Halting the connection {: .top}
+ >
+ > Halting the `conn` will not stop the execution of the HTTP decision tree
+ > of Ewebmachine.
- - can change or halt the plug `conn` passed as argument
- - can change the user state object passed from on handler to another in its arguments
- - returns something which will make decision in the HTTP decision tree (see
- documentation of functions in this module to see expected results and effects)
- - can return `{:halt,200..599}` to end the ewebmachine automate execution,
- but do not `halt` the `conn`, so the plug pipeline can continue.
+ * can change the state
- So each handler implementation is actually a "plug" returning a response giving
- information allowing to make the good response code and path in the HTTP
- specification.
+ > #### Ewebmachine and `state` {: .info}
+ > Ewebmachine never modifies the `state` is only pass it between handlers.
- ## Usage ##
+ * should return a response which will influence the HTTP decision tree. If an
+ handler returns `{:halt, 200.599}`, the execution of the HTTP decision tree
+ is immediatly stopped (the `conn` is not halted though).
- The following modules will help you to construct these handlers and use them :
+ Each handler has its own specific response type, see each handler below.
- - `Ewebmachine.Builder.Handlers` gives you macros and helpers to define the
- handlers and automatically create the plug to add them to your `conn.private[:resource_handlers]`
- - `Ewebmachine.Plug.Run` run the HTTP decision tree executing the handler
- implementations described in its `conn.private[:resource_handlers]`. The
- initial user `state` is taken in `conn.private[:machine_init]`
+ Handler functions are usually defined inside a
+ `Ewebmachine.Builder.Resources.resource/2` via the macros defined by the
+ `Ewebmachine.Builder.Handlers` module.
+ A set of common handlers can also be defined see the documentation of
+ `Ewebmachine.Builder.Handlers`.
"""
+ @type conn :: Plug.Conn.t()
+ @type state :: any
+ @type halt :: {:halt, 200..599}
+
@doc """
Returning non-true values will result in `503 Service Unavailable`.
Default: `true`
"""
- @spec service_available(conn,state) :: {boolean | halt,conn,state}
- def service_available(conn,state), do: {true,conn,state}
+ @doc group: :handler
+ @spec service_available(conn, state) :: {boolean | halt, conn, state}
+ def service_available(conn, state), do: {true, conn, state}
@doc """
Returning non-true values will result in `404 Not Found`.
Default: `true`
"""
- @spec resource_exists(conn,state) :: {boolean | halt,conn,state}
- def resource_exists(conn,state), do: {true,conn,state}
+ @doc group: :handler
+ @spec resource_exists(conn, state) :: {boolean | halt, conn, state}
+ def resource_exists(conn, state), do: {true, conn, state}
@doc """
If this returns anything other than `true`, the response will be
@@ -102,78 +65,81 @@ defmodule Ewebmachine.Handlers do
Default: `true`
"""
- @spec is_authorized(conn,state) :: {boolean | halt,conn,state}
- def is_authorized(conn,state), do: {true,conn,state}
+ @doc group: :handler
+ @spec is_authorized(conn, state) :: {boolean | halt, conn, state}
+ def is_authorized(conn, state), do: {true, conn, state}
@doc """
Returning true will result in 403 Forbidden.
Default: `false`
"""
- @spec forbidden(conn,state) :: {boolean | halt,conn,state}
- def forbidden(conn,state), do: {false,conn,state}
+ @doc group: :handler
+ @spec forbidden(conn, state) :: {boolean | halt, conn, state}
+ def forbidden(conn, state), do: {false, conn, state}
@doc """
If the resource accepts POST requests to nonexistent resources, then this should return `true`.
Default: `false`
"""
- @spec allow_missing_post(conn,state) :: {boolean | halt,conn,state}
- def allow_missing_post(conn,state), do: {false,conn,state}
+ @doc group: :handler
+ @spec allow_missing_post(conn, state) :: {boolean | halt, conn, state}
+ def allow_missing_post(conn, state), do: {false, conn, state}
@doc """
Returning true will result in 400 Bad Request.
Default: `false`
"""
- @spec malformed_request(conn,state) :: {boolean | halt,conn,state}
- def malformed_request(conn,state), do:
- {false,conn,state}
+ @doc group: :handler
+ @spec malformed_request(conn, state) :: {boolean | halt, conn, state}
+ def malformed_request(conn, state), do: {false, conn, state}
@doc """
Returning true will result in 414 Request-URI Too Long.
Default: `false`
"""
- @spec uri_too_long(conn,state) :: {boolean | halt,conn,state}
- def uri_too_long(conn,state), do:
- {false,conn,state}
+ @doc group: :handler
+ @spec uri_too_long(conn, state) :: {boolean | halt, conn, state}
+ def uri_too_long(conn, state), do: {false, conn, state}
@doc """
Returning false will result in 415 Unsupported Media Type.
Default: `true`
"""
- @spec known_content_type(conn,state) :: {boolean | halt,conn,state}
- def known_content_type(conn,state), do:
- {true,conn,state}
+ @doc group: :handler
+ @spec known_content_type(conn, state) :: {boolean | halt, conn, state}
+ def known_content_type(conn, state), do: {true, conn, state}
@doc """
Returning false will result in 501 Not Implemented.
Default: `true`
"""
- @spec valid_content_headers(conn,state) :: {boolean | halt,conn,state}
- def valid_content_headers(conn,state), do:
- {true,conn,state}
+ @doc group: :handler
+ @spec valid_content_headers(conn, state) :: {boolean | halt, conn, state}
+ def valid_content_headers(conn, state), do: {true, conn, state}
@doc """
Returning false will result in 413 Request Entity Too Large.
Default: `false`
"""
- @spec valid_entity_length(conn,state) :: {boolean | halt,conn,state}
- def valid_entity_length(conn,state), do:
- {true,conn,state}
+ @doc group: :handler
+ @spec valid_entity_length(conn, state) :: {boolean | halt, conn, state}
+ def valid_entity_length(conn, state), do: {true, conn, state}
@doc """
If the OPTIONS method is supported and is used, the return value of
this function is expected to be a list of pairs representing header
names and values that should appear in the response.
"""
- @spec options(conn,state) :: {[{String.t,String.t}] | halt,conn,state}
- def options(conn,state), do:
- {[],conn,state}
+ @doc group: :handler
+ @spec options(conn, state) :: {[{String.t(), String.t()}] | halt, conn, state}
+ def options(conn, state), do: {[], conn, state}
@doc """
If a Method not in this list is requested, then a 405 Method Not
@@ -181,116 +147,142 @@ defmodule Ewebmachine.Handlers do
Default: `["GET", "HEAD"]`
"""
- @spec allowed_methods(conn,state) :: {[String.t] | halt,conn,state}
- def allowed_methods(conn,state), do:
- {["GET", "HEAD"],conn,state}
+ @doc group: :handler
+ @spec allowed_methods(conn, state) :: {[String.t()] | halt, conn, state}
+ def allowed_methods(conn, state), do: {["GET", "HEAD"], conn, state}
@doc """
Override the known methods accepted by your automate
Default: `["GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", "OPTIONS"]`
"""
- @spec known_methods(conn,state) :: {[String.t] | halt,conn,state}
- def known_methods(conn,state), do:
- {["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"],conn,state}
+ @doc group: :handler
+ @spec known_methods(conn, state) :: {[String.t()] | halt, conn, state}
+ def known_methods(conn, state) do
+ {["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"], conn,
+ state}
+ end
@doc """
- This should return a key value tuple enumerable where the key is
- the content-type format and the value is an atom naming the
- function which can provide a resource representation in that media
- type. Content negotiation is driven by this return value. For
- example, if a client request includes an Accept header with a value
- that does not appear as a first element in any of the return
- tuples, then a 406 Not Acceptable will be
- sent.
+ This should return a `{key, value}` tuple enumerable where the key is the
+ content-type format and the value is an atom naming the function which can
+ provide a resource representation in that media type. Content negotiation is
+ driven by this return value. For example, if a client request includes an
+ `Accept` header with a value that does not appear as a first element in any
+ of the return tuples, then a 406 Not Acceptable will be sent.
+
+ ## Body producing function
+
+ Body producing function should have the following signature
+ ```elixir
+ @spec handler(conn, state) :: {iodata() | binary() | Enumerable.t(Plug.Conn.body()) | halt(), conn, state}
+ ```
+
+ If the returned body is an `t:Enumerable.t/1` other than a list, the response
+ will be streamed by the `Ewebmachine.Plug.Send` plug, therefore the
+ enumarable's element needs to be `t:Plug.Conn.body/0`.
Default: `[{"text/html", to_html}]`
"""
- @spec content_types_provided(conn,state) :: {[{String.Chars.t,atom}] | Enum.t | halt,conn,state}
- def content_types_provided(conn,state), do:
- {[{"text/html", :to_html}],conn,state}
+ @doc group: :handler
+ @spec content_types_provided(conn, state) ::
+ {[{String.t(), body_producing_fun}] | halt, conn, state}
+ when body_producing_fun: atom()
+ def content_types_provided(conn, state), do: {[{"text/html", :to_html}], conn, state}
@doc """
- This is used similarly to content_types_provided, except that it is
- for incoming resource representations -- for example, PUT requests.
- Handler functions usually want to use `Plug.read_body(conn)` to
- access the incoming request body.
-
+ This is used similarly to `content_types_provided/2`, except that it is
+ for incoming resource representations. For instance, `PUT` requests.
+
+ ## POST/PUT processing function
+
+ Functions set inside the `content_types_accepted` handler must have the
+ following signature:
+ ```elixir
+ @spec handler(conn, state) :: {any() | halt, conn, state}
+ when conn: Plug.Conn.t(), state: var()
+ ```
+ They will be called when the request is a `PUT` or when the request is a
+ `POST` and `post_is_create` returns `true`.
+
Default: `[]`
"""
- @spec content_types_accepted(conn,state) :: {[{String.Chars.t,atom}] | Enum.t | halt,conn,state}
- def content_types_accepted(conn,state), do:
- {[],conn,state}
+ @doc group: :handler
+ @spec content_types_accepted(conn, state) ::
+ {[{String.t(), body_processing_fun}] | halt, conn, state}
+ when body_processing_fun: atom()
+ def content_types_accepted(conn, state), do: {[], conn, state}
@doc """
This is called when a DELETE request should be enacted, and should
return `true` if the deletion succeeded.
"""
- @spec delete_resource(conn,state) :: {boolean | halt,conn,state}
- def delete_resource(conn,state), do:
- {false,conn,state}
+ @doc group: :handler
+ @spec delete_resource(conn, state) :: {boolean | halt, conn, state}
+ def delete_resource(conn, state), do: {false, conn, state}
@doc """
- This is only called after a successful `delete_resource` call, and
+ This is only called after a successful `delete_resource/2` call, and
should return `false` if the deletion was accepted but cannot yet be
guaranteed to have finished.
"""
- @spec delete_completed(conn,state) :: {boolean | halt,conn,state}
- def delete_completed(conn,state), do:
- {true,conn,state}
+ @doc group: :handler
+ @spec delete_completed(conn, state) :: {boolean | halt, conn, state}
+ def delete_completed(conn, state), do: {true, conn, state}
@doc """
If POST requests should be treated as a request to put content into
a (potentially new) resource as opposed to being a generic
- submission for processing, then this function should return true.
- If it does return `true`, then `create_path` will be called and the
+ submission for processing, then this function should return `true`.
+ If it does return `true`, then `create_path/2` will be called and the
rest of the request will be treated much like a PUT to the Path
entry returned by that call.
Default: `false`
"""
- @spec post_is_create(conn,state) :: {boolean | halt,conn,state}
- def post_is_create(conn,state), do:
- {false,conn,state}
+ @doc group: :handler
+ @spec post_is_create(conn, state) :: {boolean | halt, conn, state}
+ def post_is_create(conn, state), do: {false, conn, state}
@doc """
- This will be called on a POST request if `post_is_create` returns
- true. It is an error for this function not to produce a Path if
- post_is_create returns true. The Path returned should be a valid
+ This will be called on a POST request if `post_is_create/2` returns
+ `true`. It is an error for this function not to produce a Path if
+ `post_is_create/2` returns `true`. The Path returned should be a valid
URI part.
"""
- @spec create_path(conn,state) :: {nil | String.t | halt,conn,state}
- def create_path(conn,state), do:
- {nil,conn,state}
+ @doc group: :handler
+ @spec create_path(conn, state) :: {nil | String.t() | halt, conn, state}
+ def create_path(conn, state), do: {nil, conn, state}
@doc """
The base URI used in the location header on resource creation (when
- `post_is_create` is `true`), will be prepended to the `create_path`
+ `post_is_create/2` is `true`), will be prepended to the `create_path/2`
"""
- @spec base_uri(conn,state) :: {String.t | halt,conn,state}
- def base_uri(conn,state), do:
- {"#{conn.scheme}://#{conn.host}#{port_suffix(conn.scheme,conn.port)}",conn,state}
+ @doc group: :handler
+ @spec base_uri(conn, state) :: {String.t() | halt, conn, state}
+ def base_uri(conn, state),
+ do: {"#{conn.scheme}://#{conn.host}#{port_suffix(conn.scheme, conn.port)}", conn, state}
- defp port_suffix(:http,80), do: ""
- defp port_suffix(:https,443), do: ""
- defp port_suffix(_,port), do: ":#{port}"
+ defp port_suffix(:http, 80), do: ""
+ defp port_suffix(:https, 443), do: ""
+ defp port_suffix(_, port), do: ":#{port}"
@doc """
- If `post_is_create` returns `false`, then this will be called to
+ If `post_is_create/2` returns `false`, then this will be called to
process any POST requests. If it succeeds, it should return `true`.
"""
- @spec process_post(conn,state) :: {boolean | halt,conn,state}
- def process_post(conn,state), do:
- {false,conn,state}
+ @doc group: :handler
+ @spec process_post(conn, state) :: {boolean | halt, conn, state}
+ def process_post(conn, state), do: {false, conn, state}
@doc """
return false if language in
`Plug.Conn.get_resp_header(conn,"accept-language")` is not
available.
"""
- @spec language_available(conn,state) :: {boolean | halt,conn,state}
- def language_available(conn,state), do:
- {true,conn,state}
+ @doc group: :handler
+ @spec language_available(conn, state) :: {boolean | halt, conn, state}
+ def language_available(conn, state), do: {true, conn, state}
@doc """
If this is anything other than the atom `:no_charset`, it must be a
@@ -300,31 +292,33 @@ defmodule Ewebmachine.Handlers do
Default: `:no_charset`
"""
- @spec charsets_provided(conn,state) :: {:no_charset | [{String.Chars.t,(binary->binary)}] | Enum.t | halt,conn,state}
- def charsets_provided(conn,state), do:
- {:no_charset,conn,state}
+ @doc group: :handler
+ @spec charsets_provided(conn, state) ::
+ {:no_charset | [{String.Chars.t(), (binary -> binary)}] | Enum.t() | halt, conn, state}
+ def charsets_provided(conn, state), do: {:no_charset, conn, state}
## this atom causes charset-negotation to short-circuit
## the default setting is needed for non-charset responses such as image/png
## an example of how one might do actual negotiation
## [{"iso-8859-1", fun(X) -> X end}, {"utf-8", make_utf8}];
@doc """
- This must be a `{key,value}` Enumerable where `key` is a valid
+ This must be a `{key, value}` Enumerable where `key` is a valid
content encoding and `value` is a callable function in the resource
which will be called on the produced body in a GET and ensure that
it is so encoded. One useful setting is to have the function check
on method, and on GET requests return:
- ```
+ ```elixir
[identity: &(&1), gzip: &:zlib.gzip/1]
```
as this is all that is needed to support gzip content encoding.
Default: `[{"identity", fn X-> X end}]`
"""
- @spec encodings_provided(conn,state) :: {[{String.Chars.t,(binary->binary)}] | Enum.t | halt,conn,state}
- def encodings_provided(conn,state), do:
- {[{"identity", &(&1)}],conn,state}
+ @doc group: :handler
+ @spec encodings_provided(conn, state) ::
+ {[{String.Chars.t(), (binary -> binary)}] | Enum.t() | halt, conn, state}
+ def encodings_provided(conn, state), do: {[{"identity", & &1}], conn, state}
# this is handy for auto-gzip of GET-only resources:
# [{"identity", fun(X) -> X end}, {"gzip", fun(X) -> zlib:gzip(X) end}];
@@ -338,18 +332,18 @@ defmodule Ewebmachine.Handlers do
Default : `[]`
"""
- @spec variances(conn,state) :: {[String.t] | halt,conn,state}
- def variances(conn,state), do:
- {[],conn,state}
+ @doc group: :handler
+ @spec variances(conn, state) :: {[String.t()] | halt, conn, state}
+ def variances(conn, state), do: {[], conn, state}
@doc """
If this returns `true`, the client will receive a 409 Conflict.
Default : `false`
"""
- @spec is_conflict(conn,state) :: {boolean | halt,conn,state}
- def is_conflict(conn,state), do:
- {false,conn,state}
+ @doc group: :handler
+ @spec is_conflict(conn, state) :: {boolean | halt, conn, state}
+ def is_conflict(conn, state), do: {false, conn, state}
@doc """
If this returns `true`, then it is assumed that multiple
@@ -359,20 +353,20 @@ defmodule Ewebmachine.Handlers do
Default: `false`
"""
- @spec multiple_choices(conn,state) :: {boolean | halt,conn,state}
- def multiple_choices(conn,state), do:
- {false,conn,state}
+ @doc group: :handler
+ @spec multiple_choices(conn, state) :: {boolean | halt, conn, state}
+ def multiple_choices(conn, state), do: {false, conn, state}
@doc """
- If this returns `true`, the `moved_permanently` and `moved_temporarily`
+ If this returns `true`, the `moved_permanently/2` and `moved_temporarily/2`
callbacks will be invoked to determine whether the response should
be `301 Moved Permanently`, `307 Temporary Redirect`, or `410 Gone`.
Default: `false`
"""
- @spec previously_existed(conn,state) :: {boolean | halt,conn,state}
- def previously_existed(conn,state), do:
- {false,conn,state}
+ @doc group: :handler
+ @spec previously_existed(conn, state) :: {boolean | halt, conn, state}
+ def previously_existed(conn, state), do: {false, conn, state}
@doc """
If this returns `{true, uri}`, the client will receive a `301 Moved
@@ -380,9 +374,9 @@ defmodule Ewebmachine.Handlers do
Default: `false`
"""
- @spec moved_permanently(conn,state) :: {boolean | halt,conn,state}
- def moved_permanently(conn,state), do:
- {false,conn,state}
+ @doc group: :handler
+ @spec moved_permanently(conn, state) :: {boolean | halt, conn, state}
+ def moved_permanently(conn, state), do: {false, conn, state}
@doc """
If this returns `{true, uri}`, the client will receive a `307
@@ -390,9 +384,9 @@ defmodule Ewebmachine.Handlers do
Default: `false`
"""
- @spec moved_temporarily(conn,state) :: {boolean | halt,conn,state}
- def moved_temporarily(conn,state), do:
- {false,conn,state}
+ @doc group: :handler
+ @spec moved_temporarily(conn, state) :: {boolean | halt, conn, state}
+ def moved_temporarily(conn, state), do: {false, conn, state}
@doc """
If this returns a `datetime()` (`{{day,month,year},{h,m,s}}`, it
@@ -401,18 +395,26 @@ defmodule Ewebmachine.Handlers do
Default: `nil`
"""
- @spec last_modified(conn,state) :: {nil | {{day::integer,month::integer,year::integer},{hour::integer,min::integer,sec::integer}} | halt,conn,state}
- def last_modified(conn,state), do:
- {nil,conn,state}
+ @doc group: :handler
+ @spec last_modified(conn, state) ::
+ {nil
+ | {{day :: integer, month :: integer, year :: integer},
+ {hour :: integer, min :: integer, sec :: integer}}
+ | halt, conn, state}
+ def last_modified(conn, state), do: {nil, conn, state}
@doc """
If not `nil`, set the expires header
Default: `nil`
"""
- @spec expires(conn,state) :: {nil | {{day::integer,month::integer,year::integer},{hour::integer,min::integer,sec::integer}} | halt,conn,state}
- def expires(conn,state), do:
- {nil,conn,state}
+ @doc group: :handler
+ @spec expires(conn, state) ::
+ {nil
+ | {{day :: integer, month :: integer, year :: integer},
+ {hour :: integer, min :: integer, sec :: integer}}
+ | halt, conn, state}
+ def expires(conn, state), do: {nil, conn, state}
@doc """
If not `nil`, it will be used for the ETag header and
@@ -420,55 +422,51 @@ defmodule Ewebmachine.Handlers do
Default: `nil`
"""
- @spec generate_etag(conn,state) :: {nil | binary | halt,conn,state}
- def generate_etag(conn,state), do:
- {nil,conn,state}
+ @doc group: :handler
+ @spec generate_etag(conn, state) :: {nil | binary | halt, conn, state}
+ def generate_etag(conn, state), do: {nil, conn, state}
@doc """
- if `content-md5` header exists:
+ if `content-md5` header exists:
- If `:not_validated`, test if input body validate `content-md5`,
- if return `false`, then return a bad request
Useful if content-md5 validation does not imply only raw md5 hash
"""
- @spec validate_content_checksum(conn,state) :: {:not_validated | boolean | halt,conn,state}
- def validate_content_checksum(conn,state), do:
- {:not_validated,conn,state}
+ @doc group: :handler
+ @spec validate_content_checksum(conn, state) :: {:not_validated | boolean | halt, conn, state}
+ def validate_content_checksum(conn, state), do: {:not_validated, conn, state}
- @doc """
- Must be present and returning `pong` to prove that handlers are
- well linked to the automate
- """
- @spec ping(conn,state) :: {:pang | :pong | halt,conn,state}
- def ping(conn,state), do:
- {:pang,conn,state}
+ if Mix.env() == :test do
+ @doc false
+ @spec ping(conn, state) :: {:pang | :pong, conn, state}
+ def ping(conn, state), do: {:pang, conn, state}
+ end
@doc """
Last handler, always called. Response is ignored except if it is a `halt`.
"""
- @spec finish_request(conn,state) :: {any | halt, conn, state}
- def finish_request(conn,state), do:
- {false,conn,state}
+ @doc group: :handler
+ @spec finish_request(conn, state) :: {any | halt, conn, state}
+ def finish_request(conn, state), do: {false, conn, state}
@doc """
Example body-producing function, function atom name must be referenced in `content_types_provided/2`.
-
+
- If the result is an `Enumerable` of `iodata`, then the HTTP response will be
a chunk encoding response where each chunk on element of the enumeration.
- If the result is an iodata, then it is used as the HTTP response body
"""
- @spec to_html(conn,state) :: {iodata | Enum.t | halt,conn,state}
- def to_html(conn,state), do:
- {"Hello World
",conn,state}
+ @spec to_html(conn, state) :: {iodata | Enum.t() | halt, conn, state}
+ def to_html(conn, state), do: {"", conn, state}
@doc """
Example POST/PUT processing function, function atom name must be referenced
in `content_types_accepted/2`.
It will be called when the request is `PUT` or when the
- request is `POST` and `post_is_create` returns true.
+ request is `POST` and `post_is_create/2` returns true.
"""
- @spec from_json(conn,state) :: {true | halt,conn,state}
- def from_json(conn,state), do:
- {true,conn,state}
+ @spec from_json(conn, state) :: {true | halt, conn, state}
+ def from_json(conn, state), do: {true, conn, state}
end
diff --git a/lib/ewebmachine/log.ex b/lib/ewebmachine/log.ex
index 22d9399..624ef20 100644
--- a/lib/ewebmachine/log.ex
+++ b/lib/ewebmachine/log.ex
@@ -3,63 +3,112 @@ defmodule Ewebmachine.Log do
use GenServer
@moduledoc false
- def init([]), do:
- (:ets.new(:logs, [:ordered_set, :named_table]); {:ok,[]})
- def handle_cast(conn,_), do:
- (:ets.insert(:logs,{conn.private[:machine_log],conn}); {:noreply,[]})
+ def init([]) do
+ :ets.new(:logs, [:ordered_set, :named_table])
+ {:ok, []}
+ end
+
+ def handle_cast(conn, _) do
+ :ets.insert(:logs, {conn.private[:machine_log], conn})
+ {:noreply, []}
+ end
# Public API, self describing
- def start_link(_), do:
- GenServer.start_link(__MODULE__,[], name: __MODULE__)
- def put(conn), do:
- GenServer.cast(__MODULE__,conn)
- def list do # fold only needed file for log listing for perfs
- :ets.foldl(fn {_,%{method: m, request_path: path, path_info: pi, private: %{machine_log: l, machine_init_at: i}}},acc->
- [%Conn{method: m,request_path: path, path_info: pi, private: %{machine_log: l,machine_init_at: i}}|acc]
- end,[],:logs)
+ def start_link(_), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
+ def put(conn), do: GenServer.cast(__MODULE__, conn)
+ # fold only needed file for log listing for perfs
+ def list do
+ :ets.foldl(
+ fn {_,
+ %{
+ method: m,
+ request_path: path,
+ path_info: pi,
+ private: %{machine_log: l, machine_init_at: i}
+ }},
+ acc ->
+ [
+ %Conn{
+ method: m,
+ request_path: path,
+ path_info: pi,
+ private: %{machine_log: l, machine_init_at: i}
+ }
+ | acc
+ ]
+ end,
+ [],
+ :logs
+ )
+ end
+
+ def get(id) do
+ case :ets.lookup(:logs, id) do
+ [{_, conn}] -> conn
+ _ -> nil
+ end
end
- def get(id), do:
- (case :ets.lookup(:logs,id) do [{_,conn}]->conn; _->nil end)
- def id, do:
- (make_ref() |> :erlang.term_to_binary |> Base.url_encode64)
+
+ def id, do: make_ref() |> :erlang.term_to_binary() |> Base.url_encode64()
# Conn modifiers called by automate during run
def debug_init(conn) do
if conn.private[:machine_debug] do
conn
- |> Conn.put_private(:machine_log,id())
- |> Conn.put_private(:machine_init_at,:erlang.timestamp)
- |> Conn.put_private(:machine_decisions,[])
- |> Conn.put_private(:machine_calls,[])
- else conn end
+ |> Conn.put_private(:machine_log, id())
+ |> Conn.put_private(:machine_init_at, :erlang.timestamp())
+ |> Conn.put_private(:machine_decisions, [])
+ |> Conn.put_private(:machine_calls, [])
+ else
+ conn
+ end
end
- def debug_call(conn,module,function,[in_conn,in_state],{resp,out_conn,out_state}) do
+
+ def debug_call(conn, module, function, [in_conn, in_state], {resp, out_conn, out_state}) do
if conn.private[:machine_log] !== nil and module !== Ewebmachine.Handlers do
- Conn.put_private(conn,:machine_calls,
- [{module,function,[%{in_conn|private: %{}},in_state],
- {resp,%{out_conn|private: %{}},out_state}}
- |conn.private.machine_calls])
- else conn end
+ Conn.put_private(conn, :machine_calls, [
+ {module, function, [%{in_conn | private: %{}}, in_state],
+ {resp, %{out_conn | private: %{}}, out_state}}
+ | conn.private.machine_calls
+ ])
+ else
+ conn
+ end
end
+
def debug_enddecision(conn) do
if conn.private[:machine_log] do
case conn.private.machine_decisions do
- [{decision,_}|rest] ->
- conn
- |> Conn.put_private(:machine_decisions,[{decision,Enum.reverse(conn.private.machine_calls)}|rest])
- |> Conn.put_private(:machine_calls,[])
- _->conn
+ [{decision, _} | rest] ->
+ conn
+ |> Conn.put_private(:machine_decisions, [
+ {decision, Enum.reverse(conn.private.machine_calls)} | rest
+ ])
+ |> Conn.put_private(:machine_calls, [])
+
+ _ ->
+ conn
end
- else conn end
+ else
+ conn
+ end
end
- def debug_decision(conn,decision) do
+
+ def debug_decision(conn, decision) do
if conn.private[:machine_log] do
- case Regex.run(~r/^v[0-9]([a-z]*[0-9]*)$/,to_string(decision)) do
- [_,decision]->
+ case Regex.run(~r/^v[0-9]([a-z]*[0-9]*)$/, to_string(decision)) do
+ [_, decision] ->
conn = debug_enddecision(conn)
- Conn.put_private(conn,:machine_decisions,[{decision,[]}|conn.private.machine_decisions])
- _->conn
+
+ Conn.put_private(conn, :machine_decisions, [
+ {decision, []} | conn.private.machine_decisions
+ ])
+
+ _ ->
+ conn
end
- else conn end
+ else
+ conn
+ end
end
end
diff --git a/lib/ewebmachine/plug.debug.ex b/lib/ewebmachine/plug.debug.ex
index 02e1414..7abad16 100644
--- a/lib/ewebmachine/plug.debug.ex
+++ b/lib/ewebmachine/plug.debug.ex
@@ -1,59 +1,55 @@
defmodule Ewebmachine.Plug.Debug do
@moduledoc ~S"""
- A ewebmachine debug UI at `/wm_debug`
+ A plug providing a debug UI for the decision tree available at `/wm_debug`.
Add it before `Ewebmachine.Plug.Run` in your plug pipeline when you
want debugging facilities.
+ ```elixir
+ if Mix.env() == :dev, do: plug Ewebmachine.Plug.Debug
```
- if Mix.env == :dev, do: plug Ewebmachine.Plug.Debug
- ```
- Then go to `http://youhost:yourport/wm_debug`, you will see the
- request list since the launch of your server. Click on any to get
- the ewebmachine debugging UI. The list will be automatically
- updated on new query.
-
- The ewebmachine debugging UI
-
- - shows you the HTTP decision path taken by the request to the response. Every
- - the red decisions are the one where decisions differs from the
- default one because of a handler implementation :
- - click on them, then select any handler available in the right
- tab to see the `conn`, `state` inputs of the handler and the
- response.
- - The response and request right tab shows you the request and
- result at the end of the ewebmachine run.
- - click on "auto redirect on new query" and at every request, your
- browser will navigate to the debugging UI of the new request (you
- can still use back/next to navigate through requests)
+ Then go to `http://:/wm_debug`, you will see the request list
+ since the launch of your server. Click on any to get the Ewebmachine
+ debugging UI. The list will be automatically updated on new query.
+
+ The Ewebmachine debugging UI shows you the path taken by the request through
+ the decision tree. Decisions where user defined handlers are called are
+ highlighted in red. Clicking on decision will open a panel with three tabs:
+ * `Q`: contains the received http request
+ * `R`: contains the sent http response
+ * `D`: displays the `conn` and the `state` before and after the handler
+ call. The handler's response is also displayed.
+
+ The `auto redirect on new query` will at every request redirect your browser
+ to the new request debugging UI.

"""
use Plug.Router
-
+
alias Plug.Conn
alias Ewebmachine.Log
-
+
plug Plug.Static, at: "/wm_debug/static", from: :ewebmachine
plug :match
plug :dispatch
require EEx
- EEx.function_from_file :defp, :render_logs, "templates/log_list.html.eex", [:conns]
- EEx.function_from_file :defp, :render_log, "templates/log_view.html.eex", [:logconn, :conn]
+ EEx.function_from_file(:defp, :render_logs, "templates/log_list.html.eex", [:conns])
+ EEx.function_from_file(:defp, :render_log, "templates/log_view.html.eex", [:logconn, :conn])
get "/wm_debug/log/:id" do
- if (logconn=Log.get(id)) do
- conn |> send_resp(200,render_log(logconn,conn)) |> halt
+ if logconn = Log.get(id) do
+ conn |> send_resp(200, render_log(logconn, conn)) |> halt
else
- conn |> put_resp_header("location","/wm_debug") |> send_resp(302,"") |> halt
+ conn |> put_resp_header("location", "/wm_debug") |> send_resp(302, "") |> halt
end
end
get "/wm_debug" do
- html = render_logs(Log.list)
- conn |> send_resp(200,html) |> halt
+ html = render_logs(Log.list())
+ conn |> send_resp(200, html) |> halt
end
get "/wm_debug/events" do
@@ -65,58 +61,64 @@ defmodule Ewebmachine.Plug.Debug do
end
@doc false
- def to_draw(conn), do: %{
- request: """
- #{conn.method} #{conn.request_path} HTTP/1.1
- #{html_escape format_headers(conn.req_headers)}
- #{html_escape body_of(conn)}
- """,
- response: %{
- http: """
- HTTP/1.1 #{conn.status} #{Ewebmachine.Core.Utils.http_label(conn.status)}
- #{html_escape format_headers(conn.resp_headers)}
- #{html_escape (conn.resp_body || "some chunked body")}
+ def to_draw(conn) do
+ %{
+ request: """
+ #{conn.method} #{conn.request_path} HTTP/1.1
+ #{html_escape(format_headers(conn.req_headers))}
+ #{html_escape(body_of(conn))}
""",
- code: conn.status
- },
- trace: Enum.map(Enum.reverse(conn.private.machine_decisions), fn {decision,calls}->
- %{
- d: decision,
- calls: Enum.map(calls,fn {module,function,[in_conn,in_state],{resp,out_conn,out_state}}->
+ response: %{
+ http: """
+ HTTP/1.1 #{conn.status} #{Ewebmachine.Core.Utils.http_label(conn.status)}
+ #{html_escape(format_headers(conn.resp_headers))}
+ #{html_escape(conn.resp_body || "some chunked body")}
+ """,
+ code: conn.status
+ },
+ trace:
+ Enum.map(Enum.reverse(conn.private.machine_decisions), fn {decision, calls} ->
%{
- module: inspect(module),
- function: "#{function}",
- input: """
- state = #{html_escape inspect(in_state, pretty: true)}
-
- conn = #{html_escape inspect(in_conn, pretty: true)}
- """,
- output: """
- response = #{html_escape inspect(resp, pretty: true)}
-
- state = #{html_escape inspect(out_state, pretty: true)}
-
- conn = #{html_escape inspect(out_conn, pretty: true)}
- """
+ d: decision,
+ calls:
+ Enum.map(calls, fn {module, function, [in_conn, in_state],
+ {resp, out_conn, out_state}} ->
+ %{
+ module: inspect(module),
+ function: "#{function}",
+ input: """
+ state = #{html_escape(inspect(in_state, pretty: true))}
+
+ conn = #{html_escape(inspect(in_conn, pretty: true))}
+ """,
+ output: """
+ response = #{html_escape(inspect(resp, pretty: true))}
+
+ state = #{html_escape(inspect(out_state, pretty: true))}
+
+ conn = #{html_escape(inspect(out_conn, pretty: true))}
+ """
+ }
+ end)
}
end)
- }
- end)
- }
+ }
+ end
defp body_of(conn) do
case Conn.read_body(conn) do
- {:ok,body,_}->body
+ {:ok, body, _} -> body
_ -> ""
end
end
defp format_headers(headers) do
- headers |> Enum.map(fn {k,v}->"#{k}: #{v}\n" end) |> Enum.join
+ headers |> Enum.map(fn {k, v} -> "#{k}: #{v}\n" end) |> Enum.join()
end
- defp html_escape(data), do:
- to_string(for(<>, do: escape_char(char)))
+ defp html_escape(data),
+ do: to_string(for(<>, do: escape_char(char)))
+
defp escape_char(?<), do: "<"
defp escape_char(?>), do: ">"
defp escape_char(?&), do: "&"
diff --git a/lib/ewebmachine/plug.error_as_exception.ex b/lib/ewebmachine/plug.error_as_exception.ex
index 18916b7..5b1e65d 100644
--- a/lib/ewebmachine/plug.error_as_exception.ex
+++ b/lib/ewebmachine/plug.error_as_exception.ex
@@ -1,15 +1,28 @@
defmodule Ewebmachine.Plug.ErrorAsException do
@moduledoc """
- This plug checks the current response status. If it is an error, raise a plug
- exception with the status code and the HTTP error name as the message. If
- this response body is not void, use it as the exception message.
+ Raises an `#{__MODULE__}` exception on errornous HTTP status code.
+
+ If the `conn`'s state is `:set` and its `status` is higher than `399`, this
+ plug raises a `#{__MODULE__}` exception.
+
+ The `conn`'s `resp_body` is used if not empty as the exception's message,
+ otherwise the http code's label is used.
"""
- defexception [:plug_status,:message]
- def init(_), do: []
- def call(%{status: code, state: :set}=conn,_) when code > 399, do: raise(__MODULE__,conn)
- def call(conn,_), do: conn
- def exception(%{status: code,resp_body: msg}) when byte_size(msg)>0, do:
- %__MODULE__{plug_status: code, message: msg}
- def exception(%{status: code}), do:
- %__MODULE__{plug_status: code, message: Ewebmachine.Core.Utils.http_label(code)}
+ @behaviour Plug
+
+ defexception [:plug_status, :message]
+
+ @impl Plug
+ def init(_), do: []
+
+ @impl Plug
+ def call(%{status: code, state: :set} = conn, _) when code > 399, do: raise(__MODULE__, conn)
+ def call(conn, _), do: conn
+
+ @impl Exception
+ def exception(%{status: code, resp_body: msg}) when is_bitstring(msg) and byte_size(msg) > 0,
+ do: %__MODULE__{plug_status: code, message: msg}
+
+ def exception(%{status: code}),
+ do: %__MODULE__{plug_status: code, message: Ewebmachine.Core.Utils.http_label(code)}
end
diff --git a/lib/ewebmachine/plug.error_as_forward.ex b/lib/ewebmachine/plug.error_as_forward.ex
index 6026ea1..12d7987 100644
--- a/lib/ewebmachine/plug.error_as_forward.ex
+++ b/lib/ewebmachine/plug.error_as_forward.ex
@@ -1,23 +1,36 @@
defmodule Ewebmachine.Plug.ErrorAsForward do
@moduledoc """
- This plug take an argument `forward_pattern` (default to `"/error/:status"`),
- and, when the current response status is an error, simply forward to a `GET`
- to the path defined by the pattern and this status.
+ Sets the `path_info` on errornous HTTP status code to forward the request to
+ another plug.
+
+ If the `conn`'s `status` is higher than `399` , the `conn`'s `path_info` is
+ set to the `forward_pattern`, the `state` sets to `:unset`, and the `method`
+ to `GET`.
+
+ Usefull is you want to have a generic route error handler downstream in the
+ pipeline.
+
+ ## Options
+
+ * `forward_pattern` - sets the redirection path. The `:status` will be
+ replaced by the `conn`'s `status`. Defaults to `/error/:status`.
"""
+ @behaviour Plug
- @doc false
- def init(opts), do: (opts[:forward_pattern] || "/error/:status")
+ @impl Plug
+ def init(opts), do: opts[:forward_pattern] || "/error/:status"
- @doc false
+ @impl Plug
def call(%Plug.Conn{status: code, state: :set} = conn, pattern) when code > 399 do
# `path_info` info is the request path split as segments.
+ # Generate a path according to the status code.
path_info =
- # Generate a path according to the status code.
String.replace(pattern, ":status", to_string(code))
# Transform it into segments.
|> String.split("/", trim: true)
%{conn | path_info: path_info, method: "GET", state: :unset}
end
+
def call(conn, _), do: conn
end
diff --git a/lib/ewebmachine/plug.run.ex b/lib/ewebmachine/plug.run.ex
index 806751f..c095c58 100644
--- a/lib/ewebmachine/plug.run.ex
+++ b/lib/ewebmachine/plug.run.ex
@@ -1,43 +1,48 @@
defmodule Ewebmachine.Plug.Run do
@moduledoc ~S"""
- 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
- `Ewebmachine.Plug.Send`. This is useful to customize the Ewebmachine result
- after the run, for instance to customize the error body (void by default).
-
- - Decisions are make according to handlers set in `conn.private[:resource_handlers]`
- (`%{handler_name: handler_module}`) where `handler_name` is one
- of the handler function of `Ewebmachine.Handlers` and
- `handler_module` is the module implementing it.
- - Initial user state (second parameter of handler function) is
- taken from `conn.private[:machine_init]`
-
- `Ewebmachine.Builder.Handlers` `:add_handler` plug allows you to
- set these parameters in order to use this Plug.
-
- A successfull run will reset the resource handlers and initial state.
+ A plug which runs the HTTP decision tree with the set handlers defined by a
+ matching `Ewebmachine.Builder.Resources.resource/2`.
+
+ The initial user state is taken from the `do` block of the
+ `Ewebmachine.Builder.Resources.resource/2` macro.
+
+ 
+
+ This plug does not send the response to let additional plugs customize the
+ result.
+
+ To send the response, use the `Ewebmachine.Plug.Send` after this one.
"""
- @doc false
+ @behaviour Plug
+
+ @impl Plug
def init(_opts), do: []
- @doc false
+ @impl Plug
def call(conn, _opts) do
init = conn.private[:machine_init]
- if (init) do
+
+ if init do
conn = Ewebmachine.Core.v3(conn, init)
log = conn.private[:machine_log]
- if (log) do
+
+ if log do
Ewebmachine.Log.put(conn)
Ewebmachine.Events.dispatch(log)
end
- private = Map.drop(conn.private, [
- :machine_init, :resource_handlers, :machine_decisions, :machine_calls, :machine_log, :machine_init_at
- ])
- %{ conn | private: private }
+
+ private =
+ Map.drop(conn.private, [
+ :machine_init,
+ :resource_handlers,
+ :machine_decisions,
+ :machine_calls,
+ :machine_log,
+ :machine_init_at
+ ])
+
+ %{conn | private: private}
else
conn
end
diff --git a/lib/ewebmachine/plug.send.ex b/lib/ewebmachine/plug.send.ex
index 9fb4257..7912616 100644
--- a/lib/ewebmachine/plug.send.ex
+++ b/lib/ewebmachine/plug.send.ex
@@ -1,35 +1,34 @@
defmodule Ewebmachine.Plug.Send do
@moduledoc ~S"""
- Calling this plug will send the response and halt the connection
- pipeline if the `conn` has passed through an `Ewebmachine.Plug.Run`.
+ A plug which sends the response and halts the pipeline if the state is `:set`.
+
+ Should be used in a plug pipeline after the `Ewebmachine.Plug.Run` plug.
"""
import Plug.Conn
- @doc false
+ @behaviour Plug
+
+ @impl Plug
def init(_opts), do: []
- @doc false
- def call(conn, _opts) do
- if conn.state == :set do
- stream = conn.private[:machine_body_stream]
- conn =
- if stream do
- conn = send_chunked(conn,conn.status)
- conn = Enum.reduce_while(stream, conn, fn chunk, conn ->
- case Plug.Conn.chunk(conn, chunk) do
- {:ok, conn} ->
- {:cont, conn}
- {:error, :closed} ->
- {:halt, conn}
- end
- end)
- conn
- else
- send_resp(conn)
+ @impl Plug
+ def call(%Plug.Conn{state: :set, private: %{machine_body_stream: stream}} = conn, _) do
+ conn = send_chunked(conn, conn.status)
+
+ conn =
+ Enum.reduce_while(stream, conn, fn chunk, conn ->
+ case Plug.Conn.chunk(conn, chunk) do
+ {:ok, conn} ->
+ {:cont, conn}
+
+ {:error, :closed} ->
+ {:halt, conn}
end
- halt(conn)
- else
- conn
- end
+ end)
+
+ halt(conn)
end
+
+ def call(%Plug.Conn{state: :set} = conn, _), do: halt(send_resp(conn))
+ def call(%Plug.Conn{} = conn, _), do: conn
end
diff --git a/mix.exs b/mix.exs
index 7505a0e..61b9977 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,7 +1,7 @@
defmodule Ewebmachine.Mixfile do
use Mix.Project
- def version, do: "2.3.3"
+ def version, do: "2.4.0"
@description """
Ewebmachine contains macros and plugs to allow you to compose
@@ -18,13 +18,13 @@ defmodule Ewebmachine.Mixfile do
docs: docs(),
deps: deps(),
description: @description,
- package: package(),
+ package: package()
]
end
def application do
[
- mod: { Ewebmachine.App, [] },
+ mod: {Ewebmachine.App, []},
extra_applications: [:inets],
env: []
]
@@ -32,23 +32,46 @@ defmodule Ewebmachine.Mixfile do
defp docs do
[
- assets: "assets",
+ assets: %{"assets" => "assets"},
+ default_group_for_doc: fn
+ %{group: :handler} -> "Handlers"
+ _ -> nil
+ end,
extras: [
"CHANGELOG.md": [title: "Changelog"],
- "README.md": [title: "Overview"],
- "pages/demystify_dsl.md": [title: "Demystify Ewebmachine DSL"],
+ "README.md": [title: "Ewebmachine"],
+ "pages/demystify_dsl.md": [title: "Demystify Ewebmachine DSL"]
+ ],
+ formatters: ["html"],
+ groups_for_modules: [
+ # Ungrouped modules
+ #
+ # Ewebmachine
+ # Ewebmachine.Builder.Handlers
+ # Ewebmachine.Builder.Resources
+ # Ewebmachine.Core.Utils
+ # Ewebmachine.Handlers
+
+ Plugs: [
+ Ewebmachine.Plug.Debug,
+ Ewebmachine.Plug.ErrorAsException,
+ Ewebmachine.Plug.ErrorAsForward,
+ Ewebmachine.Plug.Send,
+ Ewebmachine.Plug.Run
+ ]
],
main: "readme",
+ nest_modules_by_prefix: [Ewebmachine.Plug],
+ skip_undefined_reference_warnings_on: ["CHANGELOG.md"],
source_url: git_repository(),
# We need to git tag with the corresponding format.
- source_ref: "v#{version()}",
+ source_ref: "v#{version()}"
]
end
defp deps do
[
{:plug, "~> 1.10"},
- {:plug_cowboy, "~> 2.4"},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:dialyxir, "~> 0.5", only: [:dev], runtime: false}
]
@@ -59,11 +82,20 @@ defmodule Ewebmachine.Mixfile do
links: %{
"GitHub" => git_repository(),
"Doc" => "http://hexdocs.pm/ewebmachine",
- "Changelog" => "https://hexdocs.pm/ewebmachine/changelog.html",
+ "Changelog" => "https://hexdocs.pm/ewebmachine/changelog.html"
},
maintainers: ["Arnaud Wetzel", "Yurii Rashkovskii", "Jean Parpaillon"],
licenses: ["MIT"],
- files: ["lib", "priv", "mix.exs", "README*", "templates", "LICENSE*", "CHANGELOG*", "examples"],
+ files: [
+ "lib",
+ "priv",
+ "mix.exs",
+ "README*",
+ "templates",
+ "LICENSE*",
+ "CHANGELOG*",
+ ".formatter.exs"
+ ]
]
end
diff --git a/test/run_core_test.exs b/test/run_core_test.exs
index 4c9c52a..4e6ece7 100644
--- a/test/run_core_test.exs
+++ b/test/run_core_test.exs
@@ -1,22 +1,23 @@
-Code.require_file "test_helper.exs", __DIR__
+Code.require_file("test_helper.exs", __DIR__)
defmodule CommonMacros do
- defmacro resources([do: body]) do
- name = :"#{inspect make_ref()}"
-
- {:module, module, _, _} = Module.create(
- name,
- quote do
- use Ewebmachine.Builder.Resources
- plug :resource_match
- plug Ewebmachine.Plug.Run
- plug Ewebmachine.Plug.Send
- plug :error_404
- defp error_404(conn, _), do: conn |> send_resp(404, "") |> halt()
- unquote(body)
- end,
- Macro.Env.location(__ENV__)
- )
+ defmacro resources(do: body) do
+ name = :"#{inspect(make_ref())}"
+
+ {:module, module, _, _} =
+ Module.create(
+ name,
+ quote do
+ use Ewebmachine.Builder.Resources
+ plug :resource_match
+ plug Ewebmachine.Plug.Run
+ plug Ewebmachine.Plug.Send
+ plug :error_404
+ defp error_404(conn, _), do: conn |> send_resp(404, "") |> halt()
+ unquote(body)
+ end,
+ Macro.Env.location(__ENV__)
+ )
module
end
@@ -37,9 +38,10 @@ defmodule EwebmachineTest do
defh to_html, do: "Hello World"
end
+
conn = SimpleHtml.call(conn(:get, "/"), [])
assert conn.status == 200
- assert Enum.into(conn.resp_headers,%{})["content-type"] == "text/html"
+ assert Enum.into(conn.resp_headers, %{})["content-type"] == "text/html"
assert conn.resp_body == "Hello World"
assert conn.state == :sent
end
@@ -47,224 +49,371 @@ defmodule EwebmachineTest do
test "default plugs" do
defmodule SimpleResources do
use Ewebmachine.Builder.Resources, default_plugs: true
- resource "/ok" do [] after defh(to_html, do: "toto") end
+
+ resource "/ok" do
+ []
+ after
+ defh(to_html, do: "toto")
+ end
end
+
conn = SimpleResources.call(conn(:get, "/ok"), [])
assert conn.status == 200
- assert Enum.into(conn.resp_headers,%{})["content-type"] == "text/html"
+ assert Enum.into(conn.resp_headers, %{})["content-type"] == "text/html"
assert conn.resp_body == "toto"
assert conn.state == :sent
end
test "Simple resource builder with XML and path match param" do
- app = resources do
- resource "/hello/:name" do %{name: name} after
- content_types_provided do: ["application/xml": :to_xml]
- defh to_xml, do: "#{state.name}"
+ app =
+ resources do
+ resource "/hello/:name" do
+ %{name: name}
+ after
+ content_types_provided do: ["application/xml": :to_xml]
+ defh to_xml, do: "#{state.name}"
+ end
end
- end
+
assert app.call(conn(:get, "/"), []).status == 404
conn = app.call(conn(:get, "/hello/arnaud"), [])
assert conn.status == 200
- assert Enum.into(conn.resp_headers,%{})["content-type"] == "application/xml"
+ assert Enum.into(conn.resp_headers, %{})["content-type"] == "application/xml"
assert conn.resp_body == "arnaud"
end
test "Implement not exists" do
- app = resources do
- resource "/hello/:name" do %{name: name} after
- resource_exists do: state.name !== "idonotexist"
- defh to_html, do: state.name
+ app =
+ resources do
+ resource "/hello/:name" do
+ %{name: name}
+ after
+ resource_exists do: state.name !== "idonotexist"
+ defh to_html, do: state.name
+ end
end
- end
+
assert app.call(conn(:get, "/hello/arnaud"), []).status == 200
assert app.call(conn(:get, "/hello/idonotexist"), []).status == 404
end
test "Service not available" do
- app = resources do
- resource "/notok" do %{} after
- service_available do: false
- end
- resource "/ok" do %{} after
- service_available do: true
+ app =
+ resources do
+ resource "/notok" do
+ %{}
+ after
+ service_available do: false
+ end
+
+ resource "/ok" do
+ %{}
+ after
+ service_available do: true
+ end
end
- end
+
assert app.call(conn(:get, "/notok"), []).status == 503
assert app.call(conn(:get, "/ok"), []).status == 200
end
test "Unknown method" do
- app = resources do
- resource "/notok" do %{} after known_methods(do: ["TOTO"]) end
- resource "/ok" do %{} after end
- end
+ app =
+ resources do
+ resource "/notok" do
+ %{}
+ after
+ known_methods(do: ["TOTO"])
+ end
+
+ resource "/ok" do
+ %{}
+ after
+ end
+ end
+
assert app.call(conn(:get, "/notok"), []).status == 501
assert app.call(conn(:get, "/ok"), []).status == 200
end
test "Url too long" do
- app = resources do
- resource "/notok" do %{} after uri_too_long(do: true) end
- resource "/ok" do %{} after end
- end
+ app =
+ resources do
+ resource "/notok" do
+ %{}
+ after
+ uri_too_long do: true
+ end
+
+ resource "/ok" do
+ %{}
+ after
+ end
+ end
+
assert app.call(conn(:get, "/notok"), []).status == 414
assert app.call(conn(:get, "/ok"), []).status == 200
end
test "Method allowed ?" do
- app = resources do
- resource "/notok" do %{} after allowed_methods(do: ["POST"]) end
- resource "/ok" do %{} after end
- end
+ app =
+ resources do
+ resource "/notok" do
+ %{}
+ after
+ allowed_methods do: ["POST"]
+ end
+
+ resource "/ok" do
+ %{}
+ after
+ end
+ end
+
assert app.call(conn(:get, "/notok"), []).status == 405
assert app.call(conn(:get, "/ok"), []).status == 200
end
test "Content MD5 check" do
- app = resources do
- resource "/" do %{} after
- allowed_methods do: ["PUT"]
- content_types_accepted do: ["application/json": :from_json]
- defh from_json, do: true
+ app =
+ resources do
+ resource "/" do
+ %{}
+ after
+ allowed_methods do: ["PUT"]
+ content_types_accepted do: ["application/json": :from_json]
+ defh from_json, do: true
+ end
end
- end
- headers = [{"content-type","application/json"},{"content-md5","qqsjdf"}]
- assert app.call(%{conn(:put,"/","hello\n")|req_headers: headers},[]).status == 400
- headers = [{"content-type","application/json"},{"content-md5","sZRqySSS0jR8YjW00mERhA=="}]
- assert app.call(%{conn(:put,"/","hello\n")|req_headers: headers},[]).status == 204
+
+ headers = [{"content-type", "application/json"}, {"content-md5", "qqsjdf"}]
+ assert app.call(%{conn(:put, "/", "hello\n") | req_headers: headers}, []).status == 400
+ headers = [{"content-type", "application/json"}, {"content-md5", "sZRqySSS0jR8YjW00mERhA=="}]
+ assert app.call(%{conn(:put, "/", "hello\n") | req_headers: headers}, []).status == 204
end
test "Malformed ?" do
- app = resources do
- resource "/notok" do %{} after malformed_request(do: true) end
- resource "/ok" do %{} after end
- end
+ app =
+ resources do
+ resource "/notok" do
+ %{}
+ after
+ malformed_request do: true
+ end
+
+ resource "/ok" do
+ %{}
+ after
+ end
+ end
+
assert app.call(conn(:get, "/notok"), []).status == 400
assert app.call(conn(:get, "/ok"), []).status == 200
end
test "Is authorized ?" do
- app = resources do
- resource "/notok" do %{} after is_authorized(do: "myrealm") end
- resource "/ok" do %{} after end
- end
+ app =
+ resources do
+ resource "/notok" do
+ %{}
+ after
+ is_authorized do: "myrealm"
+ end
+
+ resource "/ok" do
+ %{}
+ after
+ end
+ end
+
assert app.call(conn(:get, "/ok"), []).status == 200
conn = app.call(conn(:get, "/notok"), [])
assert conn.status == 401
- assert get_resp_header(conn,"www-authenticate") == ["myrealm"]
+ assert get_resp_header(conn, "www-authenticate") == ["myrealm"]
end
test "Encoding base64" do
- app = resources do
- resource "/" do %{} after
- encodings_provided do: [base64: &Base.encode64/1, identity: &(&1)]
- defh to_html, do: "hello"
+ app =
+ resources do
+ resource "/" do
+ %{}
+ after
+ encodings_provided do: [base64: &Base.encode64/1, identity: & &1]
+ defh to_html, do: "hello"
+ end
end
- end
- conn = app.call(%{conn(:get,"/")|req_headers: [{"accept-encoding","base64"}]}, [])
- assert get_resp_header(conn,"content-encoding") == ["base64"]
+
+ conn = app.call(%{conn(:get, "/") | req_headers: [{"accept-encoding", "base64"}]}, [])
+ assert get_resp_header(conn, "content-encoding") == ["base64"]
assert conn.resp_body == "aGVsbG8="
- conn = app.call(%{conn(:get,"/")|req_headers: [{"accept-encoding","toto"}]}, [])
+ conn = app.call(%{conn(:get, "/") | req_headers: [{"accept-encoding", "toto"}]}, [])
assert conn.status == 200
- assert get_resp_header(conn,"content-encoding") == []
+ assert get_resp_header(conn, "content-encoding") == []
assert conn.resp_body == "hello"
end
test "POST create path" do
- app = resources do
- resource "/orders" do %{} after
- allowed_methods do: ["POST"]
- post_is_create do: true
- create_path do: "/titus"
- content_types_accepted do: ["text/plain": :from_text]
- defh from_text, do:
- {true,put_private(conn,:body_post,read_body(conn)),state}
- end
- resource "/orders2" do %{} after
- allowed_methods do: ["POST"]
- post_is_create do: true
- # Check modified state is propagated
- defh create_path(conn, %{path: path} = state), do: {path, conn, state}
- content_types_accepted do: ["text/plain": :from_text]
- defh from_text(conn, state), do: {true, conn, state |> Map.put(:path, "titus")}
+ app =
+ resources do
+ resource "/orders" do
+ %{}
+ after
+ allowed_methods do: ["POST"]
+ post_is_create do: true
+ create_path do: "/titus"
+ content_types_accepted do: ["text/plain": :from_text]
+ defh from_text, do: {true, put_private(conn, :body_post, read_body(conn)), state}
+ end
+
+ resource "/orders2" do
+ %{}
+ after
+ allowed_methods do: ["POST"]
+ post_is_create do: true
+ # Check modified state is propagated
+ defh create_path(conn, %{path: path} = state), do: {path, conn, state}
+ content_types_accepted do: ["text/plain": :from_text]
+ defh from_text(conn, state), do: {true, conn, state |> Map.put(:path, "titus")}
+ end
end
- end
- conn = app.call(conn(:post,"/orders","titus") |> put_req_header("content-type", "text/plain"), [])
- assert get_resp_header(conn,"location") == ["http://www.example.com/titus"]
+
+ conn =
+ app.call(
+ conn(:post, "/orders", "titus") |> put_req_header("content-type", "text/plain"),
+ []
+ )
+
+ assert get_resp_header(conn, "location") == ["http://www.example.com/titus"]
assert conn.status == 201
- assert {:ok,"titus",_} = conn.private.body_post
- conn = app.call(conn(:post,"/orders2","titus") |> put_req_header("content-type", "text/plain"), [])
- assert get_resp_header(conn,"location") == ["http://www.example.com/orders2/titus"]
+ assert {:ok, "titus", _} = conn.private.body_post
+
+ conn =
+ app.call(
+ conn(:post, "/orders2", "titus") |> put_req_header("content-type", "text/plain"),
+ []
+ )
+
+ assert get_resp_header(conn, "location") == ["http://www.example.com/orders2/titus"]
end
test "POST process post" do
- app = resources do
- resource "/orders" do %{} after
- allowed_methods do: ["POST"]
- process_post do:
- {true,put_private(conn,:body_post,"yes"),state}
+ app =
+ resources do
+ resource "/orders" do
+ %{}
+ after
+ allowed_methods do: ["POST"]
+ process_post do: {true, put_private(conn, :body_post, "yes"), state}
+ end
end
- end
- conn = app.call(conn(:post,"/orders","titus") |> put_req_header("content-type", "text/plain"), [])
+
+ conn =
+ app.call(
+ conn(:post, "/orders", "titus") |> put_req_header("content-type", "text/plain"),
+ []
+ )
+
assert conn.status == 204
assert "yes" = conn.private[:body_post]
end
test "Cache if modified" do
- app = resources do
- resource "/notcached" do %{} after
- last_modified do: {{2013,1,1},{0,0,0}}
- end
- resource "/cached" do %{} after
- last_modified do: {{2012,12,31},{0,0,0}}
+ app =
+ resources do
+ resource "/notcached" do
+ %{}
+ after
+ last_modified do: {{2013, 1, 1}, {0, 0, 0}}
+ end
+
+ resource "/cached" do
+ %{}
+ after
+ last_modified do: {{2012, 12, 31}, {0, 0, 0}}
+ end
end
- end
- conn = app.call(%{conn(:get,"/cached")|req_headers: [{"if-modified-since","Sat, 31 Dec 2012 19:43:31 GMT"}]}, [])
+
+ conn =
+ app.call(
+ %{
+ conn(:get, "/cached")
+ | req_headers: [{"if-modified-since", "Sat, 31 Dec 2012 19:43:31 GMT"}]
+ },
+ []
+ )
+
assert conn.status == 304
- conn = app.call(%{conn(:get,"/notcached")|req_headers: [{"if-modified-since","Sat, 31 Dec 2012 19:43:31 GMT"}]}, [])
+
+ conn =
+ app.call(
+ %{
+ conn(:get, "/notcached")
+ | req_headers: [{"if-modified-since", "Sat, 31 Dec 2012 19:43:31 GMT"}]
+ },
+ []
+ )
+
assert conn.status == 200
end
test "Cache etag" do
- app = resources do
- resource "/notcached" do %{} after
- generate_etag do: "titi"
- end
- resource "/cached" do %{} after
- generate_etag do: "toto"
+ app =
+ resources do
+ resource "/notcached" do
+ %{}
+ after
+ generate_etag do: "titi"
+ end
+
+ resource "/cached" do
+ %{}
+ after
+ generate_etag do: "toto"
+ end
end
- end
- conn = app.call(%{conn(:get,"/cached")|req_headers: [{"if-none-match","toto"}]}, [])
+
+ conn = app.call(%{conn(:get, "/cached") | req_headers: [{"if-none-match", "toto"}]}, [])
assert conn.status == 304
- conn = app.call(%{conn(:get,"/notcached")|req_headers: [{"if-none-match","toto"}]}, [])
+ conn = app.call(%{conn(:get, "/notcached") | req_headers: [{"if-none-match", "toto"}]}, [])
assert conn.status == 200
end
test "halt test" do
- app = resources do
- resource "/error" do %{} after
- content_types_provided do: {:halt,407}
- defh to_html, do: "toto"
+ app =
+ resources do
+ resource "/error" do
+ %{}
+ after
+ content_types_provided do: {:halt, 407}
+ defh to_html, do: "toto"
+ end
end
- end
- conn = app.call(conn(:get,"/error"), [])
+
+ conn = app.call(conn(:get, "/error"), [])
assert conn.status == 407
assert conn.resp_body == ""
end
test "fuzzy acceptance" do
- app = resources do
- resource "/" do %{} after
- allowed_methods do: ["PUT"]
- content_types_accepted do: %{"application/*"=> :from_app, {"text/*",%{"pretty"=>"true"}}=> :from_pretty}
- defh from_app, do: {:halt,601}
- defh from_pretty, do: {:halt,602}
+ app =
+ resources do
+ resource "/" do
+ %{}
+ after
+ allowed_methods do: ["PUT"]
+
+ content_types_accepted do
+ %{"application/*" => :from_app, {"text/*", %{"pretty" => "true"}} => :from_pretty}
+ end
+
+ defh from_app, do: {:halt, 601}
+ defh from_pretty, do: {:halt, 602}
+ end
end
- end
- headers = [{"content-type","application/json; charset=utf8"}]
- assert app.call(%{conn(:put,"/","h")|req_headers: headers},[]).status == 601
- headers = [{"content-type","text/html; pretty=true; charset=utf8"}]
- assert app.call(%{conn(:put,"/","h")|req_headers: headers},[]).status == 602
+
+ headers = [{"content-type", "application/json; charset=utf8"}]
+ assert app.call(%{conn(:put, "/", "h") | req_headers: headers}, []).status == 601
+ headers = [{"content-type", "text/html; pretty=true; charset=utf8"}]
+ assert app.call(%{conn(:put, "/", "h") | req_headers: headers}, []).status == 602
end
end
diff --git a/test/test_helper.exs b/test/test_helper.exs
index 4b8b246..869559e 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -1 +1 @@
-ExUnit.start
+ExUnit.start()
diff --git a/test/utils_test.exs b/test/utils_test.exs
index b8d0c1b..fa79031 100644
--- a/test/utils_test.exs
+++ b/test/utils_test.exs
@@ -1,32 +1,35 @@
-Code.require_file "test_helper.exs", __DIR__
+Code.require_file("test_helper.exs", __DIR__)
defmodule Ewebmachine.Core.UtilsTest do
use ExUnit.Case
import Ewebmachine.Core.Utils
test "Content Type negociation found a value" do
- for accept_header<-["*", "*/*", "text/*", "text/html"] do
- assert {"text","html",%{}} = choose_media_type([{"text","html",%{}}],accept_header)
+ for accept_header <- ["*", "*/*", "text/*", "text/html"] do
+ assert {"text", "html", %{}} = choose_media_type([{"text", "html", %{}}], accept_header)
end
end
test "Content Type negociation no matching value" do
- for accept_header<-["foo", "text/xml", "application/*", "foo/bar/baz"] do
- assert nil == choose_media_type([{"text","html",%{}}], accept_header)
+ for accept_header <- ["foo", "text/xml", "application/*", "foo/bar/baz"] do
+ assert nil == choose_media_type([{"text", "html", %{}}], accept_header)
end
end
test "Content Type negociation quality selection" do
- provided = [{"text","html",%{}},{"image","jpeg",%{}}]
- for accept_header<-["image/jpeg;q=0.5, text/html",
- "text/html, image/jpeg; q=0.5",
- "text/*; q=0.8, image/*;q=0.7",
- "text/*;q=.8, image/*;q=.7"] do
- assert {"text","html",%{}} = choose_media_type(provided,accept_header)
+ provided = [{"text", "html", %{}}, {"image", "jpeg", %{}}]
+
+ for accept_header <- [
+ "image/jpeg;q=0.5, text/html",
+ "text/html, image/jpeg; q=0.5",
+ "text/*; q=0.8, image/*;q=0.7",
+ "text/*;q=.8, image/*;q=.7"
+ ] do
+ assert {"text", "html", %{}} = choose_media_type(provided, accept_header)
end
- for accept_header<-["image/*;q=1, text/html;q=0.9",
- "image/png, image/*;q=0.3"] do
- assert {"image","jpeg",%{}} = choose_media_type(provided,accept_header)
+
+ for accept_header <- ["image/*;q=1, text/html;q=0.9", "image/png, image/*;q=0.3"] do
+ assert {"image", "jpeg", %{}} = choose_media_type(provided, accept_header)
end
end
@@ -35,15 +38,18 @@ defmodule Ewebmachine.Core.UtilsTest do
end
test "rfc1123 date parsing" do
- assert {{2009,12,30},{14,39,2}} = convert_request_date("Wed, 30 Dec 2009 14:39:02 GMT")
+ assert {{2009, 12, 30}, {14, 39, 2}} = convert_request_date("Wed, 30 Dec 2009 14:39:02 GMT")
assert :bad_date = convert_request_date(:toto)
end
test "content type normalization roundtrip" do
- for type<-["audio/vnd.wave; codec=31",
- "text/x-okie; charset=iso-8859-1; declaration=f950118.AEB0com"] do
- assert type == (type |> normalize_mtype |> format_mtype)
+ for type <- [
+ "audio/vnd.wave; codec=31",
+ "text/x-okie; charset=iso-8859-1; declaration=f950118.AEB0com"
+ ] do
+ assert type == type |> normalize_mtype |> format_mtype
end
- assert "audio/vnd.wave; codec=31" = format_mtype({"audio","vnd.wave",%{codec: "31"}})
+
+ assert "audio/vnd.wave; codec=31" = format_mtype({"audio", "vnd.wave", %{codec: "31"}})
end
end