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
@@ -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