Git Product home page Git Product logo

oat's Introduction

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

oat's People

Contributors

abargnesi avatar apsoto avatar bonkydog avatar coderdave avatar dpdawson avatar emilesilvis avatar erezesk avatar iainbeeston avatar ismasan avatar kjg avatar landlessness avatar sebastianedwards avatar shekibobo avatar tjmw avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

oat's Issues

Serializing collections

I'm wondering if there is any way to specify collections? I'm working on an API which will return JSON conforming to the JSON-API spec. It works beautifully for single objects, but I can't see anyway to represent a collection that conforms to the spec. Each call to a serializer generates an entire payload for each object.

What the spec requires is output like this:

{
  "products": [
     {"id": "herp"},
     {"id": "derp"}
  ]
}

But effectively the only thing I can achieve by calling the serializer repeatedly is something like:

[
  {
    "products": [
       {"id": "herp"}
    ]
  },
  {
    "products": [
       {"id": "derp"}
    ]
  }
]

Am I missing something or is this just not possible?

:only option for filtering

It would be great if we could override the default props somehow, with the :only options familiar from Rails, like:

UserSerializer.new(current_user,
     only: {
       property: [:id, :first_name, :last_name]
     }
  )

Or is there a better way to achieve that kind of flexibility?

(an additional :include option could also be added, but seems harder to implement since you'd have to specify how to render the additional properties...)

How to serialize many records?

Hey there,

I've tried reading the docs and the code a bit, but as far as I can tell I can't find a way to render many records for a serializer. Say I have the following Rails code:

users = User.where(first_name: 'Ted')
render json: UserSerializer.new(users)

Is there a way to handle this case where I pass more than one record? I thought entities might be the way to do it but I can't assign it the UserSerializer without a getting my stack exhausted.

JsonApi adapter generates anything but JSON API conformant document

Hi guys,

I've tried to use Oat to generate JSON API response to consume by my JavaScript application and discovered that output does not even look similar to what is described here: http://jsonapi.org/format/#url-based-json-api. For example, there is no "data" section and no "included" section but there is "linked" section. JSON API does not even have such keyword...

I am really confused by this fact because it looked like Oat is what I really need and I did like the blog article where the motivation is described. However it looks like Oat does not suit my needs unless I write my own adapter (which is not hard, I understand it).

I would be really grateful if someone could explain me what's happening (or was happening) with Oat and JsonApi adapter. Was there another JSON API specification previously?

Thanks!

Nested serializers: undefined method

In following the readme to set up a nested serializer, I created this AccountSerializer:

require 'oat/adapters/hal'

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

  schema do
    map_properties :id

    entity :tasks, item.tasks do |task, task_serializer|
      task_serializer.created_at task.created_at
    end
  end
end

However, I'm getting the following error when running AccountSerializer.new(Account.first).to_json
"undefined method 'created_at' for #<ActiveRecord::Associations::CollectionProxy []>"

Am I doing something wrong?

Serialize to vanilla JSON using the same serializer class

I am attempting to define single Serializer class to handle resources for application/json and application/hal+json.

Currently I have an Evidence resource defined using two serializers:

class EvidenceCollectionJsonSerializer < BaseSerializer                         
  adapter Oat::Adapters::HAL

  schema do                                                                     
    type :'evidence-collection'                                                 

    properties do |p|                                                           
      collection :evidence, item, EvidenceJsonSerializer
      p.facets   context[:facets]
    end
  end
end

class EvidenceCollectionHALSerializer < BaseSerializer
  adapter Oat::Adapters::HAL

  schema do
    type :'evidence-collection'

    properties do |p|
      collection :evidence, item, EvidenceHALSerializer
      p.facets   context[:facets]
    end

    link :self,       link_self
    link :start,      link_start
    link :next,       link_next
  end
end

I have defined the application/json serializer with Oat::Adapters::HAL without including any links. This works accept the :evidence collection is wrapped in a HAL _embedded property.

Is there a way to represent vanilla JSON when the client does not accept hypermedia formats?

An adapter like this gist does the job.

DRY property declaration

In a project I'm working on, I've created an ApplicationSerializer, in which I defined a couple methods:

class ApplicationSerializer < Oat::Serializer
  include Oat::Adapters::HAL

  def field(name, value=nil)
    value = item.send(name) if value.nil? && item.respond_to?(name)
    property name, value
  end

  def fields(*args)
    args.each do |name|
      field name
    end
  end
end

This lets me add things in a more concise way:

class UserSerializer < ApplicationSerializer
  schema do
    fields :id, :email, :name, :created_at, :updated_at
    field :age
    field :psychic, item.psychic?
    properties do |p|
      p.active item.active?
    end
  end
end

I think it would be nice to have something like this built in to Oat. Ideally, these methods would just be called property and properties, and they would use the Adapter's methods as super. To be consistent and DRY, they would need to be declared on Oat::Serializer, but once you include Oat::Adapters::HAL or any other adapter, those methods get overridden. Otherwise, each adapter would need to define these versions of the methods. I'm not really opposed to this. The only thing I think that becomes an issue is the coupling of the serializer and the adapter since #item is on the serializer, and the adapter has no concept of item.

The other solution would be to have a different name for these methods, but then things just get inconsistent and confusing.

Any thoughts on a good way of building this in?

Array serialization

Not seeing this covered in the docs. Any thought into this yet? For JsonAPI for example, mapping a collection to individual hashes means the arrays all need to be merged. I was thinking maybe to_hash could accept a base which it would merge into rather than always creating a fresh one?

JSON:API adapter not up-to-date with the spec

I was looking at using Oat for JSON:API, and found that the adapter's not following the latest version of the spec. The specific things I noticed were:

  • Attributes (other than "id" and "type") should be nested under an "attributes" key.
  • "id" should be a string (#47)
  • Linked entities should be under a top-level "included" key (similar to #40, but the name has changed from "linked")

It's my first time using Oat, so some of these problems could have been user error.

Cross-cutting concerns

Hi,

I want to include pagination links in all collections. What's the best way to go about this? I don't think that creating a base CollectionSerializer class will work since 'schema' is a class method. And I'm not sure how I would go about this using mixins or concerns. Any advice?

Thread Safety

Hey,

I would like to know if the gem is thread safe or not?

Thanks in advance,
Erez

Deserialization

Hey! I just found your serializer and I think it's amazing. I've not yet given it a try, but it covers maaaany of my needs.

I previously built something fairly similar in PHP and people always asked me about deserialization. I said the goal of my package was to output data not injest it, so people should make another tool for that, but I came to realize I was being short sighted there.

Tools such as Roar offer deserialization, but their support for various media formats is a little lacking, and if the ability to switch adapters at runtime exists, it's not documented. There are also subtle differences in the DSL AFAIK.

Could you foresee this adapter offering deserialization?

HAL support for multiple link objects per relationship?

I am trying to capture multiple item link objects in a HAL representation. Something like:

{
  "person": {
    "name": "Oaty",
    "_links": {
      "item": [
        {
          "type": "address",
          "href": "/people/1/address/1"
        },
        {
          "type": "address",
          "href": "/people/1/address/2"
        }
      ]
    }
  }
}

The HAL adapter seems to need an :href key on the link hash (e.g. opts[:href]) to set the link object for the relationship (lib/oat/adapters/hal.rb:6).

Is there another way to set an array of link objects for a single relationship? HAL supports this (spec reference).

Remove activesupport dependency

It would be great if the active support dependency could be removed for use with either older versions of rails or non rails projects.

Top level json api meta

The json api spec allows for a meta hash to exists at the top level to hold data such as total entries in a collection, current page, etc.

Should I just add a new DSL method meta :key, "value"? What are your thoughts?

JSONAPI linked entity names.

Hi -- finally getting around to preparing tested version of the modifications I made to my fork a couple months ago, and looking for how-to-handle advice.

JSONAPI specifies that:

Related documents MUST be included in a top level "linked" object, in which they are grouped together in arrays according to their type.

The type of each relationship MAY be specified in the "links" object with the "type" key. This facilitates lookups of related documents by the client.

The oat mechanism for linked entities (entity or entities) currently collects linked entities under the relationship name, rather than the type of the linked entity. (in some examples from the JSONAPI spec, under author rather than people).

A change to use types instead of link names would be a breaking change to the current behavior of the entity/entities serializer rules. Should I handle these by:

  • fully replacing the functionality (cause the break) and modifying the tests to match,
  • provide a global flag to use new/old behavior (default which way?)
  • provide a per-invocation flag to use one behavior or the other (plus maybe a global to specify default?, which way to default if not specified?),
  • some other approach?

Thanks.

Oat, Rails Responders and hypermedia mime types

When doing the following in a Rails app (example)

# 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_json
end

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  respond_to :siren, :json

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

Works great if the http request is json using the Accepts: application/json header or the format=json query parameter. However, if I request with the Siren mime type (Accepts: application/vnd.siren+json or format=siren query parameter), it fails with a standard rails error saying it can't find a template for the siren format.

After digging into it, the root of the problem is that the serializer needs to implement a to_siren method. If I add a to_siren method to my serializer, then the responder block is called and all is well.

I'm wondering if anyone is having success with Rails and using respond_with SERIALIZER AND also using the format's specific mime-type without implementing a to_FORMAT method?

Maybe I'm using responders incorrectly? I have not found any documentation that really explains how to do something like this for an API response without a template. I managed to find something that works thanks to the README in roar-rails

I'm thinking Oat should provide support for this. I've added such in my fork, but I want to verify it's not a problem localized to my app's specific implementation before I submit a PR.

Override context on embedded serializers

We should be able to override the context that gets passed to a serializer that's embedded within another serializer. I'm thinking either we pass this after the class name, or we pass an options hash in as the last parameter with context key set to what our new overridden context should be.

My main goal is to be able to reuse serializers that should include certain things if they're embedded, and other things if they aren't. The problem is that there's no way of passing that information on to an embedded serializer. I though using @top would maybe let that happen, but that has proven unreliable.

On a related note, I feel like context should be a hash by default, and should always be assumed to be a hash. It ultimately provides a more flexible library and lets users use context in a reliably consistent way, rather than possibly guessing what kind of context you're working with.

Any thoughts on this?

Caching with Oat

A teammate and I have been working on adding caching into a HAL API built on Rails and Oat. We are dealing with large sets of data in our app, and are hoping to cache as much as we can. Ideally we could use Russian Doll Caching. We have pretty much figured out how we want to do this, but for a variety of reasons, it won't work. We currently have full-collection caching figured out. I'm hoping to address a few of the roadblocks we've come across here, and start a discussion about how to solve them.

We used Advanced Caching: Part 6 - Fast JSON APIs as a guide to get things started, and ended up with something like this:

# config/initializers/activerecord/base.rb
module ActiveRecord
  class Base
    def self.cache_key
      Digest::MD5.hexdigest "#{all.order(:id).pluck(:id).join('-')}-#{all.maximum(:updated_at).try(:to_i)}-#{all.count}"
    end
  end
end

# app/serializers/application_serializer.rb
class ApplicationSerializer < Oat::Serializer
  delegate :cache_key, to: :item

  # Cache entire JSON string
  def to_json(*args)
    Rails.cache.fetch expand_cache_key(self.class.to_s.underscore, cache_key, 'to-json') do
      super
    end
  end

  # Cache individual Hash objects before serialization
  # This also makes them available to associated serializers
  def to_hash
    Rails.cache.fetch expand_cache_key(self.class.to_s.underscore, cache_key, 'to-hash') do
      super
    end
  end

  private
  def expand_cache_key(*args)
    ActiveSupport::Cache.expand_cache_key args
  end
end

If we leave out #to_hash, we get caching of the full object/collection, no problem. However, RDC doesn't work, because we only cache the full JSON-serialized string.

Problems occur when we define #to_hash. Particularly, Marshal.dump cannot work with a Hash using default_proc, which is currently how we are initializing our @data object on Oat::Serializer. This raises a few questions:

  1. Should the base serializer really be in charge of defining the default value of an empty key? maybe
  2. Should Adapters be in charge of figuring out what keys should exist by default and what their default values should be? absolutely

dc4abe in #33 addresses this problem (though not with Adapters::JsonAPI).

Doing this at least lets us use #to_hash and will cache the full object and sub-objects (as hashes) without raising an error. The keys for each sub-object show up within the cache store.

However, RDC still does not work, because, as we quickly realized, all Oat does, in the end, is build a giant hash that gets fed to to_json. There is currently no way of caching only parts of this hash.

3ac2cb4 in #33 is my first attempt at trying to let us embed objects into our hash that can be converted to JSON. We have yet to make this work, though I have a few tricks I'm going to attempt tomorrow. It would be great if calling to_json could be done at a more granular level within a serializer, which, I believe, would give us the ability to fully implement RDC.

Going through all this, I've also realized that we should probably end up testing that calling to_json on the serializer gives us exactly what we expect. We should also probably split up each test to use isolated expectations so we can more easily see what's going wrong on a single test run.

This is a pretty big post, but I'm hoping we can seriously start to address the idea of granular caching, or at least make it easier to do so. Let me know if you have any input.

The Siren adapter renders multiple rels per link as an array of an array instead of a flat array

Although it doesn't seem to be documented, it's my understanding that Siren supports multiple rels for each link.

The following serializer:

link ['describedby', 'http://rels.foo.bar.com/type'], :href => "http://foo.bar.com/meta/user"

Ought to generate the following Siren-compliant response with a flat array of rels:

{
  "rel": [
    "describedby",
    "http://rels.foo.bar.com/type"
  ],
  "href": "http://foo.bar.com/meta/user"
}

Instead the Oat Siren adapter generates an array of an array of rels:

{
  "rel": [[
    "describedby",
    "http://rels.foo.bar.com/type"
  ]],
  "href": "http://foo.bar.com/meta/user"
}

I'm working on a pull request with a tested fix.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.