jbuilder 1.5.3 → 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.
Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +108 -0
  3. data/.gitignore +4 -1
  4. data/Appraisals +25 -0
  5. data/CONTRIBUTING.md +106 -0
  6. data/Gemfile +4 -5
  7. data/MIT-LICENSE +2 -2
  8. data/README.md +182 -40
  9. data/Rakefile +15 -10
  10. data/gemfiles/rails_5_0.gemfile +10 -0
  11. data/gemfiles/rails_5_1.gemfile +10 -0
  12. data/gemfiles/rails_5_2.gemfile +10 -0
  13. data/gemfiles/rails_6_0.gemfile +10 -0
  14. data/gemfiles/rails_6_1.gemfile +10 -0
  15. data/gemfiles/rails_head.gemfile +10 -0
  16. data/jbuilder.gemspec +25 -8
  17. data/lib/generators/rails/jbuilder_generator.rb +13 -2
  18. data/lib/generators/rails/scaffold_controller_generator.rb +9 -3
  19. data/lib/generators/rails/templates/api_controller.rb +63 -0
  20. data/lib/generators/rails/templates/controller.rb +18 -22
  21. data/lib/generators/rails/templates/index.json.jbuilder +1 -4
  22. data/lib/generators/rails/templates/partial.json.jbuilder +16 -0
  23. data/lib/generators/rails/templates/show.json.jbuilder +1 -1
  24. data/lib/jbuilder/blank.rb +11 -0
  25. data/lib/jbuilder/collection_renderer.rb +109 -0
  26. data/lib/jbuilder/dependency_tracker.rb +61 -0
  27. data/lib/jbuilder/errors.rb +24 -0
  28. data/lib/jbuilder/jbuilder.rb +7 -0
  29. data/lib/jbuilder/jbuilder_template.rb +228 -67
  30. data/lib/jbuilder/key_formatter.rb +34 -0
  31. data/lib/jbuilder/railtie.rb +31 -6
  32. data/lib/jbuilder.rb +144 -137
  33. data/test/jbuilder_dependency_tracker_test.rb +72 -0
  34. data/test/jbuilder_generator_test.rb +31 -4
  35. data/test/jbuilder_template_test.rb +319 -163
  36. data/test/jbuilder_test.rb +613 -298
  37. data/test/scaffold_api_controller_generator_test.rb +70 -0
  38. data/test/scaffold_controller_generator_test.rb +62 -19
  39. data/test/test_helper.rb +36 -0
  40. metadata +47 -22
  41. data/.travis.yml +0 -38
  42. data/Gemfile.old +0 -7
data/lib/jbuilder.rb CHANGED
@@ -1,53 +1,25 @@
1
- require 'active_support/ordered_hash'
2
- require 'active_support/core_ext/array/access'
3
- require 'active_support/core_ext/enumerable'
4
- require 'active_support/core_ext/hash'
5
- require 'active_support/cache'
6
- require 'multi_json'
7
-
8
- begin
9
- require 'active_support/proxy_object'
10
- JbuilderProxy = ActiveSupport::ProxyObject
11
- rescue LoadError
12
- require 'active_support/basic_object'
13
- JbuilderProxy = ActiveSupport::BasicObject
14
- end
15
-
16
- class Jbuilder < JbuilderProxy
17
- class NullError < ::NoMethodError
18
- def initialize(key)
19
- super "Failed to add #{key.to_s.inspect} property to null object"
20
- end
21
- end
22
-
23
- class KeyFormatter
24
- def initialize(*args)
25
- @format = ::ActiveSupport::OrderedHash.new
26
- @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
27
14
 
28
- options = args.extract_options!
29
- args.each do |name|
30
- @format[name] = []
31
- end
32
- options.each do |name, paramaters|
33
- @format[name] = paramaters
34
- end
35
- end
15
+ def initialize(options = {})
16
+ @attributes = {}
36
17
 
37
- def initialize_copy(original)
38
- @cache = {}
39
- end
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)
40
21
 
41
- def format(key)
42
- @cache[key] ||= @format.inject(key.to_s) do |result, args|
43
- func, args = args
44
- if ::Proc === func
45
- func.call result, *args
46
- else
47
- result.send func, *args
48
- end
49
- end
50
- end
22
+ yield self if ::Kernel.block_given?
51
23
  end
52
24
 
53
25
  # Yields a builder and automatically turns the result into a JSON string
@@ -55,59 +27,51 @@ class Jbuilder < JbuilderProxy
55
27
  new(*args, &block).target!
56
28
  end
57
29
 
58
- @@key_formatter = KeyFormatter.new
59
- @@ignore_nil = false
60
-
61
- def initialize(*args, &block)
62
- @attributes = ::ActiveSupport::OrderedHash.new
63
-
64
- options = args.extract_options!
65
- @key_formatter = options.fetch(:key_formatter){ @@key_formatter.clone }
66
- @ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
67
- yield self if block
68
- end
69
-
70
- BLANK = ::Object.new
30
+ BLANK = Blank.new
31
+ NON_ENUMERABLES = [ ::Struct, ::OpenStruct ].to_set
71
32
 
72
33
  def set!(key, value = BLANK, *args, &block)
73
-
74
- result = if block
75
- if BLANK != value
34
+ result = if ::Kernel.block_given?
35
+ if !_blank?(value)
76
36
  # json.comments @post.comments { |comment| ... }
77
37
  # { "comments": [ { ... }, { ... } ] }
78
38
  _scope{ array! value, &block }
79
39
  else
80
40
  # json.comments { ... }
81
41
  # { "comments": ... }
82
- _scope { yield self }
42
+ _merge_block(key){ yield self }
83
43
  end
84
44
  elsif args.empty?
85
45
  if ::Jbuilder === value
86
46
  # json.age 32
87
47
  # json.person another_jbuilder
88
48
  # { "age": 32, "person": { ... }
89
- value.attributes!
49
+ _format_keys(value.attributes!)
90
50
  else
91
51
  # json.age 32
92
52
  # { "age": 32 }
93
- value
53
+ _format_keys(value)
94
54
  end
95
- elsif _mapable_arguments?(value, *args)
55
+ elsif _is_collection?(value)
96
56
  # json.comments @post.comments, :content, :created_at
97
57
  # { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
98
58
  _scope{ array! value, *args }
99
59
  else
100
60
  # json.author @post.creator, :name, :email_address
101
61
  # { "author": { "name": "David", "email_address": "david@loudthinking.com" } }
102
- _scope { extract! value, *args }
62
+ _merge_block(key){ extract! value, *args }
103
63
  end
104
64
 
105
65
  _set_value key, result
106
66
  end
107
67
 
108
- alias_method :method_missing, :set!
109
- private :method_missing
110
-
68
+ def method_missing(*args, &block)
69
+ if ::Kernel.block_given?
70
+ set!(*args, &block)
71
+ else
72
+ set!(*args)
73
+ end
74
+ end
111
75
 
112
76
  # Specifies formatting to be applied to the key. Passing in a name of a function
113
77
  # will cause that function to be called on the key. So :upcase will upper case
@@ -170,6 +134,31 @@ class Jbuilder < JbuilderProxy
170
134
  @@ignore_nil = value
171
135
  end
172
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
+
173
162
  # Turns the current element into an array and yields a builder to add a hash.
174
163
  #
175
164
  # Example:
@@ -188,7 +177,7 @@ class Jbuilder < JbuilderProxy
188
177
  # end
189
178
  def child!
190
179
  @attributes = [] unless ::Array === @attributes
191
- @attributes << _scope { yield self }
180
+ @attributes << _scope{ yield self }
192
181
  end
193
182
 
194
183
  # Turns the current element into an array and iterates over the passed collection, adding each iteration as
@@ -203,7 +192,7 @@ class Jbuilder < JbuilderProxy
203
192
  #
204
193
  # [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ]
205
194
  #
206
- # If you are using Ruby 1.9+, you can use the call syntax instead of an explicit extract! call:
195
+ # You can use the call syntax instead of an explicit extract! call:
207
196
  #
208
197
  # json.(@people) { |person| ... }
209
198
  #
@@ -222,15 +211,17 @@ class Jbuilder < JbuilderProxy
222
211
  #
223
212
  # [1,2,3]
224
213
  def array!(collection = [], *attributes, &block)
225
- @attributes = if block && block.arity == 2
226
- _two_arguments_map_collection(collection, &block)
227
- elsif block
214
+ array = if collection.nil?
215
+ []
216
+ elsif ::Kernel.block_given?
228
217
  _map_collection(collection, &block)
229
218
  elsif attributes.any?
230
219
  _map_collection(collection) { |element| extract! element, *attributes }
231
220
  else
232
- collection
221
+ _format_keys(collection.to_a)
233
222
  end
223
+
224
+ @attributes = _merge_values(@attributes, array)
234
225
  end
235
226
 
236
227
  # Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON.
@@ -252,14 +243,14 @@ class Jbuilder < JbuilderProxy
252
243
  # json.(@person, :name, :age)
253
244
  def extract!(object, *attributes)
254
245
  if ::Hash === object
255
- _extract_hash_values(object, *attributes)
246
+ _extract_hash_values(object, attributes)
256
247
  else
257
- _extract_method_values(object, *attributes)
248
+ _extract_method_values(object, attributes)
258
249
  end
259
250
  end
260
251
 
261
252
  def call(object, *attributes, &block)
262
- if block
253
+ if ::Kernel.block_given?
263
254
  array! object, &block
264
255
  else
265
256
  extract! object, *attributes
@@ -278,82 +269,98 @@ class Jbuilder < JbuilderProxy
278
269
  @attributes
279
270
  end
280
271
 
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))
276
+ end
277
+
281
278
  # Encodes the current builder as JSON.
282
279
  def target!
283
- ::MultiJson.dump @attributes
280
+ @attributes.to_json
284
281
  end
285
282
 
286
283
  private
287
284
 
288
- def _extract_hash_values(object, *attributes)
289
- attributes.each{ |key| _set_value key, object.fetch(key) }
290
- end
285
+ def _extract_hash_values(object, attributes)
286
+ attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
287
+ end
291
288
 
292
- def _extract_method_values(object, *attributes)
293
- attributes.each do |method_name|
294
- unless object.respond_to?(method_name)
295
- message = "Private method #{method_name.inspect} was used to " +
296
- 'extract value. This will be an error in future versions of Jbuilder'
297
- end
289
+ def _extract_method_values(object, attributes)
290
+ attributes.each{ |key| _set_value key, _format_keys(object.public_send(key)) }
291
+ end
298
292
 
299
- _set_value method_name, object.send(method_name)
300
- ::ActiveSupport::Deprecation.warn message if message
301
- end
302
- end
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
303
299
 
304
- def _set_value(key, value)
305
- raise NullError, key if @attributes.nil?
306
- unless @ignore_nil && value.nil?
307
- @attributes[@key_formatter.format(key)] = value
308
- end
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)
309
311
  end
312
+ end
310
313
 
311
- def _map_collection(collection)
312
- return [] if collection.nil?
314
+ def _key(key)
315
+ @key_formatter ? @key_formatter.format(key) : key.to_s
316
+ end
313
317
 
314
- collection.map do |element|
315
- _scope { yield element }
316
- end
317
- end
318
+ def _format_keys(hash_or_array)
319
+ return hash_or_array unless @deep_format_keys
318
320
 
319
- def _scope
320
- parent_attributes, parent_formatter = @attributes, @key_formatter
321
- @attributes = ::ActiveSupport::OrderedHash.new
322
- yield
323
- @attributes
324
- ensure
325
- @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
326
327
  end
328
+ end
327
329
 
328
- def _merge(hash_or_array)
329
- if ::Array === hash_or_array
330
- @attributes = [] unless ::Array === @attributes
331
- @attributes.concat hash_or_array
332
- else
333
- @attributes.update hash_or_array
334
- end
335
- end
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
336
337
 
337
- def _two_arguments_map_collection(collection, &block)
338
- message = "Passing jbuilder object to block is " \
339
- "deprecated and will be removed soon."
338
+ def _map_collection(collection)
339
+ collection.map do |element|
340
+ _scope{ yield element }
341
+ end - [BLANK]
342
+ end
340
343
 
341
- if block.respond_to?(:parameters)
342
- arguments = block.parameters.map(&:last)
343
- actual = "|#{arguments.drop(1) * ', '}|"
344
- deprecated = "|#{arguments * ', '}|"
345
- message += "\nUse #{actual} instead of #{deprecated} as block arguments"
346
- end
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
347
352
 
348
- ::ActiveSupport::Deprecation.warn message, ::Kernel.caller(5)
353
+ def _is_collection?(object)
354
+ _object_respond_to?(object, :map, :count) && NON_ENUMERABLES.none?{ |klass| klass === object }
355
+ end
349
356
 
350
- _map_collection(collection){ |element| block[self, element] }
351
- end
357
+ def _blank?(value=@attributes)
358
+ BLANK == value
359
+ end
352
360
 
353
- def _mapable_arguments?(value, *args)
354
- value.respond_to?(:map)
355
- end
361
+ def _object_respond_to?(object, *methods)
362
+ methods.all?{ |m| object.respond_to?(m) }
363
+ end
356
364
  end
357
365
 
358
- require 'jbuilder/jbuilder_template' if defined?(ActionView::Template)
359
- require 'jbuilder/railtie' if defined?(Rails::VERSION::MAJOR) && Rails::VERSION::MAJOR == 4
366
+ require 'jbuilder/railtie' if defined?(Rails)
@@ -0,0 +1,72 @@
1
+ require 'test_helper'
2
+ require 'jbuilder/dependency_tracker'
3
+
4
+
5
+ class FakeTemplate
6
+ attr_reader :source, :handler
7
+ def initialize(source, handler = :jbuilder)
8
+ @source, @handler = source, handler
9
+ end
10
+ end
11
+
12
+
13
+ class JbuilderDependencyTrackerTest < ActiveSupport::TestCase
14
+ def make_tracker(name, source)
15
+ template = FakeTemplate.new(source)
16
+ Jbuilder::DependencyTracker.new(name, template)
17
+ end
18
+
19
+ def track_dependencies(source)
20
+ make_tracker('jbuilder_template', source).dependencies
21
+ end
22
+
23
+ test 'detects dependency via direct partial! call' do
24
+ dependencies = track_dependencies <<-RUBY
25
+ json.partial! 'path/to/partial', foo: bar
26
+ json.partial! 'path/to/another/partial', :fizz => buzz
27
+ RUBY
28
+
29
+ assert_equal %w[path/to/partial path/to/another/partial], dependencies
30
+ end
31
+
32
+ test 'detects dependency via direct partial! call with parens' do
33
+ dependencies = track_dependencies <<-RUBY
34
+ json.partial!("path/to/partial")
35
+ RUBY
36
+
37
+ assert_equal %w[path/to/partial], dependencies
38
+ end
39
+
40
+ test 'detects partial with options (1.9 style)' do
41
+ dependencies = track_dependencies <<-RUBY
42
+ json.partial! hello: 'world', partial: 'path/to/partial', foo: :bar
43
+ RUBY
44
+
45
+ assert_equal %w[path/to/partial], dependencies
46
+ end
47
+
48
+ test 'detects partial with options (1.8 style)' do
49
+ dependencies = track_dependencies <<-RUBY
50
+ json.partial! :hello => 'world', :partial => 'path/to/partial', :foo => :bar
51
+ RUBY
52
+
53
+ assert_equal %w[path/to/partial], dependencies
54
+ end
55
+
56
+ test 'detects partial in indirect collection calls' do
57
+ dependencies = track_dependencies <<-RUBY
58
+ json.comments @post.comments, partial: 'comments/comment', as: :comment
59
+ RUBY
60
+
61
+ assert_equal %w[comments/comment], dependencies
62
+ end
63
+
64
+ test 'detects explicit dependency' do
65
+ dependencies = track_dependencies <<-RUBY
66
+ # Template Dependency: path/to/partial
67
+ json.foo 'bar'
68
+ RUBY
69
+
70
+ assert_equal %w[path/to/partial], dependencies
71
+ end
72
+ end
@@ -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 /json\.array!\(@posts\) do \|post\|/, content
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 /json\.extract! @post, :id, :title, :body, :created_at, :updated_at/, content
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