jbuilder 2.12.0 → 2.14.0

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/devcontainer.json +25 -0
  3. data/.github/workflows/ruby.yml +14 -50
  4. data/Appraisals +14 -32
  5. data/CONTRIBUTING.md +1 -7
  6. data/Gemfile +2 -0
  7. data/README.md +38 -19
  8. data/Rakefile +2 -0
  9. data/bin/release +14 -0
  10. data/bin/test +6 -0
  11. data/gemfiles/rails_7_0.gemfile +1 -0
  12. data/gemfiles/{rails_6_1.gemfile → rails_7_2.gemfile} +1 -1
  13. data/gemfiles/{rails_6_0.gemfile → rails_8_0.gemfile} +1 -1
  14. data/jbuilder.gemspec +8 -4
  15. data/lib/generators/rails/jbuilder_generator.rb +2 -0
  16. data/lib/generators/rails/scaffold_controller_generator.rb +2 -0
  17. data/lib/generators/rails/templates/api_controller.rb +6 -0
  18. data/lib/generators/rails/templates/controller.rb +9 -3
  19. data/lib/jbuilder/blank.rb +2 -0
  20. data/lib/jbuilder/collection_renderer.rb +19 -77
  21. data/lib/jbuilder/errors.rb +3 -1
  22. data/lib/jbuilder/jbuilder.rb +3 -1
  23. data/lib/jbuilder/jbuilder_dependency_tracker.rb +2 -0
  24. data/lib/jbuilder/jbuilder_template.rb +36 -50
  25. data/lib/jbuilder/key_formatter.rb +19 -21
  26. data/lib/jbuilder/railtie.rb +15 -17
  27. data/lib/jbuilder/version.rb +5 -0
  28. data/lib/jbuilder.rb +38 -37
  29. data/test/jbuilder_generator_test.rb +6 -8
  30. data/test/jbuilder_template_test.rb +97 -77
  31. data/test/jbuilder_test.rb +7 -9
  32. data/test/scaffold_api_controller_generator_test.rb +52 -47
  33. data/test/scaffold_controller_generator_test.rb +34 -27
  34. data/test/test_helper.rb +1 -1
  35. metadata +16 -19
  36. data/gemfiles/rails_5_0.gemfile +0 -11
  37. data/gemfiles/rails_5_1.gemfile +0 -11
  38. data/gemfiles/rails_5_2.gemfile +0 -11
@@ -1,37 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
- require 'active_support/concern'
3
4
  require 'action_view'
4
-
5
- begin
6
- require 'action_view/renderer/collection_renderer'
7
- rescue LoadError
8
- require 'action_view/renderer/partial_renderer'
9
- end
5
+ require 'action_view/renderer/collection_renderer'
10
6
 
11
7
  class Jbuilder
12
- module CollectionRenderable # :nodoc:
13
- extend ActiveSupport::Concern
14
-
15
- class_methods do
16
- def supported?
17
- superclass.private_method_defined?(:build_rendered_template) && self.superclass.private_method_defined?(:build_rendered_collection)
18
- end
19
- end
20
-
21
- private
22
-
23
- def build_rendered_template(content, template, layout = nil)
24
- super(content || json.attributes!, template)
25
- end
26
-
27
- def build_rendered_collection(templates, _spacer)
28
- json.merge!(templates.map(&:body))
29
- end
30
-
31
- def json
32
- @options[:locals].fetch(:json)
33
- end
34
-
8
+ class CollectionRenderer < ::ActionView::CollectionRenderer # :nodoc:
35
9
  class ScopedIterator < ::SimpleDelegator # :nodoc:
36
10
  include Enumerable
37
11
 
@@ -40,16 +14,6 @@ class Jbuilder
40
14
  @scope = scope
41
15
  end
42
16
 
43
- # Rails 6.0 support:
44
- def each
45
- return enum_for(:each) unless block_given?
46
-
47
- __getobj__.each do |object|
48
- @scope.call { yield(object) }
49
- end
50
- end
51
-
52
- # Rails 6.1 support:
53
17
  def each_with_info
54
18
  return enum_for(:each_with_info) unless block_given?
55
19
 
@@ -60,51 +24,29 @@ class Jbuilder
60
24
  end
61
25
 
62
26
  private_constant :ScopedIterator
63
- end
64
-
65
- if defined?(::ActionView::CollectionRenderer)
66
- # Rails 6.1 support:
67
- class CollectionRenderer < ::ActionView::CollectionRenderer # :nodoc:
68
- include CollectionRenderable
69
27
 
70
- def initialize(lookup_context, options, &scope)
71
- super(lookup_context, options)
72
- @scope = scope
73
- end
74
-
75
- private
76
- def collection_with_template(view, template, layout, collection)
77
- super(view, template, layout, ScopedIterator.new(collection, @scope))
78
- end
28
+ def initialize(lookup_context, options, &scope)
29
+ super(lookup_context, options)
30
+ @scope = scope
79
31
  end
80
- else
81
- # Rails 6.0 support:
82
- class CollectionRenderer < ::ActionView::PartialRenderer # :nodoc:
83
- include CollectionRenderable
84
32
 
85
- def initialize(lookup_context, options, &scope)
86
- super(lookup_context)
87
- @options = options
88
- @scope = scope
89
- end
33
+ private
90
34
 
91
- def render_collection_with_partial(collection, partial, context, block)
92
- render(context, @options.merge(collection: collection, partial: partial), block)
35
+ def build_rendered_template(content, template, layout = nil)
36
+ super(content || json.attributes!, template)
93
37
  end
94
38
 
95
- private
96
- def collection_without_template(view)
97
- @collection = ScopedIterator.new(@collection, @scope)
98
-
99
- super(view)
100
- end
39
+ def build_rendered_collection(templates, _spacer)
40
+ json.merge!(templates.map(&:body))
41
+ end
101
42
 
102
- def collection_with_template(view, template)
103
- @collection = ScopedIterator.new(@collection, @scope)
43
+ def json
44
+ @options[:locals].fetch(:json)
45
+ end
104
46
 
105
- super(view, template)
106
- end
107
- end
47
+ def collection_with_template(view, template, layout, collection)
48
+ super(view, template, layout, ScopedIterator.new(collection, @scope))
49
+ end
108
50
  end
109
51
 
110
52
  class EnumerableCompat < ::SimpleDelegator
@@ -1,4 +1,6 @@
1
- require 'jbuilder/jbuilder'
1
+ # frozen_string_literal: true
2
+
3
+ require 'jbuilder/version'
2
4
 
3
5
  class Jbuilder
4
6
  class NullError < ::NoMethodError
@@ -1 +1,3 @@
1
- Jbuilder = Class.new(BasicObject)
1
+ # frozen_string_literal: true
2
+
3
+ require 'jbuilder/version'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Jbuilder::DependencyTracker
2
4
  EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'jbuilder/jbuilder'
2
4
  require 'jbuilder/collection_renderer'
3
5
  require 'action_dispatch/http/mime_type'
@@ -10,10 +12,11 @@ class JbuilderTemplate < Jbuilder
10
12
 
11
13
  self.template_lookup_options = { handlers: [:jbuilder] }
12
14
 
13
- def initialize(context, *args)
15
+ def initialize(context, options = nil)
14
16
  @context = context
15
17
  @cached_root = nil
16
- super(*args)
18
+
19
+ options.nil? ? super() : super(**options)
17
20
  end
18
21
 
19
22
  # Generates JSON using the template specified with the `:partial` option. For example, the code below will render
@@ -52,7 +55,9 @@ class JbuilderTemplate < Jbuilder
52
55
  if args.one? && _is_active_model?(args.first)
53
56
  _render_active_model_partial args.first
54
57
  else
55
- _render_explicit_partial(*args)
58
+ options = args.extract_options!.dup
59
+ options[:partial] = args.first if args.present?
60
+ _render_partial_with_options options
56
61
  end
57
62
  end
58
63
 
@@ -118,7 +123,9 @@ class JbuilderTemplate < Jbuilder
118
123
  options = args.first
119
124
 
120
125
  if args.one? && _partial_options?(options)
121
- partial! options.merge(collection: collection)
126
+ options = options.dup
127
+ options[:collection] = collection
128
+ _render_partial_with_options options
122
129
  else
123
130
  super
124
131
  end
@@ -128,7 +135,7 @@ class JbuilderTemplate < Jbuilder
128
135
  options = args.first
129
136
 
130
137
  if args.one? && _partial_options?(options)
131
- _set_inline_partial name, object, options
138
+ _set_inline_partial name, object, options.dup
132
139
  else
133
140
  super
134
141
  end
@@ -136,15 +143,17 @@ class JbuilderTemplate < Jbuilder
136
143
 
137
144
  private
138
145
 
146
+ alias_method :method_missing, :set!
147
+
139
148
  def _render_partial_with_options(options)
140
- options.reverse_merge! locals: options.except(:partial, :as, :collection, :cached)
141
- options.reverse_merge! ::JbuilderTemplate.template_lookup_options
149
+ options[:locals] ||= options.except(:partial, :as, :collection, :cached)
150
+ options[:handlers] ||= ::JbuilderTemplate.template_lookup_options[:handlers]
142
151
  as = options[:as]
143
152
 
144
- if as && options.key?(:collection) && CollectionRenderer.supported?
153
+ if as && options.key?(:collection)
145
154
  collection = options.delete(:collection) || []
146
155
  partial = options.delete(:partial)
147
- options[:locals].merge!(json: self)
156
+ options[:locals][:json] = self
148
157
  collection = EnumerableCompat.new(collection) if collection.respond_to?(:count) && !collection.respond_to?(:size)
149
158
 
150
159
  if options.has_key?(:layout)
@@ -155,21 +164,14 @@ class JbuilderTemplate < Jbuilder
155
164
  ::Kernel.raise ::NotImplementedError, "The `:spacer_template' option is not supported in collection rendering."
156
165
  end
157
166
 
158
- results = CollectionRenderer
159
- .new(@context.lookup_context, options) { |&block| _scope(&block) }
160
- .render_collection_with_partial(collection, partial, @context, nil)
161
-
162
- array! if results.respond_to?(:body) && results.body.nil?
163
- elsif as && options.key?(:collection) && !CollectionRenderer.supported?
164
- # For Rails <= 5.2:
165
- as = as.to_sym
166
- collection = options.delete(:collection)
167
- locals = options.delete(:locals)
168
- array! collection do |member|
169
- member_locals = locals.clone
170
- member_locals.merge! collection: collection
171
- member_locals.merge! as => member
172
- _render_partial options.merge(locals: member_locals)
167
+ if collection.present?
168
+ results = CollectionRenderer
169
+ .new(@context.lookup_context, options) { |&block| _scope(&block) }
170
+ .render_collection_with_partial(collection, partial, @context, nil)
171
+
172
+ array! if results.respond_to?(:body) && results.body.nil?
173
+ else
174
+ array!
173
175
  end
174
176
  else
175
177
  _render_partial options
@@ -177,7 +179,7 @@ class JbuilderTemplate < Jbuilder
177
179
  end
178
180
 
179
181
  def _render_partial(options)
180
- options[:locals].merge! json: self
182
+ options[:locals][:json] = self
181
183
  @context.render options
182
184
  end
183
185
 
@@ -233,34 +235,18 @@ class JbuilderTemplate < Jbuilder
233
235
  value = if object.nil?
234
236
  []
235
237
  elsif _is_collection?(object)
236
- _scope{ _render_partial_with_options options.merge(collection: object) }
237
- else
238
- locals = ::Hash[options[:as], object]
239
- _scope{ _render_partial_with_options options.merge(locals: locals) }
240
- end
241
-
242
- set! name, value
243
- end
244
-
245
- def _render_explicit_partial(name_or_options, locals = {})
246
- case name_or_options
247
- when ::Hash
248
- # partial! partial: 'name', foo: 'bar'
249
- options = name_or_options
238
+ _scope do
239
+ options[:collection] = object
240
+ _render_partial_with_options options
241
+ end
250
242
  else
251
- # partial! 'name', locals: {foo: 'bar'}
252
- if locals.one? && (locals.keys.first == :locals)
253
- options = locals.merge(partial: name_or_options)
254
- else
255
- options = { partial: name_or_options, locals: locals }
243
+ _scope do
244
+ options[:locals] = { options[:as] => object }
245
+ _render_partial_with_options options
256
246
  end
257
- # partial! 'name', foo: 'bar'
258
- as = locals.delete(:as)
259
- options[:as] = as if as.present?
260
- options[:collection] = locals[:collection] if locals.key?(:collection)
261
247
  end
262
248
 
263
- _render_partial_with_options options
249
+ _set_value name, value
264
250
  end
265
251
 
266
252
  def _render_active_model_partial(object)
@@ -275,7 +261,7 @@ class JbuilderHandler
275
261
  def self.call(template, source = nil)
276
262
  source ||= template.source
277
263
  # this juggling is required to keep line numbers right in the error
278
- %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{source}
264
+ %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{source};
279
265
  json.target! unless (__already_defined && __already_defined != "method")}
280
266
  end
281
267
  end
@@ -1,32 +1,30 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'jbuilder/jbuilder'
2
- require 'active_support/core_ext/array'
3
4
 
4
5
  class Jbuilder
5
6
  class KeyFormatter
6
- def initialize(*args)
7
- @format = {}
8
- @cache = {}
9
-
10
- options = args.extract_options!
11
- args.each do |name|
12
- @format[name] = []
13
- end
14
- options.each do |name, parameters|
15
- @format[name] = parameters
16
- end
17
- end
18
-
19
- def initialize_copy(original)
7
+ def initialize(*formats, **formats_with_options)
8
+ @mutex = Mutex.new
9
+ @formats = formats
10
+ @formats_with_options = formats_with_options
20
11
  @cache = {}
21
12
  end
22
13
 
23
14
  def format(key)
24
- @cache[key] ||= @format.inject(key.to_s) do |result, args|
25
- func, args = args
26
- if ::Proc === func
27
- func.call result, *args
28
- else
29
- result.send func, *args
15
+ @mutex.synchronize do
16
+ @cache[key] ||= begin
17
+ value = key.is_a?(Symbol) ? key.name : key.to_s
18
+
19
+ @formats.each do |func|
20
+ value = func.is_a?(Proc) ? func.call(value) : value.send(func)
21
+ end
22
+
23
+ @formats_with_options.each do |func, params|
24
+ value = func.is_a?(Proc) ? func.call(value, *params) : value.send(func, *params)
25
+ end
26
+
27
+ value
30
28
  end
31
29
  end
32
30
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rails'
2
4
  require 'jbuilder/jbuilder_template'
3
5
 
@@ -9,28 +11,24 @@ class Jbuilder
9
11
  require 'jbuilder/jbuilder_dependency_tracker'
10
12
  end
11
13
 
12
- if Rails::VERSION::MAJOR >= 5
13
- module ::ActionController
14
- module ApiRendering
15
- include ActionView::Rendering
16
- end
14
+ module ::ActionController
15
+ module ApiRendering
16
+ include ActionView::Rendering
17
17
  end
18
+ end
18
19
 
19
- ActiveSupport.on_load :action_controller do
20
- if name == 'ActionController::API'
21
- include ActionController::Helpers
22
- include ActionController::ImplicitRender
23
- end
24
- end
20
+ ActiveSupport.on_load :action_controller_api do
21
+ include ActionController::Helpers
22
+ include ActionController::ImplicitRender
23
+ helper_method :combined_fragment_cache_key
24
+ helper_method :view_cache_dependencies
25
25
  end
26
26
  end
27
27
 
28
- if Rails::VERSION::MAJOR >= 4
29
- generators do |app|
30
- Rails::Generators.configure! app.config.generators
31
- Rails::Generators.hidden_namespaces.uniq!
32
- require 'generators/rails/scaffold_controller_generator'
33
- end
28
+ generators do |app|
29
+ Rails::Generators.configure! app.config.generators
30
+ Rails::Generators.hidden_namespaces.uniq!
31
+ require 'generators/rails/scaffold_controller_generator'
34
32
  end
35
33
  end
36
34
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jbuilder < BasicObject
4
+ VERSION = "2.14.0"
5
+ end
data/lib/jbuilder.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support'
2
4
  require 'jbuilder/jbuilder'
3
5
  require 'jbuilder/blank'
@@ -5,24 +7,24 @@ require 'jbuilder/key_formatter'
5
7
  require 'jbuilder/errors'
6
8
  require 'json'
7
9
  require 'active_support/core_ext/hash/deep_merge'
8
- begin
9
- require 'ostruct'
10
- rescue LoadError
11
- end
12
10
 
13
11
  class Jbuilder
14
12
  @@key_formatter = nil
15
13
  @@ignore_nil = false
16
14
  @@deep_format_keys = false
17
15
 
18
- def initialize(options = {})
16
+ def initialize(
17
+ key_formatter: @@key_formatter,
18
+ ignore_nil: @@ignore_nil,
19
+ deep_format_keys: @@deep_format_keys,
20
+ &block
21
+ )
19
22
  @attributes = {}
23
+ @key_formatter = key_formatter
24
+ @ignore_nil = ignore_nil
25
+ @deep_format_keys = deep_format_keys
20
26
 
21
- @key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil}
22
- @ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
23
- @deep_format_keys = options.fetch(:deep_format_keys, @@deep_format_keys)
24
-
25
- yield self if ::Kernel.block_given?
27
+ yield self if block
26
28
  end
27
29
 
28
30
  # Yields a builder and automatically turns the result into a JSON string
@@ -31,7 +33,6 @@ class Jbuilder
31
33
  end
32
34
 
33
35
  BLANK = Blank.new
34
- NON_ENUMERABLES = defined?(::OpenStruct) ? [::Struct, ::OpenStruct].to_set : [::Struct].to_set
35
36
 
36
37
  def set!(key, value = BLANK, *args, &block)
37
38
  result = if ::Kernel.block_given?
@@ -62,20 +63,12 @@ class Jbuilder
62
63
  else
63
64
  # json.author @post.creator, :name, :email_address
64
65
  # { "author": { "name": "David", "email_address": "david@loudthinking.com" } }
65
- _merge_block(key){ extract! value, *args }
66
+ _merge_block(key){ _extract value, args }
66
67
  end
67
68
 
68
69
  _set_value key, result
69
70
  end
70
71
 
71
- def method_missing(*args, &block)
72
- if ::Kernel.block_given?
73
- set!(*args, &block)
74
- else
75
- set!(*args)
76
- end
77
- end
78
-
79
72
  # Specifies formatting to be applied to the key. Passing in a name of a function
80
73
  # will cause that function to be called on the key. So :upcase will upper case
81
74
  # the key. You can also pass in lambdas for more complex transformations.
@@ -104,13 +97,13 @@ class Jbuilder
104
97
  #
105
98
  # { "_first_name": "David" }
106
99
  #
107
- def key_format!(*args)
108
- @key_formatter = KeyFormatter.new(*args)
100
+ def key_format!(...)
101
+ @key_formatter = KeyFormatter.new(...)
109
102
  end
110
103
 
111
104
  # Same as the instance method key_format! except sets the default.
112
- def self.key_format(*args)
113
- @@key_formatter = KeyFormatter.new(*args)
105
+ def self.key_format(...)
106
+ @@key_formatter = KeyFormatter.new(...)
114
107
  end
115
108
 
116
109
  # If you want to skip adding nil values to your JSON hash. This is useful
@@ -219,7 +212,7 @@ class Jbuilder
219
212
  elsif ::Kernel.block_given?
220
213
  _map_collection(collection, &block)
221
214
  elsif attributes.any?
222
- _map_collection(collection) { |element| extract! element, *attributes }
215
+ _map_collection(collection) { |element| _extract element, attributes }
223
216
  else
224
217
  _format_keys(collection.to_a)
225
218
  end
@@ -245,18 +238,14 @@ class Jbuilder
245
238
  #
246
239
  # json.(@person, :name, :age)
247
240
  def extract!(object, *attributes)
248
- if ::Hash === object
249
- _extract_hash_values(object, attributes)
250
- else
251
- _extract_method_values(object, attributes)
252
- end
241
+ _extract object, attributes
253
242
  end
254
243
 
255
244
  def call(object, *attributes, &block)
256
245
  if ::Kernel.block_given?
257
246
  array! object, &block
258
247
  else
259
- extract! object, *attributes
248
+ _extract object, attributes
260
249
  end
261
250
  end
262
251
 
@@ -285,6 +274,16 @@ class Jbuilder
285
274
 
286
275
  private
287
276
 
277
+ alias_method :method_missing, :set!
278
+
279
+ def _extract(object, attributes)
280
+ if ::Hash === object
281
+ _extract_hash_values(object, attributes)
282
+ else
283
+ _extract_method_values(object, attributes)
284
+ end
285
+ end
286
+
288
287
  def _extract_hash_values(object, attributes)
289
288
  attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
290
289
  end
@@ -315,7 +314,13 @@ class Jbuilder
315
314
  end
316
315
 
317
316
  def _key(key)
318
- @key_formatter ? @key_formatter.format(key) : key.to_s
317
+ if @key_formatter
318
+ @key_formatter.format(key)
319
+ elsif key.is_a?(::Symbol)
320
+ key.name
321
+ else
322
+ key.to_s
323
+ end
319
324
  end
320
325
 
321
326
  def _format_keys(hash_or_array)
@@ -354,16 +359,12 @@ class Jbuilder
354
359
  end
355
360
 
356
361
  def _is_collection?(object)
357
- _object_respond_to?(object, :map, :count) && NON_ENUMERABLES.none?{ |klass| klass === object }
362
+ object.respond_to?(:map) && object.respond_to?(:count) && !(::Struct === object)
358
363
  end
359
364
 
360
365
  def _blank?(value=@attributes)
361
366
  BLANK == value
362
367
  end
363
-
364
- def _object_respond_to?(object, *methods)
365
- methods.all?{ |m| object.respond_to?(m) }
366
- end
367
368
  end
368
369
 
369
370
  require 'jbuilder/railtie' if defined?(Rails)
@@ -56,15 +56,13 @@ class JbuilderGeneratorTest < Rails::Generators::TestCase
56
56
  end
57
57
  end
58
58
 
59
- if Rails::VERSION::MAJOR >= 6
60
- test 'handles virtual attributes' do
61
- run_generator %w(Message content:rich_text video:attachment photos:attachments)
59
+ test 'handles virtual attributes' do
60
+ run_generator %w(Message content:rich_text video:attachment photos:attachments)
62
61
 
63
- assert_file 'app/views/messages/_message.json.jbuilder' do |content|
64
- assert_match %r{json\.content message\.content\.to_s}, content
65
- assert_match %r{json\.video url_for\(message\.video\)}, content
66
- 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
67
- end
62
+ assert_file 'app/views/messages/_message.json.jbuilder' do |content|
63
+ assert_match %r{json\.content message\.content\.to_s}, content
64
+ assert_match %r{json\.video url_for\(message\.video\)}, content
65
+ 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
68
66
  end
69
67
  end
70
68
  end