• Stars
    star
    278
  • Rank 148,454 (Top 3 %)
  • Language
    Ruby
  • License
    MIT License
  • Created about 11 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Adapters-based API serializers with Hypermedia support for Ruby apps.

Oat

Build Status Gem Version

Adapters-based API serializers with Hypermedia support for Ruby apps. Read the blog post for context and motivation.

What

Oat lets you design your API payloads succinctly while conforming to your media type of choice (hypermedia or not). The details of the media type are dealt with by pluggable adapters.

Oat ships with adapters for HAL, Siren and JsonAPI, and it's easy to write your own.

Serializers

A serializer describes one or more of your API's entities.

You extend from Oat::Serializer to define your own serializers.

require 'oat/adapters/hal'
class ProductSerializer < Oat::Serializer
  adapter Oat::Adapters::HAL

  schema do
    type "product"
    link :self, href: product_url(item)

    properties do |props|
      props.title item.title
      props.price item.price
      props.description item.blurb
    end
  end

end

Then in your app (for example a Rails controller)

product = Product.find(params[:id])
render json: ProductSerializer.new(product)

Serializers require a single object as argument, which can be a model instance, a presenter or any other domain object.

The full serializer signature is item, context, adapter_class.

  • item a model or presenter instance. It is available in your serializer's schema as item.
  • context (optional) a context hash that is passed to the serializer and sub-serializers as the context variable. Useful if you need to pass request-specific data.
  • adapter_class (optional) A serializer's adapter can be configured at class-level or passed here to the initializer. Useful if you want to switch adapters based on request data. More on this below.

Defining Properties

There are a few different ways of defining properties on a serializer.

Properties can be added explicitly using property. In this case, you can map an arbitrary value to an arbitrary key:

require 'oat/adapters/hal'
class ProductSerializer < Oat::Serializer
  adapter Oat::Adapters::HAL

  schema do
    type "product"
    link :self, href: product_url(item)

    property :title, item.title
    property :price, item.price
    property :description, item.blurb
    property :the_number_one, 1
  end
end

Similarly, properties can be added within a block using properties to be more concise or make the code more readable. Again, these will set arbitrary values for arbitrary keys:

require 'oat/adapters/hal'
class ProductSerializer < Oat::Serializer
  adapter Oat::Adapters::HAL

  schema do
    type "product"
    link :self, href: product_url(item)

    properties do |p|
      p.title           item.title
      p.price           item.price
      p.description     item.blurb
      p.the_number_one  1
    end
  end
end

In many cases, you will want to simply map the properties of item to a property in the serializer. This can be easily done using map_properties. This method takes a list of method or attribute names to which item will respond. Note that you cannot assign arbitrary values and keys using map_properties - the serializer will simply add a key and call that method on item to assign the value.

require 'oat/adapters/hal'
class ProductSerializer < Oat::Serializer
  adapter Oat::Adapters::HAL

  schema do
    type "product"
    link :self, href: product_url(item)

    map_properties :title, :price
    property :description, item.blurb
    property :the_number_one, 1
  end
end

Defining Links

Links to other resources can be added by using link with a name and an options hash. Most adapters expect just an href in the options hash, but some might support additional properties. Some adapters also suport passing templated: true in the options hash to indicate special treatment of a link template.

Adding meta-information

You can add meta-information about your JSON document via meta :property, "value". When using the JsonAPI adapter these properties are rendered in a top level "meta" node. When using the HAL or Siren adapters meta just acts as an alias to property, so the properties are rendered like normal properties.

Adapters

Using the included HAL adapter, the ProductSerializer above would render the following JSON:

{
    "_links": {
        "self": {"href": "http://example.com/products/1"}
    },
    "title": "Some product",
    "price": 1000,
    "description": "..."
}

You can easily swap adapters. The same ProductSerializer, this time using the Siren adapter:

adapter Oat::Adapters::Siren

... Renders this JSON:

{
    "class": ["product"],
    "links": [
        { "rel": [ "self" ], "href": "http://example.com/products/1" }
    ],
    "properties": {
        "title": "Some product",
        "price": 1000,
        "description": "..."
    }
}

At the moment Oat ships with adapters for HAL, Siren and JsonAPI, but it's easy to write your own.

Note: Oat adapters are not required by default. Your code should explicitly require the ones it needs:

# HAL
require 'oat/adapters/hal'
# Siren
require 'oat/adapters/siren'
# JsonAPI
require 'oat/adapters/json_api'

Switching adapters dynamically

Adapters can also be passed as an argument to serializer instances.

ProductSerializer.new(product, nil, Oat::Adapters::HAL)

That means that your app could switch adapters on run time depending, for example, on the request's Accept header or anything you need.

Note: a different library could be written to make adapter-switching auto-magical for different frameworks, for example using Responders in Rails. Also see Rails Integration.

Nested serializers

It's common for a media type to include "embedded" entities within a payload. For example an account entity may have many users. An Oat serializer can inline such relationships:

class AccountSerializer < Oat::Serializer
  adapter Oat::Adapters::HAL

  schema do
    property :id, item.id
    property :status, item.status
    # user entities
    entities :users, item.users do |user, user_serializer|
      user_serializer.properties do |props|
        props.name user.name
        props.email user.email
      end
    end
  end
end

Another, more reusable option is to use a nested serializer. Instead of a block, you pass another serializer class that will handle serializing user entities.

class AccountSerializer < Oat::Serializer
  adapter Oat::Adapters::HAL

  schema do
    property :id, item.id
    property :status, item.status
    # user entities
    entities :users, item.users, UserSerializer
  end
end

And the UserSerializer may look like this:

class UserSerializer < Oat::Serializer
  adapter Oat::Adapters::HAL

  schema do
    property :name, item.name
    property :email, item.name
  end
end

In the user serializer, item refers to the user instance being wrapped by the serializer.

The bundled hypermedia adapters ship with an entities method to add arrays of entities, and an entity method to add a single entity.

# single entity
entity :child, item.child do |child, s|
  s.name child.name
  s.id child.id
end

# list of entities
entities :children, item.children do |child, s|
  s.name child.name
  s.id child.id
end

Both can be expressed using a separate serializer:

# single entity
entity :child, item.child, ChildSerializer

# list of entities
entities :children, item.children, ChildSerializer

The way sub-entities are rendered in the final payload is up to the adapter. In HAL the example above would be:

{
  ...,
  "_embedded": {
    "child": {"name": "child's name", "id": 1},
    "children": [
      {"name": "child 2 name", "id": 2},
      {"name": "child 3 name", "id": 3},
      ...
    ]
  }
}

Nested serializers when using the JsonAPI adapter

Collections are easy to express in HAL and Siren because they're no different from any other "entity". JsonAPI, however, doesn't work that way. In JsonAPI there's a distinction between "side-loaded" entities and the collection that is the subject of the resource. For this reason a collection method was added to the Oat DSL specifically for use with the JsonAPI adapter.

In the HAL and Siren adapters, collection is aliased to entities, but in the JsonAPI adapter, it sets the resource's main collection array as per the spec. entities keep the current behaviour of side-loading entities in the resource.

Subclassing

Serializers can be subclassed, for example if you want all your serializers to share the same adapter or add shared helper methods.

class MyAppSerializer < Oat::Serializer
  adapter Oat::Adapters::HAL

  protected

  def format_price(price)
    Money.new(price, 'GBP').format
  end
end
class ProductSerializer < MyAppSerializer
  schema do
    property :title, item.title
    property :price, format_price(item.price)
  end
end

This is useful if you want your serializers to better express your app's domain. For example, a serializer for a social app:

class UserSerializer < SocialSerializer
  schema do
    name item.name
    email item.email
    # friend entities
    friends item.friends
  end
end

The superclass defines the methods name, email and friends, which in turn delegate to the adapter's setters.

class SocialSerializer < Oat::Serializer
  adapter Oat::Adapters::HAL # or whatever

  # friendly setters
  protected

  def name(value)
    property :name, value
  end

  def email(value)
    property :email, value
  end

  def friends(objects)
    entities :friends, objects, FriendSerializer
  end
end

You can specify multiple schema blocks, including across class hierarchies. This allows us to append schema attributes or override previously defined attributes:

class ExtendedUserSerializer < UserSerializer
  schema do
    name item.full_name # name property will now by the user's full name
    property :dob, item.dob # additional date of birth attribute
  end
end

URLs

Hypermedia is all about the URLs linking your resources together. Oat adapters can have methods to declare links in your entity schema but it's up to your code/framework how to create those links. A simple stand-alone implementation could be:

class ProductSerializer < Oat::Serializer
  adapter Oat::Adapters::HAL

  schema do
    link :self, href: product_url(item.id)
    ...
  end

  protected

  # helper URL method
  def product_url(id)
    "https://api.com/products/#{id}"
  end
end

In frameworks like Rails, you'll probably want to use the URL helpers created by the routes.rb file. Two options:

Pass a context hash to serializers

You can pass a context hash as second argument to serializers. This object will be passed to nested serializers too. For example, you can pass the controller instance itself.

# users_controller.rb

def show
  user = User.find(params[:id])
  render json: UserSerializer.new(user, controller: self)
end

Then, in the UserSerializer:

class ProductSerializer < Oat::Serializer
  adapter Oat::Adapters::HAL

  schema do
    # `context[:controller]` is the controller, which responds to URL helpers.
    link :self, href: context[:controller].product_url(item)
    ...
  end
end

The context hash is passed down to each nested serializer called by a parent. In some cases, you might want to include extra context information for one or more nested serializers. This can be done by passing options into your call to entity or entities.

class CategorySerializer < Oat::Serializer
  adapter Oat::Adapters::HAL

  schema do
    map_properties :id, :name

    # category entities
    # passing this option ensures that only direct children are embedded within
    # the parent serialized category
    entities :subcategories, item.subcategories, CategorySerializer, embedded: true if context[:embedded]
  end
end

The additional options are merged into the current context before being passed down to the nested serializer.

Mixin Rails' routing module

Alternatively, you can mix in Rails routing helpers directly into your serializers.

class MyAppParentSerializer < Oat::Serializer
  include ActionDispatch::Routing::UrlFor
  include Rails.application.routes.url_helpers
  def self.default_url_options
    Rails.application.routes.default_url_options
  end

  adapter Oat::Adapters::HAL
end

Then your serializer sub-classes can just use the URL helpers

class ProductSerializer < MyAppParentSerializer
  schema do
    # `product_url` is mixed in from Rails' routing system.
    link :self, href: product_url(item)
    ...
  end
end

However, since serializers don't have access to the current request, for this to work you must configure each environment's base host. In config/environments/production.rb:

config.after_initialize do
  Rails.application.routes.default_url_options[:host] = 'api.com'
end

NOTE: Rails URL helpers could be handled by a separate oat-rails gem.

Custom adapters.

An adapter's primary concern is to abstract away the details of specific media types.

Methods defined in an adapter are exposed as schema setters in your serializers. Ideally different adapters should expose the same methods so your serializers can switch adapters without loosing compatibility. For example all bundled adapters expose the following methods:

  • type The type of the entity. Renders as "class" in Siren, root node name in JsonAPI, not used in HAL.
  • link Add a link with rel and href. Renders inside "_links" in HAL, "links" in Siren and JsonAP.
  • property Add a property to the entity. Top level attributes in HAL and JsonAPI, "properties" node in Siren.
  • properties Yield a properties object to set many properties at once.
  • entity Add a single sub-entity. "_embedded" node in HAL, "entities" in Siren, "linked" in JsonAPI.
  • entities Add a collection of sub-entities.

You can define these in your own custom adapters if you're using your own media type or need to implement a different spec.

class CustomAdapter < Oat::Adapter

  def type(*types)
    data[:rel] = types
  end

  def property(name, value)
    data[:attr][name] = value
  end

  def entity(name, obj, serializer_class = nil, &block)
    data[:nested_documents] = serializer_from_block_or_class(obj, serializer_class, &block).to_hash
  end

  ... etc
end

An adapter class provides a data object (just a Hash) that stores your data in the structure you want. An adapter's public methods are exposed to your serializers.

Unconventional or domain specific adapters

Although adapters should in general comply with a common interface, you can still create your own domain-specific adapters if you need to.

Let's say you're working on a media-type specification specializing in describing social networks and want your payload definitions to express the concept of "friendship". You want your serializers to look like:

class UserSerializer < Oat::Serializer
  adapter SocialAdapter

  schema do
    name item.name
    email item.email

    # Friend entity
    friends item.friends do |friend, friend_serializer|
      friend_serializer.name friend.name
      friend_serializer.email friend.email
    end
  end
end

A custom media type could return JSON looking looking like this:

{
    "name": "Joe",
    "email": "[email protected]",
    "friends": [
        {"name": "Jane", "email":"[email protected]"},
        ...
    ]
}

The adapter for that would be:

class SocialAdapter < Oat::Adapter

  def name(value)
    data[:name] = value
  end

  def email(value)
    data[:email] = value
  end

  def friends(friend_list, serializer_class = nil, &block)
    data[:friends] = friend_list.map do |obj|
      serializer_from_block_or_class(obj, serializer_class, &block).to_hash
    end
  end
end

But you can easily write an adapter that turns your domain-specific serializers into HAL-compliant JSON.

class SocialHalAdapter < Oat::Adapters::HAL

  def name(value)
    property :name, value
  end

  def email(value)
    property :email, value
  end

  def friends(friend_list, serializer_class = nil, &block)
    entities :friends, friend_list, serializer_class, &block
  end
end

The result for the SocialHalAdapter is:

{
    "name": "Joe",
    "email": "[email protected]",
    "_embedded": {
        "friends": [
            {"name": "Jane", "email":"[email protected]"},
            ...
        ]
    }
}

You can take a look at the built-in Hypermedia adapters for guidance.

Rails Integration

The Rails responder functionality works out of the box with Oat when the requests specify JSON as their response format via a header Accept: application/json or query parameter format=json.

However, if you want to also support the mime type of your Hypermedia format of choice, it will require a little bit of code.

The example below uses Siren, but the same pattern can be used for HAL and JsonAPI.

Register the Siren mime-type and a responder:

# config/initializers/oat.rb
Mime::Type.register 'application/vnd.siren+json', :siren

ActionController::Renderers.add :siren do |resource, options|
  self.content_type ||= Mime[:siren]
  resource.to_siren
end

In your controller, add :siren to the respond_to:

class UsersController < ApplicationController
  respond_to :siren, :json

  def show
    user = User.find(params[:id])
    respond_with UserSerializer.new(user)
  end
end

Finally, add a to_siren method to your serializer:

class UserSerializer < Oat::Serializer
  adapter Oat::Adapters::Siren

  schema do
    property :name, item.name
    property :email, item.name
  end

  def to_siren
    to_json
  end
end

Now http requests that specify the Siren mime type will work as expected.

NOTE The key thing that makes this all work together is that the object passed to respond_with implements a to_FORMAT method, where FORMAT is the symbol used to register the mime type and responder (:siren). Without it, Rails will not invoke your responder block.

Installation

Add this line to your application's Gemfile:

gem 'oat'

And then execute:

$ bundle

Or install it yourself as:

$ gem install oat

TODO / contributions welcome

  • JsonAPI top-level meta
  • testing module that can be used for testing spec-compliance in user apps?

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Contributors

Many thanks to all contributors! https://github.com/ismasan/oat/graphs/contributors

More Repositories

1

hash_mapper

DSL for mapping between data structures
Ruby
130
star
2

parametric

Declarative input schemas for Ruby apps.
Ruby
39
star
3

sluggable-finder

Automatically create SEO friendly, unique permalinks for your ActiveRecord objects. Behaves exactly like ActiveRecord#find
Ruby
27
star
4

plumber.js

[WiP] Plumbing and pipelines for JavaScript apps
JavaScript
23
star
5

jbundle

Bundle and minify JavaScript projects
Ruby
20
star
6

plumb

Data structures, validation, coercion and processing toolkit for Ruby
Ruby
17
star
7

jquery-patterns

jQuery utitlities and patterns that I use in different projects
JavaScript
16
star
8

Sockete

Client-side WebSocket mock for easy testing of Websocket clients
JavaScript
13
star
9

faraday_throttler

Redis-backed request throttler to protect backend APIs against request stampedes
Ruby
10
star
10

adash

Simple websockets based Activity dashboard
JavaScript
9
star
11

ar_publish_control

Gem to add publish control to your ActiveRecord models, including start and end publishing dates.
Ruby
9
star
12

websockets_examples

WebSocket examples for varied talks
JavaScript
8
star
13

mini_flickr

Simple gem to fetch photos from Flickr's REST API
Ruby
5
star
14

pusher-chat-tutorial

Step by step tutorial with Sinatra and Pusher, Check the branches.
JavaScript
5
star
15

bridger

Helpers to write and test hypermedia APIs in Ruby Rack apps, framework agnostic
Ruby
5
star
16

proto_processor

Helper modules for building task processors
Ruby
4
star
17

proxied-api

Multi-component API with EM proxy and ZMQ
Ruby
3
star
18

sourced

Barebones Event Sourcing for Ruby. Add water.
Ruby
3
star
19

anisoptera

Async Rack app for image thumbnailing
Ruby
3
star
20

websockets_hub

Subscribe services like Twitter, IRC and others and pipe them to websockets
Ruby
3
star
21

example_rails_app

Skeleton rails app to try out some general patterns
3
star
22

js_host

Asset host for versioned Javascript APIs
Ruby
3
star
23

bototo

Eventmachine-based DSL for creating Campfire bots
Ruby
3
star
24

websockets-examples

Fooling around with Websockets
Ruby
2
star
25

ar_finder_extension

Draft convention for ActiveRecord extensions
Ruby
2
star
26

mini_twitter

Super simple Twitter RSS parser for your latest tweets as ruby objects
Ruby
2
star
27

muppet

Easy CLI server provisioning using Sprinkle
Ruby
2
star
28

catwalk.js

Spiking models, collections and emitters in JavaScript
JavaScript
2
star
29

simple_roles

Simple roles to include in your classes
2
star
30

panda_gem

Gem for Panda, the video platform
Ruby
2
star
31

strest

ORM-like utilities to interface to hybrid REST / RPC HTTP APIs
2
star
32

sinatra_auth_bootic

Sinatra extension for quick and easy apps using Bootic's API (in progress)
Ruby
2
star
33

warden-bootic

Warden oauth2 strategy for Bootic's API
Ruby
2
star
34

em_airbrake

Async Airbrake notifier for EventMachine apps
Ruby
2
star
35

persistent

Tiny plug &amp; play persistence layer for your Ruby objects
Ruby
2
star
36

async-emol-check

Revisa si un RUT es miembro del Club de Lectores del Mercurio
Ruby
1
star
37

ismasan.github.com

My personal Github site
1
star
38

demo-orders-api

Demo orders API to showcase Ruby and hypermedia
Ruby
1
star
39

html

Components-based HTML builder
Ruby
1
star
40

lrug-hypermedia-ruby-2017-04

LRUG presentation on hypermedia and Ruby
1
star
41

redis_stats_server

Small async redis-backed stats server
Ruby
1
star
42

ApiBee

Small Ruby client for discoverable, lazily-loaded, paginated JSON APIs
Ruby
1
star
43

merrit

Resource-oriented, Rack-based API modelling library. WiP.
Ruby
1
star
44

tecepe

Launch small (evented) TCP JSON services on a given host:port
Ruby
1
star
45

parametric-active_model

Turn Parametric schemas into ActiveModel-compliant forms
Ruby
1
star
46

caching_proxy

(WiP) Redis-backed reverse caching proxy with control API, written in Golang.
Go
1
star
47

rack-oauth2utils

Middleware for catching OAuth2 access tokens in Rack apps
Ruby
1
star
48

emol_partner_check

Pequeña aplicación web. Verifica que RUT de usuario es socio de El Mercurio
Ruby
1
star
49

rack-wiki

Rack-based markdown wiki
Ruby
1
star
50

snishimura.com

Bootic template for snishimura.com
CSS
1
star
51

bootic_navidad_2011

Productos destacados tiendas Bootic 2011
CSS
1
star