Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save NullVoxPopuli/32941b91c90f1226e828 to your computer and use it in GitHub Desktop.
Save NullVoxPopuli/32941b91c90f1226e828 to your computer and use it in GitHub Desktop.
A Dynamic Presenter Pattern with Support for API Versioning
# helper methods for using presenters for rendering objects using for
# API requests
#
#
# usage:
# in application_controller,
# include Presntable
#
# in your controller's action methods:
# format.json{ render json: json_for(ar_object_or_collection) }
#
# the options hash, for the second parameter of json_for, is just
# passed through to as_json, so you can use include, except, add attributes, etc
#
module Presentable
protected
# @param [Class] klass the class to get the presenter name for
def presenter_for(klass)
# figure out the API version
current_version = AppConfig["current_api_version"]
requested_api_version = request.fullpath.scan( /\/api\/v(\d)\//).flatten.first
api_version = ( requested_api_version or current_version )
# presenters aren't namespaced
name = klass.name.gsub("::", "")
presenterClass = "Api::V#{api_version}::#{name}Presenter".constantize
return presenterClass
end
# takes either a single object or multiple objects as an
# array / association
def json_for(object, options = {})
klass = object.respond_to?(:length) ? Array : object.class
presenter = presenter_for(klass).new(object)
if options[:render]
render json: presenter.as_json(options)
else
presenter.as_json(options)
end
end
end
# default way to render a collection
module Api
class ArrayPresenter < Presenter
def as_json( options = {} )
result = []
presenter_prefix = self.class.name.deconstantize
@resource.each { | element |
presenter_suffix = element.class.name.gsub("::", "")
presenter_class_name = "#{presenter_prefix}::#{presenter_suffix}Presenter"
presenter = presenter_class_name.constantize.new( element )
result << presenter.as_json( include_root: true )
}
return result
end
end
end
module Api
class Presenter
attr_reader :resource
# if we always want to include root
@@root = false
def self.root?; @@root; end
def self.root=(r); @@root = r; end
def initialize( resource )
@resource = resource
end
def set_root_of( data, options )
root = ( options[:root_name] or self.class::ROOT_NAME )
return { root => data}
end
def as_json(options = {})
# this is never shown to the api caller
options[:except] ||= []
options[:except] << :account_id
# get raw attributes initially
data_hash = @resource.as_json(options.merge(root: false))
data_hash.merge!(options[:attributes]) if options[:attributes]
# add root in, if requested
data_hash = set_root_of(data_hash, options) if options[:include_root] or self.class.root?
return data_hash
end
end
end
# an example older presenter. This had plans to become a "DefaultPresenter"
# sort of like how there is a default serializer, but I didn't get around to it
# we are already forcing ourselves in to the serializers...
module Api
module V1
class ContentPresenter < Presenter
ROOT_NAME = :content
root = true
end
end
end
# an alternative way to render a collection
# overrides app/presenters/api/array_presenter.rb
module Api
module V3
class ArrayPresenter < Api::ArrayPresenter
def as_json( include_root: true )
collection = []
presenter_prefix = self.class.name.deconstantize
@resource.each { | element |
presenter_suffix = element.class.name.gsub("::", "")
presenter_class_name = "#{presenter_prefix}::#{presenter_suffix}Presenter"
presenter = presenter_class_name.constantize.new( element )
collection << presenter.as_json(include_root: false)
}
if include_root
data_hash = {}
resource_name = ""
resource_name = @resource.class_name unless @resource.is_a?(Array)
resource_name = @resource.first.class.name if @resource.is_a?(Array) && @resource.length > 0
root_name = resource_name.underscore.gsub("/", "_").pluralize
data_hash[root_name] = collection
return data_hash
end
return collection
end
end
end
end
# a way to include additional attributes
module Api
module V3
class ContentPresenter < Presenter
ROOT_NAME = :content
def as_json( include_root: true )
snippet = @resource.body || ""
if @resource.body.present? && @resource.body.length > 100
snippet = @resource.body[0..100]
end
additional_attributes = {
author: @resource.user.name,
snippet: snippet
}
return super(
include_root: include_root,
attributes: additional_attributes
)
end
end
end
end
# a way to exclude attributes and include additional attributes
module Api
module V3
class UserPresenter < Presenter
ROOT_NAME = :user
def as_json( include_root: true )
return super(
include_root: include_root,
attributes: {
avatar_url: @resource.avatar.url,
avatar_head_url: @resource.avatar.url(:normal),
email_template: @resource.proposal_email_template
},
except: [
:salt,
:admin,
:notifications,
:role,
:security_question_answer,
:security_question_id,
:crypted_password, #deprecated
:remember_token_expires_at,
:password_changed_at,
:login, #deprecated
:proposal_email_template
]
)
end
end
end
end
@NullVoxPopuli
Copy link
Author

github doesn't allow / in gist files' names, so I used - instead. All of these files are in app/presenters

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment