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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -0
- data/README.md +105 -23
- data/SECURITY.md +14 -0
- data/lib/rabl/builder.rb +19 -1
- data/lib/rabl/cache_engine.rb +21 -20
- data/lib/rabl/configuration.rb +71 -4
- data/lib/rabl/digestor.rb +4 -22
- data/lib/rabl/engine.rb +342 -36
- 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 +12 -14
- data/lib/rabl/renderer.rb +14 -6
- data/lib/rabl/sources.rb +264 -25
- data/lib/rabl/template.rb +7 -47
- 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 +25 -11
- metadata +12 -9
- data/lib/rabl/digestor/rails3.rb +0 -12
- data/lib/rabl/digestor/rails41.rb +0 -12
- data/lib/rabl/digestor/rails5.rb +0 -6
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
|
|
51
|
-
|
|
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 =
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
335
|
-
# format_json({ :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.
|
|
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?(
|
|
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
|
-
|
|
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
|
-
|
|
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#{
|
|
680
|
+
src = "def #{msg} locals, &block\n#{source}\nend"
|
|
435
681
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|