Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8ae94d6
chore: run mix format
Krapaince Nov 28, 2025
3983d21
chore: add ignore-revs file
Krapaince Dec 2, 2025
b05e511
docs: enhance documentation
Krapaince Nov 28, 2025
41d71ee
fix: clear ex_doc warnings by updating options
Krapaince Nov 28, 2025
37a6cce
chore: disable epub format documentation generation
Krapaince Nov 28, 2025
f8b97c3
fix: make Ewebmachine's plugs implement the Plug behaviour
Krapaince Nov 28, 2025
8f9628e
chore: remove testing intended symbols from public one
Krapaince Dec 2, 2025
5fa798a
chore: remove dead code
Krapaince Dec 5, 2025
23f4012
refactor: remove __using__ macro of Core.DSL module
Krapaince Dec 5, 2025
17bf51c
chore: remove dead comment
Krapaince Dec 5, 2025
f342c33
docs: make Ewebmachine.Events modules invisible
Krapaince Dec 5, 2025
1e09f57
docs: rewrite project's documentation
Krapaince Dec 5, 2025
beb9119
refactor: lift the @moduledoc attribute just beneath defmodule
Krapaince Dec 5, 2025
3550c0d
chore: make resource_quote/4 function private
Krapaince Dec 5, 2025
12beed0
refactor: rewrite resources_plugs macro
Krapaince Dec 5, 2025
e7827ba
refactor: use Enum.find_value/3 instead of Enum.find_value/2
Krapaince Dec 5, 2025
a932bc2
chore: add a reference for developers on weighted encoding values
Krapaince Dec 5, 2025
5adfeff
refactor: unnest Plug.Send plug implementation
Krapaince Dec 5, 2025
7e51658
fix: add missing @impl attribute on ErrorAsException
Krapaince Dec 5, 2025
40a77cd
fix: handle non bitstring body when creating a Ewebmachine.Plug.Error…
Krapaince Dec 5, 2025
7770fc7
docs: add documentation for the handler given to content_types_provided
Krapaince Dec 5, 2025
7969899
docs: add documentation for the handler given to content_types_accepted
Krapaince Dec 5, 2025
a706d17
refactor!: make the to_html handler returns an empty string
Krapaince Dec 5, 2025
701ddb7
chore!: remove the plug_cowboy dependency
Krapaince Dec 8, 2025
0d44f2e
chore: remove examples project
Krapaince Dec 8, 2025
b9e1d93
chore: bump version to 2.4.0
Krapaince Dec 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -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]
]
1 change: 1 addition & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ad87df770d2be55ab1becbc05de5d9edf961bad2
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 `"<html><body><h1>Hello World</h1></body></html>"`

### 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
Expand Down
212 changes: 121 additions & 91 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,129 +1,159 @@
# Ewebmachine [![Build Status](https://github.com/kbrw/ewebmachine/actions/workflows/.github/workflows/build-and-test.yml/badge.svg)](https://github.com/kbrw/ewebmachine/actions/workflows/build-and-test.yml) [![Hex.pm](https://img.shields.io/hexpm/v/ewebmachine.svg)](https://hex.pm/packages/ewebmachine) [![Documentation](https://img.shields.io/badge/documentation-gray)](https://hexdocs.pm/ewebmachine) ![Hex.pm License](https://img.shields.io/hexpm/l/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: "<h1> Error ! : '#{Ewebmachine.Core.Utils.http_label(state.s)}'</h1>"
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: "<Person><name>#{state.name}</name></Person>"
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`.

![Debug UI example](./assets/debug_ui.png)
*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.

![Debug UI example](./assets/debug_ui.png)

# 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).
19 changes: 0 additions & 19 deletions examples/hello/.gitignore

This file was deleted.

9 changes: 0 additions & 9 deletions examples/hello/README.md

This file was deleted.

Loading
Loading