rabl 0.17.0 → 0.19.0

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.
data/lib/rabl/digestor.rb CHANGED
@@ -1,28 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'action_view'
2
4
 
3
5
  module Rabl
6
+ # Rails' digestor, under rabl's name: rabl.rb defines it only with the rest of the Rails integration, so
7
+ # Engine#digestor_available? checks for it before adding a template digest to a cache key
4
8
  class Digestor < ActionView::Digestor
5
- # Override the original digest function to ignore partial which
6
- # rabl doesn't use the Rails conventional _ symbol.
7
- if Gem::Version.new(Rails.version) >= Gem::Version.new('5.0.0.beta1')
8
- require 'rabl/digestor/rails5'
9
- elsif Gem::Version.new(Rails.version) >= Gem::Version.new('4.1')
10
- require 'rabl/digestor/rails41'
11
- else
12
- require 'rabl/digestor/rails3'
13
- end
14
-
15
- private
16
- def dependency_digest
17
- template_digests = (dependencies - [template.virtual_path]).collect do |template_name|
18
- if Gem::Version.new(Rails.version) >= Gem::Version.new('4.1')
19
- Digestor.digest(:name => template_name, :finder => finder)
20
- else
21
- Digestor.digest(template_name, format, finder)
22
- end
23
- end
24
-
25
- (template_digests + injected_dependencies).join("-")
26
- end
27
9
  end
28
10
  end
data/lib/rabl/engine.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rabl
2
4
  class Engine
3
5
  include Helpers
@@ -8,6 +10,64 @@ module Rabl
8
10
  FORMATS = [:json, :xml, :plist, :bson, :msgpack]
9
11
  SAFE_FORMATS = FORMATS + [:mpac, :dumpable, :hash]
10
12
 
13
+ # JSONP callback names that are accepted: identifiers, optionally dotted (e.g. "jQuery_1.cb")
14
+ RABL_JSON_CALLBACK_NAME = /\A[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\z/
15
+ RABL_JSON_CALLBACK_MAX_LENGTH = 128
16
+
17
+ # Array#to_xml names its items after the singular of the dasherized root, which misses
18
+ # inflections that match only the underscored name: rules written in underscored form
19
+ # (inflect.irregular 'lesson_progress', 'lessons_progresses', #559), underscored uncountable
20
+ # words (inflect.uncountable 'user_settings') and rules using \w (/^(\w+)ves$/). An XmlArray
21
+ # names them after the singular of the root as given, as rabl does for JSON item roots
22
+ # (to_xml then dasherizes it like any other key), when that singular comes from a newer rule
23
+ # than the one ActiveSupport applies to the dasherized root. Otherwise the name is left to
24
+ # Array#to_xml, so built-in inflections, inflections written with dashes (the workaround in
25
+ # #559) and uncountable words at the end of a name ("animal-species") are named as before.
26
+ # The arrays of one rendering share children_names, so each root's name is worked out once.
27
+ class XmlArray < Array
28
+ def initialize(items, children_names)
29
+ super(items)
30
+ @children_names = children_names
31
+ end
32
+
33
+ def to_xml(options = {}, &block)
34
+ root = options[:root].to_s
35
+ unless options[:children] || !options[:root] || root.empty?
36
+ renamed = ActiveSupport::XmlMini.rename_key(root, options)
37
+ children = @children_names.fetch([root, renamed]) do |key|
38
+ @children_names[key] = children_name(root, renamed, options)
39
+ end
40
+ options = options.merge(:children => children)
41
+ end
42
+ super(options, &block)
43
+ end
44
+
45
+ private
46
+
47
+ # The singular of root when it comes from a newer rule than the one ActiveSupport applies
48
+ # to the renamed root, or else the singular of the renamed root, as Array#to_xml names items
49
+ def children_name(root, renamed, options)
50
+ singular = root.singularize
51
+ renamed_singular = renamed.singularize
52
+ xml_mini = ActiveSupport::XmlMini
53
+ return renamed_singular if xml_mini.rename_key(singular, options) == xml_mini.rename_key(renamed_singular, options)
54
+
55
+ singular_rule_rank(root) < singular_rule_rank(renamed) ? singular : renamed_singular
56
+ end
57
+
58
+ # The position of the singular rule ActiveSupport applies to word. The rules are newest
59
+ # first, so a lower rank is a rule defined later; -1 for an uncountable word, and the rule
60
+ # count when no rule matches.
61
+ def singular_rule_rank(word)
62
+ inflections = ActiveSupport::Inflector.inflections
63
+ return -1 if inflections.uncountables.uncountable?(word)
64
+
65
+ rules = inflections.singulars
66
+ rules.index { |(rule, _)| rule.is_a?(Regexp) ? rule.match?(word) : word.include?(rule) } || rules.size
67
+ end
68
+ end
69
+ private_constant :XmlArray
70
+
11
71
  # Constructs a new ejs engine based on given vars, handler and declarations
12
72
  # Rabl::Engine.new("...source...", { :format => "xml", :root => true, :view_path => "/path/to/views" })
13
73
  def initialize(source, options = {})
@@ -17,8 +77,11 @@ module Rabl
17
77
 
18
78
  @_view_path = options[:view_path]
19
79
  @_context_scope = options[:scope]
80
+ @_data_item = options[:data_item]
20
81
 
21
82
  @_cache_read_on_render = true
83
+ @_skip_json_callback = false
84
+ @_rendering = false
22
85
  end
23
86
 
24
87
  def source=(source)
@@ -35,6 +98,17 @@ module Rabl
35
98
  reset_settings!
36
99
  reset_options!
37
100
 
101
+ # A block (child, glue, or extends/partial with a block) is identified by its
102
+ # location and the engine of the template that defined it
103
+ @_block_identity = block && [block, @_options[:parent_engine]]
104
+ @_template_identity = nil
105
+ @_identity_memo = nil
106
+
107
+ # A cache key belongs to one render; the template sets it again
108
+ remove_instance_variable(:@_cache_key) if defined?(@_cache_key)
109
+ remove_instance_variable(:@_replaced_data) if defined?(@_replaced_data)
110
+ @_full_cache_key = nil
111
+
38
112
  eval_source(locals, &block)
39
113
 
40
114
  instance_exec(root_object, &block) if block_given?
@@ -47,32 +121,46 @@ module Rabl
47
121
  def render(context_scope = nil, locals = nil, &block)
48
122
  apply(context_scope, locals, &block) if context_scope || locals || block
49
123
 
50
- cache_results do
51
- send("to_#{@_options[:format]}")
124
+ return cache_results { send("to_#{@_options[:format]}") } unless @_options[:format].to_s == "json"
125
+
126
+ # The JSONP callback comes from the request, so it is added after the cache.
127
+ result = cache_results do
128
+ begin
129
+ @_skip_json_callback = true
130
+ to_json
131
+ ensure
132
+ @_skip_json_callback = false
133
+ end
52
134
  end
135
+
136
+ rabl_wrap_json_callback(result)
53
137
  end
54
138
 
55
139
  def cache_key
56
140
  return unless defined?(@_cache_key)
57
141
 
58
142
  @_full_cache_key ||= begin
59
- cache_key = Array(@_cache_key) + [@_options[:root_name], @_options[:format]]
143
+ cache_key = rabl_cache_key_parts + [@_options[:root_name], @_options[:format]]
144
+ # a nested engine caches its output before it is escaped, so it gets its own key
145
+ cache_key << 'nested' if @_options[:nested]
60
146
 
61
147
  if digestor_available? && respond_to?(:lookup_context) && lookup_context
62
148
  template = @_options[:template] || @virtual_path
63
149
 
64
- digest =
65
- if Rails.version.to_s =~ /^[678]/
66
- Digestor.digest(name: template, finder: lookup_context, format: :rabl)
67
- elsif Gem::Version.new(Rails.version) >= Gem::Version.new('4.1')
68
- Digestor.digest(:name => template, :finder => lookup_context)
69
- else
70
- Digestor.digest(template, :rabl, lookup_context)
71
- end
72
-
73
- cache_key << digest
150
+ cache_key << Digestor.digest(name: template, finder: lookup_context, format: :rabl)
151
+ # The digest covers the template source, but not the block that defines this engine
152
+ if @_block_identity
153
+ block, parent = @_block_identity
154
+ key = [:block, block, parent && parent.rabl_template_identity]
155
+ cache_key << rabl_memoized_digest(key) { TemplateCacheKey.digest(rabl_block_identity) }
156
+ end
157
+ else
158
+ cache_key << rabl_template_identity
74
159
  end
75
160
 
161
+ cache_key << "collection" if is_collection?(root_object)
162
+ cache_key.concat(TemplateCacheKey.shape(@_options, source_lookup_format))
163
+
76
164
  cache_key
77
165
  end
78
166
  end
@@ -80,12 +168,15 @@ module Rabl
80
168
  # Returns a hash representation of the data object
81
169
  # to_hash(:root => true, :child_root => true)
82
170
  def to_hash(options = {})
83
- options.reverse_merge!(@_options)
171
+ # options given to the engine (e.g. Rabl::Renderer's :root) win over the format defaults
172
+ options.merge!(@_options)
84
173
 
85
174
  data = root_object
86
175
 
87
176
  options[:root_name] = determine_object_root(data, root_name, options[:root])
177
+ options[:template_engine] = self
88
178
 
179
+ @_rendering = true
89
180
  result =
90
181
  if is_object?(data) || !data # object @user
91
182
  Builder.new(data, @_settings, options).to_hash
@@ -93,9 +184,12 @@ module Rabl
93
184
  MultiBuilder.new(data, @_settings, options).to_a
94
185
  end
95
186
 
96
- result = escape_output(result) if Rabl.configuration.escape_all_output
187
+ # A nested engine (child, glue, extends, partial) is escaped with the engine it is part of
188
+ result = escape_output(result) if Rabl.configuration.escape_all_output && !@_options[:nested]
97
189
 
98
190
  result
191
+ ensure
192
+ @_rendering = false
99
193
  end
100
194
 
101
195
  def to_dumpable(options = {})
@@ -111,7 +205,7 @@ module Rabl
111
205
  def to_json(options = {})
112
206
  options.reverse_merge!({ :root => Rabl.configuration.include_json_root })
113
207
  result = to_dumpable(options)
114
- format_json(result)
208
+ rabl_wrap_json_callback(format_json(result))
115
209
  end
116
210
 
117
211
  # Returns a msgpack representation of the data object
@@ -134,6 +228,8 @@ module Rabl
134
228
  # Returns an xml representation of the data object
135
229
  # to_xml(:root => true)
136
230
  def to_xml(options = {})
231
+ load_xml_support!
232
+
137
233
  options = {
138
234
  :root => (include_root = Rabl.configuration.include_xml_root),
139
235
  :child_root => include_root && Rabl.configuration.include_child_root
@@ -143,7 +239,14 @@ module Rabl
143
239
 
144
240
  result = to_hash(options)
145
241
 
146
- result.to_xml(xml_options)
242
+ # Name the items after the object root and unwrap them, or Array#to_xml wraps each
243
+ # { object_root => ... } item again in the singular collection root (#581)
244
+ if result.is_a?(Array) && is_name_value?(object_root_name)
245
+ xml_options[:children] = object_root_name.to_s
246
+ result = result.map { |item| item.is_a?(Hash) && item.key?(object_root_name) ? item[object_root_name] : item }
247
+ end
248
+
249
+ xml_arrays(result).to_xml(xml_options)
147
250
  end
148
251
 
149
252
  # Returns a bson representation of the data object
@@ -156,7 +259,14 @@ module Rabl
156
259
  result = { root_name => result }
157
260
  end
158
261
 
159
- Rabl.configuration.bson_engine.serialize(result).to_s
262
+ bson_engine = Rabl.configuration.bson_engine
263
+ if bson_engine.respond_to?(:serialize)
264
+ # BSON < 2.0 API
265
+ bson_engine.serialize(result).to_s
266
+ else
267
+ # BSON >= 2.0 API
268
+ result.to_bson.to_s
269
+ end
160
270
  end
161
271
 
162
272
  # Sets the object to be used as the data source for this template
@@ -165,9 +275,20 @@ module Rabl
165
275
  # object @users
166
276
  def object(template_data)
167
277
  current_data = (@_locals[:object].nil? || template_data == false) ? template_data : @_locals[:object]
278
+ # Rendered for an item (extends, partial, child), the template gets the item instead of
279
+ # the data it names, e.g. the outer @user; cache keys then add the item (see #rabl_cache_key_parts)
280
+ if !current_data.equal?(template_data)
281
+ alias_form = template_data.is_a?(Hash) && template_data.size == 1 && is_name_value?(template_data.values.first)
282
+ @_replaced_data = alias_form ? template_data.keys.first : template_data # object @user => :person names @user
283
+ end
168
284
  @_data_object = data_object(current_data)
169
285
  @_root_name_data = template_data.is_a?(Hash) && !current_data.is_a?(Hash) ? template_data : current_data
170
- @_root_name_data = @_root_name_data.values.first if @_root_name_data.is_a?(Hash)
286
+ if @_root_name_data.is_a?(Hash)
287
+ alias_form = @_root_name_data.size == 1 # object @user => :person
288
+ @_root_name_data = @_root_name_data.values.first
289
+ # object @user => "person" names the root the same way
290
+ @_root_name_data = @_root_name_data.to_sym if alias_form && @_root_name_data.is_a?(String)
291
+ end
171
292
 
172
293
  # If we turn this around, `@_root_name_date ==` may trigger data to be loaded unnecessarily.
173
294
  # TODO: is nil a different semantic? otherwise don't use `false ==`, use !
@@ -201,13 +322,19 @@ module Rabl
201
322
  # collection @users => :people
202
323
  # collection @users, :root => :person
203
324
  # collection @users, :object_root => :person
325
+ # collection @users => :people, :object_root => false
204
326
  def collection(data, options = {})
327
+ data, options = split_folded_options(data, options, [:root, :object_root])
328
+
205
329
  @_collection_name = options[:root] if options[:root]
206
330
  @_collection_name ||= data.values.first if data.is_a?(Hash)
207
331
 
208
332
  @_object_root_name = options[:object_root] if options.has_key?(:object_root)
209
333
 
210
- object(data_object(data) || [])
334
+ collection_data = data_object(data)
335
+ object(collection_data || [])
336
+ # a nil collection is given to object as [], but the template names nil (see #object)
337
+ @_replaced_data = collection_data if defined?(@_replaced_data)
211
338
  end
212
339
 
213
340
  # Sets the cache key to be used by ActiveSupport::Cache.expand_cache_key
@@ -236,6 +363,8 @@ module Rabl
236
363
  attribute(key, conditions.merge(:as => as))
237
364
  end
238
365
  else # array of attributes i.e :foo, :bar, :baz
366
+ warn_if_rendering(:attribute)
367
+
239
368
  options = args.extract_options!
240
369
  args.each do |name|
241
370
  @_settings[:attributes] << { :name => name, :options => options }
@@ -248,25 +377,42 @@ module Rabl
248
377
  # node(:foo) { "bar" }
249
378
  # node(:foo, :if => lambda { ... }) { "bar" }
250
379
  def node(name = nil, options = {}, &block)
380
+ warn_if_rendering(:node)
381
+
382
+ if name.is_a?(Hash) && name.present? && options.blank? && (name.keys - [:if, :unless]).empty?
383
+ name, options = nil, name # node(:if => lambda { ... }) { ... } has conditions but no name
384
+ end
385
+
251
386
  @_settings[:node] << { :name => name, :options => options, :block => block }
252
387
  end
253
388
  alias_method :code, :node
254
389
 
255
390
  # Creates a child node that is included in json output
256
391
  # child(@user) { attribute :full_name }
392
+ # child(:posts => :items, :if => lambda { ... }) { attribute :title }
257
393
  def child(data, options = {}, &block)
394
+ warn_if_rendering(:child)
395
+
396
+ data, options = split_folded_options(data, options, [:if, :unless, :root, :object_root])
397
+
258
398
  @_settings[:child] << { :data => data, :options => options, :block => block }
259
399
  end
260
400
 
261
401
  # Glues data from a child node to the json_output
262
402
  # glue(@user) { attribute :full_name => :user_full_name }
263
403
  def glue(data, options = {}, &block)
404
+ warn_if_rendering(:glue)
405
+
406
+ data, options = split_folded_options(data, options, [:if, :unless])
407
+
264
408
  @_settings[:glue] << { :data => data, :options => options, :block => block }
265
409
  end
266
410
 
267
411
  # Extends an existing rabl template with additional attributes in the block
268
412
  # extends("users/show", :object => @user) { attribute :full_name }
269
413
  def extends(file, options = {}, &block)
414
+ return if warn_if_rendering(:extends)
415
+
270
416
  options.reverse_merge!({ :view_path => options[:view_path] || view_path })
271
417
 
272
418
  @_settings[:extends] << { :file => file, :options => options, :block => block }
@@ -298,7 +444,81 @@ module Rabl
298
444
  @_cache_read_on_render
299
445
  end
300
446
 
447
+ # Returns the key given to `cache` in the template, if any. When the template is rendered
448
+ # for an item (see #object) and its key has the data the template names (`cache @user`, or
449
+ # the nil in `cache ['v1', @topic]`) but not the item, the item is added, so each item gets
450
+ # its own key; the other parts (such as the current user) are kept.
451
+ # ceiling: only the key's own parts are compared, not those of an array nested in it;
452
+ # walk nested arrays too if such keys turn up.
453
+ def rabl_cache_key_parts
454
+ return [] unless defined?(@_cache_key)
455
+
456
+ parts = Array(@_cache_key)
457
+ return parts unless defined?(@_replaced_data)
458
+
459
+ item = root_object
460
+ return parts if parts.any? { |part| part.equal?(item) } || parts.none? { |part| part.equal?(@_replaced_data) }
461
+
462
+ parts + [item]
463
+ end
464
+
465
+ # Identifies the template for cache keys: a digest of its location, the view path its
466
+ # partials resolve from, its source and its block
467
+ def rabl_template_identity
468
+ @_template_identity ||= begin
469
+ location = @_options[:source_location]
470
+
471
+ if @_options[:parent_engine] && (location || !@_source)
472
+ # Within one render, a template's location determines its source
473
+ # (nested lookups rather than an Array key, as this runs for every item)
474
+ memo = (rabl_identity_memo[location] ||= {})
475
+ memo = (memo[view_path] ||= {})
476
+
477
+ if @_block_identity
478
+ block, parent = @_block_identity
479
+ memo = (memo[block] ||= {})
480
+ memo[parent && parent.rabl_template_identity] ||= rabl_template_digest
481
+ else
482
+ memo[:template] ||= rabl_template_digest
483
+ end
484
+ else
485
+ rabl_template_digest
486
+ end
487
+ end
488
+ end
489
+
301
490
  protected
491
+ def rabl_template_digest
492
+ root = TemplateCacheKey.root
493
+ view_paths = Array(view_path).map { |path| TemplateCacheKey.path(path, root) }
494
+ TemplateCacheKey.digest([TemplateCacheKey.path(@_options[:source_location], root), view_paths, @_source, rabl_block_identity(root)])
495
+ end
496
+
497
+ # Returns the digest for key, computing it once per render: the memo is kept by the
498
+ # top-level engine, so the items of a collection share their sub-templates' digests
499
+ def rabl_memoized_digest(key)
500
+ rabl_identity_memo[key] ||= yield
501
+ end
502
+
503
+ def rabl_identity_memo
504
+ @_identity_memo ||= (parent = @_options[:parent_engine]) ? parent.rabl_identity_memo : {}
505
+ end
506
+
507
+ def rabl_template_engine
508
+ self
509
+ end
510
+
511
+ # Identifies the block that defines this engine, if any: its location and the
512
+ # template that defined it
513
+ def rabl_block_identity(root = TemplateCacheKey.root)
514
+ return unless @_block_identity
515
+
516
+ block, parent = @_block_identity
517
+ location = block.source_location
518
+ location &&= [TemplateCacheKey.path(location[0], root), location[1], location.size > 2 ? location[2] : TemplateCacheKey.column(block)]
519
+ [location, parent && parent.rabl_template_identity]
520
+ end
521
+
302
522
  # Returns a guess at the default object for this template
303
523
  # default_object => @user
304
524
  def default_object
@@ -331,25 +551,43 @@ module Rabl
331
551
  (context_scope.params if context_scope.respond_to?(:params)) || {}
332
552
  end
333
553
 
334
- # Returns data as json embraced with callback when detected
335
- # format_json({ :foo => "bar" }) => "test({ foo : 'bar' })"
336
- # format_json("{ foo : "bar" }") => "test({ foo : 'bar' })"
554
+ # Returns data as json
555
+ # format_json({ :foo => "bar" }) => "{\"foo\":\"bar\"}"
337
556
  def format_json(json_output)
338
557
  unless json_output.is_a?(String)
339
558
  json_engine = Rabl.configuration.json_engine
340
- json_output = if json_engine.respond_to?(:dump)
559
+ json_output = if defined?(::Oj) && json_engine.equal?(::Oj)
560
+ oj_options = Rabl.configuration.oj_dump_options
561
+ oj_options.empty? ? ::Oj.dump(json_output) : ::Oj.dump(json_output, oj_options)
562
+ elsif json_engine.respond_to?(:dump)
341
563
  json_engine.dump(json_output)
342
564
  else
343
565
  json_engine.encode(json_output)
344
566
  end
345
567
  end
346
568
 
347
- use_callback = Rabl.configuration.enable_json_callbacks && request_params[:callback].present?
348
- json_output = "#{request_params[:callback]}(#{json_output})" if use_callback
349
-
350
569
  json_output
351
570
  end
352
571
 
572
+ # Returns json embraced with the request's callback when callbacks are enabled
573
+ # and the callback name is valid, otherwise the json unchanged
574
+ # rabl_wrap_json_callback("{}") => "/**/test({})"
575
+ def rabl_wrap_json_callback(json)
576
+ callback = rabl_json_callback_name unless @_skip_json_callback
577
+ callback ? "/**/#{callback}(#{json})" : json
578
+ end
579
+
580
+ # Returns the request's callback parameter if callbacks are enabled and it is a valid name
581
+ def rabl_json_callback_name
582
+ return unless Rabl.configuration.enable_json_callbacks
583
+
584
+ callback = request_params[:callback]
585
+ return unless callback.is_a?(String) && callback.length <= RABL_JSON_CALLBACK_MAX_LENGTH
586
+ return unless callback.ascii_only? && callback.match?(RABL_JSON_CALLBACK_NAME)
587
+
588
+ callback
589
+ end
590
+
353
591
  # Augments respond to supporting context_scope methods
354
592
  def respond_to?(name, include_private = false)
355
593
  context_scope.respond_to?(name, include_private) || super
@@ -398,8 +636,9 @@ module Rabl
398
636
  end
399
637
  end
400
638
 
639
+ # rabl.rb loads Rabl::Digestor with the rest of the Rails integration
401
640
  def digestor_available?
402
- defined?(Rails) && Rails.version =~ /^[45678]/
641
+ defined?(::Rabl::Digestor)
403
642
  end
404
643
 
405
644
  def valid_format?(format)
@@ -426,21 +665,88 @@ module Rabl
426
665
  end
427
666
 
428
667
  def eval_source(locals, &block)
429
- return unless @_source.present?
668
+ source = @_source
669
+ # A UTF-8 byte order mark (as some editors save files) isn't part of the template. It is
670
+ # checked by its bytes first, as it is invalid in the encoding the source may be read in
671
+ # (US-ASCII under LANG=C), where reading the source as text raises.
672
+ source = source.byteslice(3..-1) if source.is_a?(String) && source.getbyte(0) == 0xEF && source.getbyte(1) == 0xBB && source.getbyte(2) == 0xBF
673
+ return unless source.present?
430
674
 
431
- msg = "cached_source_#{@_source.hash.abs}"
675
+ # One method per source and location, so blocks report the file that defined them
676
+ line = @_options[:source_line] || 1
677
+ msg = "cached_source_#{[@_source, @_options[:source_location], line].hash.abs}"
432
678
 
433
679
  unless self.respond_to? msg then
434
- src = "def #{msg} locals, &block\n#{@_source}\nend"
680
+ src = "def #{msg} locals, &block\n#{source}\nend"
435
681
 
436
- if @_options[:source_location]
437
- self.class.class_eval(src, @_options[:source_location])
438
- else
439
- self.class.class_eval(src)
440
- end
682
+ # The def line comes before the template's first line, so errors report the
683
+ # template's own line numbers
684
+ self.class.class_eval(src, @_options[:source_location] || "(eval)", line - 1)
441
685
  end
442
686
 
443
687
  send msg, locals, &block
444
688
  end
689
+
690
+ # Hash#to_xml and Array#to_xml come from ActiveSupport (which uses the builder gem);
691
+ # Rails loads them, but other apps may not have.
692
+ def load_xml_support!
693
+ return if defined?(::Builder::XmlMarkup) && {}.respond_to?(:to_xml) && [].respond_to?(:to_xml)
694
+
695
+ require 'active_support/core_ext/array/conversions'
696
+ require 'active_support/core_ext/hash/conversions'
697
+
698
+ begin
699
+ require 'builder' unless defined?(::Builder::XmlMarkup)
700
+ rescue LoadError
701
+ raise LoadError, "rabl needs the builder gem to render XML; add `gem 'builder'` to your Gemfile"
702
+ end
703
+ end
704
+
705
+ # Returns a copy of the data with each Array in it, at any depth, as an XmlArray; arrays
706
+ # with a to_xml of their own are left as they are
707
+ def xml_arrays(data, children_names = {})
708
+ case data
709
+ when Hash
710
+ copy = data.dup
711
+ data.each { |key, value| copy[key] = xml_arrays(value, children_names) }
712
+ copy
713
+ when Array
714
+ return data unless data.method(:to_xml).owner == Array
715
+
716
+ XmlArray.new(data.map { |value| xml_arrays(value, children_names) }, children_names)
717
+ else
718
+ data
719
+ end
720
+ end
721
+
722
+ # attribute, node, child, glue and extends build the template's settings, which are read while
723
+ # it renders (#477). Called then (e.g. inside a node block), they warn; attribute, node, child
724
+ # and glue work as before, and extends is ignored, since its settings (view path included)
725
+ # would otherwise be the node's value. Returns true when the template is rendering.
726
+ # ceiling: warns on every call, so a misuse inside a large collection repeats the warning;
727
+ # dedupe by call site if that turns out to be noisy.
728
+ def warn_if_rendering(method_name)
729
+ return false unless @_rendering
730
+
731
+ effect = method_name == :extends ? "is ignored" : "changes the template for the rest of the render"
732
+ site = caller.find { |line| !line.start_with?(__FILE__) }.to_s.sub(/:in .*\z/m, '')
733
+ Kernel.warn "rabl: #{method_name} called while the template renders (e.g. inside a node block) at #{site} " \
734
+ "#{effect}; to render another template there, use partial(\"template\", :object => object)"
735
+ true
736
+ end
737
+
738
+ # Ruby folds `child :posts => :items, :if => cond` into a single hash, so the options
739
+ # arrive inside the data. They are split out only when no options were given separately
740
+ # and what remains is a single alias (`data => name`); any other hash is left as data.
741
+ # split_folded_options({ :posts => :items, :if => true }, {}, [:if]) => [{ :posts => :items }, { :if => true }]
742
+ def split_folded_options(data, options, option_keys)
743
+ return [data, options] unless data.is_a?(Hash) && options.blank?
744
+
745
+ found = data.keys & option_keys
746
+ alias_data = data.except(*found)
747
+ return [data, options] unless found.any? && alias_data.size == 1 && is_name_value?(alias_data.values.first)
748
+
749
+ [alias_data, data.slice(*found)]
750
+ end
445
751
  end
446
752
  end