trailblazer / roar-rails

Use Roar's representers in Rails.
http://roar.apotomo.de
MIT License
235 stars 65 forks source link

roar-rails

Makes using Roar's representers in your Rails app fun.

Build Status Gem Version

roar-rails development will be discontinued in the future and we will encourage users to begin migrating to Trailblazer (and trailblazer-rails).


Roar is a framework for parsing and rendering REST documents. For a better overview about representers please check the roar repository.

Roar-rails gives you conventions and convenient access to a lot of Roar's functionality within your Rails app.

Features

This gem works with all Rails >= 3.x.

Prerequisites

Add it to your app's Gemfile.

gem "roar-rails"

Note: For Rails >= 4.2, you need to add the responders gem, too, if you use respond_with. This has to be before the roar-rails entry in the Gemfile.

gem "responders"
gem "roar-rails"

Generators

The generator will create the representer modules in app/representers for you.

Here's an example.

rails g representer Band id name

This will create the file app/representers/band_representer.rb with the following content,

  module BandRepresenter
    include Roar::JSON

    property :id
    property :name
  end

You can change the format (e.g. XML), and pass arbitrary options to customize the generated representer. For all available options, just run

rails g representer

Rendering with #respond_with

roar-rails provides a number of baked-in rendering methods.

Conventional Rendering

Easily render resources using representers with the built-in responder.

class SingersController < ApplicationController
  include Roar::Rails::ControllerAdditions
  respond_to :json

  def show
    singer = Singer.find_by_id(params[:id])
    respond_with singer
  end
end

The representer name will be infered from the passed model class (e.g. a Singer instance gets the SingerRepresenter). If the passed model is a collection it will be extended using a representer. The representer name will be computed from the controller name (e.g. a SingersController uses the SingersRepresenter).

Need to use a representer with a different name than your model? You may always pass it in using the :represent_with option:

respond_with singers, :represent_with => MusicianCollectionRepresenter
end

Represents Configuration

If you don't want to use conventions or pass representers you can configure them on the class level using ::represents. This will also call respond_to for you.

class SingersController < ApplicationController
  represents :json, Musician

This will use the MusicianRepresenter for models and MusiciansRepresenter for representing collections.

Note that ::represents also allows fine-tuning.

class SingersController < ApplicationController
  represents :json, :entity => MusicianRepresenter, :collection => MusicianCollectionRepresenter

You might pass strings as representer names to ::represents, they will be constantized at run-time when needed.

Rendering with #render

In place of #respond_with, you can also use #render to serialize objects using representers.

class SingersController < ApplicationController
  include Roar::Rails::ControllerAdditions
  include Roar::Rails::ControllerAdditions::Render

  def show
    singer = Singer.find_by_id(params[:id])
    render json: singer
  end
end

Old API Support

If you don't want to write a dedicated representer for a collection of items (highly recommended, thou) but rather use a representer for each item, use the :represent_items_with option.

class SingersController < ApplicationController

  def index
    singers = Musician.find(:all)
    respond_with singers, :represent_items_with => SingerRepresenter
  end
end

Parsing incoming documents

In #create and #update actions it is often necessary to parse the incoming representation and map it to a model instance. Use the #consume! method for this. The client must provide a Content-Type request header with proper MIME type to let #consume! know which representer to use.

class SingersController < ApplicationController
  respond_to :json

  def create
    singer = Singer.new
    consume!(singer)

    respond_with singer
  end
end

For instance, if content type is set to application/xml the consume! call will roughly do the following.

singer.
  extend(SingerRepresenter)
  from_xml(request.body)

So, #consume! helps you figuring out the representer module and reading the incoming document. Just like Rails, depending on the registered MIME type for Content-type it picks the deserialize method (e.g. from_json vs. from_xml)

It is important to provide a known content type in the request. If it is missing or not supported by the responder #consume! will raise an exception Roar::Rails::ControllerAdditions::UnsupportedMediaType. Unless you rescue the exception the action will stop and respond with HTTP status 406 Unsupported Media Type.

Note that #consume! respects settings from #represents. It uses the same mechanics known from #respond_with to choose a representer.

consume!(singer, :represent_with => MusicianRepresenter)

Using Decorators

If you prefer roar's decorator approach over extend, just go for it. roar-rails will figure out automatically which represent strategy to use. Be sure to use roar >= 0.11.17.

class SingerRepresenter < Roar::Decorator
  include Roar::JSON
  include Roar::Hypermedia

  property :name

  link :self do
    singer_url(represented)
  end
end

In decorators' link blocks you currently have to use represented to get the actual represented model (this is self in module representers).

Passing Options

Both rendering and consuming support passing user options to the representer.

With #respond_with, any additional options will be passed to to_json (or whatever format you're using).

respond_with @singer, :current_user => current_user

Same goes with #consume!, passing options to from_json.

consume! Singer.new, :current_user => current_user

Note: If you pass in options to a representer, you must process them youself. For rendering, use :getter in the representer.

property :username, getter: lambda { |args| args[:current_user].name }

That'll render the current_user's name as the username property.

More docs about passing and processing option can be found here.

URL Helpers

Any URL helpers from the Rails app are automatically available in representers.

module FruitRepresenter
  include Roar::JSON
  include Roar::Hypermedia

  link :self do
    fruit_url self
  end
end

To get the hyperlinks up and running, please make sure to set the right host name in your environment files (config/environments):

config.representer.default_url_options = {:host => "127.0.0.1:3000"}

Attention: If you are using representers from a gem your Rails URL helpers might not work in these modules. This is due to a loading order problem in Rails. As a workaround, don't require the representers in the gem but load them as late as possible, usually it works when you require in the controller. We are working on fixing that problem.

Representing Formats Exclusively

By default, roar-rails will extend/decorate any model passed to respond_with for any request format. When adding roar-rails to a legacy project, you might want to restrict roar-rails' representing and fall back to the old behavior for certain formats. This can be configured both globally and on a per action basis.

To restrict representing globally to a particular format you can set the config.representer.represented_formats in your environment's configuration to an array of formats. For example the following will only represent hal and json requests.

config.representer.represented_formats = [:hal, :json]

The global configuration (or lack thereof) can be overridden by supplying the :represented_formats array when calling respond_with. The following will only represent @resource for the hal format in the show action. For any other format, it will expose the resource using Rails' old behavior.

class MyController < ApplicationController
  def show
    ...
    respond_with @resource, :represented_formats => [:hal]
  end
end

You can entirely suppress roar-rails in respond_with by passing in an empty array.

class MyController < ApplicationController
  def show
    ...
    respond_with @resource, :represented_formats => []
  end
end

Testing

Autoloading

Put your representers in app/representers and they will be autoloaded by Rails. Also, frequently used modules as media representers and features don't need to be required manually.

JSON-API

In a JSON-API environment, only one representer is written for both singular and collection resources. However, you have to configure represents accordingly so it knows what representer to use for collections.

class SongsController < ApplicationController
  represents :json_api, entity: SongRepresenter, collection: SongRepresenter.for_collection

Rails 4.1+ and HAL/JSON-API

Note: this is a temporary work-around, we're trying to fix that in Rails/roar-rails itself [May 2014].

Rails 4.1 and up expects you to manually register a global HAL or JSON-API renderer, or respond_with will throw an ActionController::MissingRenderer exception.

One fix is to add this to config/initializers/mime_types.rb right below Mime::Type.register 'application/hal+json', :hal:

ActionController::Renderers.add :hal do |obj, options|
  self.content_type ||= Mime[:hal]
  obj
end

Similarly, for JSON-API, below Mime::Type.register 'application/vnd.api+json', :json_api add:

ActionController::Renderers.add :json_api do |obj, options|
  self.content_type ||= Mime[:json_api]
  obj
end

Contributors

License

Roar-rails is released under the MIT License.