jbuilder 1.5.3 → 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 (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