PlugRest

Build Status Hex Version

A port of Cowboy’s cowboy_rest module to Plug.

PlugRest supplements Plug.Router with an additional resource macro, which matches a URL path with a resource handler module implementing REST semantics via a series of optional callbacks.

Hello World

Define a router to match a path with a resource handler:

defmodule MyRouter do
  use PlugRest.Router

  resource "/hello", HelloResource
end

Define the resource handler and implement the optional callbacks:

defmodule HelloResource do
  use PlugRest.Resource

  def to_html(conn, state) do
    {"Hello world", conn, state}
  end
end

Installation

If starting a new project, generate a supervisor application:

$ mix new my_app --sup

Add PlugRest to your project in two steps:

  1. Add :cowboy, :plug, and :plug_rest to your list of dependencies in mix.exs:

        def deps do
          [{:cowboy, "~> 1.0.0"},
           {:plug, "~> 1.0"},
           {:plug_rest, "~> 0.5.0"}]
        end
        ```
  2. Add these dependencies to your applications list:

          def application do
            [applications: [:cowboy, :plug, :plug_rest]]
        end
        ```

Resources

Create a file at lib/my_app/hello_resource.ex to hold your Resource Handler:

defmodule MyApp.HelloResource do
  use PlugRest.Resource

  def allowed_methods(conn, state) do
    {["GET"], conn, state}
  end

  def content_types_provided(conn, state) do
    {[{"text/html", :to_html}], conn, state}
  end

  def to_html(conn, state) do
    {"Hello world", conn, state}
  end
end

Router

Create a file at lib/my_app/router.ex to hold the Router:

defmodule MyApp.Router do
  use PlugRest.Router

  resource "/hello", MyApp.HelloResource

  match "/match" do
    send_resp(conn, 200, "Match")
  end
end

The PlugRest Router adds a resource macro which accepts a URL path and a Module that will handle all the callbacks on the Resource.

You can also use the match macros from Plug.Router. This provides an escape hatch to bypass the REST mechanism for a particular route and send a Plug response manually.

If no routes match, the PlugRest will send a response with a 404 status code to the client automatically.

Dynamic path segments

Router paths can have segments that match URLs dynamically:

  resource "/users/:id", MyApp.UserResource

The path parameters can be accessed in your resource with read_path_params/1:

    def to_html(conn, state) do
      params = read_path_params(conn)
      user_id = params["id"]
      {"Hello #{user_id}", conn, state}
    end

Application

Finally, add the Router to your supervision tree by editing lib/my_app.ex:

    # Define workers and child supervisors to be supervised
    children = [
      Plug.Adapters.Cowboy.child_spec(:http, MyApp.Router, [], [port: 4001])
    ]

Running

Compile your application and then run it:

$ iex -S mix

Your server will be running and the resource will be available at http://localhost:4001/hello.

Testing

Use Plug.Test to help verify your resources’s responses to separate requests. Create a file at test/resources/hello_resource_test.exs to hold your test:

defmodule MyApp.HelloResourceTest do
  use ExUnit.Case
  use Plug.Test

  alias MyApp.Router

  test "get hello resource" do
    conn = conn(:get, "/hello")

    conn = Router.call(conn, [])

    assert conn.status == 200
    assert conn.resp_body == "Hello world"
  end
end

Run the test with:

$ mix test

Phoenix

You can use PlugRest in your Phoenix app. Add :plug_rest to your dependencies, save your REST router at web/rest_router.ex, and put your resources in web/resources/. Then use the forward macro in your Phoenix web/router.ex:

  forward "/rest", HelloPhoenix.RestRouter

The resource will be served at http://localhost:4001/rest/hello.

Information

The Cowboy documentation has more details on the REST protocol:

Differences between PlugRest and cowboy_rest:

  • Each callback accepts a Plug conn struct instead of a Cowboy Req record.
  • The init/2 callback is not required. However, if it does exist, it should return {:ok, conn, state}.
  • The content callbacks (like to_html) return {body, conn, state} where the body is one of iodata or {:chunked, Enum.t}.
  • Media types take the form {binary, binary, %{binary => binary}}, following Plug.Conn.Utils parsers.
  • The content types provided and accepted callbacks can describe each type with a String "text/html" or tuple {"text", "html", %{}},
  • In a media type {type, subtype, params} tuple, the params are a map rather than a list of tuples.

Upgrading

PlugRest is still in an initial development phase. Expect breaking changes at least in each minor version.

See the CHANGELOG for more information.

License

PlugRest copyright © 2016, Christopher Adams

cowboy_rest copyright © 2011-2014, Loïc Hoguin essen@ninenines.eu

Cowboy logo copyright © 2016, dikaio