1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
|
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
|