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.
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
@@ -1,4 +1,5 @@
1
- require 'jbuilder'
1
+ require 'jbuilder/jbuilder'
2
+ require 'jbuilder/collection_renderer'
2
3
  require 'action_dispatch/http/mime_type'
3
4
  require 'active_support/cache'
4
5
 
@@ -9,34 +10,49 @@ class JbuilderTemplate < Jbuilder
9
10
 
10
11
  self.template_lookup_options = { handlers: [:jbuilder] }
11
12
 
12
- def initialize(context, *args, &block)
13
+ def initialize(context, *args)
13
14
  @context = context
14
- super(*args, &block)
15
+ @cached_root = nil
16
+ super(*args)
15
17
  end
16
18
 
17
- def partial!(name_or_options, locals = {})
18
- case name_or_options
19
- when ::Hash
20
- # partial! partial: 'name', locals: { foo: 'bar' }
21
- options = name_or_options
22
- else
23
- # partial! 'name', foo: 'bar'
24
- options = { partial: name_or_options, locals: locals }
25
- as = locals.delete(:as)
26
- options[:as] = as if as.present?
27
- options[:collection] = locals[:collection] if locals.key?(:collection)
28
- end
29
-
30
- _handle_partial_options options
31
- end
32
-
33
- def array!(collection = [], *attributes, &block)
34
- options = attributes.extract_options!
35
-
36
- if options.key?(:partial)
37
- partial! options[:partial], options.merge(collection: collection)
19
+ # Generates JSON using the template specified with the `:partial` option. For example, the code below will render
20
+ # the file `views/comments/_comments.json.jbuilder`, and set a local variable comments with all this message's
21
+ # comments, which can be used inside the partial.
22
+ #
23
+ # Example:
24
+ #
25
+ # json.partial! 'comments/comments', comments: @message.comments
26
+ #
27
+ # There are multiple ways to generate a collection of elements as JSON, as ilustrated below:
28
+ #
29
+ # Example:
30
+ #
31
+ # json.array! @posts, partial: 'posts/post', as: :post
32
+ #
33
+ # # or:
34
+ # json.partial! 'posts/post', collection: @posts, as: :post
35
+ #
36
+ # # or:
37
+ # json.partial! partial: 'posts/post', collection: @posts, as: :post
38
+ #
39
+ # # or:
40
+ # json.comments @post.comments, partial: 'comments/comment', as: :comment
41
+ #
42
+ # Aside from that, the `:cached` options is available on Rails >= 6.0. This will cache the rendered results
43
+ # effectively using the multi fetch feature.
44
+ #
45
+ # Example:
46
+ #
47
+ # json.array! @posts, partial: "posts/post", as: :post, cached: true
48
+ #
49
+ # json.comments @post.comments, partial: "comments/comment", as: :comment, cached: true
50
+ #
51
+ def partial!(*args)
52
+ if args.one? && _is_active_model?(args.first)
53
+ _render_active_model_partial args.first
38
54
  else
39
- super
55
+ _render_explicit_partial(*args)
40
56
  end
41
57
  end
42
58
 
@@ -48,9 +64,9 @@ class JbuilderTemplate < Jbuilder
48
64
  # json.cache! ['v1', @person], expires_in: 10.minutes do
49
65
  # json.extract! @person, :name, :age
50
66
  # end
51
- def cache!(key=nil, options={}, &block)
67
+ def cache!(key=nil, options={})
52
68
  if @context.controller.perform_caching
53
- value = ::Rails.cache.fetch(_cache_key(key, options), options) do
69
+ value = _cache_fragment_for(key, options) do
54
70
  _scope { yield self }
55
71
  end
56
72
 
@@ -60,7 +76,28 @@ class JbuilderTemplate < Jbuilder
60
76
  end
61
77
  end
62
78
 
63
- # Conditionally catches the json depending in the condition given as first parameter. Has the same
79
+ # Caches the json structure at the root using a string rather than the hash structure. This is considerably
80
+ # faster, but the drawback is that it only works, as the name hints, at the root. So you cannot
81
+ # use this approach to cache deeper inside the hierarchy, like in partials or such. Continue to use #cache! there.
82
+ #
83
+ # Example:
84
+ #
85
+ # json.cache_root! @person do
86
+ # json.extract! @person, :name, :age
87
+ # end
88
+ #
89
+ # # json.extra 'This will not work either, the root must be exclusive'
90
+ def cache_root!(key=nil, options={})
91
+ if @context.controller.perform_caching
92
+ raise "cache_root! can't be used after JSON structures have been defined" if @attributes.present?
93
+
94
+ @cached_root = _cache_fragment_for([ :root, key ], options) { yield; target! }
95
+ else
96
+ yield
97
+ end
98
+ end
99
+
100
+ # Conditionally caches the json depending in the condition given as first parameter. Has the same
64
101
  # signature as the `cache` helper method in `ActionView::Helpers::CacheHelper` and so can be used in
65
102
  # the same way.
66
103
  #
@@ -73,60 +110,171 @@ class JbuilderTemplate < Jbuilder
73
110
  condition ? cache!(*args, &block) : yield
74
111
  end
75
112
 
76
- protected
77
- def _handle_partial_options(options)
78
- options.reverse_merge! locals: {}
79
- options.reverse_merge! ::JbuilderTemplate.template_lookup_options
80
- as = options[:as]
113
+ def target!
114
+ @cached_root || super
115
+ end
116
+
117
+ def array!(collection = [], *args)
118
+ options = args.first
81
119
 
82
- if as && options.key?(:collection)
83
- collection = options.delete(:collection) || []
84
- array!(collection) do |member|
85
- options[:locals].merge! as => member
86
- options[:locals].merge! collection: collection
87
- _render_partial options
88
- end
89
- else
90
- _render_partial options
120
+ if args.one? && _partial_options?(options)
121
+ partial! options.merge(collection: collection)
122
+ else
123
+ super
124
+ end
125
+ end
126
+
127
+ def set!(name, object = BLANK, *args)
128
+ options = args.first
129
+
130
+ if args.one? && _partial_options?(options)
131
+ _set_inline_partial name, object, options
132
+ else
133
+ super
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ 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
142
+ as = options[:as]
143
+
144
+ if as && options.key?(:collection) && CollectionRenderer.supported?
145
+ collection = options.delete(:collection) || []
146
+ partial = options.delete(:partial)
147
+ options[:locals].merge!(json: self)
148
+
149
+ if options.has_key?(:layout)
150
+ raise ::NotImplementedError, "The `:layout' option is not supported in collection rendering."
151
+ end
152
+
153
+ if options.has_key?(:spacer_template)
154
+ raise ::NotImplementedError, "The `:spacer_template' option is not supported in collection rendering."
155
+ end
156
+
157
+ results = CollectionRenderer
158
+ .new(@context.lookup_context, options) { |&block| _scope(&block) }
159
+ .render_collection_with_partial(collection, partial, @context, nil)
160
+
161
+ array! if results.respond_to?(:body) && results.body.nil?
162
+ elsif as && options.key?(:collection) && !CollectionRenderer.supported?
163
+ # For Rails <= 5.2:
164
+ as = as.to_sym
165
+ collection = options.delete(:collection)
166
+ locals = options.delete(:locals)
167
+ array! collection do |member|
168
+ member_locals = locals.clone
169
+ member_locals.merge! collection: collection
170
+ member_locals.merge! as => member
171
+ _render_partial options.merge(locals: member_locals)
91
172
  end
173
+ else
174
+ _render_partial options
92
175
  end
176
+ end
93
177
 
94
- def _render_partial(options)
95
- options[:locals].merge! json: self
96
- @context.render options
178
+ def _render_partial(options)
179
+ options[:locals].merge! json: self
180
+ @context.render options
181
+ end
182
+
183
+ def _cache_fragment_for(key, options, &block)
184
+ key = _cache_key(key, options)
185
+ _read_fragment_cache(key, options) || _write_fragment_cache(key, options, &block)
186
+ end
187
+
188
+ def _read_fragment_cache(key, options = nil)
189
+ @context.controller.instrument_fragment_cache :read_fragment, key do
190
+ ::Rails.cache.read(key, options)
97
191
  end
192
+ end
98
193
 
99
- def _cache_key(key, options)
100
- if @context.respond_to?(:cache_fragment_name)
101
- # Current compatibility, fragment_name_with_digest is private again and cache_fragment_name
102
- # should be used instead.
103
- @context.cache_fragment_name(key, options)
104
- elsif @context.respond_to?(:fragment_name_with_digest)
105
- # Backwards compatibility for period of time when fragment_name_with_digest was made public.
106
- @context.fragment_name_with_digest(key)
107
- else
108
- ::ActiveSupport::Cache.expand_cache_key(key.is_a?(::Hash) ? url_for(key).split('://').last : key, :jbuilder)
194
+ def _write_fragment_cache(key, options = nil)
195
+ @context.controller.instrument_fragment_cache :write_fragment, key do
196
+ yield.tap do |value|
197
+ ::Rails.cache.write(key, value, options)
109
198
  end
110
199
  end
200
+ end
111
201
 
112
- private
202
+ def _cache_key(key, options)
203
+ name_options = options.slice(:skip_digest, :virtual_path)
204
+ key = _fragment_name_with_digest(key, name_options)
205
+
206
+ if @context.respond_to?(:combined_fragment_cache_key)
207
+ key = @context.combined_fragment_cache_key(key)
208
+ else
209
+ key = url_for(key).split('://', 2).last if ::Hash === key
210
+ end
211
+
212
+ ::ActiveSupport::Cache.expand_cache_key(key, :jbuilder)
213
+ end
214
+
215
+ def _fragment_name_with_digest(key, options)
216
+ if @context.respond_to?(:cache_fragment_name)
217
+ @context.cache_fragment_name(key, **options)
218
+ else
219
+ key
220
+ end
221
+ end
222
+
223
+ def _partial_options?(options)
224
+ ::Hash === options && options.key?(:as) && options.key?(:partial)
225
+ end
226
+
227
+ def _is_active_model?(object)
228
+ object.class.respond_to?(:model_name) && object.respond_to?(:to_partial_path)
229
+ end
113
230
 
114
- def _mapable_arguments?(value, *args)
115
- return true if super
116
- options = args.last
117
- ::Hash === options && options.key?(:as)
231
+ def _set_inline_partial(name, object, options)
232
+ value = if object.nil?
233
+ []
234
+ elsif _is_collection?(object)
235
+ _scope{ _render_partial_with_options options.merge(collection: object) }
236
+ else
237
+ locals = ::Hash[options[:as], object]
238
+ _scope{ _render_partial_with_options options.merge(locals: locals) }
118
239
  end
240
+
241
+ set! name, value
242
+ end
243
+
244
+ def _render_explicit_partial(name_or_options, locals = {})
245
+ case name_or_options
246
+ when ::Hash
247
+ # partial! partial: 'name', foo: 'bar'
248
+ options = name_or_options
249
+ else
250
+ # partial! 'name', locals: {foo: 'bar'}
251
+ if locals.one? && (locals.keys.first == :locals)
252
+ options = locals.merge(partial: name_or_options)
253
+ else
254
+ options = { partial: name_or_options, locals: locals }
255
+ end
256
+ # partial! 'name', foo: 'bar'
257
+ as = locals.delete(:as)
258
+ options[:as] = as if as.present?
259
+ options[:collection] = locals[:collection] if locals.key?(:collection)
260
+ end
261
+
262
+ _render_partial_with_options options
263
+ end
264
+
265
+ def _render_active_model_partial(object)
266
+ @context.render object, json: self
267
+ end
119
268
  end
120
269
 
121
270
  class JbuilderHandler
122
271
  cattr_accessor :default_format
123
- self.default_format = Mime::JSON
272
+ self.default_format = :json
124
273
 
125
- def self.call(template)
274
+ def self.call(template, source = nil)
275
+ source ||= template.source
126
276
  # this juggling is required to keep line numbers right in the error
127
- %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{template.source}
277
+ %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{source}
128
278
  json.target! unless (__already_defined && __already_defined != "method")}
129
279
  end
130
280
  end
131
-
132
- ActionView::Template.register_template_handler :jbuilder, JbuilderHandler
@@ -0,0 +1,34 @@
1
+ require 'jbuilder/jbuilder'
2
+ require 'active_support/core_ext/array'
3
+
4
+ class Jbuilder
5
+ 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)
20
+ @cache = {}
21
+ end
22
+
23
+ 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
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,11 +1,36 @@
1
- require 'rails/railtie'
1
+ require 'rails'
2
+ require 'jbuilder/jbuilder_template'
2
3
 
3
4
  class Jbuilder
4
5
  class Railtie < ::Rails::Railtie
5
- generators do |app|
6
- Rails::Generators.configure! app.config.generators
7
- Rails::Generators.hidden_namespaces.uniq!
8
- require 'generators/rails/scaffold_controller_generator'
6
+ initializer :jbuilder do
7
+ ActiveSupport.on_load :action_view do
8
+ ActionView::Template.register_template_handler :jbuilder, JbuilderHandler
9
+ require 'jbuilder/dependency_tracker'
10
+ end
11
+
12
+ if Rails::VERSION::MAJOR >= 5
13
+ module ::ActionController
14
+ module ApiRendering
15
+ include ActionView::Rendering
16
+ end
17
+ end
18
+
19
+ ActiveSupport.on_load :action_controller do
20
+ if self == ActionController::API
21
+ include ActionController::Helpers
22
+ include ActionController::ImplicitRender
23
+ end
24
+ end
25
+ end
26
+ end
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
9
34
  end
10
35
  end
11
- end
36
+ end