Remodel is an Elixir presenter package used to transform data structures. This is especially useful when a desired representation doesn't match the schema defined within the database.
In particular, Remodel enables:
- Renaming or aliasing attributes to change the name from the model
- Arbitrary attributes based on combining data in an object
- Inclusion of attributes only if a certain conditions have been met
- Addition of root nodes on arrays or instances
- Easy conversion to a list representation (eg, for CSVs)
Install Remodel as a hex dependency in your .mix
file:
defp deps do
[{:remodel, "~> 0.0.4"}]
end
and run mix deps.get
to install the package.
You can use Remodel to prepare an Elixir Map (or anything that identifies as a Map, such as a Struct or an Ecto.Model instance) to be serialized through a JSON, XML, or CSV serializer.
With Remodel, the data is derived from maps (ORM-agnostic) and the representation of the API output is described by attributes and methods using a simple Elixir DSL. This allows you to keep your data separated from the structure you wish to output.
Once you have installed Remodel (explained above), you can construct a Remodel schema and then render the model
from your Plug or Phoenix applications from the controller (or route) very easily. Using Phoenix and Ecto as an example, assuming you have a Post
model filled with blog posts, you can render an API representation by creating a route:
# web/controllers/post_controller.ex
def index(conn, params) do
posts = Repo.all(from p in Post,
select: p,
order_by: [asc: p.id],
preload: :author)
json(conn, PostSerializer.to_map(posts))
end
Then we can create the following Remodel schema to express the desired API output of posts
:
# web/serializers/post_serializer.ex
defmodule PostSerializer do
use Remodel
attributes [:id, :title, :author_name]
def author_name(record) do
"#{record.author.first_name} #{record.author.last_name}"
end
end
Which would output the following JSON when visiting the appropriate action:
[
{
"id" : 1,
"title": "Introducing Remodel",
"author_name" : "Sean Stavropoulos"
},
{
"id" : 2,
...
}
]
Basic usage to define a few simple attributes for the response:
attributes [:id, :foo, :bar]
or use with aliased attributes:
# Take the value of model attribute `foo` and name the node `bar`
attribute :foo, as: :bar
# => %{bar: 5}
or show attributes only if a condition is true:
# atom representing a method in the serializer
attribute :foo, if: :published
def published(record) do
!is_nil(record.published_at)
end
Some JSON configurations will include a single root node named after the object's type or an abstractable name for the object depending on context. Remodel can accomodate these JSON types on an individual, or a default basis.
Example usage of root nodes:
defmodule UserSerializer do
use Remodel
@array_root :users
attribute :id
end
[%{id: 1}, %{id: 2}] |> UserSerializer.to_map #=> %{users: [%{id: 1}, %{id: 2}]}
[%{id: 1}, %{id: 2}] |> UserSerializer.to_map(array_root: :superusers) #=> %{superusers: [%{id: 1}, %{id: 2}]}
Example usage of instance root nodes:
defmodule UserSerializer do
use Remodel
@instance_root :user
attribute :id
end
[%{id: 1}, %{id: 2}] |> UserSerializer.to_map #=> %{[%{user: %{id: 1}}, %{user: %{id: 2}}]}
[%{id: 1}, %{id: 2}] |> UserSerializer.to_map(instance_root: :user) #=> %{[%{user: %{id: 1}}, %{user: %{id: 2}}]}
Remodel includes two formatters to transform map structures. The first, as visible in the above examples, converts an input map to an output map. Remodel also includes a transformation useful for working with list-based tools, such as most CSV generators.
defmodule UserSerializer do
use Remodel
attributes [:id, :full_name]
def full_name(record) do
"#{record.first_name} #{record.last_name}"
end
end
# Save all users from the database to a CSV
Repo.all(from u in User, select: %{id: u.id, first_name: u.first_name, last_name: u.last_name})
|> UserSerializer.to_list(headers: true) # => [[:id, :full_name], [1, "Joe Armstrong"], ...]
|> CSVLixir.write
|> File.write("foo.csv")
:ok
Some serialization configurations may depend on data outside of the serialized resource (for example, utilizing information about the currently authenticated user). Remodel allows passing in any scope data type which can be referenced throughout the serialization process.
Here's an example to only display a particular attribute if a user is requesting their own serialized user object:
defmodule UserSerializer do
use Remodel
attribute :id
attribute :email, if: :personal_info_visible?
def personal_info_visible?(record, scope) do
scope.id == record.id
end
end
# Assume a current_user variable has been set to the authenticated user
current_user = Repo.get(User, 1)
Repo.one(from u in User, where: u.id == 1, select: u, limit: 1) |> UserSerializer.to_map(scope: current_user)
#=> %{id: 1, email: "[email protected]"}
Repo.one(from u in User, where: u.id == 2, select: u, limit: 1) |> UserSerializer.to_map(scope: current_user)
#=> %{id: 2}
All attribute and conditional functions must accept either one argument (the record), or two arguments (the record, and any given scope)
Often when you have pagination with your data you use a meta field for showing additional information (current_page, total_pages, etc).
meta
will only be included if you have a Serializer that supports root
defmodule UserSerializer do
use Remodel
@array_root :users
attribute :id
end
[%{id: 1}, %{id: 2}] |> UserSerializer.to_map(meta: %{ page: 1 } #=> %{users: [%{id: 1}, %{id: 2}], "meta" => %{ page: 1}}
Remodel is a brand new project, and I am hopeful that there is a place in the Elixir ecosystem for such a tool. In no particular order of importance, some features I would like to add are:
- Typespecs and Documentation
- DRY up test suite
- Anonymous functions for
if
clauses: (eg:attribute :foo, if: fn(record) -> record.bar end
) - Benchmarking and performance optimizations
- Handling of Ecto Associations
There are a few excellent libraries that helped inspire Remodel and they are listed below:
Copyright 2015 Sean Stavropoulos
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.