lib/rabl/builder.rb
require 'active_support/inflector' # for the sake of camelcasing keys
module Rabl
class Builder
include Helpers
include Partials
SETTING_TYPES = {
:attributes => :name,
:node => :name,
:child => :data,
:glue => :data,
:extends => :file
} unless const_defined?(:SETTING_TYPES)
# Constructs a new rabl hash based on given object and options
# options = { :format => "json", :root => true, :child_root => true,
# :attributes, :node, :child, :glue, :extends }
#
def initialize(object, settings = {}, options = {})
@_object = object
@settings = settings
@options = options
@_context_scope = options[:scope]
@_view_path = options[:view_path]
end
def engines
return @_engines if defined?(@_engines)
@_engines = []
# Append onto @_engines
compile_settings(:extends)
compile_settings(:child)
compile_settings(:glue)
@_engines
end
def replace_engine(engine, value)
engines[engines.index(engine)] = value
end
def to_hash(object = nil, settings = nil, options = nil)
@_object = object if object
@options.merge!(options) if options
@settings.merge!(settings) if settings
cache_results do
@_result = {}
# Merges directly into @_result
compile_settings(:attributes)
merge_engines_into_result
# Merges directly into @_result
compile_settings(:node)
replace_nil_values if Rabl.configuration.replace_nil_values_with_empty_strings
replace_empty_string_values if Rabl.configuration.replace_empty_string_values_with_nil_values
remove_nil_values if Rabl.configuration.exclude_nil_values
result = @_result
result = { @options[:root_name] => result } if @options[:root_name].present?
result
end
end
protected
def replace_nil_values
@_result = deep_replace_nil_values(@_result)
end
def deep_replace_nil_values(hash)
hash.inject({}) do |new_hash, (k, v)|
new_hash[k] = if v.is_a?(Hash)
deep_replace_nil_values(v)
else
v.nil? ? '' : v
end
new_hash
end
end
def replace_empty_string_values
@_result = deep_replace_empty_string_values(@_result)
end
def deep_replace_empty_string_values(hash)
hash.inject({}) do |new_hash, (k, v)|
new_hash[k] = if v.is_a?(Hash)
deep_replace_empty_string_values(v)
else
(!v.nil? && v != "") ? v : nil
end
new_hash
end
end
def remove_nil_values
@_result = @_result.inject({}) do |new_hash, (k, v)|
new_hash[k] = v unless v.nil?
new_hash
end
end
def compile_settings(type)
return unless @settings.has_key?(type)
settings_type = SETTING_TYPES[type]
@settings[type].each do |setting|
send(type, setting[settings_type], setting[:options] || {}, &setting[:block])
end
end
def merge_engines_into_result
engines.each do |engine|
case engine
when Hash
# engine was stored in the form { name => #<Engine> }
engine.each do |key, value|
engine[key] = value.render if value.is_a?(Engine)
end
when Engine
engine = engine.render
end
@_result.merge!(engine) if engine.is_a?(Hash)
end
end
# Indicates an attribute or method should be included in the json output
# attribute :foo, :as => "bar"
# attribute :foo, :as => "bar", :if => lambda { |m| m.foo }
def attribute(name, options = {})
return unless
@_object &&
attribute_present?(name) &&
resolve_condition(options)
attribute = data_object_attribute(name)
name = create_key(options[:as] || name)
@_result[name] = attribute
end
alias_method :attributes, :attribute
# Creates an arbitrary node that is included in the json output
# node(:foo) { "bar" }
# node(:foo, :if => lambda { |m| m.foo.present? }) { "bar" }
def node(name, options = {}, &block)
return unless resolve_condition(options)
return if @options.has_key?(:except) && [@options[:except]].flatten.include?(name)
result = block.call(@_object)
if name.present?
@_result[create_key(name)] = result
elsif result.is_a?(Hash) # merge hash into root hash
@_result.merge!(result)
end
end
alias_method :code, :node
# Creates a child node that is included in json output
# child(@user) { attribute :full_name }
# child(@user => :person) { ... }
# child(@users => :people) { ... }
def child(data, options = {}, &block)
return unless data.present? && resolve_condition(options)
name = is_name_value?(options[:root]) ? options[:root] : data_name(data)
object = data_object(data)
engine_options = @options.slice(:child_root)
engine_options[:root] = is_collection?(object) && options.fetch(:object_root, @options[:child_root]) # child @users
engine_options[:object_root_name] = options[:object_root] if is_name_value?(options[:object_root])
object = { object => name } if data.is_a?(Hash) && object # child :users => :people
engines << { create_key(name) => object_to_engine(object, engine_options, &block) }
end
# Glues data from a child node to the json_output
# glue(@user) { attribute :full_name => :user_full_name }
def glue(data, options = {}, &block)
return unless data.present? && resolve_condition(options)
object = data_object(data)
engine = object_to_engine(object, :root => false, &block)
engines << engine if engine
end
# Extends an existing rabl template with additional attributes in the block
# extends("users/show") { attribute :full_name }
def extends(file, options = {}, &block)
return unless resolve_condition(options)
options = @options.slice(:child_root).merge!(:object => @_object).merge!(options)
engines << partial_as_engine(file, options, &block)
end
# Evaluate conditions given a symbol/proc/lambda/variable to evaluate
def call_condition_proc(condition, object)
case condition
when Proc then condition.call(object)
when Symbol then condition.to_proc.call(object)
else condition
end
end
# resolve_condition(:if => true) => true
# resolve_condition(:if => 'Im truthy') => true
# resolve_condition(:if => lambda { |m| false }) => false
# resolve_condition(:unless => lambda { |m| false }) => true
# resolve_condition(:unless => lambda { |m| false }, :if => proc { true}) => true
def resolve_condition(options)
result = true
result &&= call_condition_proc(options[:if], @_object) if
options.key?(:if)
result &&= !call_condition_proc(options[:unless], @_object) if
options.key?(:unless)
result
end
private
# Checks if an attribute is present. If not, check if the configuration specifies that this is an error
# attribute_present?(created_at) => true
def attribute_present?(name)
@_object.respond_to?(name) ||
(Rabl.configuration.raise_on_missing_attribute &&
raise("Failed to render missing attribute #{name}"))
end
# Returns a guess at the format in this context_scope
# request_format => "xml"
def request_format
format = @options[:format]
format = "json" if !format || format == "hash"
format
end
# Caches the results of the block based on object cache_key
# cache_results { compile_hash(options) }
def cache_results(&block)
if template_cache_configured? && Rabl.configuration.cache_all_output && @_object.respond_to?(:cache_key)
cache_key = [@_object, @options[:root_name], @options[:format]]
fetch_result_from_cache(cache_key, &block)
else # skip cache
yield
end
end
def create_key(name)
if Rabl.configuration.camelize_keys
name.to_s.camelize(Rabl.configuration.camelize_keys == :upper ? :upper : :lower).to_sym
else
name.to_sym
end
end
end
end