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 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
|
require 'active_support'
require 'jbuilder/jbuilder'
require 'jbuilder/blank'
require 'jbuilder/key_formatter'
require 'jbuilder/errors'
require 'json'
require 'active_support/core_ext/hash/deep_merge'
begin
require 'ostruct'
rescue LoadError
end
class Jbuilder
@@key_formatter = nil
@@ignore_nil = false
@@deep_format_keys = false
def initialize(options = {})
@attributes = {}
@key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil}
@ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
@deep_format_keys = options.fetch(:deep_format_keys, @@deep_format_keys)
yield self if ::Kernel.block_given?
end
# Yields a builder and automatically turns the result into a JSON string
def self.encode(*args, &block)
new(*args, &block).target!
end
BLANK = Blank.new
NON_ENUMERABLES = defined?(::OpenStruct) ? [::Struct, ::OpenStruct].to_set : [::Struct].to_set
def set!(key, value = BLANK, *args, &block)
result = if ::Kernel.block_given?
if !_blank?(value)
# json.comments @post.comments { |comment| ... }
# { "comments": [ { ... }, { ... } ] }
_scope{ array! value, &block }
else
# json.comments { ... }
# { "comments": ... }
_merge_block(key){ yield self }
end
elsif args.empty?
if ::Jbuilder === value
# json.age 32
# json.person another_jbuilder
# { "age": 32, "person": { ... }
_format_keys(value.attributes!)
else
# json.age 32
# { "age": 32 }
_format_keys(value)
end
elsif _is_collection?(value)
# json.comments @post.comments, :content, :created_at
# { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
_scope{ array! value, *args }
else
# json.author @post.creator, :name, :email_address
# { "author": { "name": "David", "email_address": "david@loudthinking.com" } }
_merge_block(key){ extract! value, *args }
end
_set_value key, result
end
def method_missing(*args, &block)
if ::Kernel.block_given?
set!(*args, &block)
else
set!(*args)
end
end
# Specifies formatting to be applied to the key. Passing in a name of a function
# will cause that function to be called on the key. So :upcase will upper case
# the key. You can also pass in lambdas for more complex transformations.
#
# Example:
#
# json.key_format! :upcase
# json.author do
# json.name "David"
# json.age 32
# end
#
# { "AUTHOR": { "NAME": "David", "AGE": 32 } }
#
# You can pass parameters to the method using a hash pair.
#
# json.key_format! camelize: :lower
# json.first_name "David"
#
# { "firstName": "David" }
#
# Lambdas can also be used.
#
# json.key_format! ->(key){ "_" + key }
# json.first_name "David"
#
# { "_first_name": "David" }
#
def key_format!(*args)
@key_formatter = KeyFormatter.new(*args)
end
# Same as the instance method key_format! except sets the default.
def self.key_format(*args)
@@key_formatter = KeyFormatter.new(*args)
end
# If you want to skip adding nil values to your JSON hash. This is useful
# for JSON clients that don't deal well with nil values, and would prefer
# not to receive keys which have null values.
#
# Example:
# json.ignore_nil! false
# json.id User.new.id
#
# { "id": null }
#
# json.ignore_nil!
# json.id User.new.id
#
# {}
#
def ignore_nil!(value = true)
@ignore_nil = value
end
# Same as instance method ignore_nil! except sets the default.
def self.ignore_nil(value = true)
@@ignore_nil = value
end
# Deeply apply key format to nested hashes and arrays passed to
# methods like set!, merge! or array!.
#
# Example:
#
# json.key_format! camelize: :lower
# json.settings({some_value: "abc"})
#
# { "settings": { "some_value": "abc" }}
#
# json.key_format! camelize: :lower
# json.deep_format_keys!
# json.settings({some_value: "abc"})
#
# { "settings": { "someValue": "abc" }}
#
def deep_format_keys!(value = true)
@deep_format_keys = value
end
# Same as instance method deep_format_keys! except sets the default.
def self.deep_format_keys(value = true)
@@deep_format_keys = value
end
# Turns the current element into an array and yields a builder to add a hash.
#
# Example:
#
# json.comments do
# json.child! { json.content "hello" }
# json.child! { json.content "world" }
# end
#
# { "comments": [ { "content": "hello" }, { "content": "world" } ]}
#
# More commonly, you'd use the combined iterator, though:
#
# json.comments(@post.comments) do |comment|
# json.content comment.formatted_content
# end
def child!
@attributes = [] unless ::Array === @attributes
@attributes << _scope{ yield self }
end
# Turns the current element into an array and iterates over the passed collection, adding each iteration as
# an element of the resulting array.
#
# Example:
#
# json.array!(@people) do |person|
# json.name person.name
# json.age calculate_age(person.birthday)
# end
#
# [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ]
#
# You can use the call syntax instead of an explicit extract! call:
#
# json.(@people) { |person| ... }
#
# It's generally only needed to use this method for top-level arrays. If you have named arrays, you can do:
#
# json.people(@people) do |person|
# json.name person.name
# json.age calculate_age(person.birthday)
# end
#
# { "people": [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ] }
#
# If you omit the block then you can set the top level array directly:
#
# json.array! [1, 2, 3]
#
# [1,2,3]
def array!(collection = [], *attributes, &block)
array = if collection.nil?
[]
elsif ::Kernel.block_given?
_map_collection(collection, &block)
elsif attributes.any?
_map_collection(collection) { |element| extract! element, *attributes }
else
_format_keys(collection.to_a)
end
@attributes = _merge_values(@attributes, array)
end
# Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON.
#
# Example:
#
# @person = Struct.new(:name, :age).new('David', 32)
#
# or you can utilize a Hash
#
# @person = { name: 'David', age: 32 }
#
# json.extract! @person, :name, :age
#
# { "name": David", "age": 32 }, { "name": Jamie", "age": 31 }
#
# You can also use the call syntax instead of an explicit extract! call:
#
# json.(@person, :name, :age)
def extract!(object, *attributes)
if ::Hash === object
_extract_hash_values(object, attributes)
else
_extract_method_values(object, attributes)
end
end
def call(object, *attributes, &block)
if ::Kernel.block_given?
array! object, &block
else
extract! object, *attributes
end
end
# Returns the nil JSON.
def nil!
@attributes = nil
end
alias_method :null!, :nil!
# Returns the attributes of the current builder.
def attributes!
@attributes
end
# Merges hash, array, or Jbuilder instance into current builder.
def merge!(object)
hash_or_array = ::Jbuilder === object ? object.attributes! : object
@attributes = _merge_values(@attributes, _format_keys(hash_or_array))
end
# Encodes the current builder as JSON.
def target!
@attributes.to_json
end
private
def _extract_hash_values(object, attributes)
attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
end
def _extract_method_values(object, attributes)
attributes.each{ |key| _set_value key, _format_keys(object.public_send(key)) }
end
def _merge_block(key)
current_value = _blank? ? BLANK : @attributes.fetch(_key(key), BLANK)
::Kernel.raise NullError.build(key) if current_value.nil?
new_value = _scope{ yield self }
_merge_values(current_value, new_value)
end
def _merge_values(current_value, updates)
if _blank?(updates)
current_value
elsif _blank?(current_value) || updates.nil? || current_value.empty? && ::Array === updates
updates
elsif ::Array === current_value && ::Array === updates
current_value + updates
elsif ::Hash === current_value && ::Hash === updates
current_value.deep_merge(updates)
else
::Kernel.raise MergeError.build(current_value, updates)
end
end
def _key(key)
@key_formatter ? @key_formatter.format(key) : key.to_s
end
def _format_keys(hash_or_array)
return hash_or_array unless @deep_format_keys
if ::Array === hash_or_array
hash_or_array.map { |value| _format_keys(value) }
elsif ::Hash === hash_or_array
::Hash[hash_or_array.collect { |k, v| [_key(k), _format_keys(v)] }]
else
hash_or_array
end
end
def _set_value(key, value)
::Kernel.raise NullError.build(key) if @attributes.nil?
::Kernel.raise ArrayError.build(key) if ::Array === @attributes
return if @ignore_nil && value.nil? or _blank?(value)
@attributes = {} if _blank?
@attributes[_key(key)] = value
end
def _map_collection(collection)
collection.map do |element|
_scope{ yield element }
end - [BLANK]
end
def _scope
parent_attributes, parent_formatter, parent_deep_format_keys = @attributes, @key_formatter, @deep_format_keys
@attributes = BLANK
yield
@attributes
ensure
@attributes, @key_formatter, @deep_format_keys = parent_attributes, parent_formatter, parent_deep_format_keys
end
def _is_collection?(object)
_object_respond_to?(object, :map, :count) && NON_ENUMERABLES.none?{ |klass| klass === object }
end
def _blank?(value=@attributes)
BLANK == value
end
def _object_respond_to?(object, *methods)
methods.all?{ |m| object.respond_to?(m) }
end
end
require 'jbuilder/railtie' if defined?(Rails)
|