Skip to content

About

Use Roar with Grape.

Resources

Contributing

Stars

59 stars

Watchers

6 watching

Forks

Repository files navigation

Grape::Roar

Gem Version Coverage Status test

Use Roar with Grape.

Table of Contents

Demo

See grape-with-roar.

Installation

CI covers Grape 1.x through 4.x on compatible Ruby versions, including Ruby 4.0. Grape 1.x requires Rack 2.x; newer Grape versions are tested with Rack 3.x. See CONTRIBUTING for the core and ORM compatibility matrices and local test commands.

Add the grape, roar and grape-roar gems to Gemfile.

gem 'grape'
gem 'roar'
gem 'grape-roar'

If you're upgrading from an older version of this gem, please see UPGRADING.

Usage

Tell your API to use Grape::Formatter::Roar

class API < Grape::API
  format :json
  formatter :json, Grape::Formatter::Roar
end

Use Grape's Present

Include Grape::Roar::Representer into a representer module after any Roar mixins, then use Grape's present keyword.

module ProductRepresenter
  include Roar::JSON
  include Roar::Hypermedia
  include Grape::Roar::Representer

  property :title
  property :id
end
get 'product/:id' do
  present Product.find(params[:id]), with: ProductRepresenter
end

Presenting collections works the same way. The following example returns an embedded set of products in the HAL Hypermedia format.

module ProductsRepresenter
  include Roar::JSON::HAL
  include Roar::Hypermedia
  include Grape::Roar::Representer

  collection :entries, extend: ProductRepresenter, as: :products, embedded: true
end
get 'products' do
  present Product.all, with: ProductsRepresenter
end

Dynamic Rendering Options

To supply Roar rendering options, such as wrap, or request-specific callback data, render a native Hash with to_hash and return it from the endpoint. Put callback data in user_options; include env there if any links or properties use the request:

get 'product/:id' do
  product = Product.find(params[:id])
  ProductRepresenter.represent(product).to_hash(
    wrap: :product,
    user_options: { env: env, student: current_student }
  )
end

This returns a JSON object wrapped under "product", not a JSON-encoded string. It works with both representer modules and decorators. Hypermedia callbacks receive your user options directly; for example, link(:student) { |opts| "/students/#{opts.fetch(:student).id}" } can use the current student.

present product, with: ProductRepresenter does not forward arbitrary options to Roar's later rendering step. Use the explicit to_hash call above instead, and do not return to_json output through the formatter: the formatter would encode that string again. Keep options local to each rendering call rather than modifying shared representer definitions.

Native Array Representations

To render a plain array using a single-item representer, use Roar's native collection factory explicitly. This works with both representer modules and decorators:

get 'products' do
  ProductRepresenter.for_collection.prepare(Product.all.to_a).to_hash(user_options: { env: env })
end

The response is an array of individually represented products. Empty collections return [], and callbacks receive the request environment. Return the native Array from to_hash, not a JSON string, so the formatter encodes it exactly once.

present products, with: ProductsRepresenter still treats ProductsRepresenter as the representation of the whole collection. This preserves existing collection properties, envelopes, and HAL _embedded responses. Automatically treating every Array as a collection of individually represented resources would change those responses; use the explicit factory above when that is what you want.

Representer Lookup

Grape can infer a representer from a model's Entity constant. Set it to a representer module or decorator, then call present without with::

class Product
  Entity = ProductRepresenter
end

get 'product/:id' do
  present Product.find(params[:id])
end

For decorator classes, an API-local mapping avoids adding a constant to the model:

class API < Grape::API
  format :json
  formatter :json, Grape::Formatter::Roar

  represent Product, with: ProductRepresenter

  get 'product/:id' do
    present Product.find(params[:id])
  end
end

An explicit present object, with: OtherRepresenter overrides inference. These are Grape's existing lookup mechanisms; grape-roar does not guess global constants from model names, choose between singular and plural representers, or automatically represent arbitrary endpoint return values. Call present to request inference.

Hash-backed Representers

A plain property :some_property reads object.some_property; it does not read object[:some_property]. When presenting a Hash, specify a getter instead of relying on implicit conversion to an object with attribute methods:

module HashRepresenter
  include Roar::JSON
  include Grape::Roar::Representer

  property :some_property, getter: ->(*) { fetch(:some_property) }
end
get 'hash' do
  present({ some_property: 'From a hash', internal: 'Not exposed' }, with: HashRepresenter)
end

The response is {"some_property":"From a hash"}. Only declared properties are serialized. For string-keyed hashes, such as those returned by JSON.parse, use fetch('some_property') in the getter instead. Hash key types are not converted automatically, and fetch raises KeyError if the configured key is absent.

The same getter works in a class inheriting from Grape::Roar::Decorator with Roar::JSON included. Getters execute against the represented Hash by default; a decorator wraps it rather than adding representer methods to the Hash itself.

Accessing the Request Inside a Representer

The formatter invokes to_json on presented objects and provides access to the requesting environment via the env option. The following example renders a full request URL in a representer.

module ProductRepresenter
  include Roar::JSON
  include Roar::Hypermedia
  include Grape::Roar::Representer

  link :self do |opts|
    request = Grape::Request.new(opts[:env])
    "#{request.url}"
  end
end

Decorators

If you prefer to use a decorator class instead of modules.

class ProductRepresenter < Grape::Roar::Decorator
  include Roar::JSON
  include Roar::Hypermedia

  link :self do |opts|
    "#{request(opts).url}/#{represented.id}"
  end

  private

  def request(opts)
    Grape::Request.new(opts[:env])
  end
end
get 'products' do
  present Product.all, with: ProductsRepresenter
end

Presenter Parameter Types

Grape supports type: Presenter when the class implements its custom-type .parse contract. Define this on an application-specific input decorator: validate the input, parse only declared writable properties, and return a decorator instance so Grape's type check succeeds.

class ProductInputRepresenter < Grape::Roar::Decorator
  include Roar::JSON

  property :title

  def self.parse(value)
    raise ArgumentError, 'Expected a product object' unless value.is_a?(::Hash)

    attributes = value.transform_keys(&:to_s)
    title = attributes['title']
    raise ArgumentError, 'Expected a nonblank title' unless title.is_a?(::String) && !title.strip.empty?

    decorator = new(Product.new)
    decorator.from_hash(attributes)
    decorator
  end
end

class ProductsAPI < Grape::API
  format :json
  formatter :json, Grape::Formatter::Roar

  params do
    requires :product, type: ProductInputRepresenter
  end
  post 'products' do
    params[:product]
  end
end

Send a JSON body such as {"product":{"title":"Lonestar"}} with Content-Type: application/json. Grape converts parse failures into HTTP 400 validation errors. The response is {"title":"Lonestar"}; undeclared properties are not assigned to the model. The coerced parameter is the decorator, and params[:product].represented gives the model for application-specific processing or persistence.

Roar properties and Swagger response metadata do not automatically define required fields, validation, or a model constructor. Keep input validation explicit, use a separate input representer when writable fields differ from output fields, and do not add a generic parser that guesses which model to construct. from_hash returns the represented model, so return the decorator explicitly from .parse when using the decorator as the parameter type.

Swagger Documentation

The optional grape-swagger-representable parser documents Roar decorator classes. Add it and grape-swagger to your application's Gemfile; neither is required for rendering:

gem 'grape-swagger'
gem 'grape-swagger-representable'

Require the parser before calling add_swagger_documentation. Declare response field metadata under each property's documentation: option:

require 'grape-swagger-representable'

class ProductRepresenter < Grape::Roar::Decorator
  include Roar::JSON

  property :id, documentation: { type: Integer, desc: 'Product ID' }
  property :title, documentation: { type: String, desc: 'Product title' }
end

class ProductsAPI < Grape::API
  format :json
  formatter :json, Grape::Formatter::Roar

  desc 'Get a product', success: ProductRepresenter
  get 'product/:id' do
    present Product.find(params[:id]), with: ProductRepresenter
  end

  add_swagger_documentation
end

/swagger_doc/product describes an integer id and string title, including their descriptions, while /product/:id renders the same properties with Roar. The parser registers decorator classes, not representer modules; use a decorator for a documented response model. Response documentation does not validate incoming parameters, and custom links, getters, and envelopes still need schemas that match their actual output.

CRUD Endpoints

Keep persistence application-local: use a dedicated input representer for writable fields, validate parameters with Grape, and call your model's persistence methods explicitly. For example, with an ActiveRecord-style model exposing find, save!, and destroy!:

class ProductInputRepresenter < Grape::Roar::Decorator
  include Roar::JSON

  property :title
end

class ProductOutputRepresenter < Grape::Roar::Decorator
  include Roar::JSON

  property :id
  property :title
end

class ProductsAPI < Grape::API
  format :json
  formatter :json, Grape::Formatter::Roar

  helpers do
    def parse_product(product)
      attributes = declared(params, include_missing: false).fetch(:product).transform_keys(&:to_s)
      ProductInputRepresenter.new(product).from_hash(attributes)
    end
  end

  params do
    requires :product, type: Hash do
      requires :title, type: String, allow_blank: false
    end
  end
  post 'products' do
    product = parse_product(Product.new)
    product.save!
    present product, with: ProductOutputRepresenter
  end

  params do
    requires :id, type: Integer
    requires :product, type: Hash do
      optional :title, type: String, allow_blank: false
    end
  end
  patch 'products/:id' do
    product = parse_product(Product.find(params[:id]))
    product.save!
    present product, with: ProductOutputRepresenter
  end

  params do
    requires :id, type: Integer
  end
  delete 'products/:id' do
    product = Product.find(params[:id])
    product.destroy!
    present product, with: ProductOutputRepresenter
  end
end

Send create/update input as {"product":{"title":"Lonestar"}} with Content-Type: application/json. declared filters request fields and the input representer only assigns title; output fields such as id are not writable. include_missing: false preserves existing values on partial updates. from_hash returns the model, while present applies the output representer after a successful write.

Scope record lookups to the authorized user and apply your application's authorization policy before mutations. Adapt persistence calls to your ORM. Configure application-specific rescue_from handlers for expected not-found, validation, or failed-destruction exceptions, mapping them to appropriate HTTP errors. The example deliberately uses raising persistence methods and does not swallow failures or return success when a write fails. Roar does not infer persistence, authorization, or validation rules from output properties.

Nested Representers

Use property for a single nested object and collection for multiple objects, specifying the representer at each level with extend:. For example, notifications can contain comments, posts, and images:

class ImageRepresenter < Grape::Roar::Decorator
  include Roar::JSON

  property :id
  property :url
end

class PostRepresenter < Grape::Roar::Decorator
  include Roar::JSON

  property :id
  property :title
  property :image, extend: ImageRepresenter
end

class CommentRepresenter < Grape::Roar::Decorator
  include Roar::JSON

  property :id
  property :body
  property :post, extend: PostRepresenter
end

class NotificationRepresenter < Grape::Roar::Decorator
  include Roar::JSON

  property :id
  property :comment, extend: CommentRepresenter
end

class NotificationsRepresenter < Grape::Roar::Decorator
  include Roar::JSON

  collection :entries, as: :notifications, extend: NotificationRepresenter
end
get 'notifications' do
  present Notification.all, with: NotificationsRepresenter
end

The response has a top-level notifications array; each notification contains its comment, whose post contains an image. Each representer controls only its own object's attributes, so an image's id does not replace its post's id.

Decorators wrap objects instead of extending them with representer modules. This is useful when the same model instance is shared between notifications or rendered with different representations. Keep the nested objects separate and let Roar compose the response rather than merging child attributes into a parent's hash.

Caching Representations

present object, with: Representer returns a represented object, not a plain data snapshot. Module representers extend the object and attach serialization state, which can include Procs that a cache backend cannot serialize. Do not cache the represented model itself or change represent to return JSON globally.

Instead, render a plain Hash with Roar's to_hash before storing it. For example, using your application's cache store as CACHE:

get 'products/:id' do
  cache_key = ['product-representation', request.url]
  CACHE.fetch(cache_key) do
    product = Product.find(params[:id])
    ProductRepresenter.represent(product).to_hash(user_options: { env: env })
  end
end

Return the cached Hash directly; the formatter encodes it as a JSON object. Do not call present on it with the model representer again, or cache a to_json string and return that through the JSON formatter, which would encode it a second time.

Rendering with user_options: { env: env } preserves request-aware links when populating the cache. The example includes the full request URL in the key because its self link depends on that URL. Include every other input that affects the response, such as authenticated user, locale, or header-based API version, in your cache key and invalidate cached representations when their source data changes. Alternatively, cache raw data and build a fresh model to present outside the cache block so request-dependent rendering runs on each request.

Relation Extensions

If you use either ActiveRecord or Mongoid, you can use the Grape::Roar::Extensions::Relations DSL to expose the relationships in between your models as a HAL response. The DSL methods used are the same regardless of what your ORM/ODM is, as long as there exists an adapter for it.

Designing Representers

Arguments passed to #relation are forwarded to roar. Single member relations (e.g. belongs_to) are represented using #property, collections are represented using #collection; arguments provided to #relation will be passed through these methods (i.e. additional arguments roar and representable accept).

A default base URI is constructed from a Grape::Request by concatenating the #base_url and #script_name properties. The resource path is extracted from the name of the relation.

Otherwise, the extensions attempt to look up the correct representer module/class for the objects (e.g. we infer the extend argument). You can always specify the correct representer to use on your own.

Example Models
class Item < ActiveRecord::Base
  belongs_to :cart
end

class Cart < ActiveRecord::Base
  has_many :items  
end
Example Representers
class ItemEntity < Grape::Roar::Decorator
  include Roar::JSON
  include Roar::JSON::HAL
  include Roar::Hypermedia

  include Grape::Roar::Extensions::Relations

  # Cart will be presented under the _embedded key
  relation :belongs_to, :cart, embedded: true

  link_self
end

class CartEntity < Grape::Roar::Decorator
  include Roar::JSON
  include Roar::JSON::HAL
  include Roar::Hypermedia

  include Grape::Roar::Extensions::Relations

  # Items will be presented under the _links key
  relation :has_many, :items, embedded: false

  link_self
end

Although this example uses Grape::Roar::Decorator, you can also use a module as show in prior examples above. If doing so, you no longer have to mix in Grape::Roar::Representer.

Example Item
{
    "_embedded": {
        "cart": {
            "_links": {
                "self": {
                    "href": "http://example.org/carts/1"
                },
                "items": [{
                    "href": "http://example.org/items/1"
                }]
            }
        }
    },
    "_links": {
        "self": {
            "href": "http://example.org/items/1"
        }
    }
}
Example Cart
{
    "_links": {
        "self": {
            "href": "http://example.org/carts/1"
        },
        "items": [{
            "href": "http://example.org/items/1"
        }, {
            "href": "http://example.org/items/2"
        }, {
            "href": "http://example.org/items/3"
        }, {
            "href": "http://example.org/items/4"
        }, {
            "href": "http://example.org/items/5"
        }]
    }
}

Errors

Should you incorrectly describe a relationship (e.g. you specify has_one but your model specifies has_many), an exception will be raised to notify you of the mismatch:

Grape::Roar::Extensions::Relations::Exceptions::InvalidRelationError:
  Expected Mongoid::Association::Referenced::HasOne, got Mongoid::Association::Referenced::HasMany!

Change how URLs are presented

The opts hash below is the same one as shown in prior examples.

Override base URI mappings
class BarEntity < Grape::Roar::Decorator
  include Roar::JSON
  include Roar::JSON::HAL
  include Roar::Hypermedia

  include Grape::Roar::Extensions::Relations

  # This is our default implementation
  map_base_url do |opts|
    request = Grape::Request.new(opts[:env])
    "#{request.base_url}#{request.script_name}"
  end

  relation :has_many, :bars, embedded: false
end
Override resource URI mappings
class BarEntity < Grape::Roar::Decorator
  include Roar::JSON
  include Roar::JSON::HAL
  include Roar::Hypermedia

  include Grape::Roar::Extensions::Relations

  # This is our default implementation
  map_resource_path do |_opts, object, relation_name|
    "#{relation_name}/#{object.id}"
  end

  relation :has_many, :bars, embedded: false
end

Designing Adapters

If you have custom domain objects, you can create an adapter to make your models compatible with the DSL methods. Below is an example of the ActiveRecord adapter.

Example: ActiveRecord Adapter
module Extensions
  module RelationalModels
    module Adapter
      class ActiveRecord < Base
        include Validations::ActiveRecord

        # We map your domain object to the correct adapter
        # at runtime.
        valid_for { |klass| klass < ::ActiveRecord::Base }

        def collection_methods
          @collection_methods ||= %i(has_many has_and_belongs_to_many)
        end

        def name_for_represented(represented)
          klass_name = case represented
                       when ::ActiveRecord::Relation
                         represented.klass.name
                       else
                         represented.class.name
                       end
          klass_name.demodulize.pluralize.downcase
        end

        def single_entity_methods
          @single_entity_methods ||= %i(has_one belongs_to)
        end
      end
    end
  end
end
Validations

Errors are handled by creating methods corresponding to those in collection_methods and single_entity_methods. For example, this is the validator for belongs_to:

module ActiveRecord
  include Validations::Misc

  def belongs_to_valid?(relation)
    relation = klass.reflections[relation]

    return true if relation.is_a?(
      ::ActiveRecord::Reflection::BelongsToReflection
    )

    # Provided by Validations::Misc
    invalid_relation(
      ::ActiveRecord::Reflection::BelongsToReflection,
      relation.class
    )
  end
end

After writing your validation methods, just mix them into your adapter. You can choose to not write validation methods; they are only invoked if your adapter responds to them.

Contributing

See CONTRIBUTING.

Copyright and License

MIT License, see LICENSE for details.

(c) 2012-2025 Daniel Doubrovkine & Contributors, Artsy

About

Use Roar with Grape.

Resources

Contributing

Stars

59 stars

Watchers

6 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages