# Relax
A [jsonapi.org](http://jsonapi.org) serializer and optional server implementation in Elixir.
Relax can be used as a standalone API with a router and resources, or integrated into Phoenix.
## TLDR;
[ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers) inspired DSL for json serialization.
```elixir
defmodule PostSerializer do
use Relax.Serializer
serialize "posts" do
attributes [:id, :title, :body]
has_many :comments, href: "/v1/posts/:id/comments"
end
end
Post.all |> PostSerializer.as_json(conn) |> JSON.encode!
```
## Standalone Example
Simple Plug based DSLs for routing/dispatching API requests.
```elixir
defmodule MyApp do
# Our Router is just a plug router and we can start it as such.
def start, do: Plug.Adapters.Cowboy.http MyApp.Router, []
# Our router is our main entry point for all requests.
# Relax.Router is just a DSL on top of Plug.Router, so the standard plug
# stack still works and is used.
defmodule Router do
use Relax.Router
plug :match
plug :dispatch
version :v1 do
# Dispatch all /v1/posts/* requests to MyApp.API.Posts plug.
resource :posts, MyApp.API.Posts
end
end
# Our "Resource" similar to a controller, is just different DSL on a Plug.Router.
# By including Relax.Resource we define matches for GET /:id, GET /:comma,:seperated,:ids, GET /, POST /, PUT(or PATCH) /:id, DELETE /:id.
# Each match is then dispatched to the proper callback.
defmodule API.Posts do
# Don't match put or delete (:update or :delete)
use Relax.Resource, only: [:find_all, :find_many, :find_one, :create]
# Every resource is expected to define a serializer. This will be used by each request.
serializer MyApp.Serializer.Post
plug :match
plug :dispatch
# Call back for GET / - returns 200 with all posts serialized
def find_all(conn), do: okay(conn, MyApp.Post.all)
# Call back for GET /:id1,:id2,...,:idn - returns 200 with posts serialized
def find_many(conn, ids), do: okay(conn, MyApp.Post.find_by_ids(ids))
# Call back for GET /:id1 returns 200 with posts serialized or 404
def find_one(conn, id) do
case MyApp.Post.find(id) do
nil -> not_found(conn)
post -> okay(conn, post)
end
end
end
# Defines how we serialize a Post.
# Serializer assumes each model is a Map.
defmodule Serializer.Post do
use Relax.Serializer
serialize "posts" do
attributes [:id, :title, :body]
# include comments in our response as a compound document.
has_many :comments, serializer: Serializer.Comment
end
# Relax.Serializer is not DB specific, so each relationship must be defined.
def comments(post), do: post.comments.all
end
defmodule Serializer.Comment do
serialize "comments" do
attributes [:id, :body, :troll_name]
# In this serializer the relationship is just a field on the map, no need for a function.
has_one :post, field: :post_id
end
end
end
```
## Phoenix Example
TODO: Better Phoenix support, this is currently untested.
```elixir
defmodule MyApp do
defmodule PostController do
use Phoenix.Controller
plug :action
def index(conn, _params) do
posts = MyApp.Post.all
|> MyApp.Serializer.Post.as_json(conn, %{})
|> JSON.encode!
json conn, posts
end
end
end
```
## Installation
Currently a WIP, use at your own risk.
```elixir
{:relax, "~> 0.0.1"}
```
## Usage
### Relax.Serializer
It should be possible to integrate Relax into any existing applications/frameworks just using the serialization layer.
Given any map data structure:
```elixir
defmodel MyApp.Models.Post do
defstruct id: nil, title: "Foo", body: "Bar", posted_at: nil, comment_ids: []
end
defmodel MyApp.Models.Comment do
defstruct id: nil, post_id: nil, body: "spam"
end
```
You can use a separate DSL to define the json representation. Each serializer returns a map based on the given model and connection.
```elixir
defmodule MyApp.Serializers.V1.Post do
use Relax.Serializer
serialize "posts" do
attributes [:id, :title, :body, :is_published]
has_many :comments, ids: true
end
def is_published(post, _conn) do
post.posted_at != nil
end
def comments(post, _conn) do
post.comment_ids
end
end
```
You can then pass the model to the serializer to get the jsonapi.org formated data structure for conversion to JSON.
```elixir
# In a standard plug:
json = %MyApp.Models.Post{id: 1, title: "Foo"}
|> MyApp.Serializers.V1.Post.as_json(conn)
|> Poison.Encoder.encode([])
# Don't forget the jsonapi.org content type!
conn
|> put_resp_header("content-type", "application/vnd.api+json")
|> send_resp(200, json)
```
### Relax.Router
The Relax.Router is a thin layer on top of the existing Plug.Router implementation. It provides version and resource macros to let you quickly define resources.
You can still use `Plug.Route.forward/2` and `Plug.Route.match/2` as well as hook into the plug stack normally.
```elixir
defmodule MyApp.Router do
use Relax.Router
plug :match
plug :dispatch
forward "/app", to: MyApp.Static
version :v1 do
resource :posts, MyApp.API.V1.Posts do
resource :comments, MyApp.API.V1.Posts.Comments
end
resource :comments, MyApp.API.V1.Comments
end
match _ do
Plug.Conn.send_resp(conn, 404, "")
end
end
```
### Relax.Resource
Relax.Resource wraps macros routing to proper actions, serializing and sending responses, and filtering params.
A Relax.Resource delegates the appropriate path matches to the actions `find_all/1', `find_many/2`, `find_one/2`, `create/1`, `update/2`, and `delete/2`.
In your resource you can choose to only support a subset of these using `:only` or `:except`.
Once again, normal Plug.Route plug stack, functions, and matching work, however they will be defined after the pre-generated resource matches.
```elixir
defmodule API.V1.Posts do
use Relax.Resource, only: [:find_all, :find_one, :find_many]
plug :match
plug :dispatch
serializer Serializers.V1.Post
def find_all(conn) do
okay(conn, Post.all)
end
def find_one(conn, id) do
case Post.find(id) do
nil -> not_found(conn)
post -> okay(conn, post)
end
end
def find_many(conn, list_of_ids) do
okay(conn, Post.find(list_of_ids))
end
def create(conn) do
filter_params(conn, {"posts", [:title, :body]}) do
case MyApp.Models.Post.create(params) do
{:ok, post} -> created(conn, post)
{:error, errors} -> invalid(conn, errors)
end
end
end
post '/:id/publish' do
#...
okay(conn, post)
end
def match(_), do: not_found(conn)
end
```
## License
Relax source code is released under Apache 2 License. Check LICENSE file for more information.