-
Notifications
You must be signed in to change notification settings - Fork 440
/
jbuilder.rb
365 lines (325 loc) · 9.8 KB
/
jbuilder.rb
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
require 'active_support'
require 'jbuilder/jbuilder'
require 'jbuilder/blank'
require 'jbuilder/key_formatter'
require 'jbuilder/errors'
require 'jbuilder/version'
require 'json'
require 'active_support/core_ext/hash/deep_merge'
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
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) && !(::Struct === 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)