Created
May 14, 2014 18:08
-
-
Save NullVoxPopuli/32941b91c90f1226e828 to your computer and use it in GitHub Desktop.
A Dynamic Presenter Pattern with Support for API Versioning
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
github doesn't allow
/
in gist files' names, so I used-
instead. All of these files are inapp/presenters