actionview 7.1.3.4 → 7.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -422
  3. data/lib/action_view/base.rb +20 -3
  4. data/lib/action_view/cache_expiry.rb +9 -3
  5. data/lib/action_view/dependency_tracker/{ripper_tracker.rb → ruby_tracker.rb} +4 -3
  6. data/lib/action_view/dependency_tracker.rb +1 -1
  7. data/lib/action_view/gem_version.rb +3 -3
  8. data/lib/action_view/helpers/asset_tag_helper.rb +15 -3
  9. data/lib/action_view/helpers/cache_helper.rb +4 -4
  10. data/lib/action_view/helpers/csrf_helper.rb +1 -1
  11. data/lib/action_view/helpers/form_helper.rb +197 -192
  12. data/lib/action_view/helpers/form_tag_helper.rb +76 -43
  13. data/lib/action_view/helpers/output_safety_helper.rb +4 -4
  14. data/lib/action_view/helpers/tag_helper.rb +208 -18
  15. data/lib/action_view/helpers/url_helper.rb +4 -78
  16. data/lib/action_view/layouts.rb +2 -4
  17. data/lib/action_view/log_subscriber.rb +8 -4
  18. data/lib/action_view/railtie.rb +0 -1
  19. data/lib/action_view/render_parser/prism_render_parser.rb +127 -0
  20. data/lib/action_view/{ripper_ast_parser.rb → render_parser/ripper_render_parser.rb} +152 -9
  21. data/lib/action_view/render_parser.rb +21 -169
  22. data/lib/action_view/renderer/abstract_renderer.rb +1 -1
  23. data/lib/action_view/renderer/renderer.rb +32 -38
  24. data/lib/action_view/rendering.rb +4 -4
  25. data/lib/action_view/template/renderable.rb +7 -1
  26. data/lib/action_view/template/resolver.rb +0 -2
  27. data/lib/action_view/template.rb +28 -4
  28. data/lib/action_view/test_case.rb +12 -14
  29. data/lib/action_view/unbound_template.rb +4 -4
  30. data/lib/action_view.rb +1 -1
  31. metadata +16 -15
@@ -18,7 +18,7 @@ module ActionView
18
18
  info do
19
19
  message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
20
20
  message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
21
- message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
21
+ message << " (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
22
22
  end
23
23
  end
24
24
  subscribe_log_level :render_template, :debug
@@ -27,7 +27,7 @@ module ActionView
27
27
  debug do
28
28
  message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
29
29
  message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
30
- message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
30
+ message << " (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
31
31
  message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil?
32
32
  message
33
33
  end
@@ -37,7 +37,7 @@ module ActionView
37
37
  def render_layout(event)
38
38
  info do
39
39
  message = +" Rendered layout #{from_rails_root(event.payload[:identifier])}"
40
- message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
40
+ message << " (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
41
41
  end
42
42
  end
43
43
  subscribe_log_level :render_layout, :info
@@ -48,7 +48,7 @@ module ActionView
48
48
  debug do
49
49
  message = +" Rendered collection of #{from_rails_root(identifier)}"
50
50
  message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
51
- message << " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
51
+ message << " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
52
52
  message
53
53
  end
54
54
  end
@@ -96,6 +96,10 @@ module ActionView
96
96
 
97
97
  def finish(name, id, payload)
98
98
  end
99
+
100
+ def silenced?(_)
101
+ logger.nil? || !logger.debug?
102
+ end
99
103
  end
100
104
 
101
105
  def self.attach_to(*)
@@ -116,7 +116,6 @@ module ActionView
116
116
  view_reloader = ActionView::CacheExpiry::ViewReloader.new(watcher: app.config.file_watcher)
117
117
 
118
118
  app.reloaders << view_reloader
119
- view_reloader.execute
120
119
  app.reloader.to_run do
121
120
  require_unload_lock!
122
121
  view_reloader.execute
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module RenderParser
5
+ class PrismRenderParser < Base # :nodoc:
6
+ def render_calls
7
+ queue = [Prism.parse(@code).value]
8
+ templates = []
9
+
10
+ while (node = queue.shift)
11
+ queue.concat(node.compact_child_nodes)
12
+ next unless node.is_a?(Prism::CallNode)
13
+
14
+ options = render_call_options(node)
15
+ next unless options
16
+
17
+ render_type = (options.keys & RENDER_TYPE_KEYS)[0]
18
+ template, object_template = render_call_template(options[render_type])
19
+ next unless template
20
+
21
+ if options.key?(:object) || options.key?(:collection) || object_template
22
+ next if options.key?(:object) && options.key?(:collection)
23
+ next unless options.key?(:partial)
24
+ end
25
+
26
+ if options[:spacer_template].is_a?(Prism::StringNode)
27
+ templates << partial_to_virtual_path(:partial, options[:spacer_template].unescaped)
28
+ end
29
+
30
+ templates << partial_to_virtual_path(render_type, template)
31
+
32
+ if render_type != :layout && options[:layout].is_a?(Prism::StringNode)
33
+ templates << partial_to_virtual_path(:layout, options[:layout].unescaped)
34
+ end
35
+ end
36
+
37
+ templates
38
+ end
39
+
40
+ private
41
+ # Accept a call node and return a hash of options for the render call.
42
+ # If it doesn't match the expected format, return nil.
43
+ def render_call_options(node)
44
+ # We are only looking for calls to render or render_to_string.
45
+ name = node.name.to_sym
46
+ return if name != :render && name != :render_to_string
47
+
48
+ # We are only looking for calls with arguments.
49
+ arguments = node.arguments
50
+ return unless arguments
51
+
52
+ arguments = arguments.arguments
53
+ length = arguments.length
54
+
55
+ # Get rid of any parentheses to get directly to the contents.
56
+ arguments.map! do |argument|
57
+ current = argument
58
+
59
+ while current.is_a?(Prism::ParenthesesNode) &&
60
+ current.body.is_a?(Prism::StatementsNode) &&
61
+ current.body.body.length == 1
62
+ current = current.body.body.first
63
+ end
64
+
65
+ current
66
+ end
67
+
68
+ # We are only looking for arguments that are either a string with an
69
+ # array of locals or a keyword hash with symbol keys.
70
+ options =
71
+ if (length == 1 || length == 2) && !arguments[0].is_a?(Prism::KeywordHashNode)
72
+ { partial: arguments[0], locals: arguments[1] }
73
+ elsif length == 1 &&
74
+ arguments[0].is_a?(Prism::KeywordHashNode) &&
75
+ arguments[0].elements.all? do |element|
76
+ element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode)
77
+ end
78
+ arguments[0].elements.to_h do |element|
79
+ [element.key.unescaped.to_sym, element.value]
80
+ end
81
+ end
82
+
83
+ return unless options
84
+
85
+ # Here we validate that the options have the keys we expect.
86
+ keys = options.keys
87
+ return if !keys.intersect?(RENDER_TYPE_KEYS)
88
+ return if (keys - ALL_KNOWN_KEYS).any?
89
+
90
+ # Finally, we can return a valid set of options.
91
+ options
92
+ end
93
+
94
+ # Accept the node that is being passed in the position of the template
95
+ # and return the template name and whether or not it is an object
96
+ # template.
97
+ def render_call_template(node)
98
+ object_template = false
99
+ template =
100
+ if node.is_a?(Prism::StringNode)
101
+ path = node.unescaped
102
+ path.include?("/") ? path : "#{directory}/#{path}"
103
+ else
104
+ dependency =
105
+ case node.type
106
+ when :class_variable_read_node
107
+ node.slice[2..]
108
+ when :instance_variable_read_node
109
+ node.slice[1..]
110
+ when :global_variable_read_node
111
+ node.slice[1..]
112
+ when :local_variable_read_node
113
+ node.slice
114
+ when :call_node
115
+ node.name.to_s
116
+ else
117
+ return
118
+ end
119
+
120
+ "#{dependency.pluralize}/#{dependency.singularize}"
121
+ end
122
+
123
+ [template, object_template]
124
+ end
125
+ end
126
+ end
127
+ end
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ripper"
4
-
5
3
  module ActionView
6
- class RenderParser
7
- module RipperASTParser # :nodoc:
4
+ module RenderParser
5
+ class RipperRenderParser < Base # :nodoc:
8
6
  class Node < ::Array # :nodoc:
9
7
  attr_reader :type
10
8
 
@@ -183,16 +181,161 @@ module ActionView
183
181
  end
184
182
  end
185
183
 
186
- extend self
187
-
188
- def parse_render_nodes(code)
189
- parser = RenderCallExtractor.new(code)
184
+ def render_calls
185
+ parser = RenderCallExtractor.new(@code)
190
186
  parser.parse
191
187
 
192
188
  parser.render_calls.group_by(&:first).to_h do |method, nodes|
193
189
  [ method.to_sym, nodes.collect { |v| v[1] } ]
194
- end
190
+ end.map do |method, nodes|
191
+ nodes.map { |n| parse_render(n) }
192
+ end.flatten.compact
195
193
  end
194
+
195
+ private
196
+ def resolve_path_directory(path)
197
+ if path.include?("/")
198
+ path
199
+ else
200
+ "#{directory}/#{path}"
201
+ end
202
+ end
203
+
204
+ # Convert
205
+ # render("foo", ...)
206
+ # into either
207
+ # render(template: "foo", ...)
208
+ # or
209
+ # render(partial: "foo", ...)
210
+ def normalize_args(string, options_hash)
211
+ if options_hash
212
+ { partial: string, locals: options_hash }
213
+ else
214
+ { partial: string }
215
+ end
216
+ end
217
+
218
+ def parse_render(node)
219
+ node = node.argument_nodes
220
+
221
+ if (node.length == 1 || node.length == 2) && !node[0].hash?
222
+ if node.length == 1
223
+ options = normalize_args(node[0], nil)
224
+ elsif node.length == 2
225
+ options = normalize_args(node[0], node[1])
226
+ end
227
+
228
+ return nil unless options
229
+
230
+ parse_render_from_options(options)
231
+ elsif node.length == 1 && node[0].hash?
232
+ options = parse_hash_to_symbols(node[0])
233
+
234
+ return nil unless options
235
+
236
+ parse_render_from_options(options)
237
+ else
238
+ nil
239
+ end
240
+ end
241
+
242
+ def parse_hash(node)
243
+ node.hash? && node.to_hash
244
+ end
245
+
246
+ def parse_hash_to_symbols(node)
247
+ hash = parse_hash(node)
248
+
249
+ return unless hash
250
+
251
+ hash.transform_keys do |key_node|
252
+ key = parse_sym(key_node)
253
+
254
+ return unless key
255
+
256
+ key
257
+ end
258
+ end
259
+
260
+ def parse_render_from_options(options_hash)
261
+ renders = []
262
+ keys = options_hash.keys
263
+
264
+ if (keys & RENDER_TYPE_KEYS).size < 1
265
+ # Must have at least one of render keys
266
+ return nil
267
+ end
268
+
269
+ if (keys - ALL_KNOWN_KEYS).any?
270
+ # de-opt in case of unknown option
271
+ return nil
272
+ end
273
+
274
+ render_type = (keys & RENDER_TYPE_KEYS)[0]
275
+
276
+ node = options_hash[render_type]
277
+
278
+ if node.string?
279
+ template = resolve_path_directory(node.to_string)
280
+ else
281
+ if node.variable_reference?
282
+ dependency = node.variable_name.sub(/\A(?:\$|@{1,2})/, "")
283
+ elsif node.vcall?
284
+ dependency = node.variable_name
285
+ elsif node.call?
286
+ dependency = node.call_method_name
287
+ else
288
+ return
289
+ end
290
+
291
+ object_template = true
292
+ template = "#{dependency.pluralize}/#{dependency.singularize}"
293
+ end
294
+
295
+ return unless template
296
+
297
+ if spacer_template = render_template_with_spacer?(options_hash)
298
+ virtual_path = partial_to_virtual_path(:partial, spacer_template)
299
+ renders << virtual_path
300
+ end
301
+
302
+ if options_hash.key?(:object) || options_hash.key?(:collection) || object_template
303
+ return nil if options_hash.key?(:object) && options_hash.key?(:collection)
304
+ return nil unless options_hash.key?(:partial)
305
+ end
306
+
307
+ virtual_path = partial_to_virtual_path(render_type, template)
308
+ renders << virtual_path
309
+
310
+ # Support for rendering multiple templates (i.e. a partial with a layout)
311
+ if layout_template = render_template_with_layout?(render_type, options_hash)
312
+ virtual_path = partial_to_virtual_path(:layout, layout_template)
313
+
314
+ renders << virtual_path
315
+ end
316
+
317
+ renders
318
+ end
319
+
320
+ def parse_str(node)
321
+ node.string? && node.to_string
322
+ end
323
+
324
+ def parse_sym(node)
325
+ node.symbol? && node.to_symbol
326
+ end
327
+
328
+ def render_template_with_layout?(render_type, options_hash)
329
+ if render_type != :layout && options_hash.key?(:layout)
330
+ parse_str(options_hash[:layout])
331
+ end
332
+ end
333
+
334
+ def render_template_with_spacer?(options_hash)
335
+ if options_hash.key?(:spacer_template)
336
+ parse_str(options_hash[:spacer_template])
337
+ end
338
+ end
196
339
  end
197
340
  end
198
341
  end
@@ -1,176 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_view/ripper_ast_parser"
4
-
5
3
  module ActionView
6
- class RenderParser # :nodoc:
7
- def initialize(name, code)
8
- @name = name
9
- @code = code
10
- @parser = RipperASTParser
11
- end
12
-
13
- def render_calls
14
- render_nodes = @parser.parse_render_nodes(@code)
15
-
16
- render_nodes.map do |method, nodes|
17
- nodes.map { |n| send(:parse_render, n) }
18
- end.flatten.compact
19
- end
4
+ module RenderParser # :nodoc:
5
+ ALL_KNOWN_KEYS = [:partial, :template, :layout, :formats, :locals, :object, :collection, :as, :status, :content_type, :location, :spacer_template]
6
+ RENDER_TYPE_KEYS = [:partial, :template, :layout]
20
7
 
21
- private
22
- def directory
23
- File.dirname(@name)
24
- end
25
-
26
- def resolve_path_directory(path)
27
- if path.include?("/")
28
- path
29
- else
30
- "#{directory}/#{path}"
31
- end
32
- end
33
-
34
- # Convert
35
- # render("foo", ...)
36
- # into either
37
- # render(template: "foo", ...)
38
- # or
39
- # render(partial: "foo", ...)
40
- def normalize_args(string, options_hash)
41
- if options_hash
42
- { partial: string, locals: options_hash }
43
- else
44
- { partial: string }
45
- end
46
- end
47
-
48
- def parse_render(node)
49
- node = node.argument_nodes
50
-
51
- if (node.length == 1 || node.length == 2) && !node[0].hash?
52
- if node.length == 1
53
- options = normalize_args(node[0], nil)
54
- elsif node.length == 2
55
- options = normalize_args(node[0], node[1])
56
- end
57
-
58
- return nil unless options
59
-
60
- parse_render_from_options(options)
61
- elsif node.length == 1 && node[0].hash?
62
- options = parse_hash_to_symbols(node[0])
63
-
64
- return nil unless options
65
-
66
- parse_render_from_options(options)
67
- else
68
- nil
69
- end
70
- end
71
-
72
- def parse_hash(node)
73
- node.hash? && node.to_hash
74
- end
75
-
76
- def parse_hash_to_symbols(node)
77
- hash = parse_hash(node)
78
-
79
- return unless hash
80
-
81
- hash.transform_keys do |key_node|
82
- key = parse_sym(key_node)
83
-
84
- return unless key
85
-
86
- key
87
- end
88
- end
89
-
90
- ALL_KNOWN_KEYS = [:partial, :template, :layout, :formats, :locals, :object, :collection, :as, :status, :content_type, :location, :spacer_template]
91
-
92
- RENDER_TYPE_KEYS =
93
- [:partial, :template, :layout]
94
-
95
- def parse_render_from_options(options_hash)
96
- renders = []
97
- keys = options_hash.keys
98
-
99
- if (keys & RENDER_TYPE_KEYS).size < 1
100
- # Must have at least one of render keys
101
- return nil
102
- end
103
-
104
- if (keys - ALL_KNOWN_KEYS).any?
105
- # de-opt in case of unknown option
106
- return nil
107
- end
108
-
109
- render_type = (keys & RENDER_TYPE_KEYS)[0]
110
-
111
- node = options_hash[render_type]
112
-
113
- if node.string?
114
- template = resolve_path_directory(node.to_string)
115
- else
116
- if node.variable_reference?
117
- dependency = node.variable_name.sub(/\A(?:\$|@{1,2})/, "")
118
- elsif node.vcall?
119
- dependency = node.variable_name
120
- elsif node.call?
121
- dependency = node.call_method_name
122
- else
123
- return
124
- end
125
-
126
- object_template = true
127
- template = "#{dependency.pluralize}/#{dependency.singularize}"
128
- end
129
-
130
- return unless template
131
-
132
- if spacer_template = render_template_with_spacer?(options_hash)
133
- virtual_path = partial_to_virtual_path(:partial, spacer_template)
134
- renders << virtual_path
135
- end
136
-
137
- if options_hash.key?(:object) || options_hash.key?(:collection) || object_template
138
- return nil if options_hash.key?(:object) && options_hash.key?(:collection)
139
- return nil unless options_hash.key?(:partial)
140
- end
141
-
142
- virtual_path = partial_to_virtual_path(render_type, template)
143
- renders << virtual_path
144
-
145
- # Support for rendering multiple templates (i.e. a partial with a layout)
146
- if layout_template = render_template_with_layout?(render_type, options_hash)
147
- virtual_path = partial_to_virtual_path(:layout, layout_template)
148
-
149
- renders << virtual_path
150
- end
151
-
152
- renders
153
- end
154
-
155
- def parse_str(node)
156
- node.string? && node.to_string
157
- end
158
-
159
- def parse_sym(node)
160
- node.symbol? && node.to_symbol
8
+ class Base # :nodoc:
9
+ def initialize(name, code)
10
+ @name = name
11
+ @code = code
161
12
  end
162
13
 
163
14
  private
164
- def render_template_with_layout?(render_type, options_hash)
165
- if render_type != :layout && options_hash.key?(:layout)
166
- parse_str(options_hash[:layout])
167
- end
168
- end
169
-
170
- def render_template_with_spacer?(options_hash)
171
- if options_hash.key?(:spacer_template)
172
- parse_str(options_hash[:spacer_template])
173
- end
15
+ def directory
16
+ File.dirname(@name)
174
17
  end
175
18
 
176
19
  def partial_to_virtual_path(render_type, partial_path)
@@ -180,9 +23,18 @@ module ActionView
180
23
  partial_path
181
24
  end
182
25
  end
26
+ end
183
27
 
184
- def layout_to_virtual_path(layout_path)
185
- "layouts/#{layout_path}"
186
- end
28
+ # Check if prism is available. If it is, use it. Otherwise, use ripper.
29
+ begin
30
+ require "prism"
31
+ rescue LoadError
32
+ require "ripper"
33
+ require_relative "render_parser/ripper_render_parser"
34
+ Default = RipperRenderParser
35
+ else
36
+ require_relative "render_parser/prism_render_parser"
37
+ Default = PrismRenderParser
38
+ end
187
39
  end
188
40
  end
@@ -79,7 +79,7 @@ module ActionView
79
79
  path = if object.respond_to?(:to_partial_path)
80
80
  object.to_partial_path
81
81
  else
82
- raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
82
+ raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement #to_partial_path.")
83
83
  end
84
84
 
85
85
  if view.prefix_partial_path_with_controller_namespace
@@ -45,12 +45,6 @@ module ActionView
45
45
  end
46
46
  end
47
47
 
48
- # Direct access to template rendering.
49
- def render_template(context, options) # :nodoc:
50
- render_template_to_object(context, options).body
51
- end
52
-
53
- # Direct access to partial rendering.
54
48
  def render_partial(context, options, &block) # :nodoc:
55
49
  render_partial_to_object(context, options, &block).body
56
50
  end
@@ -59,46 +53,46 @@ module ActionView
59
53
  @cache_hits ||= {}
60
54
  end
61
55
 
62
- def render_template_to_object(context, options) # :nodoc:
63
- TemplateRenderer.new(@lookup_context).render(context, options)
64
- end
56
+ private
57
+ def render_template_to_object(context, options)
58
+ TemplateRenderer.new(@lookup_context).render(context, options)
59
+ end
65
60
 
66
- def render_partial_to_object(context, options, &block) # :nodoc:
67
- partial = options[:partial]
68
- if String === partial
69
- collection = collection_from_options(options)
61
+ def render_partial_to_object(context, options, &block)
62
+ partial = options[:partial]
63
+ if String === partial
64
+ collection = collection_from_options(options)
70
65
 
71
- if collection
72
- # Collection + Partial
73
- renderer = CollectionRenderer.new(@lookup_context, options)
74
- renderer.render_collection_with_partial(collection, partial, context, block)
75
- else
76
- if options.key?(:object)
77
- # Object + Partial
78
- renderer = ObjectRenderer.new(@lookup_context, options)
79
- renderer.render_object_with_partial(options[:object], partial, context, block)
66
+ if collection
67
+ # Collection + Partial
68
+ renderer = CollectionRenderer.new(@lookup_context, options)
69
+ renderer.render_collection_with_partial(collection, partial, context, block)
80
70
  else
81
- # Partial
82
- renderer = PartialRenderer.new(@lookup_context, options)
83
- renderer.render(partial, context, block)
71
+ if options.key?(:object)
72
+ # Object + Partial
73
+ renderer = ObjectRenderer.new(@lookup_context, options)
74
+ renderer.render_object_with_partial(options[:object], partial, context, block)
75
+ else
76
+ # Partial
77
+ renderer = PartialRenderer.new(@lookup_context, options)
78
+ renderer.render(partial, context, block)
79
+ end
84
80
  end
85
- end
86
- else
87
- collection = collection_from_object(partial) || collection_from_options(options)
88
-
89
- if collection
90
- # Collection + Derived Partial
91
- renderer = CollectionRenderer.new(@lookup_context, options)
92
- renderer.render_collection_derive_partial(collection, context, block)
93
81
  else
94
- # Object + Derived Partial
95
- renderer = ObjectRenderer.new(@lookup_context, options)
96
- renderer.render_object_derive_partial(partial, context, block)
82
+ collection = collection_from_object(partial) || collection_from_options(options)
83
+
84
+ if collection
85
+ # Collection + Derived Partial
86
+ renderer = CollectionRenderer.new(@lookup_context, options)
87
+ renderer.render_collection_derive_partial(collection, context, block)
88
+ else
89
+ # Object + Derived Partial
90
+ renderer = ObjectRenderer.new(@lookup_context, options)
91
+ renderer.render_object_derive_partial(partial, context, block)
92
+ end
97
93
  end
98
94
  end
99
- end
100
95
 
101
- private
102
96
  def collection_from_options(options)
103
97
  if options.key?(:collection)
104
98
  collection = options[:collection]