Pragma::Decorator
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:
$ bundleOr install it yourself as:
$ gem install pragma-decoratorUsage
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
endJust 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_jsonThis 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
endThis 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
endArray 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
endThis 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
endRendering 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
endCollection
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
endYou can now do this:
API::V1::Invoice::Decorator::Collection.new(Invoice.all).to_jsonWhich 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
endNow, you can run this code:
API::V1::Invoice::Decorator::Collection.new(Invoice.all).to_jsonWhich 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
endContributing
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.