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

Made with Slides.com