rabl 0.17.0 → 0.18.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -0
- data/README.md +82 -9
- data/lib/rabl/builder.rb +19 -1
- data/lib/rabl/cache_engine.rb +4 -2
- data/lib/rabl/configuration.rb +71 -4
- data/lib/rabl/digestor/rails3.rb +2 -0
- data/lib/rabl/digestor/rails41.rb +2 -0
- data/lib/rabl/digestor/rails5.rb +2 -0
- data/lib/rabl/digestor.rb +2 -0
- data/lib/rabl/engine.rb +341 -25
- data/lib/rabl/helpers.rb +37 -10
- data/lib/rabl/multi_builder.rb +8 -2
- data/lib/rabl/partials.rb +2 -0
- data/lib/rabl/railtie.rb +2 -0
- data/lib/rabl/renderer.rb +4 -1
- data/lib/rabl/sources.rb +261 -14
- data/lib/rabl/template.rb +6 -3
- data/lib/rabl/template_cache_key.rb +93 -0
- data/lib/rabl/tracker.rb +2 -0
- data/lib/rabl/version.rb +3 -1
- data/lib/rabl.rb +22 -6
- metadata +4 -3
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,66 @@ 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
|
+
uncountables = inflections.uncountables
|
|
64
|
+
# ActiveSupport < 5 keeps them in a plain Array, checked by the last word
|
|
65
|
+
return -1 if uncountables.respond_to?(:uncountable?) ? uncountables.uncountable?(word) : uncountables.include?(word.downcase[/\b\w+\Z/])
|
|
66
|
+
|
|
67
|
+
rules = inflections.singulars
|
|
68
|
+
rules.index { |(rule, _)| rule.is_a?(Regexp) ? rule.match?(word) : word.include?(rule) } || rules.size
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
private_constant :XmlArray
|
|
72
|
+
|
|
11
73
|
# Constructs a new ejs engine based on given vars, handler and declarations
|
|
12
74
|
# Rabl::Engine.new("...source...", { :format => "xml", :root => true, :view_path => "/path/to/views" })
|
|
13
75
|
def initialize(source, options = {})
|
|
@@ -17,8 +79,11 @@ module Rabl
|
|
|
17
79
|
|
|
18
80
|
@_view_path = options[:view_path]
|
|
19
81
|
@_context_scope = options[:scope]
|
|
82
|
+
@_data_item = options[:data_item]
|
|
20
83
|
|
|
21
84
|
@_cache_read_on_render = true
|
|
85
|
+
@_skip_json_callback = false
|
|
86
|
+
@_rendering = false
|
|
22
87
|
end
|
|
23
88
|
|
|
24
89
|
def source=(source)
|
|
@@ -35,6 +100,17 @@ module Rabl
|
|
|
35
100
|
reset_settings!
|
|
36
101
|
reset_options!
|
|
37
102
|
|
|
103
|
+
# A block (child, glue, or extends/partial with a block) is identified by its
|
|
104
|
+
# location and the engine of the template that defined it
|
|
105
|
+
@_block_identity = block && [block, @_options[:parent_engine]]
|
|
106
|
+
@_template_identity = nil
|
|
107
|
+
@_identity_memo = nil
|
|
108
|
+
|
|
109
|
+
# A cache key belongs to one render; the template sets it again
|
|
110
|
+
remove_instance_variable(:@_cache_key) if defined?(@_cache_key)
|
|
111
|
+
remove_instance_variable(:@_replaced_data) if defined?(@_replaced_data)
|
|
112
|
+
@_full_cache_key = nil
|
|
113
|
+
|
|
38
114
|
eval_source(locals, &block)
|
|
39
115
|
|
|
40
116
|
instance_exec(root_object, &block) if block_given?
|
|
@@ -47,16 +123,28 @@ module Rabl
|
|
|
47
123
|
def render(context_scope = nil, locals = nil, &block)
|
|
48
124
|
apply(context_scope, locals, &block) if context_scope || locals || block
|
|
49
125
|
|
|
50
|
-
cache_results
|
|
51
|
-
|
|
126
|
+
return cache_results { send("to_#{@_options[:format]}") } unless @_options[:format].to_s == "json"
|
|
127
|
+
|
|
128
|
+
# The JSONP callback comes from the request, so it is added after the cache.
|
|
129
|
+
result = cache_results do
|
|
130
|
+
begin
|
|
131
|
+
@_skip_json_callback = true
|
|
132
|
+
to_json
|
|
133
|
+
ensure
|
|
134
|
+
@_skip_json_callback = false
|
|
135
|
+
end
|
|
52
136
|
end
|
|
137
|
+
|
|
138
|
+
rabl_wrap_json_callback(result)
|
|
53
139
|
end
|
|
54
140
|
|
|
55
141
|
def cache_key
|
|
56
142
|
return unless defined?(@_cache_key)
|
|
57
143
|
|
|
58
144
|
@_full_cache_key ||= begin
|
|
59
|
-
cache_key =
|
|
145
|
+
cache_key = rabl_cache_key_parts + [@_options[:root_name], @_options[:format]]
|
|
146
|
+
# a nested engine caches its output before it is escaped, so it gets its own key
|
|
147
|
+
cache_key << 'nested' if @_options[:nested]
|
|
60
148
|
|
|
61
149
|
if digestor_available? && respond_to?(:lookup_context) && lookup_context
|
|
62
150
|
template = @_options[:template] || @virtual_path
|
|
@@ -71,8 +159,19 @@ module Rabl
|
|
|
71
159
|
end
|
|
72
160
|
|
|
73
161
|
cache_key << digest
|
|
162
|
+
# The digest covers the template source, but not the block that defines this engine
|
|
163
|
+
if @_block_identity
|
|
164
|
+
block, parent = @_block_identity
|
|
165
|
+
key = [:block, block, parent && parent.rabl_template_identity]
|
|
166
|
+
cache_key << rabl_memoized_digest(key) { TemplateCacheKey.digest(rabl_block_identity) }
|
|
167
|
+
end
|
|
168
|
+
else
|
|
169
|
+
cache_key << rabl_template_identity
|
|
74
170
|
end
|
|
75
171
|
|
|
172
|
+
cache_key << "collection" if is_collection?(root_object)
|
|
173
|
+
cache_key.concat(TemplateCacheKey.shape(@_options, source_lookup_format))
|
|
174
|
+
|
|
76
175
|
cache_key
|
|
77
176
|
end
|
|
78
177
|
end
|
|
@@ -80,12 +179,15 @@ module Rabl
|
|
|
80
179
|
# Returns a hash representation of the data object
|
|
81
180
|
# to_hash(:root => true, :child_root => true)
|
|
82
181
|
def to_hash(options = {})
|
|
83
|
-
options.
|
|
182
|
+
# options given to the engine (e.g. Rabl::Renderer's :root) win over the format defaults
|
|
183
|
+
options.merge!(@_options)
|
|
84
184
|
|
|
85
185
|
data = root_object
|
|
86
186
|
|
|
87
187
|
options[:root_name] = determine_object_root(data, root_name, options[:root])
|
|
188
|
+
options[:template_engine] = self
|
|
88
189
|
|
|
190
|
+
@_rendering = true
|
|
89
191
|
result =
|
|
90
192
|
if is_object?(data) || !data # object @user
|
|
91
193
|
Builder.new(data, @_settings, options).to_hash
|
|
@@ -93,9 +195,12 @@ module Rabl
|
|
|
93
195
|
MultiBuilder.new(data, @_settings, options).to_a
|
|
94
196
|
end
|
|
95
197
|
|
|
96
|
-
|
|
198
|
+
# A nested engine (child, glue, extends, partial) is escaped with the engine it is part of
|
|
199
|
+
result = escape_output(result) if Rabl.configuration.escape_all_output && !@_options[:nested]
|
|
97
200
|
|
|
98
201
|
result
|
|
202
|
+
ensure
|
|
203
|
+
@_rendering = false
|
|
99
204
|
end
|
|
100
205
|
|
|
101
206
|
def to_dumpable(options = {})
|
|
@@ -111,7 +216,7 @@ module Rabl
|
|
|
111
216
|
def to_json(options = {})
|
|
112
217
|
options.reverse_merge!({ :root => Rabl.configuration.include_json_root })
|
|
113
218
|
result = to_dumpable(options)
|
|
114
|
-
format_json(result)
|
|
219
|
+
rabl_wrap_json_callback(format_json(result))
|
|
115
220
|
end
|
|
116
221
|
|
|
117
222
|
# Returns a msgpack representation of the data object
|
|
@@ -134,6 +239,8 @@ module Rabl
|
|
|
134
239
|
# Returns an xml representation of the data object
|
|
135
240
|
# to_xml(:root => true)
|
|
136
241
|
def to_xml(options = {})
|
|
242
|
+
load_xml_support!
|
|
243
|
+
|
|
137
244
|
options = {
|
|
138
245
|
:root => (include_root = Rabl.configuration.include_xml_root),
|
|
139
246
|
:child_root => include_root && Rabl.configuration.include_child_root
|
|
@@ -143,7 +250,14 @@ module Rabl
|
|
|
143
250
|
|
|
144
251
|
result = to_hash(options)
|
|
145
252
|
|
|
146
|
-
|
|
253
|
+
# Name the items after the object root and unwrap them, or Array#to_xml wraps each
|
|
254
|
+
# { object_root => ... } item again in the singular collection root (#581)
|
|
255
|
+
if result.is_a?(Array) && is_name_value?(object_root_name)
|
|
256
|
+
xml_options[:children] = object_root_name.to_s
|
|
257
|
+
result = result.map { |item| item.is_a?(Hash) && item.key?(object_root_name) ? item[object_root_name] : item }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
xml_arrays(result).to_xml(xml_options)
|
|
147
261
|
end
|
|
148
262
|
|
|
149
263
|
# Returns a bson representation of the data object
|
|
@@ -156,7 +270,14 @@ module Rabl
|
|
|
156
270
|
result = { root_name => result }
|
|
157
271
|
end
|
|
158
272
|
|
|
159
|
-
Rabl.configuration.bson_engine
|
|
273
|
+
bson_engine = Rabl.configuration.bson_engine
|
|
274
|
+
if bson_engine.respond_to?(:serialize)
|
|
275
|
+
# BSON < 2.0 API
|
|
276
|
+
bson_engine.serialize(result).to_s
|
|
277
|
+
else
|
|
278
|
+
# BSON >= 2.0 API
|
|
279
|
+
result.to_bson.to_s
|
|
280
|
+
end
|
|
160
281
|
end
|
|
161
282
|
|
|
162
283
|
# Sets the object to be used as the data source for this template
|
|
@@ -165,9 +286,20 @@ module Rabl
|
|
|
165
286
|
# object @users
|
|
166
287
|
def object(template_data)
|
|
167
288
|
current_data = (@_locals[:object].nil? || template_data == false) ? template_data : @_locals[:object]
|
|
289
|
+
# Rendered for an item (extends, partial, child), the template gets the item instead of
|
|
290
|
+
# the data it names, e.g. the outer @user; cache keys then add the item (see #rabl_cache_key_parts)
|
|
291
|
+
if !current_data.equal?(template_data)
|
|
292
|
+
alias_form = template_data.is_a?(Hash) && template_data.size == 1 && is_name_value?(template_data.values.first)
|
|
293
|
+
@_replaced_data = alias_form ? template_data.keys.first : template_data # object @user => :person names @user
|
|
294
|
+
end
|
|
168
295
|
@_data_object = data_object(current_data)
|
|
169
296
|
@_root_name_data = template_data.is_a?(Hash) && !current_data.is_a?(Hash) ? template_data : current_data
|
|
170
|
-
|
|
297
|
+
if @_root_name_data.is_a?(Hash)
|
|
298
|
+
alias_form = @_root_name_data.size == 1 # object @user => :person
|
|
299
|
+
@_root_name_data = @_root_name_data.values.first
|
|
300
|
+
# object @user => "person" names the root the same way
|
|
301
|
+
@_root_name_data = @_root_name_data.to_sym if alias_form && @_root_name_data.is_a?(String)
|
|
302
|
+
end
|
|
171
303
|
|
|
172
304
|
# If we turn this around, `@_root_name_date ==` may trigger data to be loaded unnecessarily.
|
|
173
305
|
# TODO: is nil a different semantic? otherwise don't use `false ==`, use !
|
|
@@ -201,13 +333,19 @@ module Rabl
|
|
|
201
333
|
# collection @users => :people
|
|
202
334
|
# collection @users, :root => :person
|
|
203
335
|
# collection @users, :object_root => :person
|
|
336
|
+
# collection @users => :people, :object_root => false
|
|
204
337
|
def collection(data, options = {})
|
|
338
|
+
data, options = split_folded_options(data, options, [:root, :object_root])
|
|
339
|
+
|
|
205
340
|
@_collection_name = options[:root] if options[:root]
|
|
206
341
|
@_collection_name ||= data.values.first if data.is_a?(Hash)
|
|
207
342
|
|
|
208
343
|
@_object_root_name = options[:object_root] if options.has_key?(:object_root)
|
|
209
344
|
|
|
210
|
-
|
|
345
|
+
collection_data = data_object(data)
|
|
346
|
+
object(collection_data || [])
|
|
347
|
+
# a nil collection is given to object as [], but the template names nil (see #object)
|
|
348
|
+
@_replaced_data = collection_data if defined?(@_replaced_data)
|
|
211
349
|
end
|
|
212
350
|
|
|
213
351
|
# Sets the cache key to be used by ActiveSupport::Cache.expand_cache_key
|
|
@@ -236,6 +374,8 @@ module Rabl
|
|
|
236
374
|
attribute(key, conditions.merge(:as => as))
|
|
237
375
|
end
|
|
238
376
|
else # array of attributes i.e :foo, :bar, :baz
|
|
377
|
+
warn_if_rendering(:attribute)
|
|
378
|
+
|
|
239
379
|
options = args.extract_options!
|
|
240
380
|
args.each do |name|
|
|
241
381
|
@_settings[:attributes] << { :name => name, :options => options }
|
|
@@ -248,25 +388,42 @@ module Rabl
|
|
|
248
388
|
# node(:foo) { "bar" }
|
|
249
389
|
# node(:foo, :if => lambda { ... }) { "bar" }
|
|
250
390
|
def node(name = nil, options = {}, &block)
|
|
391
|
+
warn_if_rendering(:node)
|
|
392
|
+
|
|
393
|
+
if name.is_a?(Hash) && name.present? && options.blank? && (name.keys - [:if, :unless]).empty?
|
|
394
|
+
name, options = nil, name # node(:if => lambda { ... }) { ... } has conditions but no name
|
|
395
|
+
end
|
|
396
|
+
|
|
251
397
|
@_settings[:node] << { :name => name, :options => options, :block => block }
|
|
252
398
|
end
|
|
253
399
|
alias_method :code, :node
|
|
254
400
|
|
|
255
401
|
# Creates a child node that is included in json output
|
|
256
402
|
# child(@user) { attribute :full_name }
|
|
403
|
+
# child(:posts => :items, :if => lambda { ... }) { attribute :title }
|
|
257
404
|
def child(data, options = {}, &block)
|
|
405
|
+
warn_if_rendering(:child)
|
|
406
|
+
|
|
407
|
+
data, options = split_folded_options(data, options, [:if, :unless, :root, :object_root])
|
|
408
|
+
|
|
258
409
|
@_settings[:child] << { :data => data, :options => options, :block => block }
|
|
259
410
|
end
|
|
260
411
|
|
|
261
412
|
# Glues data from a child node to the json_output
|
|
262
413
|
# glue(@user) { attribute :full_name => :user_full_name }
|
|
263
414
|
def glue(data, options = {}, &block)
|
|
415
|
+
warn_if_rendering(:glue)
|
|
416
|
+
|
|
417
|
+
data, options = split_folded_options(data, options, [:if, :unless])
|
|
418
|
+
|
|
264
419
|
@_settings[:glue] << { :data => data, :options => options, :block => block }
|
|
265
420
|
end
|
|
266
421
|
|
|
267
422
|
# Extends an existing rabl template with additional attributes in the block
|
|
268
423
|
# extends("users/show", :object => @user) { attribute :full_name }
|
|
269
424
|
def extends(file, options = {}, &block)
|
|
425
|
+
return if warn_if_rendering(:extends)
|
|
426
|
+
|
|
270
427
|
options.reverse_merge!({ :view_path => options[:view_path] || view_path })
|
|
271
428
|
|
|
272
429
|
@_settings[:extends] << { :file => file, :options => options, :block => block }
|
|
@@ -298,7 +455,81 @@ module Rabl
|
|
|
298
455
|
@_cache_read_on_render
|
|
299
456
|
end
|
|
300
457
|
|
|
458
|
+
# Returns the key given to `cache` in the template, if any. When the template is rendered
|
|
459
|
+
# for an item (see #object) and its key has the data the template names (`cache @user`, or
|
|
460
|
+
# the nil in `cache ['v1', @topic]`) but not the item, the item is added, so each item gets
|
|
461
|
+
# its own key; the other parts (such as the current user) are kept.
|
|
462
|
+
# ceiling: only the key's own parts are compared, not those of an array nested in it;
|
|
463
|
+
# walk nested arrays too if such keys turn up.
|
|
464
|
+
def rabl_cache_key_parts
|
|
465
|
+
return [] unless defined?(@_cache_key)
|
|
466
|
+
|
|
467
|
+
parts = Array(@_cache_key)
|
|
468
|
+
return parts unless defined?(@_replaced_data)
|
|
469
|
+
|
|
470
|
+
item = root_object
|
|
471
|
+
return parts if parts.any? { |part| part.equal?(item) } || parts.none? { |part| part.equal?(@_replaced_data) }
|
|
472
|
+
|
|
473
|
+
parts + [item]
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Identifies the template for cache keys: a digest of its location, the view path its
|
|
477
|
+
# partials resolve from, its source and its block
|
|
478
|
+
def rabl_template_identity
|
|
479
|
+
@_template_identity ||= begin
|
|
480
|
+
location = @_options[:source_location]
|
|
481
|
+
|
|
482
|
+
if @_options[:parent_engine] && (location || !@_source)
|
|
483
|
+
# Within one render, a template's location determines its source
|
|
484
|
+
# (nested lookups rather than an Array key, as this runs for every item)
|
|
485
|
+
memo = (rabl_identity_memo[location] ||= {})
|
|
486
|
+
memo = (memo[view_path] ||= {})
|
|
487
|
+
|
|
488
|
+
if @_block_identity
|
|
489
|
+
block, parent = @_block_identity
|
|
490
|
+
memo = (memo[block] ||= {})
|
|
491
|
+
memo[parent && parent.rabl_template_identity] ||= rabl_template_digest
|
|
492
|
+
else
|
|
493
|
+
memo[:template] ||= rabl_template_digest
|
|
494
|
+
end
|
|
495
|
+
else
|
|
496
|
+
rabl_template_digest
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
301
501
|
protected
|
|
502
|
+
def rabl_template_digest
|
|
503
|
+
root = TemplateCacheKey.root
|
|
504
|
+
view_paths = Array(view_path).map { |path| TemplateCacheKey.path(path, root) }
|
|
505
|
+
TemplateCacheKey.digest([TemplateCacheKey.path(@_options[:source_location], root), view_paths, @_source, rabl_block_identity(root)])
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Returns the digest for key, computing it once per render: the memo is kept by the
|
|
509
|
+
# top-level engine, so the items of a collection share their sub-templates' digests
|
|
510
|
+
def rabl_memoized_digest(key)
|
|
511
|
+
rabl_identity_memo[key] ||= yield
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def rabl_identity_memo
|
|
515
|
+
@_identity_memo ||= (parent = @_options[:parent_engine]) ? parent.rabl_identity_memo : {}
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def rabl_template_engine
|
|
519
|
+
self
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Identifies the block that defines this engine, if any: its location and the
|
|
523
|
+
# template that defined it
|
|
524
|
+
def rabl_block_identity(root = TemplateCacheKey.root)
|
|
525
|
+
return unless @_block_identity
|
|
526
|
+
|
|
527
|
+
block, parent = @_block_identity
|
|
528
|
+
location = block.source_location
|
|
529
|
+
location &&= [TemplateCacheKey.path(location[0], root), location[1], location.size > 2 ? location[2] : TemplateCacheKey.column(block)]
|
|
530
|
+
[location, parent && parent.rabl_template_identity]
|
|
531
|
+
end
|
|
532
|
+
|
|
302
533
|
# Returns a guess at the default object for this template
|
|
303
534
|
# default_object => @user
|
|
304
535
|
def default_object
|
|
@@ -331,25 +562,43 @@ module Rabl
|
|
|
331
562
|
(context_scope.params if context_scope.respond_to?(:params)) || {}
|
|
332
563
|
end
|
|
333
564
|
|
|
334
|
-
# Returns data as json
|
|
335
|
-
# format_json({ :foo => "bar" }) => "
|
|
336
|
-
# format_json("{ foo : "bar" }") => "test({ foo : 'bar' })"
|
|
565
|
+
# Returns data as json
|
|
566
|
+
# format_json({ :foo => "bar" }) => "{\"foo\":\"bar\"}"
|
|
337
567
|
def format_json(json_output)
|
|
338
568
|
unless json_output.is_a?(String)
|
|
339
569
|
json_engine = Rabl.configuration.json_engine
|
|
340
|
-
json_output = if json_engine.
|
|
570
|
+
json_output = if defined?(::Oj) && json_engine.equal?(::Oj)
|
|
571
|
+
oj_options = Rabl.configuration.oj_dump_options
|
|
572
|
+
oj_options.empty? ? ::Oj.dump(json_output) : ::Oj.dump(json_output, oj_options)
|
|
573
|
+
elsif json_engine.respond_to?(:dump)
|
|
341
574
|
json_engine.dump(json_output)
|
|
342
575
|
else
|
|
343
576
|
json_engine.encode(json_output)
|
|
344
577
|
end
|
|
345
578
|
end
|
|
346
579
|
|
|
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
580
|
json_output
|
|
351
581
|
end
|
|
352
582
|
|
|
583
|
+
# Returns json embraced with the request's callback when callbacks are enabled
|
|
584
|
+
# and the callback name is valid, otherwise the json unchanged
|
|
585
|
+
# rabl_wrap_json_callback("{}") => "/**/test({})"
|
|
586
|
+
def rabl_wrap_json_callback(json)
|
|
587
|
+
callback = rabl_json_callback_name unless @_skip_json_callback
|
|
588
|
+
callback ? "/**/#{callback}(#{json})" : json
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# Returns the request's callback parameter if callbacks are enabled and it is a valid name
|
|
592
|
+
def rabl_json_callback_name
|
|
593
|
+
return unless Rabl.configuration.enable_json_callbacks
|
|
594
|
+
|
|
595
|
+
callback = request_params[:callback]
|
|
596
|
+
return unless callback.is_a?(String) && callback.length <= RABL_JSON_CALLBACK_MAX_LENGTH
|
|
597
|
+
return unless callback.ascii_only? && callback =~ RABL_JSON_CALLBACK_NAME
|
|
598
|
+
|
|
599
|
+
callback
|
|
600
|
+
end
|
|
601
|
+
|
|
353
602
|
# Augments respond to supporting context_scope methods
|
|
354
603
|
def respond_to?(name, include_private = false)
|
|
355
604
|
context_scope.respond_to?(name, include_private) || super
|
|
@@ -426,21 +675,88 @@ module Rabl
|
|
|
426
675
|
end
|
|
427
676
|
|
|
428
677
|
def eval_source(locals, &block)
|
|
429
|
-
|
|
678
|
+
source = @_source
|
|
679
|
+
# A UTF-8 byte order mark (as some editors save files) isn't part of the template. It is
|
|
680
|
+
# checked by its bytes first, as it is invalid in the encoding the source may be read in
|
|
681
|
+
# (US-ASCII under LANG=C), where reading the source as text raises.
|
|
682
|
+
source = source.byteslice(3..-1) if source.is_a?(String) && source.getbyte(0) == 0xEF && source.getbyte(1) == 0xBB && source.getbyte(2) == 0xBF
|
|
683
|
+
return unless source.present?
|
|
430
684
|
|
|
431
|
-
|
|
685
|
+
# One method per source and location, so blocks report the file that defined them
|
|
686
|
+
line = @_options[:source_line] || 1
|
|
687
|
+
msg = "cached_source_#{[@_source, @_options[:source_location], line].hash.abs}"
|
|
432
688
|
|
|
433
689
|
unless self.respond_to? msg then
|
|
434
|
-
src = "def #{msg} locals, &block\n#{
|
|
690
|
+
src = "def #{msg} locals, &block\n#{source}\nend"
|
|
435
691
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
self.class.class_eval(src)
|
|
440
|
-
end
|
|
692
|
+
# The def line comes before the template's first line, so errors report the
|
|
693
|
+
# template's own line numbers
|
|
694
|
+
self.class.class_eval(src, @_options[:source_location] || "(eval)", line - 1)
|
|
441
695
|
end
|
|
442
696
|
|
|
443
697
|
send msg, locals, &block
|
|
444
698
|
end
|
|
699
|
+
|
|
700
|
+
# Hash#to_xml and Array#to_xml come from ActiveSupport (which uses the builder gem);
|
|
701
|
+
# Rails loads them, but other apps may not have.
|
|
702
|
+
def load_xml_support!
|
|
703
|
+
return if defined?(::Builder::XmlMarkup) && {}.respond_to?(:to_xml) && [].respond_to?(:to_xml)
|
|
704
|
+
|
|
705
|
+
require 'active_support/core_ext/array/conversions'
|
|
706
|
+
require 'active_support/core_ext/hash/conversions'
|
|
707
|
+
|
|
708
|
+
begin
|
|
709
|
+
require 'builder' unless defined?(::Builder::XmlMarkup)
|
|
710
|
+
rescue LoadError
|
|
711
|
+
raise LoadError, "rabl needs the builder gem to render XML; add `gem 'builder'` to your Gemfile"
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Returns a copy of the data with each Array in it, at any depth, as an XmlArray; arrays
|
|
716
|
+
# with a to_xml of their own are left as they are
|
|
717
|
+
def xml_arrays(data, children_names = {})
|
|
718
|
+
case data
|
|
719
|
+
when Hash
|
|
720
|
+
copy = data.dup
|
|
721
|
+
data.each { |key, value| copy[key] = xml_arrays(value, children_names) }
|
|
722
|
+
copy
|
|
723
|
+
when Array
|
|
724
|
+
return data unless data.method(:to_xml).owner == Array
|
|
725
|
+
|
|
726
|
+
XmlArray.new(data.map { |value| xml_arrays(value, children_names) }, children_names)
|
|
727
|
+
else
|
|
728
|
+
data
|
|
729
|
+
end
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# attribute, node, child, glue and extends build the template's settings, which are read while
|
|
733
|
+
# it renders (#477). Called then (e.g. inside a node block), they warn; attribute, node, child
|
|
734
|
+
# and glue work as before, and extends is ignored, since its settings (view path included)
|
|
735
|
+
# would otherwise be the node's value. Returns true when the template is rendering.
|
|
736
|
+
# ceiling: warns on every call, so a misuse inside a large collection repeats the warning;
|
|
737
|
+
# dedupe by call site if that turns out to be noisy.
|
|
738
|
+
def warn_if_rendering(method_name)
|
|
739
|
+
return false unless @_rendering
|
|
740
|
+
|
|
741
|
+
effect = method_name == :extends ? "is ignored" : "changes the template for the rest of the render"
|
|
742
|
+
site = caller.find { |line| !line.start_with?(__FILE__) }.to_s.sub(/:in .*\z/m, '')
|
|
743
|
+
Kernel.warn "rabl: #{method_name} called while the template renders (e.g. inside a node block) at #{site} " \
|
|
744
|
+
"#{effect}; to render another template there, use partial(\"template\", :object => object)"
|
|
745
|
+
true
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# Ruby folds `child :posts => :items, :if => cond` into a single hash, so the options
|
|
749
|
+
# arrive inside the data. They are split out only when no options were given separately
|
|
750
|
+
# and what remains is a single alias (`data => name`); any other hash is left as data.
|
|
751
|
+
# split_folded_options({ :posts => :items, :if => true }, {}, [:if]) => [{ :posts => :items }, { :if => true }]
|
|
752
|
+
def split_folded_options(data, options, option_keys)
|
|
753
|
+
return [data, options] unless data.is_a?(Hash) && options.blank?
|
|
754
|
+
|
|
755
|
+
found = data.keys & option_keys
|
|
756
|
+
alias_data = data.except(*found)
|
|
757
|
+
return [data, options] unless found.any? && alias_data.size == 1 && is_name_value?(alias_data.values.first)
|
|
758
|
+
|
|
759
|
+
[alias_data, data.slice(*found)]
|
|
760
|
+
end
|
|
445
761
|
end
|
|
446
762
|
end
|
data/lib/rabl/helpers.rb
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_support/inflector' # for the sake of pluralizing
|
|
4
|
+
require 'erb' # ERB::Util.h for escape_all_output
|
|
2
5
|
|
|
3
6
|
module Rabl
|
|
4
7
|
module Helpers
|
|
5
8
|
# data_object(data) => <AR Object>
|
|
6
9
|
# data_object(@user => :person) => @user
|
|
7
|
-
# data_object(:user => :person) => @_object.
|
|
10
|
+
# data_object(:user => :person) => @_object.public_send(:user)
|
|
8
11
|
def data_object(data)
|
|
12
|
+
return data if data_item?(data)
|
|
13
|
+
|
|
9
14
|
data = data.keys.first if data.is_a?(Hash) && data.keys.size == 1
|
|
10
|
-
data = @_object.
|
|
15
|
+
data = @_object.public_send(data) if data.is_a?(Symbol) && defined?(@_object) && @_object && @_object.respond_to?(data)
|
|
11
16
|
data
|
|
12
17
|
end
|
|
13
18
|
|
|
14
|
-
# data_object_attribute(data) => @_object.
|
|
19
|
+
# data_object_attribute(data) => @_object.public_send(data)
|
|
20
|
+
# public_send, so a name like :format reaches method_missing (Hashie::Mash) instead of private Kernel#format
|
|
15
21
|
def data_object_attribute(data)
|
|
16
|
-
attribute = @_object.
|
|
22
|
+
attribute = @_object.public_send(data)
|
|
17
23
|
attribute = attribute.as_json if
|
|
18
24
|
is_collection?(attribute, false) &&
|
|
19
25
|
attribute.respond_to?(:as_json)
|
|
@@ -34,6 +40,7 @@ module Rabl
|
|
|
34
40
|
|
|
35
41
|
if is_collection?(data) # data is a collection
|
|
36
42
|
object_name = collection_root_name if collection_root_name
|
|
43
|
+
object_name ||= data_token if data_token.is_a?(Symbol) # child :units => "units", whatever the table (#394)
|
|
37
44
|
object_name ||= data.table_name if data.respond_to?(:table_name)
|
|
38
45
|
|
|
39
46
|
if object_name.nil? && data.respond_to?(:first)
|
|
@@ -41,7 +48,6 @@ module Rabl
|
|
|
41
48
|
object_name = data_name(first).to_s.pluralize if first.present?
|
|
42
49
|
end
|
|
43
50
|
|
|
44
|
-
object_name ||= data_token if data_token.is_a?(Symbol)
|
|
45
51
|
object_name
|
|
46
52
|
elsif is_object?(data) # data is an object
|
|
47
53
|
object_name = object_root_name if object_root_name
|
|
@@ -82,6 +88,8 @@ module Rabl
|
|
|
82
88
|
# is_collection?(@user) => false
|
|
83
89
|
# is_collection?([]) => true
|
|
84
90
|
def is_collection?(obj, follow_symbols = true)
|
|
91
|
+
return false if data_item?(obj)
|
|
92
|
+
|
|
85
93
|
data_obj = follow_symbols ? data_object(obj) : obj
|
|
86
94
|
data_obj &&
|
|
87
95
|
data_obj.respond_to?(:map) && data_obj.respond_to?(:each) &&
|
|
@@ -89,6 +97,12 @@ module Rabl
|
|
|
89
97
|
defined?(Hashie::Mash) && data_obj.is_a?(Hashie::Mash))
|
|
90
98
|
end
|
|
91
99
|
|
|
100
|
+
# True for the Hash item that `extends` handed to this engine as its object (#396).
|
|
101
|
+
# It is data: never `@object => :name` alias syntax, and never a collection.
|
|
102
|
+
def data_item?(obj)
|
|
103
|
+
obj.is_a?(Hash) && defined?(@_data_item) && obj.equal?(@_data_item)
|
|
104
|
+
end
|
|
105
|
+
|
|
92
106
|
# Returns the context_scope wrapping this engine, used for retrieving data, invoking methods, etc
|
|
93
107
|
# In Rails, this is the controller and in Padrino this is the request context
|
|
94
108
|
def context_scope
|
|
@@ -128,10 +142,14 @@ module Rabl
|
|
|
128
142
|
return if object.nil?
|
|
129
143
|
|
|
130
144
|
options.reverse_merge!({
|
|
131
|
-
:format => "hash"
|
|
145
|
+
:format => "hash",
|
|
132
146
|
:view_path => view_path,
|
|
147
|
+
:nested => true,
|
|
133
148
|
:root => (options[:root] || false)
|
|
134
149
|
})
|
|
150
|
+
options[:parent_engine] = rabl_template_engine
|
|
151
|
+
# a nested template renders as a hash, but looks its templates up in the format being rendered
|
|
152
|
+
options[:source_format] ||= manual_source_format if respond_to?(:manual_source_format, true)
|
|
135
153
|
|
|
136
154
|
Engine.new(options[:source], options).apply(context_scope, :object => object, :locals => options[:locals], &block)
|
|
137
155
|
end
|
|
@@ -139,12 +157,12 @@ module Rabl
|
|
|
139
157
|
# Fetches a key from the cache and stores rabl template result otherwise
|
|
140
158
|
# fetch_from_cache('some_key') { ...rabl template result... }
|
|
141
159
|
def fetch_result_from_cache(cache_key, cache_options = {}, &block)
|
|
142
|
-
expanded_cache_key =
|
|
160
|
+
expanded_cache_key = TemplateCacheKey.expand(cache_key)
|
|
143
161
|
Rabl.configuration.cache_engine.fetch(expanded_cache_key, cache_options, &block)
|
|
144
162
|
end
|
|
145
163
|
|
|
146
164
|
def write_result_to_cache(cache_key, cache_options = {}, &block)
|
|
147
|
-
expanded_cache_key =
|
|
165
|
+
expanded_cache_key = TemplateCacheKey.expand(cache_key)
|
|
148
166
|
result = yield
|
|
149
167
|
Rabl.configuration.cache_engine.write(expanded_cache_key, result, cache_options)
|
|
150
168
|
result
|
|
@@ -159,13 +177,22 @@ module Rabl
|
|
|
159
177
|
end
|
|
160
178
|
end
|
|
161
179
|
|
|
180
|
+
protected
|
|
181
|
+
# Returns the engine of the template being rendered, if any (see Engine#rabl_template_identity)
|
|
182
|
+
def rabl_template_engine
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
162
186
|
module Escaper
|
|
187
|
+
# Returns escaped copies; the response may be shared with the cache, so it is never changed in place.
|
|
163
188
|
def escape_output(response)
|
|
164
189
|
case response
|
|
165
190
|
when Hash
|
|
166
|
-
|
|
191
|
+
escaped = response.dup
|
|
192
|
+
response.each { |k, v| escaped[k] = escape_value(v) }
|
|
193
|
+
escaped
|
|
167
194
|
when Array
|
|
168
|
-
response.map!{|v| escape_value(v) }
|
|
195
|
+
response.dup.map! { |v| escape_value(v) }
|
|
169
196
|
else
|
|
170
197
|
response
|
|
171
198
|
end
|