Why AMS Adapters, in Rails 5, are so important to a wonderful RESTful API
{
"data": {
"type": "Developer",
"id": "Tiago Freire",
"attributes": {
"nickname" : "Kuruma",
"background_knowledge": [ "C", "C++", "C#", "Java"],
"current_language:": "Ruby",
"interest": "Rust"
},
"relationships": {
"jobs": {
"data": { "type": "Software Boutique", "id": "Codeminer42" },
"links": {
"related": {
"href": "http://www.codeminer42.com"
}
}
},
"consultancy": {
"data": { "type": "Host and Cloud services", "id": "Locaweb" },
"links": {
"related": {
"href": "http://www.locaweb.com.br"
}
}
}
}
}
}
{
"data": {
"type": "Developer",
"id": "Bruno Bacarini",
"attributes": {
"nickname" : "bacarini",
"background_knowledge": ["Java"],
"current_language:": "Ruby"
},
"relationships": {
"jobs": [
{ "data": {
"type": "Host and Cloud services",
"id": "Locaweb"
},
"links": {
"related": {
"href": "http://www.locaweb.com.br"
}
}
},{
"data": {
"type": "e-recruitment",
"id": "VAGAS.com"
},
"links": {
"related": {
"href": "http://www.vagas.com.br"
}
}
}]
}
}
}
Developer Experience
JSON API
Rails 5
REST
API
AMS
Hypermedia
Adapters
Patterns
Documentation
Good
Experience
Motivation
Developer Experience
REST
Representational State Transfer
Protocol
HTTP
The Hypertext Transfer Protocol (HTTP) is a stateless application-level protocol for distributed, collaborative, hypertext information systems.
Resources
Any information that can be named can be a resource: a document or image, a temporal service (e.g. "today's weather in Los Angeles"), a collection of other resources, a non-virtual object (e.g. a person), and so on. In other words, any concept that might be the target of an author's hypertext reference must fit within the definition of a resource.
Roy T. Fielding
HTTP verbs
[C] POST
[R] GET
[U] PUT / PATCH
[D] DELETE
Hypermedia control
Machines can follow links when they understand the data format and relationship types.
Roy T. Fielding
So, where are the links?
Media Type
Content-type
and
Accept
Collection+JSON
HAL
JSON-LD
SIREN
JSON-API
...
APIs
RAILS-API
RAILS 5
rails new appname --api
ActiveModel::
Serializer
#app/controllers
class PostsController < ApplicationController
def index
@posts = Post.all
render json: @posts
end
end
#app/serializers
class PostSerializer < ActiveModel::Serializer
attributes :id, :name, :date
has_many :comments
end
module ActionController
module Serialization
extend ActiveSupport::Concern
include ActionController::Renderers
...
[:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
define_method renderer_method do |resource, options|
options.fetch(:context) { options[:context] = request }
serializable_resource = get_serializer(resource, options)
super(serializable_resource, options)
end
end
...
end
end
Adapters
Steve Klabnik Yehuda Katz
JsonAPI
=
anti-bikeshedding weapon
How to use adapters (Specially JsonAPI)?
#controller/action
render json: @posts, adapter: :json_api
#OR
#config/initializer
ActiveModel::Serializer.config.adapter = :json_api
Accept: application/vnd.example[.version].param[+json]
curl -v -H 'Accept:application/vnd.example.v1+json' localhost:3000
ActiveModel::Serializer::Adapter.register(:json_v1, Json)
ActiveModel::Serializer::Adapter.register(:json_v2, JsonApi)
module ActiveModel
class Serializer
class Adapter
....
def self.create(resource, options = {})
override = options.delete(:adapter)
klass = override ? adapter_class(override) : ActiveModel::Serializer.adapter
klass.new(resource, options)
end
...
end
end
end
Creating my own adapter
module Example
class UsefulAdapter < ActiveModel::Serializer::Adapter
end
end
# Automatically register adapters when subclassing
def self.inherited(subclass)
ActiveModel::Serializer::Adapter.register(subclass.to_s.demodulize, subclass)
end
First option
class MyAdapter; end
ActiveModel::Serializer::Adapter.register(:special_adapter, MyAdapter)
Second option
Relationships
Included
Pagination
Meta
#app/serializers
class PostSerializer < ActiveModel::Serializer
attributes :id, :title
has_many :comments
has_one :author
end
"data": [{
"type": "posts",
"id": "1",
"attributes": { "title": "JSON API paints my bikeshed!" },
"relationships": {
"author": {
"links": {
"self": "http://example.com/posts/1/relationships/author",
"related": "http://example.com/posts/1/author"
},
"data": { "type": "people", "id": "9" }
},
"comments": {
"links": {
"self": "http://example.com/posts/1/relationships/comments",
"related": "http://example.com/posts/1/comments"
},
"data": [{ "type": "comments", "id": "5" }]
}
},
"links": { "self": "http://example.com/posts/1" }
}],
class PostsController < ApplicationController
def show
@post = Post.find params[:id]
render json: @post, adapter: :json_api, include: ['comments', 'author']
end
end
"included": [{
"type": "people", "id": "9",
"attributes": {
"first-name": "Dan",
"last-name": "Gebhardt",
"twitter": "dgeb"
},
"links": { "self": "http://example.com/people/9" }
}, {
"type": "comments", "id": "5",
"attributes": { "body": "First!" },
"links": { "self": "http://example.com/comments/5" }
}],
Nested Associations
(JsonAPI adapter)
class PostsController < ApplicationController
def show
@post = Post.find params[:id]
render json: @post, adapter: :json_api, include: [author: [:bio]]
end
end
class PostSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :author
end
class AuthorSerializer < ActiveModel::Serializer
attributes :id, :name
has_one :bio
end
class BioSerializer < ActiveModel::Serializer
attributes :id, :title
belongs_to :author
end
"included": [
{
"id": "1",
"type": "author",
"attributes": {
"name": "bruno"
},
"relationships": {
"bio": {
"data": [
{
"id": "1",
"type": "bio"
}
]
}
}
},
{
"id": "1",
"type": "bio",
"attributes": {
"title": "foobar"
}
},
module ActiveModel
class Serializer
class Adapter
class JsonApi < Adapter
...
def add_links(options)
links = @hash.fetch(:links) { {} }
resources = serializer.instance_variable_get(:@resource)
if is_paginated?(resources)
@hash[:links] = add_pagination_links(links, resources, options)
end
end
def add_pagination_links(links, resources, options)
pagination_links = JsonApi::PaginationLinks.new(resources, options[:context])
.serializable_hash(options)
links.update(pagination_links)
end
def is_paginated?(resource)
resource.respond_to?(:current_page) &&
resource.respond_to?(:total_pages) &&
resource.respond_to?(:size)
end
...
end
end
end
end
"links": {
"self": "http://example.com/posts?page%5Bnumber%5D=3&page%5Bsize%5D=1",
"first": "http://example.com/posts?page%5Bnumber%5D=1&page%5Bsize%5D=1",
"prev": "http://example.com/posts?page%5Bnumber%5D=2&page%5Bsize%5D=1",
"next": "http://example.com/posts?page%5Bnumber%5D=4&page%5Bsize%5D=1",
"last": "http://example.com/posts?page%5Bnumber%5D=10&page%5Bsize%5D=1"
}
class PostsController < ApplicationController
def show
@post = Post.find params[:id]
render json: @post, adapter: :json_api, meta: { "total-pages" => 10 }
end
end
"meta": {
"total-pages": 10
}
{
"data": [{ "type": "posts", "id": "1",
"attributes": { "title": "JSON API paints my bikeshed!" },
"relationships": {
"author": {
"links": {
"self": "http://example.com/posts/1/relationships/author",
"related": "http://example.com/posts/1/author"
},
"data": { "type": "people", "id": "9" }
},
"comments": {
"links": {
"self": "http://example.com/posts/1/relationships/comments",
"related": "http://example.com/posts/1/comments"
},
"data": [{ "type": "comments", "id": "5" }]
}
},
"links": { "self": "http://example.com/posts/1" }
}],
"included": [{
"type": "people", "id": "9",
"attributes": {
"first-name": "Dan",
"last-name": "Gebhardt",
"twitter": "dgeb"
},
"links": { "self": "http://example.com/people/9" }
}, {
"type": "comments", "id": "5",
"attributes": { "body": "First!" },
"links": { "self": "http://example.com/comments/5" }
}],
"links": {
"self": "http://example.com/posts?page%5Bnumber%5D=3&page%5Bsize%5D=1",
"first": "http://example.com/posts?page%5Bnumber%5D=1&page%5Bsize%5D=1",
"prev": "http://example.com/posts?page%5Bnumber%5D=2&page%5Bsize%5D=1",
"next": "http://example.com/posts?page%5Bnumber%5D=4&page%5Bsize%5D=1",
"last": "http://example.com/posts?page%5Bnumber%5D=10&page%5Bsize%5D=1"
},
"meta": {
"total-pages": 10
}
}
Here are the links!
"data": [{
"type": "posts",
"id": "1",
"attributes": { "title": "JSON API paints my bikeshed!" },
"relationships": {
"author": {
"links": {
"self": "http://example.com/posts/1/relationships/author",
"related": "http://example.com/posts/1/author"
},
"data": { "type": "people", "id": "9" }
},
"comments": {
"links": {
"self": "http://example.com/posts/1/relationships/comments",
"related": "http://example.com/posts/1/comments"
},
"data": [{ "type": "comments", "id": "5" }]
}
},
"links": { "self": "http://example.com/posts/1" }
}],
Buuut there is still a lot of work to do...
Deserialization (for both JSON-API and JSON)
Documentation
A better AC/Serializers integration API
Provide better naming for JSON Adapter
Add support to all conventions on JSON-API
Bring Filter back from older version
Russian Doll Cache (use #fetch_multi)
Start handle nested associations
ams = Array.new
ams << hyperlinks
ams << media-type flexibility
ams << API more attractive
ams << API more explorable
Good
Experience
Thank you all
ams-adapters
By Bruno Bacarini
ams-adapters
Web APIs have revolutionized all kinds of products and services, and still continue to do so. Nowadays the most relevant architecture is REST along with the JSON media type. Furthermore, lots of specifications to serialize those media types are appearing. JSON API has released its first version last May. We are going to talk about the importance of those Active Model Serializers (AMS) format adapters, now the default on Rails 5. In additional, we would like to show how AMS can help us implement hypermedia controls and how it can affect your RESTful API.
- 2,455