# Rest Api Builder
This library helps to create Plug compatible Rest API routers that can be manually created or generated based upon a Resource Provider library.
## Provider Implementations
* [rest_api_builder_essp](https://hex.pm/packages/rest_api_builder_essp) - Implements a resource provider based upon the [Ecto Schema Store](https://hex.pm/packages/ecto_schema_store) library.
## Installation
```elixir
def deps do
[{:rest_api_builder, "~> 0.6"}]
end
```
## Creating an API resource router
All API routers are based upon `Plug.Router` and may be included into any standard `Plug.Router` path
or forwarded to in a Phoenix Router definition.
In a API module the following paths are built in:
* index - `GET /`
* create - `POST /`
* show - `GET /:id`
* update - `PUT /:id` or `PATCH /:id`
* delete - `DELETE /:id`
```elixir
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all
import Plug.Conn
# Called before a specific resource is loaded on show, update, or delete.
# You will generally want to set the value into Plug.Conn.assigns
def handle_preload(%Plug.Conn{path_params: %{"id" => id}, assigns: assigns} = conn) do
assign(conn, :resource, %{id: id})
end
def handle_index(conn) do
# Will send back a 200 status with the resource in the body as JSON.
send_resource conn, [%{id: 1}, %{id: 2}]
end
def handle_create(conn) do
# Will send back a 201 status with the resource in the body as JSON.
send_resource conn, %{id: 3}
end
def handle_show(%Plug.Conn{assigns: %{resource: resource}} = conn) do
# Will send back a 200 status with the resource in the body as JSON.
send_resource conn, resource
end
def handle_update(%Plug.Conn{assigns: %{resource: resource}} = conn) do
# Will send back a 403 status with an errors JSON body containing the content provided.
send_errors conn, 403, "Cannot update resource"
end
def handle_delete(%Plug.Conn{assigns: %{resource: resource}} = conn) do
# Will send back a 204 status with no content.
send_resource conn, nil
end
end
```
## Reference API Provider
Other libraries can implement a macro that will fill in the functions above based upon some factor relevent to the library.
For reference implementation purposes, the following documentation will use the `ecto_schem_store` api provider.
There will be other implementations but based upon this early release the reference implementation is based upon another
library I wrote for simplicity purposes.
The EctoSchemaStore library builds upon ecto to create customizable CRUD modules that utilize and Ecto Repo and Schema.
```elixir
defmodule Customer do
use EctoTest.Web, :model
schema "customers" do
field :name, :string
field :email, :string
field :account_closed, :boolean, default: false
timestamps
end
def changeset(model, params) do
model
|> cast(params, [:name, :email])
end
end
```
You can create a store with the following:
```elixir
defmodule CustomerStore do
use EctoSchemaStore, schema: Customer, repo: MyApp.Repo
end
```
To learn more about how to use that libary please visit that project. This will be enough for the following examples.
## Using a Provider
This library provides a macro to load a provider module into your API.
```elixir
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
end
```
That's it. Since the EctoSchemaStore library knows how to interact with itself, it will generate the REST action functions for us.
This same process can be followed any library that adds `use RestApiBuilder.Provider` to a module. More on building an API Provider later.
## Adding to Plug Router
If you have an existing `Plug.Router` module you can add your API with the following:
```elixir
forward "/customers", to: CustomerApi
```
## Adding to Phoenix Router
```elixir
forward "/customers", CustomerApi
```
Any pipeline of plugs can be applied before forwarding if you would like.
## Include Other API routers
Any API module can act as a base for other API modules which will utilize the plural name provided in those modules.
A better example of a real API would be to set up a version collection.
```elixir
defmodule ApiV1 do
use RestApiBuilder
include CustomersApi
include PartnersApi
end
```
Then include this module in your router.
```elixir
# Plug Router
forward "/api/v1", to: ApiV1
# Phoenix Router
forward "/api/v1", ApiV1
```
To get to the rest resources you would submit to `/api/v1/customers` or to `/api/v1/partners`. The resource names were pulled from the child modules.
You can include any REST API module into any other.
## Activating Actions
By default no REST actions are activated and an eror will be returned if you attempt to use one of the built in actions.
To activate all you can provided a option on the `use` statement.
```elixir
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all
end
```
Actions can also be activate one at a time.
```elixir
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
activate :index
activate :show
activate :create
activate :update
activate :delete
end
```
Or combined into list.
```elixir
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
activate [:index, :show, :create, :update, :delete]
end
```
`:all` is the same as listing all of the actions. Providing a subset of the list of action will only turn on those actions producing an error for others.
## Plugs
Since a REST API module is based upon Plug Router, it predefines plugs that facilitate its functionality.
To append your own plugs, you must tell the library to allow for custom plugs.
Although you could use the plug statment directly, it would never fire since the :dispatcher plug will have already fired.
The plugs command is provided to allow you to drop your plugs into the middle of the process. Your plugs will fire after
the `preload` function has done its work if you would like to use the loaded resource.
All plugs will be applied at every level of a REST path so any parent resources will have any security checks applied before any children.
More on children later.
```elixir
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all, default_plugs: false
import Plug.Conn
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
plugs do
if Mix.env == :dev do
plug :verify_active_customer, verify: false
else
plug :verify_active_customer, verify: true
end
plug prevent_inactive
end
def verify_active_customer(%{assigns: %{current: customer}} = conn, verify: true) do
assign conn, :verified, !customer.account_closed
end
def verify_active_customer(conn, _opts) do
assign conn, :verified, true
end
def prevent_inactive(%{assigns: %{verified: true}} = conn, _opts), do: conn
def prevent_inactive(%{assigns: %{verified: false}} = conn, _opts) do
conn
|> send_resp(404, "Not Found")
|> halt
end
end
```
## Links
A REST API module can add links to the resource. Some basic ones are automatically provided. A provider module may also add additional.
Links exist for both the entire resource group and for individual resources.
```elixir
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
group_link :google, "http://www.google.com"
link :author, "/author"
export_links()
end
```
The `export_links` macro will write out the links to the resource depending upon the encoding used for the resource.
Custom links can also be added using `group_link` and `link`.
## Plug Router Matching
You can provide Plug Router level matching since Plug,Router is imported into the API module. You need to make sure you
activate any REST actions after the custom matching or the the REST actions may pre-empt the path.
```elixir
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
import Plug.Conn
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
post "/my_action" do
conn
|> send_resp(200, "You got to the action.")
end
activate :all
get "/:id/my_action" do
# Any route with :id in the first level will have the preload plug load the resource.
send_resource conn, CustomerStore.to_map(conn.assigns[:current])
end
group_link :my_action, "/my_action"
link :my_action, "/my_action"
export_links()
end
```
## Features
Some helper macros are provided to help setup route matching. These add ons to the standard REST action are called
features in this library. There are group features which apply to the base path or regular features which apply to
an individual resource. Using features also sets up the link path for the proivided feature to be added to the
resource.
```elixir
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
import Plug.Conn
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
group_feature :my_action do
conn
|> send_resp(200, "You got to the action.")
end
activate :all
feature :my_action do
# Any route with :id in the first level will have the preload plug load the resource.
send_resource conn, CustomerStore.to_map(conn.assigns[:current])
end
export_links()
end
```
## Children
Similar `include` the children macro will load another REST API module into the path but associated with a resource.
Providers can then use the parent resource to narrow down the child resources.
```elixir
# Child Resource
defmodule MessagesApi do
use RestApiBuilder, plural_name: :messages, singular_name: :message, activate: :all
provider RestApiBuilder.EctoSchemaStoreProvider, store: MessageStore,
parent: :customer_id
export_links()
end
# Parent Resource
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
activate :all
children MessagesApi
export_links()
end
```
The messages resource can be reach via `/api/v2/customers/12/messages` a single child resource could be retrieved
via `/api/v2/customers/12/messages/37`.
The child resource will be filtered down by the API Provider library based upon the parent. If message 37 existed
but did not belong to customer 12, then a HTTP 404 would be returned even though the message id is valid. If
customer 12 did not exist or the user does not have access, then the message would never be looked up and the
parents error would be returned.
The exact enforcement of relationships is defined by the API provider. If you write your own then you will have to
define this enforcement and relatioship yourself.
If the relationships are validated, then links will be created for parenbt and children on the current resource.
## Modifying Actions
If the provider allows, then the actions can be overloaded in your code. This will allow you to perform some action before or
after the provider's default action.
```elixir
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
def handle_create(conn) do
# Do some action before the resource is created by the provider.
conn = super(conn)
# Do some action after the resource is created by the provider.
conn
end
end
```
## Event Announcements ##
An API module supports the concept of an event through the [Event Queues](https://hex.pm/packages/event_queues) library on Hex.
Event Queues must be included in your application and each queue and handler added to your application supervisor. Visit
the instructions at (https://hexdocs.pm/event_queues) for more details.
Events:
* `:after_create`
* `:after_update`
* `:after_delete`
Macros:
* `create_queue` - Creates a Queue for instances where one is not already set up. Accessible at {api module name}.Queue
* `announces` - Register a what events to announce and what modules to send the event. By default will use {api module name}.Queue
```elixir
defmodule CustomersApi do
use RestApiBuilder, plural_name: :customers, singular_name: :customer, activate: :all
provider RestApiBuilder.EctoSchemaStoreProvider, store: CustomerStore
create_queue()
announces events: [:after_create, :after_update, :after_delete]
defmodule Handler do
use EventQueues, type: :handler, subscribe: SampleApi.Apis.UsersApi.Queue
def handle(event) do
IO.inspect event
end
end
end
```
The event queue and any handlers must be started as part of the application.
```elixir
# Start the event queue and handler.
CustomersApi.Queue.start_link
CustomersApi.Handler.start_link
# As part of the App supervisor
worker(CustomersApi.Queue, []),
worker(CustomersApi.Handler, [])
```
## Direct Access ##
The API can be accessed internally to your application without needing to make an HTTP call. When directly accessed
via other application code, the Plug.Conn being passed contains the `:direct_access` value in `assigns` set to `true`.
Direct access does not consume JSON text or generate the JSON response. These translations are skipped over as that
converting to JSON and back provides no advantage internally. Therefore, you may receive a more complete resource
from direct access then you would get as an HTTP call.
Execute process:
`process(http_method, path, opts)` or `process!(http_method, path, opts)`
The http_method can be `:get`, `:post`, `:put`, `:patch`, or `:delete`.
Options:
* `params` - Will set the params on Plug.Conn. Keyword list or map.
* `assigns` - Will set values on the assigns of the Plug.Conn passed in. Keyword list or map.
* `headers` - Map or list of tuples with headers.
The following convience methods can be used to access the standard REST paths.
Functions:
* run_index, run_index!
* run_show, run_show!
* run_create, run_create!
* run_update, run_update!
* run_delete, run_delete!
All functions share the same parameters `(path, opts)`.
The path is always relative to the API module you are directly calling on.
```elixir
# Equivalent paths
{:ok, customers} = ApiV1.process :get, "/customers"
{:ok, customers} = ApiV1.run_index "/customers"
{:ok, customers} = CustomersApi.run_index "/"
# Submitting resource
{:ok, customer} = ApiV1.run_create "/customers", params: %{name: "Bob Person"}
customer = ApiV1.run_create! "/customers", params: %{name: "Bob Person"}
```
Direct Access can be used to build an internal api. This will allow you to use your REST API as a traditional API.
```elixir
defmodule InternalApi do
def list_customers do
ApiV1.run_index! "/customers"
end
def create_customer(name) do
ApiV1.run_create "/customers", params: %{name: name}
end
def create_customer!(name) do
ApiV1.run_create! "/customers", params: %{name: name}
end
end
```
When designing plugs, you will want to consider both web access and direct access and may want alternate functionality or
less security when the `:direct_access` value is present in assigns.
## Testing ##
Any API module can be tested using `Plug.Test` or the direct access methods directly on the modules. You can also
perform controller style tests like you would normally when using Phoenix Framework.
```elixir
defmodule CustomersApiTest do
use MyApp.ConnCase
test "Test Creating a Customer" do
customer = ApiV1.run_create! "/customers", params: %{name: "Bob Person"}
assert "Bob Person" == customer.name
end
test "Test Creating a Customer using Test Conn", %{conn: conn} do
response =
conn
|> post("/api/v1/customers", %{customer: %{name: "Bob"}})
|> json_response(201)
assert "Bob Person" == response["customer"]["name"]
end
end
```