jbuilder 1.5.3 → 2.11.5

Sign up to get free protection for your applications and to get access to all the features.
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