jbuilder 2.0.6 → 2.11.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) 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 -12
  7. data/MIT-LICENSE +1 -1
  8. data/README.md +171 -45
  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 +20 -6
  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 +16 -20
  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 +1 -1
  27. data/lib/jbuilder/errors.rb +24 -0
  28. data/lib/jbuilder/jbuilder.rb +7 -0
  29. data/lib/jbuilder/jbuilder_template.rb +213 -65
  30. data/lib/jbuilder/key_formatter.rb +34 -0
  31. data/lib/jbuilder/railtie.rb +31 -6
  32. data/lib/jbuilder.rb +148 -114
  33. data/test/jbuilder_dependency_tracker_test.rb +3 -4
  34. data/test/jbuilder_generator_test.rb +31 -4
  35. data/test/jbuilder_template_test.rb +313 -195
  36. data/test/jbuilder_test.rb +615 -219
  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 +38 -23
  41. data/.travis.yml +0 -21
  42. data/CHANGELOG.md +0 -89
  43. data/Gemfile.old +0 -14
data/lib/jbuilder.rb CHANGED
@@ -1,51 +1,25 @@
1
- require 'active_support/core_ext/array/access'
2
- require 'active_support/core_ext/enumerable'
3
- require 'active_support/core_ext/hash'
4
- require 'multi_json'
5
-
6
- begin
7
- require 'active_support/proxy_object'
8
- JbuilderProxy = ActiveSupport::ProxyObject
9
- rescue LoadError
10
- require 'active_support/basic_object'
11
- JbuilderProxy = ActiveSupport::BasicObject
12
- end
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
- options = args.extract_options!
27
- args.each do |name|
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
- def initialize_copy(original)
36
- @cache = {}
37
- 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)
38
21
 
39
- def format(key)
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
- @@key_formatter = KeyFormatter.new
57
- @@ignore_nil = false
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
- result = if block
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
- _scope { yield self }
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 _mapable_arguments?(value, *args)
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
- _scope { extract! value, *args }
62
+ _merge_block(key){ extract! value, *args }
100
63
  end
101
64
 
102
65
  _set_value key, result
103
66
  end
104
67
 
105
- alias_method :method_missing, :set!
106
- private :method_missing
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 { yield self }
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
- # 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:
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
- @attributes = if block
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, *attributes)
246
+ _extract_hash_values(object, attributes)
251
247
  else
252
- _extract_method_values(object, *attributes)
248
+ _extract_method_values(object, attributes)
253
249
  end
254
250
  end
255
251
 
256
252
  def call(object, *attributes, &block)
257
- if block
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 array into current builder.
277
- def merge!(hash_or_array)
278
- if ::Array === hash_or_array
279
- @attributes = [] unless ::Array === @attributes
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
- ::MultiJson.dump(@attributes)
280
+ @attributes.to_json
289
281
  end
290
282
 
291
283
  private
292
284
 
293
- def _extract_hash_values(object, *attributes)
294
- attributes.each{ |key| _set_value key, object.fetch(key) }
295
- end
285
+ def _extract_hash_values(object, attributes)
286
+ attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
287
+ end
296
288
 
297
- def _extract_method_values(object, *attributes)
298
- attributes.each{ |key| _set_value key, object.public_send(key) }
299
- end
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
- def _set_value(key, value)
302
- raise NullError, key if @attributes.nil?
303
- unless @ignore_nil && value.nil?
304
- @attributes[@key_formatter.format(key)] = value
305
- 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
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
- def _map_collection(collection)
309
- return [] if collection.nil?
314
+ def _key(key)
315
+ @key_formatter ? @key_formatter.format(key) : key.to_s
316
+ end
310
317
 
311
- collection.map do |element|
312
- _scope { yield element }
313
- end
314
- end
318
+ def _format_keys(hash_or_array)
319
+ return hash_or_array unless @deep_format_keys
315
320
 
316
- def _scope
317
- parent_attributes, parent_formatter = @attributes, @key_formatter
318
- @attributes = {}
319
- yield
320
- @attributes
321
- ensure
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
- def _mapable_arguments?(value, *args)
326
- value.respond_to?(:map)
327
- 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
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/jbuilder_template' if defined?(ActionView::Template)
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 'test/unit'
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 collecton calls' do
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 depedency' do
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 /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