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,378