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
@@ -1,43 +1,58 @@
1
- require 'jbuilder'
1
+ require 'jbuilder/jbuilder'
2
+ require 'jbuilder/collection_renderer'
2
3
  require 'action_dispatch/http/mime_type'
4
+ require 'active_support/cache'
3
5
 
4
6
  class JbuilderTemplate < Jbuilder
5
7
  class << self
6
8
  attr_accessor :template_lookup_options
7
9
  end
8
10
 
9
- self.template_lookup_options = { :handlers => [:jbuilder] }
11
+ self.template_lookup_options = { handlers: [:jbuilder] }
10
12
 
11
- def initialize(context, *args, &block)
13
+ def initialize(context, *args)
12
14
  @context = context
13
- super(*args, &block)
15
+ @cached_root = nil
16
+ super(*args)
14
17
  end
15
18
 
16
- def partial!(name_or_options, locals = {})
17
- case name_or_options
18
- when ::Hash
19
- # partial! partial: 'name', locals: { foo: 'bar' }
20
- options = name_or_options
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
21
54
  else
22
- # partial! 'name', foo: 'bar'
23
- options = { :partial => name_or_options, :locals => locals }
24
- as = locals.delete(:as)
25
- options[:as] = as if as.present?
26
- options[:collection] = locals[:collection] if locals.key?(:collection)
27
- end
28
-
29
- options[:collection] ||= [] if options.key?(:collection)
30
-
31
- _handle_partial_options options
32
- end
33
-
34
- def array!(collection = [], *attributes, &block)
35
- options = attributes.extract_options!
36
-
37
- if options.key?(:partial)
38
- partial! options[:partial], options.merge(:collection => collection)
39
- else
40
- super
55
+ _render_explicit_partial(*args)
41
56
  end
42
57
  end
43
58
 
@@ -49,71 +64,217 @@ class JbuilderTemplate < Jbuilder
49
64
  # json.cache! ['v1', @person], expires_in: 10.minutes do
50
65
  # json.extract! @person, :name, :age
51
66
  # end
52
- def cache!(key=nil, options={}, &block)
67
+ def cache!(key=nil, options={})
53
68
  if @context.controller.perform_caching
54
- value = ::Rails.cache.fetch(_cache_key(key), options) do
69
+ value = _cache_fragment_for(key, options) do
55
70
  _scope { yield self }
56
71
  end
57
72
 
58
- _merge(value)
73
+ merge! value
59
74
  else
60
75
  yield
61
76
  end
62
77
  end
63
78
 
64
- protected
65
- def _handle_partial_options(options)
66
- options.reverse_merge! :locals => {}
67
- options.reverse_merge! ::JbuilderTemplate.template_lookup_options
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
101
+ # signature as the `cache` helper method in `ActionView::Helpers::CacheHelper` and so can be used in
102
+ # the same way.
103
+ #
104
+ # Example:
105
+ #
106
+ # json.cache_if! !admin?, @person, expires_in: 10.minutes do
107
+ # json.extract! @person, :name, :age
108
+ # end
109
+ def cache_if!(condition, *args, &block)
110
+ condition ? cache!(*args, &block) : yield
111
+ end
112
+
113
+ def target!
114
+ @cached_root || super
115
+ end
116
+
117
+ def array!(collection = [], *args)
118
+ options = args.first
119
+
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
68
165
  collection = options.delete(:collection)
69
- as = options[:as]
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)
172
+ end
173
+ else
174
+ _render_partial options
175
+ end
176
+ end
70
177
 
71
- if collection && as
72
- array!(collection) do |member|
73
- options[:locals].merge!(as => member, :collection => collection)
74
- _render_partial options
75
- end
76
- else
77
- _render_partial 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)
191
+ end
192
+ end
193
+
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)
78
198
  end
79
199
  end
200
+ end
201
+
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
80
214
 
81
- def _render_partial(options)
82
- options[:locals].merge!(:json => self)
83
- @context.render options
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
84
220
  end
221
+ end
85
222
 
86
- def _cache_key(key)
87
- if @context.respond_to?(:cache_fragment_name)
88
- # Current compatibility, fragment_name_with_digest is private again and cache_fragment_name
89
- # should be used instead.
90
- @context.cache_fragment_name(key)
91
- elsif @context.respond_to?(:fragment_name_with_digest)
92
- # Backwards compatibility for period of time when fragment_name_with_digest was made public.
93
- @context.fragment_name_with_digest(key)
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
230
+
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) }
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)
94
253
  else
95
- ::ActiveSupport::Cache.expand_cache_key(key.is_a?(::Hash) ? url_for(key).split('://').last : key, :jbuilder)
254
+ options = { partial: name_or_options, locals: locals }
96
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)
97
260
  end
98
261
 
99
- private
262
+ _render_partial_with_options options
263
+ end
100
264
 
101
- def _mapable_arguments?(value, *args)
102
- return true if super
103
- options = args.last
104
- ::Hash === options && options.key?(:as)
105
- end
265
+ def _render_active_model_partial(object)
266
+ @context.render object, json: self
267
+ end
106
268
  end
107
269
 
108
270
  class JbuilderHandler
109
271
  cattr_accessor :default_format
110
- self.default_format = Mime::JSON
272
+ self.default_format = :json
111
273
 
112
- def self.call(template)
274
+ def self.call(template, source = nil)
275
+ source ||= template.source
113
276
  # this juggling is required to keep line numbers right in the error
114
- %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{template.source}
115
- json.target! unless __already_defined}
277
+ %{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{source}
278
+ json.target! unless (__already_defined && __already_defined != "method")}
116
279
  end
117
280
  end
118
-
119
- 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