Skip to content

Instantly share code, notes, and snippets.

@jsmestad
Last active December 31, 2015 18:49
Show Gist options
  • Save jsmestad/8029732 to your computer and use it in GitHub Desktop.
Save jsmestad/8029732 to your computer and use it in GitHub Desktop.
module Tradesman
module Controller
class ApiResponder < ActionController::Responder
SerializerWrapper = Struct.new(:serializer, :object) do
def serializable_hash(options={})
instance = serializer.new(object, options)
if instance.respond_to?(:serializable_hash)
instance.serializable_hash
else
instance.as_json options
end
end
end
# http://api.rubyonrails.org/classes/ActionController/Responder.html
#
# def respond(*)
# responds(resource, options)
# # if paginated?
# # controller.headers['X-Records'] = resource.total_entries.to_s
# # controller.headers['X-Pages'] = resource.total_pages.to_s
# # controller.headers['X-Per-Page'] = resource.per_page.to_s
# # end
# # super
# end
def to_json#(object, options={})
# binding.pry
#
# if resource.respond_to?(:to_ary)
# normalise_object(resource, options)
# else
# end
render json: render_json(normalise_object(resource, options), options)#, options
end
# Given a json object or encoded json, will encode it
# and set it to be the output of the given page.
def render_json(json, options={})
# Setup data from options
# self.status = options[:status] if options[:status]
# self.content_type = options[:content_type] if options[:content_type]
options = options.slice(*RENDERING_OPTIONS)
# Don't convert raw strings to JSON.
json = encode_to_json(json) unless json.respond_to?(:to_str)
# Encode the object to json.
# self.status ||= :ok
# self.content_type ||= "application/vnd.artemis.v1+json" #Mime::JSON
# self.response_body = json
# headers['Content-Length'] = Rack::Utils.bytesize(json).to_s
end
# def paginated?
# resource.respond_to?(:total_entries) &&
# resource.respond_to?(:total_pages) &&
# resource.respond_to?(:per_page)
# end
#
def self.version
controller.api_version
end
def self.collection?(object)
object.is_a?(Array) || object.respond_to?(:to_ary)
end
def self.normalise_object(object, options = {})
# First, prepare the object for serialization.
object = normalise_to_serializer object, options
# Convert the object using a standard grape-like lookup chain.
if object.is_a?(Array) || object.is_a?(Set)
object.map { |o| normalise_object(o, options) }
elsif object.respond_to?(:serializable_hash)
object.serializable_hash options
elsif object.respond_to?(:as_json)
object.as_json options
else
object
end
end
def self.normalise_to_serializer(object, options)
serializer = options.delete(:serializer) || serializer_name(object, options)
return object unless serializer
SerializerWrapper.new(serializer, object)
end
# def to_json
# # if collection?(resource)
# # resource.map { |r| }
# # else
# # end
# # presenter = options.delete(:serializer) || serializer(controller.api_version)
# if resource.respond_to?(:to_ary)
# options[:root] ||= controller.controller_name
# end
# puts resource
# puts options
# # binding.pry
# render json: serialized(options).to_json, status: options[:status]
# end
private
RENDERING_OPTIONS = [:status, :content_type]
def normalise_object(object, options = {})
self.class.normalise_object object, options.except(*RENDERING_OPTIONS).reverse_merge(default_serializer_options)
end
def default_serializer_options
{
url_options: controller.url_options
# root: false
}
end
def encode_to_json(object)
ActiveSupport::JSON.encode object
end
# Look for something versioned first, then try for default UserSerializer
#
def self.serializer_name(object, options)
name = object.class.name
begin
Object::const_get("#{name}#{version.to_s.capitalize}Serializer")
rescue NameError
Object::const_get("#{name}Serializer")
end
rescue NameError
nil
end
def serialized(options)
presenter = options.delete(:serializer)
opts = {
url_options: controller.url_options,
scope: options.delete(:scope)
}
opts[:root] = options.delete(:root) if options.has_key?(:root)
if s = presenter || serializer(version)
s.new(resource, opts)
else
resource
end
end
end
end
end
module Tradesman
class Railtie < Rails::Railtie
require 'active_support/i18n'
config.i18n.railties_load_path << File.expand_path('../locale/en.yml', __FILE__)
# initializer "tradesman.url_helpers" do |app|
# ActiveSupport.on_load(:tradesman) do
ActionDispatch::Routing::Mapper.send :include, Tradesman::Routing
# end
# end
# initializer "tradesman.setup_testing" do |app|
# ActiveSupport.on_load(:tradesman) do
# include ActionController::Testing if Rails.env.test?
# end
# end
# initializer "tradesman.error_handling" do |app|
# require 'rails-api/action_controller/api'
# ActiveSupport.on_load(:tradesman) do
ActiveSupport.on_load(:action_controller) do
ApplicationController.send :responder=, Tradesman::Controller::ApiResponder
include Tradesman::Controller::ErrorHandling
include Tradesman::Controller::Versioning
include Tradesman::Controller::StrongParameters
include ActionController::ImplicitRender
end
# end
initializer "tradesman.activerecord_hooks" do
if defined?(ActiveRecord)
require 'tradesman/active_record'
end
end
# rake_tasks do
# load "tradesman/tasks/tradesman.rake"
# end
end
end
module Tradesman
module Routing
# Scopes a set of given api routes, allowing for option versions.
# @param [Hash] options options to pass through to the route e.g. `:module`.
# @option options [Array<Integer>, Integer] :versions the versions to support
# @option options [Array<Integer>, Integer] :version the single version to support
# @raise [ArgumentError] raised when the version isn't provided.
def tradesman(options = {}, &blk)
defaults = {format: 'json'}
versions = (Array(options.delete(:versions)) + Array(options.delete(:version))).flatten.map(&:to_s)
versions.each do |version|
raise ArgumentError, "Got invalid version: '#{version}'" unless version =~ /\A\d+\Z/
end
versions_regexp = /(#{versions.uniq.join("|")})/
raise ArgumentError, 'please provide atleast one version' if versions.empty?
options = options.deep_merge({
constraints: RoutingConstraints.new(versions: versions_regexp, default: !!options.delete(:default)),
defaults: defaults
})
scope options, &blk
end
alias api tradesman
end
class RoutingConstraints
def initialize(options)
@version = options[:version]
@versions = options[:versions]
@default = options[:default]
end
def matches?(req)
if @versions.present?
@default || !!req.headers['Accept'][/#{Tradesman.versioning_format}/, 1].match(@versions)
else
@default || !!req.headers['Accept'][/#{Tradesman.versioning_format}/, 1].match(@version)
end
end
end
end
module Tradesman
module Controller
module StrongParameters
extend ActiveSupport::Concern
included do
if defined?(ActionController::StrongParameters)
include ActionController::StrongParameters
map_error! ActionController::ParameterMissing, Tradesman::BadRequest
map_error! ActionController::UnpermittedParameters, Tradesman::BadRequest
end
end
end
end
end
module Tradesman
module Controller
module Versioning
extend ActiveSupport::Concern
included do
class_attribute :_version_range
end
module ClassMethods
def version(version)
version = version..version if version.is_a?(Integer)
self._version_range = version
before_filter :verify_api_version
end
end
protected
def accept_header_version
request.headers['Accept'][/#{Tradesman.versioning_format}/, 1].to_i
end
def version
if !instance_variable_defined?(:@version)
@version = begin
version = accept_header_version
version.presence && Integer(version)
rescue ArgumentError
nil
end
end
@version
end
def verify_api_version
error! :invalid_version unless version.present? && _version_range.include?(version)
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment