Powerful and elegant model presenters with a twist.
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
bin
lib/pragma
spec
.gitignore
.rspec
.rubocop.yml
.travis.yml
CHANGELOG.md
Gemfile
LICENSE.txt
README.md
Rakefile
pragma-decorator.gemspec

README.md

Pragma::Decorator

Build Status Coverage Status Maintainability

Decorators are a way to easily convert your API resources to JSON with minimum hassle.

They are built on top of ROAR. We provide some useful helpers for rendering collections, expanding associations and much more.

Installation

Add this line to your application's Gemfile:

gem 'pragma-decorator'

And then execute:

$ bundle

Or install it yourself as:

$ gem install pragma-decorator

Usage

Creating a decorator is as simple as inheriting from Pragma::Decorator::Base:

module API
  module V1
    module User
      module Decorator
        class Instance < Pragma::Decorator::Base
          property :id
          property :email
          property :full_name
        end
      end
    end
  end
end

Just instantiate the decorator by passing it an object to decorate, then call #to_hash or #to_json:

decorator = API::V1::User::Decorator::Instance.new(user)
decorator.to_json

This will produce the following JSON:

{
  "id": 1,
  "email": "jdoe@example.com",
  "full_name": "John Doe"
}

Since Pragma::Decorator is built on top of ROAR (which, in turn, is built on top of Representable), you should consult their documentation for the basic usage of decorators; the rest of this section only covers the features provided specifically by Pragma::Decorator.

Object Types

It is recommended that decorators expose the type of the decorated object. You can achieve this with the Type mixin:

module API
  module V1
    module User
      module Decorator
        class Instance < Pragma::Decorator::Base
          include Pragma::Decorator::Type
        end
      end
    end
  end
end

This would result in the following representation:

{
  "type": "user",
  "...": "..."
}

You can also set a custom type name (just make sure to use it consistently!):

module API
  module V1
    module User
      module Decorator
        class Instance < Pragma::Decorator::Base
          def type
            :custom_type
          end
        end
      end
    end
  end
end

Array and ActiveRecord::Relation are already overridden as list to avoid exposing internal details. If you want to specify your own global overrides, you can do it by adding entries to the Pragma::Decorator::Type.overrides hash:

Pragma::Decorator::Type.overrides['Article'] = 'post'

Timestamps

UNIX time is your safest bet when rendering/parsing timestamps in your API, as it doesn't require a timezone indicator (the timezone is always UTC).

You can use the Timestamp mixin for converting Time instances to UNIX times:

module API
  module V1
    module User
      module Decorator
        class Instance < Pragma::Decorator::Base
          include Pragma::Decorator::Timestamp

          timestamp :created_at
        end
      end
    end
  end
end

This will render a user like this:

{
  "type": "user",
  "created_at": 1480287994
}

The #timestamp method supports all the options supported by #property.

Associations

Pragma::Decorator::Association allows you to define associations in your decorator (currently, only belongs_to/has_one associations are supported):

module API
  module V1
    module Invoice
      module Decorator
        class Instance < Pragma::Decorator::Base
          include Pragma::Decorator::Association

          belongs_to :customer, decorator: API::V1::Customer::Decorator::Instance
        end
      end
    end
  end
end

Rendering an invoice will now create the following representation:

{
  "customer": 19
}

You can pass expand[]=customer as a request parameter and have the customer property expanded into a full object!

{
  "customer": {
    "id": 19,
    "...": "..."
  }
}

This also works for nested associations. For instance, if the customer decorator had a company association, you could pass expand[]=customer&expand[]=customer.company to get the company expanded too.

Note that you will have to pass the associations to expand as a user option when rendering:

decorator = API::V1::Invoice::Decorator::Instance.new(invoice)
decorator.to_json(user_options: {
  expand: ['customer', 'customer.company', 'customer.company.contact']
})

Needless to say, this is done automatically for you when you use all components together through the pragma gem! :)

Associations support all the options supported by #property. Additionally, decorator can be a callable object, which is useful for polymorphic associations:

module API
  module V1
    module Discount
      module Decorator
        class Instance < Pragma::Decorator::Base
          include Pragma::Decorator::Association

          belongs_to :discountable, decorator: -> (discountable) {
            "API::V1::#{discountable.class}::Decorator::Instance".constantize
          }
        end
      end
    end
  end
end

Collection

Pragma::Decorator::Collection wraps collections in a data property so that you can include metadata about them:

module API
  module V1
    module Invoice
      module Decorator
        class Collection < Pragma::Decorator::Base
          include Pragma::Decorator::Collection
          decorate_with Instance # this is optional, the default is 'Instance'

          property :total_cents, exec_context: :decorator

          def total_cents
            represented.sum(:total_cents)
          end
        end
      end
    end
  end
end

You can now do this:

API::V1::Invoice::Decorator::Collection.new(Invoice.all).to_json

Which will produce the following JSON:

{
  "data": [{
    "id": 1,
    "total_cents": 1500,
  }, {
    "id": 2,
    "total_cents": 3000,
  }],
  "total_cents": 4500
}

This is very useful, for instance, when you have a paginated collection, but want to include data about the entire collection (not just the current page) in the response.

Pagination

Speaking of pagination, you can use Pragma::Decorator::Pagination in combination with Collection to include pagination data in your response:

module API
  module V1
    module Invoice
      module Decorator
        class Collection < Pragma::Decorator::Base
          include Pragma::Decorator::Collection
          include Pragma::Decorator::Pagination

          decorate_with Instance
        end
      end
    end
  end
end

Now, you can run this code:

API::V1::Invoice::Decorator::Collection.new(Invoice.all).to_json

Which will produce the following JSON:

{
  "data": [{
    "id": 1,
    "...": "...",
  }, {
    "id": 2,
    "...": "...",
  }],
  "total_entries": 2,
  "per_page": 30,
  "total_pages": 1,
  "previous_page": null,
  "current_page": 1,
  "next_page": null
}

It works with both will_paginate and Kaminari!

Restricting property visibility

If you want to show or hide certain properties programmatically, you can do it with the if option:

module API
  module V1
    module User
      module Decorator
        class Instance < Pragma::Decorator::Base
          property :id
          property :first_name
          property :last_name
          property :email, if: -> (user_options:, decorated:, **) {
            # Only show the email to admins or to the same user.
            user_options[:current_user].admin? || user_options[:current_user] == decorated
          }
        end
      end
    end
  end
end

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/pragmarb/pragma-decorator.

License

The gem is available as open source under the terms of the MIT License.