jbuilder 2.0.6 → 2.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +108 -0
- data/.gitignore +4 -1
- data/Appraisals +25 -0
- data/CONTRIBUTING.md +106 -0
- data/Gemfile +4 -12
- data/MIT-LICENSE +1 -1
- data/README.md +171 -45
- data/Rakefile +15 -10
- data/gemfiles/rails_5_0.gemfile +10 -0
- data/gemfiles/rails_5_1.gemfile +10 -0
- data/gemfiles/rails_5_2.gemfile +10 -0
- data/gemfiles/rails_6_0.gemfile +10 -0
- data/gemfiles/rails_6_1.gemfile +10 -0
- data/gemfiles/rails_head.gemfile +10 -0
- data/jbuilder.gemspec +20 -6
- data/lib/generators/rails/jbuilder_generator.rb +13 -2
- data/lib/generators/rails/scaffold_controller_generator.rb +9 -3
- data/lib/generators/rails/templates/api_controller.rb +63 -0
- data/lib/generators/rails/templates/controller.rb +16 -20
- data/lib/generators/rails/templates/index.json.jbuilder +1 -4
- data/lib/generators/rails/templates/partial.json.jbuilder +16 -0
- data/lib/generators/rails/templates/show.json.jbuilder +1 -1
- data/lib/jbuilder/blank.rb +11 -0
- data/lib/jbuilder/collection_renderer.rb +109 -0
- data/lib/jbuilder/dependency_tracker.rb +1 -1
- data/lib/jbuilder/errors.rb +24 -0
- data/lib/jbuilder/jbuilder.rb +7 -0
- data/lib/jbuilder/jbuilder_template.rb +213 -65
- data/lib/jbuilder/key_formatter.rb +34 -0
- data/lib/jbuilder/railtie.rb +31 -6
- data/lib/jbuilder.rb +148 -114
- data/test/jbuilder_dependency_tracker_test.rb +3 -4
- data/test/jbuilder_generator_test.rb +31 -4
- data/test/jbuilder_template_test.rb +313 -195
- data/test/jbuilder_test.rb +615 -219
- data/test/scaffold_api_controller_generator_test.rb +70 -0
- data/test/scaffold_controller_generator_test.rb +62 -19
- data/test/test_helper.rb +36 -0
- metadata +38 -23
- data/.travis.yml +0 -21
- data/CHANGELOG.md +0 -89
- data/Gemfile.old +0 -14
data/lib/jbuilder.rb
CHANGED
@@ -1,51 +1,25 @@
|
|
1
|
-
require 'active_support
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
require '
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
class Jbuilder < JbuilderProxy
|
15
|
-
class NullError < ::NoMethodError
|
16
|
-
def initialize(key)
|
17
|
-
super "Failed to add #{key.to_s.inspect} property to null object"
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
class KeyFormatter
|
22
|
-
def initialize(*args)
|
23
|
-
@format = {}
|
24
|
-
@cache = {}
|
1
|
+
require 'active_support'
|
2
|
+
require 'jbuilder/jbuilder'
|
3
|
+
require 'jbuilder/blank'
|
4
|
+
require 'jbuilder/key_formatter'
|
5
|
+
require 'jbuilder/errors'
|
6
|
+
require 'json'
|
7
|
+
require 'ostruct'
|
8
|
+
require 'active_support/core_ext/hash/deep_merge'
|
9
|
+
|
10
|
+
class Jbuilder
|
11
|
+
@@key_formatter = nil
|
12
|
+
@@ignore_nil = false
|
13
|
+
@@deep_format_keys = false
|
25
14
|
|
26
|
-
|
27
|
-
|
28
|
-
@format[name] = []
|
29
|
-
end
|
30
|
-
options.each do |name, paramaters|
|
31
|
-
@format[name] = paramaters
|
32
|
-
end
|
33
|
-
end
|
15
|
+
def initialize(options = {})
|
16
|
+
@attributes = {}
|
34
17
|
|
35
|
-
|
36
|
-
|
37
|
-
|
18
|
+
@key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil}
|
19
|
+
@ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
|
20
|
+
@deep_format_keys = options.fetch(:deep_format_keys, @@deep_format_keys)
|
38
21
|
|
39
|
-
|
40
|
-
@cache[key] ||= @format.inject(key.to_s) do |result, args|
|
41
|
-
func, args = args
|
42
|
-
if ::Proc === func
|
43
|
-
func.call result, *args
|
44
|
-
else
|
45
|
-
result.send func, *args
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
22
|
+
yield self if ::Kernel.block_given?
|
49
23
|
end
|
50
24
|
|
51
25
|
# Yields a builder and automatically turns the result into a JSON string
|
@@ -53,58 +27,51 @@ class Jbuilder < JbuilderProxy
|
|
53
27
|
new(*args, &block).target!
|
54
28
|
end
|
55
29
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
def initialize(options = {}, &block)
|
60
|
-
@attributes = {}
|
61
|
-
|
62
|
-
@key_formatter = options.fetch(:key_formatter){ @@key_formatter.clone }
|
63
|
-
@ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
|
64
|
-
yield self if block
|
65
|
-
end
|
66
|
-
|
67
|
-
BLANK = ::Object.new
|
30
|
+
BLANK = Blank.new
|
31
|
+
NON_ENUMERABLES = [ ::Struct, ::OpenStruct ].to_set
|
68
32
|
|
69
33
|
def set!(key, value = BLANK, *args, &block)
|
70
|
-
|
71
|
-
|
72
|
-
if BLANK != value
|
34
|
+
result = if ::Kernel.block_given?
|
35
|
+
if !_blank?(value)
|
73
36
|
# json.comments @post.comments { |comment| ... }
|
74
37
|
# { "comments": [ { ... }, { ... } ] }
|
75
38
|
_scope{ array! value, &block }
|
76
39
|
else
|
77
40
|
# json.comments { ... }
|
78
41
|
# { "comments": ... }
|
79
|
-
|
42
|
+
_merge_block(key){ yield self }
|
80
43
|
end
|
81
44
|
elsif args.empty?
|
82
45
|
if ::Jbuilder === value
|
83
46
|
# json.age 32
|
84
47
|
# json.person another_jbuilder
|
85
48
|
# { "age": 32, "person": { ... }
|
86
|
-
value.attributes!
|
49
|
+
_format_keys(value.attributes!)
|
87
50
|
else
|
88
51
|
# json.age 32
|
89
52
|
# { "age": 32 }
|
90
|
-
value
|
53
|
+
_format_keys(value)
|
91
54
|
end
|
92
|
-
elsif
|
55
|
+
elsif _is_collection?(value)
|
93
56
|
# json.comments @post.comments, :content, :created_at
|
94
57
|
# { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
|
95
58
|
_scope{ array! value, *args }
|
96
59
|
else
|
97
60
|
# json.author @post.creator, :name, :email_address
|
98
61
|
# { "author": { "name": "David", "email_address": "david@loudthinking.com" } }
|
99
|
-
|
62
|
+
_merge_block(key){ extract! value, *args }
|
100
63
|
end
|
101
64
|
|
102
65
|
_set_value key, result
|
103
66
|
end
|
104
67
|
|
105
|
-
|
106
|
-
|
107
|
-
|
68
|
+
def method_missing(*args, &block)
|
69
|
+
if ::Kernel.block_given?
|
70
|
+
set!(*args, &block)
|
71
|
+
else
|
72
|
+
set!(*args)
|
73
|
+
end
|
74
|
+
end
|
108
75
|
|
109
76
|
# Specifies formatting to be applied to the key. Passing in a name of a function
|
110
77
|
# will cause that function to be called on the key. So :upcase will upper case
|
@@ -167,6 +134,31 @@ class Jbuilder < JbuilderProxy
|
|
167
134
|
@@ignore_nil = value
|
168
135
|
end
|
169
136
|
|
137
|
+
# Deeply apply key format to nested hashes and arrays passed to
|
138
|
+
# methods like set!, merge! or array!.
|
139
|
+
#
|
140
|
+
# Example:
|
141
|
+
#
|
142
|
+
# json.key_format! camelize: :lower
|
143
|
+
# json.settings({some_value: "abc"})
|
144
|
+
#
|
145
|
+
# { "settings": { "some_value": "abc" }}
|
146
|
+
#
|
147
|
+
# json.key_format! camelize: :lower
|
148
|
+
# json.deep_format_keys!
|
149
|
+
# json.settings({some_value: "abc"})
|
150
|
+
#
|
151
|
+
# { "settings": { "someValue": "abc" }}
|
152
|
+
#
|
153
|
+
def deep_format_keys!(value = true)
|
154
|
+
@deep_format_keys = value
|
155
|
+
end
|
156
|
+
|
157
|
+
# Same as instance method deep_format_keys! except sets the default.
|
158
|
+
def self.deep_format_keys(value = true)
|
159
|
+
@@deep_format_keys = value
|
160
|
+
end
|
161
|
+
|
170
162
|
# Turns the current element into an array and yields a builder to add a hash.
|
171
163
|
#
|
172
164
|
# Example:
|
@@ -185,7 +177,7 @@ class Jbuilder < JbuilderProxy
|
|
185
177
|
# end
|
186
178
|
def child!
|
187
179
|
@attributes = [] unless ::Array === @attributes
|
188
|
-
@attributes << _scope
|
180
|
+
@attributes << _scope{ yield self }
|
189
181
|
end
|
190
182
|
|
191
183
|
# Turns the current element into an array and iterates over the passed collection, adding each iteration as
|
@@ -200,7 +192,7 @@ class Jbuilder < JbuilderProxy
|
|
200
192
|
#
|
201
193
|
# [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ]
|
202
194
|
#
|
203
|
-
#
|
195
|
+
# You can use the call syntax instead of an explicit extract! call:
|
204
196
|
#
|
205
197
|
# json.(@people) { |person| ... }
|
206
198
|
#
|
@@ -219,13 +211,17 @@ class Jbuilder < JbuilderProxy
|
|
219
211
|
#
|
220
212
|
# [1,2,3]
|
221
213
|
def array!(collection = [], *attributes, &block)
|
222
|
-
|
214
|
+
array = if collection.nil?
|
215
|
+
[]
|
216
|
+
elsif ::Kernel.block_given?
|
223
217
|
_map_collection(collection, &block)
|
224
218
|
elsif attributes.any?
|
225
219
|
_map_collection(collection) { |element| extract! element, *attributes }
|
226
220
|
else
|
227
|
-
collection
|
221
|
+
_format_keys(collection.to_a)
|
228
222
|
end
|
223
|
+
|
224
|
+
@attributes = _merge_values(@attributes, array)
|
229
225
|
end
|
230
226
|
|
231
227
|
# Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON.
|
@@ -247,14 +243,14 @@ class Jbuilder < JbuilderProxy
|
|
247
243
|
# json.(@person, :name, :age)
|
248
244
|
def extract!(object, *attributes)
|
249
245
|
if ::Hash === object
|
250
|
-
_extract_hash_values(object,
|
246
|
+
_extract_hash_values(object, attributes)
|
251
247
|
else
|
252
|
-
_extract_method_values(object,
|
248
|
+
_extract_method_values(object, attributes)
|
253
249
|
end
|
254
250
|
end
|
255
251
|
|
256
252
|
def call(object, *attributes, &block)
|
257
|
-
if
|
253
|
+
if ::Kernel.block_given?
|
258
254
|
array! object, &block
|
259
255
|
else
|
260
256
|
extract! object, *attributes
|
@@ -273,60 +269,98 @@ class Jbuilder < JbuilderProxy
|
|
273
269
|
@attributes
|
274
270
|
end
|
275
271
|
|
276
|
-
# Merges hash or
|
277
|
-
def merge!(
|
278
|
-
|
279
|
-
|
280
|
-
@attributes.concat hash_or_array
|
281
|
-
else
|
282
|
-
@attributes.update hash_or_array
|
283
|
-
end
|
272
|
+
# Merges hash, array, or Jbuilder instance into current builder.
|
273
|
+
def merge!(object)
|
274
|
+
hash_or_array = ::Jbuilder === object ? object.attributes! : object
|
275
|
+
@attributes = _merge_values(@attributes, _format_keys(hash_or_array))
|
284
276
|
end
|
285
277
|
|
286
278
|
# Encodes the current builder as JSON.
|
287
279
|
def target!
|
288
|
-
|
280
|
+
@attributes.to_json
|
289
281
|
end
|
290
282
|
|
291
283
|
private
|
292
284
|
|
293
|
-
|
294
|
-
|
295
|
-
|
285
|
+
def _extract_hash_values(object, attributes)
|
286
|
+
attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
|
287
|
+
end
|
296
288
|
|
297
|
-
|
298
|
-
|
299
|
-
|
289
|
+
def _extract_method_values(object, attributes)
|
290
|
+
attributes.each{ |key| _set_value key, _format_keys(object.public_send(key)) }
|
291
|
+
end
|
300
292
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
293
|
+
def _merge_block(key)
|
294
|
+
current_value = _blank? ? BLANK : @attributes.fetch(_key(key), BLANK)
|
295
|
+
raise NullError.build(key) if current_value.nil?
|
296
|
+
new_value = _scope{ yield self }
|
297
|
+
_merge_values(current_value, new_value)
|
298
|
+
end
|
299
|
+
|
300
|
+
def _merge_values(current_value, updates)
|
301
|
+
if _blank?(updates)
|
302
|
+
current_value
|
303
|
+
elsif _blank?(current_value) || updates.nil? || current_value.empty? && ::Array === updates
|
304
|
+
updates
|
305
|
+
elsif ::Array === current_value && ::Array === updates
|
306
|
+
current_value + updates
|
307
|
+
elsif ::Hash === current_value && ::Hash === updates
|
308
|
+
current_value.deep_merge(updates)
|
309
|
+
else
|
310
|
+
raise MergeError.build(current_value, updates)
|
306
311
|
end
|
312
|
+
end
|
307
313
|
|
308
|
-
|
309
|
-
|
314
|
+
def _key(key)
|
315
|
+
@key_formatter ? @key_formatter.format(key) : key.to_s
|
316
|
+
end
|
310
317
|
|
311
|
-
|
312
|
-
|
313
|
-
end
|
314
|
-
end
|
318
|
+
def _format_keys(hash_or_array)
|
319
|
+
return hash_or_array unless @deep_format_keys
|
315
320
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
@attributes, @key_formatter = parent_attributes, parent_formatter
|
321
|
+
if ::Array === hash_or_array
|
322
|
+
hash_or_array.map { |value| _format_keys(value) }
|
323
|
+
elsif ::Hash === hash_or_array
|
324
|
+
::Hash[hash_or_array.collect { |k, v| [_key(k), _format_keys(v)] }]
|
325
|
+
else
|
326
|
+
hash_or_array
|
323
327
|
end
|
328
|
+
end
|
324
329
|
|
325
|
-
|
326
|
-
|
327
|
-
|
330
|
+
def _set_value(key, value)
|
331
|
+
raise NullError.build(key) if @attributes.nil?
|
332
|
+
raise ArrayError.build(key) if ::Array === @attributes
|
333
|
+
return if @ignore_nil && value.nil? or _blank?(value)
|
334
|
+
@attributes = {} if _blank?
|
335
|
+
@attributes[_key(key)] = value
|
336
|
+
end
|
337
|
+
|
338
|
+
def _map_collection(collection)
|
339
|
+
collection.map do |element|
|
340
|
+
_scope{ yield element }
|
341
|
+
end - [BLANK]
|
342
|
+
end
|
343
|
+
|
344
|
+
def _scope
|
345
|
+
parent_attributes, parent_formatter, parent_deep_format_keys = @attributes, @key_formatter, @deep_format_keys
|
346
|
+
@attributes = BLANK
|
347
|
+
yield
|
348
|
+
@attributes
|
349
|
+
ensure
|
350
|
+
@attributes, @key_formatter, @deep_format_keys = parent_attributes, parent_formatter, parent_deep_format_keys
|
351
|
+
end
|
352
|
+
|
353
|
+
def _is_collection?(object)
|
354
|
+
_object_respond_to?(object, :map, :count) && NON_ENUMERABLES.none?{ |klass| klass === object }
|
355
|
+
end
|
356
|
+
|
357
|
+
def _blank?(value=@attributes)
|
358
|
+
BLANK == value
|
359
|
+
end
|
360
|
+
|
361
|
+
def _object_respond_to?(object, *methods)
|
362
|
+
methods.all?{ |m| object.respond_to?(m) }
|
363
|
+
end
|
328
364
|
end
|
329
365
|
|
330
|
-
require 'jbuilder/
|
331
|
-
require 'jbuilder/dependency_tracker'
|
332
|
-
require 'jbuilder/railtie' if defined?(Rails::VERSION::MAJOR) && Rails::VERSION::MAJOR == 4
|
366
|
+
require 'jbuilder/railtie' if defined?(Rails)
|
@@ -1,5 +1,4 @@
|
|
1
|
-
require '
|
2
|
-
require 'active_support/test_case'
|
1
|
+
require 'test_helper'
|
3
2
|
require 'jbuilder/dependency_tracker'
|
4
3
|
|
5
4
|
|
@@ -54,7 +53,7 @@ class JbuilderDependencyTrackerTest < ActiveSupport::TestCase
|
|
54
53
|
assert_equal %w[path/to/partial], dependencies
|
55
54
|
end
|
56
55
|
|
57
|
-
test 'detects partial in indirect
|
56
|
+
test 'detects partial in indirect collection calls' do
|
58
57
|
dependencies = track_dependencies <<-RUBY
|
59
58
|
json.comments @post.comments, partial: 'comments/comment', as: :comment
|
60
59
|
RUBY
|
@@ -62,7 +61,7 @@ class JbuilderDependencyTrackerTest < ActiveSupport::TestCase
|
|
62
61
|
assert_equal %w[comments/comment], dependencies
|
63
62
|
end
|
64
63
|
|
65
|
-
test 'detects explicit
|
64
|
+
test 'detects explicit dependency' do
|
66
65
|
dependencies = track_dependencies <<-RUBY
|
67
66
|
# Template Dependency: path/to/partial
|
68
67
|
json.foo 'bar'
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'test_helper'
|
1
2
|
require 'rails/generators/test_case'
|
2
3
|
require 'generators/rails/jbuilder_generator'
|
3
4
|
|
@@ -13,19 +14,45 @@ class JbuilderGeneratorTest < Rails::Generators::TestCase
|
|
13
14
|
%w(index show).each do |view|
|
14
15
|
assert_file "app/views/posts/#{view}.json.jbuilder"
|
15
16
|
end
|
17
|
+
assert_file "app/views/posts/_post.json.jbuilder"
|
16
18
|
end
|
17
19
|
|
18
20
|
test 'index content' do
|
19
21
|
run_generator
|
20
22
|
|
21
23
|
assert_file 'app/views/posts/index.json.jbuilder' do |content|
|
22
|
-
assert_match
|
23
|
-
assert_match /json\.extract! post, :id, :title, :body/, content
|
24
|
-
assert_match /json\.url post_url\(post, format: :json\)/, content
|
24
|
+
assert_match %r{json\.array! @posts, partial: "posts/post", as: :post}, content
|
25
25
|
end
|
26
26
|
|
27
27
|
assert_file 'app/views/posts/show.json.jbuilder' do |content|
|
28
|
-
assert_match
|
28
|
+
assert_match %r{json\.partial! "posts/post", post: @post}, content
|
29
|
+
end
|
30
|
+
|
31
|
+
assert_file 'app/views/posts/_post.json.jbuilder' do |content|
|
32
|
+
assert_match %r{json\.extract! post, :id, :title, :body}, content
|
33
|
+
assert_match %r{:created_at, :updated_at}, content
|
34
|
+
assert_match %r{json\.url post_url\(post, format: :json\)}, content
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
test 'timestamps are not generated in partial with --no-timestamps' do
|
39
|
+
run_generator %w(Post title body:text --no-timestamps)
|
40
|
+
|
41
|
+
assert_file 'app/views/posts/_post.json.jbuilder' do |content|
|
42
|
+
assert_match %r{json\.extract! post, :id, :title, :body$}, content
|
43
|
+
assert_no_match %r{:created_at, :updated_at}, content
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
if Rails::VERSION::MAJOR >= 6
|
48
|
+
test 'handles virtual attributes' do
|
49
|
+
run_generator %w(Message content:rich_text video:attachment photos:attachments)
|
50
|
+
|
51
|
+
assert_file 'app/views/messages/_message.json.jbuilder' do |content|
|
52
|
+
assert_match %r{json\.content message\.content\.to_s}, content
|
53
|
+
assert_match %r{json\.video url_for\(message\.video\)}, content
|
54
|
+
assert_match %r{json\.photos do\n json\.array!\(message\.photos\) do \|photo\|\n json\.id photo\.id\n json\.url url_for\(photo\)\n end\nend}, content
|
55
|
+
end
|
29
56
|
end
|
30
57
|
end
|
31
58
|
end
|