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/multi_builder.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Rabl
|
|
2
4
|
class MultiBuilder
|
|
3
5
|
include Helpers
|
|
@@ -53,7 +55,7 @@ module Rabl
|
|
|
53
55
|
# Maps a cache key to an engine
|
|
54
56
|
def map_cache_key_to_engine(engine)
|
|
55
57
|
if cache_key = cache_key_for(engine)
|
|
56
|
-
result_cache_key =
|
|
58
|
+
result_cache_key = TemplateCacheKey.expand(cache_key)
|
|
57
59
|
@cache_key_to_engine[result_cache_key] = engine
|
|
58
60
|
disable_cache_read_on_render(engine)
|
|
59
61
|
end
|
|
@@ -94,7 +96,11 @@ module Rabl
|
|
|
94
96
|
@cache_results.each do |key, value|
|
|
95
97
|
engine = @cache_key_to_engine[key]
|
|
96
98
|
builder = @engine_to_builder[engine]
|
|
97
|
-
|
|
99
|
+
next unless value && engine && builder
|
|
100
|
+
|
|
101
|
+
# a child is stored as { name => #<Engine> }; the cache holds only its rendered body
|
|
102
|
+
value = { engine.keys.first => value } if engine.is_a?(Hash)
|
|
103
|
+
builder.replace_engine(engine, value)
|
|
98
104
|
end
|
|
99
105
|
end
|
|
100
106
|
end
|
data/lib/rabl/partials.rb
CHANGED
data/lib/rabl/railtie.rb
CHANGED
data/lib/rabl/renderer.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Rabl
|
|
2
4
|
class Renderer
|
|
3
5
|
# Defines class method rendering in supported formats
|
|
@@ -63,7 +65,8 @@ module Rabl
|
|
|
63
65
|
def process_source(source)
|
|
64
66
|
return source if source.is_a?(String) && source =~ /\n/
|
|
65
67
|
|
|
66
|
-
source,
|
|
68
|
+
source, location = engine.fetch_source(source, { :view_path => options[:view_path] })
|
|
69
|
+
options[:source_location] ||= location
|
|
67
70
|
source
|
|
68
71
|
end
|
|
69
72
|
|
data/lib/rabl/sources.rb
CHANGED
|
@@ -1,20 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Rabl
|
|
2
4
|
module Sources
|
|
3
5
|
include Helpers
|
|
4
6
|
|
|
7
|
+
# Each distinct view path list or set of lookup details => the number standing for it in
|
|
8
|
+
# source cache keys, so a key is quick to compare however many view paths there are.
|
|
9
|
+
# The numbers come from its size, so it must never be cleared without LOOKUP_IDS_BY_OBJECT.
|
|
10
|
+
LOOKUP_IDS = {}
|
|
11
|
+
LOOKUP_IDS_LOCK = Mutex.new
|
|
12
|
+
# Rails' frozen view path lists and details keys => their numbers, worked out once per object.
|
|
13
|
+
# ceiling: CRuby 3.2 and higher only (JRuby's WeakMap holds its keys strongly, and before 3.2
|
|
14
|
+
# CRuby's slows its GC down with many dead keys); elsewhere the numbers are worked out on
|
|
15
|
+
# each lookup. Upgrade path: enable it on an engine once its WeakMap holds keys weakly
|
|
16
|
+
# without that cost.
|
|
17
|
+
LOOKUP_IDS_BY_OBJECT = (ObjectSpace::WeakMap.new if RUBY_ENGINE == "ruby" && RUBY_VERSION >= "3.2")
|
|
18
|
+
# I18n's fallbacks (by identity) => locale => default locale => format => the number standing
|
|
19
|
+
# for the manual lookup's format and locale chain in source cache keys, worked out once for each.
|
|
20
|
+
# ceiling: the fallbacks are read once per locale, so fallbacks changed in place later
|
|
21
|
+
# (I18n.fallbacks.map) aren't seen by the cache until they are assigned anew
|
|
22
|
+
# (I18n.fallbacks = ...); and an I18n.default_locale reassigned by another thread while an entry
|
|
23
|
+
# is made can store it under the old default. Upgrade path: key by the fallbacks' contents.
|
|
24
|
+
MANUAL_LOOKUP_IDS = [nil]
|
|
25
|
+
MANUAL_LOOKUP_IDS_LOCK = Mutex.new
|
|
26
|
+
|
|
5
27
|
# Returns source for a given relative file
|
|
6
28
|
# fetch_source("show", :view_path => "...") => "...contents..."
|
|
7
29
|
def fetch_source(file, options = {})
|
|
8
|
-
|
|
30
|
+
resolver = source_resolver
|
|
31
|
+
lookup = source_lookup(resolver) if Rabl.configuration.cache_sources
|
|
32
|
+
# a lookup that can't be told apart from others (see source_lookup) isn't cached
|
|
33
|
+
return resolve_source(file, options, resolver) if lookup == false
|
|
34
|
+
|
|
35
|
+
Rabl.source_cache(file, Array(options[:view_path]), lookup) do
|
|
36
|
+
resolve_source(file, options, resolver)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
# Looks the file up with the given resolver and returns its source and path
|
|
42
|
+
def resolve_source(file, options, resolver)
|
|
43
|
+
view_paths = Array(options[:view_path]) + Array(Rabl.configuration.view_paths)
|
|
9
44
|
|
|
10
|
-
Rabl.source_cache(file, view_paths) do
|
|
11
45
|
file_path = \
|
|
12
|
-
|
|
46
|
+
case resolver
|
|
47
|
+
when :padrino
|
|
13
48
|
fetch_padrino_source(file, options)
|
|
14
|
-
|
|
49
|
+
when :rails
|
|
15
50
|
_view_paths = view_paths + Array(context_scope.view_paths.to_a)
|
|
16
|
-
fetch_rails_source(file, options) || fetch_manual_template(_view_paths, file)
|
|
17
|
-
|
|
51
|
+
fetch_rails_source(file, options) || fetch_manual_template(_view_paths, file, rails_lookup_format, rails_lookup_locales)
|
|
52
|
+
when :sinatra
|
|
18
53
|
fetch_sinatra_source(file, options)
|
|
19
54
|
else # generic template resolution
|
|
20
55
|
fetch_manual_template(view_paths, file)
|
|
@@ -26,9 +61,186 @@ module Rabl
|
|
|
26
61
|
|
|
27
62
|
[File.read(file_path.to_s), file_path.to_s]
|
|
28
63
|
end
|
|
29
|
-
end
|
|
30
64
|
|
|
31
|
-
|
|
65
|
+
# Returns which framework's template lookup fetch_source uses in this context_scope
|
|
66
|
+
def source_resolver
|
|
67
|
+
if defined?(Padrino) && context_scope.respond_to?(:settings) && context_scope.respond_to?(:resolve_template)
|
|
68
|
+
:padrino
|
|
69
|
+
elsif defined?(Rails) && context_scope.respond_to?(:view_paths)
|
|
70
|
+
:rails
|
|
71
|
+
elsif defined?(Sinatra) && context_scope.respond_to?(:settings) && context_scope.settings.respond_to?(:views)
|
|
72
|
+
:sinatra
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns the format fetch_source looks templates up in: under Rails' lookup the
|
|
77
|
+
# request's (see rails_lookup_format), elsewhere the one being rendered (json for hash)
|
|
78
|
+
def source_lookup_format
|
|
79
|
+
(source_resolver == :rails ? rails_lookup_format : manual_source_format).to_s
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns what, besides the file and the :view_path option, the resolver's lookup
|
|
83
|
+
# depends on, so the source cache keeps each lookup's result apart: under Rails the
|
|
84
|
+
# format, the lookup details (formats, locale, variants, handlers, and registered
|
|
85
|
+
# details such as versioncake's versions) and the view paths; elsewhere the format
|
|
86
|
+
# and locales the manual lookup prefers, and under Sinatra the app's views. Returns
|
|
87
|
+
# false when a view path has no stable name, so the lookup isn't cached.
|
|
88
|
+
# ceiling: outside Rails the cache (and MANUAL_LOOKUP_IDS) keeps an entry per locale looked
|
|
89
|
+
# up in, as Rails' details keys do, so it is bounded by I18n.available_locales only while
|
|
90
|
+
# I18n.enforce_available_locales is on (the default).
|
|
91
|
+
def source_lookup(resolver)
|
|
92
|
+
case resolver
|
|
93
|
+
when :rails
|
|
94
|
+
view_paths = rails_view_paths_id or return false
|
|
95
|
+
details = rails_details_id
|
|
96
|
+
return false if details == false
|
|
97
|
+
[rails_lookup_format, details, view_paths]
|
|
98
|
+
when :sinatra
|
|
99
|
+
[context_scope.settings.views, manual_lookup_id]
|
|
100
|
+
else
|
|
101
|
+
manual_lookup_id
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the number standing for the format and locales the manual lookup prefers.
|
|
106
|
+
# Nested Hashes of what the locales come from, as this runs on every source cache hit.
|
|
107
|
+
def manual_lookup_id
|
|
108
|
+
format = manual_source_format
|
|
109
|
+
return lookup_id([:manual, format, []]) unless defined?(::I18n) && ::I18n.respond_to?(:config)
|
|
110
|
+
|
|
111
|
+
config = ::I18n.config
|
|
112
|
+
locale, default_locale = config.locale, config.default_locale
|
|
113
|
+
fallbacks = ::I18n.fallbacks if ::I18n.respond_to?(:fallbacks)
|
|
114
|
+
table = MANUAL_LOOKUP_IDS[0]
|
|
115
|
+
ids = table[1] if table && table[0].equal?(fallbacks)
|
|
116
|
+
ids &&= ids[locale]
|
|
117
|
+
ids &&= ids[default_locale]
|
|
118
|
+
(ids && ids[format]) || MANUAL_LOOKUP_IDS_LOCK.synchronize do
|
|
119
|
+
table = MANUAL_LOOKUP_IDS[0]
|
|
120
|
+
# only the current fallbacks are kept, so assigning them per request doesn't grow it
|
|
121
|
+
table = MANUAL_LOOKUP_IDS[0] = [fallbacks, {}] unless table && table[0].equal?(fallbacks)
|
|
122
|
+
ids = (table[1][locale] ||= {})[default_locale] ||= {}
|
|
123
|
+
# lookup_id takes its own lock, never the other way round
|
|
124
|
+
ids[format] ||= lookup_id([:manual, format, manual_source_locales])
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Returns the number standing for the Rails view paths, the same for the same paths,
|
|
129
|
+
# or false when one of them has no stable name (a resolver without to_path)
|
|
130
|
+
def rails_view_paths_id
|
|
131
|
+
paths = context_scope.view_paths
|
|
132
|
+
paths = paths.respond_to?(:paths) ? paths.paths : Array(paths.to_a)
|
|
133
|
+
# Rails 7.1 and higher freeze a PathSet's paths
|
|
134
|
+
lookup_id_by_object(paths.frozen? && paths) do
|
|
135
|
+
names = paths.map do |path|
|
|
136
|
+
if path.is_a?(String)
|
|
137
|
+
path
|
|
138
|
+
elsif path.respond_to?(:to_path)
|
|
139
|
+
path.to_path.to_s
|
|
140
|
+
else
|
|
141
|
+
break
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
names ? lookup_id([:view_paths, names]) : false # false is remembered too
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns the number standing for the Rails lookup context's details
|
|
149
|
+
def rails_details_id
|
|
150
|
+
lookup_context = context_scope.lookup_context if context_scope.respond_to?(:lookup_context)
|
|
151
|
+
return unless lookup_context
|
|
152
|
+
# Rails reads the registered details from ActionView::LookupContext itself (7.0 and higher keep
|
|
153
|
+
# them there only, so a subclass's own are nil). Module#=== checks the object itself, not what
|
|
154
|
+
# a delegator forwards to.
|
|
155
|
+
names = ActionView::LookupContext.registered_details if defined?(ActionView::LookupContext) && ActionView::LookupContext === lookup_context
|
|
156
|
+
names ||= lookup_context.class.registered_details if lookup_context.class.respond_to?(:registered_details)
|
|
157
|
+
return false unless names # what the lookup depends on can't be told, so it isn't cached
|
|
158
|
+
|
|
159
|
+
# Rails' details_key is the same object for the same details until Rails clears them
|
|
160
|
+
# (in development, when templates change), and the details give the same number again then
|
|
161
|
+
details_key = lookup_context.details_key if lookup_context.respond_to?(:details_key)
|
|
162
|
+
lookup_id_by_object(details_key) do
|
|
163
|
+
# Rails' own accessors: LookupContext#locale returns the first locale, the lookup
|
|
164
|
+
# uses the whole chain (I18n.locale, its fallbacks and the default locale)
|
|
165
|
+
accessors = ActionView::LookupContext::Accessors if defined?(ActionView::LookupContext::Accessors)
|
|
166
|
+
details = names.map do |name|
|
|
167
|
+
if accessors && accessors === lookup_context && accessors.method_defined?(name)
|
|
168
|
+
accessors.instance_method(name).bind_call(lookup_context)
|
|
169
|
+
else
|
|
170
|
+
lookup_context.public_send(name)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
lookup_id([:details, details])
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Returns the number for object, or the block's number, which it is then kept for
|
|
178
|
+
def lookup_id_by_object(object)
|
|
179
|
+
return yield unless object && LOOKUP_IDS_BY_OBJECT
|
|
180
|
+
|
|
181
|
+
id = LOOKUP_IDS_BY_OBJECT[object]
|
|
182
|
+
id.nil? ? (LOOKUP_IDS_BY_OBJECT[object] = yield) : id
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Returns the number standing for value, the same for an equal value
|
|
186
|
+
def lookup_id(value)
|
|
187
|
+
LOOKUP_IDS[value] || LOOKUP_IDS_LOCK.synchronize do
|
|
188
|
+
LOOKUP_IDS.fetch(value) { LOOKUP_IDS[Rabl.frozen_copy(value)] = LOOKUP_IDS.size }
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Returns the format Rails 3.2 and higher look templates up in:
|
|
193
|
+
# the request's format, or for HTML requests the format being rendered (json for
|
|
194
|
+
# the default handler and for hash), as a Symbol, the only form Rails 7.0 and higher match
|
|
195
|
+
def rails_source_format
|
|
196
|
+
format = context_scope.request.format.to_sym
|
|
197
|
+
format == :html ? manual_source_format.to_sym : format
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Returns the format Rails looks the template up in or, when there is none (a view
|
|
201
|
+
# without a request, or a request format Rails doesn't know, where that lookup fails
|
|
202
|
+
# and the manual lookup is used), the format being rendered
|
|
203
|
+
def rails_lookup_format
|
|
204
|
+
rails_source_format || manual_source_format
|
|
205
|
+
rescue StandardError
|
|
206
|
+
manual_source_format
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Returns the locale chain of the Rails lookup context, which the source cache key holds
|
|
210
|
+
# (it can differ from I18n's, e.g. in I18n.with_locale or outside a controller action),
|
|
211
|
+
# or I18n's chain when the lookup context isn't Rails' own
|
|
212
|
+
def rails_lookup_locales
|
|
213
|
+
lookup_context = context_scope.lookup_context if context_scope.respond_to?(:lookup_context)
|
|
214
|
+
accessors = ActionView::LookupContext::Accessors if defined?(ActionView::LookupContext::Accessors)
|
|
215
|
+
# Module#=== checks the object itself, not what a delegator forwards to (see rails_details_id)
|
|
216
|
+
return manual_source_locales unless lookup_context && accessors && accessors === lookup_context && accessors.method_defined?(:locale)
|
|
217
|
+
|
|
218
|
+
Array(accessors.instance_method(:locale).bind_call(lookup_context)).map(&:to_s)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Returns the format the manual lookup prefers: the format being rendered (json for hash),
|
|
222
|
+
# which nested templates are given as :source_format
|
|
223
|
+
def manual_source_format
|
|
224
|
+
# Engine keeps its options in @_options, Builder in @options
|
|
225
|
+
options = (@_options if defined?(@_options)) || (@options if defined?(@options))
|
|
226
|
+
format = options[:source_format] || options[:format] if options
|
|
227
|
+
format ||= request_format if defined?(request_format)
|
|
228
|
+
format = format.to_s
|
|
229
|
+
format.empty? || format == "hash" ? "json" : format
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Returns the locales the manual lookup prefers, best first: like Rails' lookup,
|
|
233
|
+
# I18n's locale, its fallbacks and the default locale
|
|
234
|
+
def manual_source_locales
|
|
235
|
+
return [] unless defined?(::I18n) && ::I18n.respond_to?(:config)
|
|
236
|
+
|
|
237
|
+
config = ::I18n.config
|
|
238
|
+
locales = [config.locale]
|
|
239
|
+
locales.concat(::I18n.fallbacks[config.locale]) if ::I18n.respond_to?(:fallbacks)
|
|
240
|
+
locales << config.default_locale
|
|
241
|
+
locales.uniq.map(&:to_s)
|
|
242
|
+
end
|
|
243
|
+
|
|
32
244
|
# Returns the rabl template path for padrino views using configured views
|
|
33
245
|
def fetch_padrino_source(file, options = {})
|
|
34
246
|
view_path = Array(options[:view_path] || context_scope.settings.views)
|
|
@@ -51,9 +263,7 @@ module Rabl
|
|
|
51
263
|
context_scope.lookup_context.find(file, [], partial)
|
|
52
264
|
else # Rails 3.2 and higher
|
|
53
265
|
# pull format directly from rails unless it is html
|
|
54
|
-
|
|
55
|
-
source_format = request_format unless request_format == :html
|
|
56
|
-
context_scope.lookup_context.find(file, [], partial, [], { :formats => [source_format] })
|
|
266
|
+
context_scope.lookup_context.find(file, [], partial, [], { :formats => [rails_source_format] })
|
|
57
267
|
end
|
|
58
268
|
end
|
|
59
269
|
template = lookup_proc.call(false) rescue nil
|
|
@@ -71,9 +281,46 @@ module Rabl
|
|
|
71
281
|
fetch_manual_template(view_path, file)
|
|
72
282
|
end
|
|
73
283
|
|
|
74
|
-
# Returns the rabl template by looking up files within the view_path and specified file path
|
|
75
|
-
|
|
76
|
-
|
|
284
|
+
# Returns the rabl template by looking up files within the view_path and specified file path.
|
|
285
|
+
# Like Rails' lookup, it takes a template named for the format and locales it is given
|
|
286
|
+
# ("show.json.rabl", "show.fr.json.rabl", "show.fr.rabl" or "show.rabl"), the best named
|
|
287
|
+
# in the first view path, and the file before the partial, that has one; otherwise the
|
|
288
|
+
# first file found ("show.xml.rabl" for json), as it always has.
|
|
289
|
+
# ceiling: other details in a name, such as variants ("show.json+mobile.rabl"), never match
|
|
290
|
+
# here; Rails' own lookup handles those where it is used.
|
|
291
|
+
def fetch_manual_template(view_path, file, format = nil, locales = nil)
|
|
292
|
+
found = Dir[File.join("{#{view_path.join(",")}}", "{#{file},#{partialized(file)}}" + ".{*.,}rabl")]
|
|
293
|
+
return found.first if found.size < 2 # nothing to choose between, as for most templates
|
|
294
|
+
|
|
295
|
+
format ||= manual_source_format
|
|
296
|
+
locales ||= manual_source_locales
|
|
297
|
+
format = format.to_s
|
|
298
|
+
prefixes = [file, partialized(file)].map { |name| "#{File.basename(name)}." }
|
|
299
|
+
best = best_rank = best_group = nil
|
|
300
|
+
|
|
301
|
+
# Dir returns the files of each view path and name together, in the order given
|
|
302
|
+
found.each do |path|
|
|
303
|
+
dir, base = File.split(path)
|
|
304
|
+
prefix = prefixes.find { |name| base.start_with?(name) } or next
|
|
305
|
+
group = [dir, prefix]
|
|
306
|
+
break if best && group != best_group
|
|
307
|
+
|
|
308
|
+
rank = manual_template_rank(base[prefix.size...-4].split("."), format, locales)
|
|
309
|
+
best, best_rank, best_group = path, rank, group if rank && (!best || (rank <=> best_rank) < 0)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
best || found.first
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Ranks the details in a template's name ("fr.json" => ["fr", "json"]) the way Rails
|
|
316
|
+
# sorts them, the locale first: a smaller rank is better, nil means they don't match
|
|
317
|
+
def manual_template_rank(details, format, locales)
|
|
318
|
+
format_rank = details.last == format ? 1 : 2
|
|
319
|
+
details = details[0...-1] if format_rank == 1
|
|
320
|
+
return [locales.size, format_rank] if details.empty?
|
|
321
|
+
|
|
322
|
+
locale_rank = locales.index(details.first)
|
|
323
|
+
[locale_rank, format_rank] if locale_rank && details.size == 1
|
|
77
324
|
end
|
|
78
325
|
|
|
79
326
|
# Returns a partialized version of a file path
|
data/lib/rabl/template.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# TILT Template
|
|
2
4
|
if defined?(Tilt)
|
|
3
5
|
class RablTemplate < Tilt::Template
|
|
@@ -11,7 +13,8 @@ if defined?(Tilt)
|
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def evaluate(context_scope, locals, &block)
|
|
14
|
-
|
|
16
|
+
# an inline template (Sinatra's __END__ section) starts at line, in its app's file
|
|
17
|
+
options = @options.merge(:source_location => file, :source_line => line)
|
|
15
18
|
::Rabl::Engine.new(data, options).apply(context_scope, locals, &block).render
|
|
16
19
|
end
|
|
17
20
|
end
|
|
@@ -51,7 +54,7 @@ if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rai
|
|
|
51
54
|
def self.call(template)
|
|
52
55
|
source = template.source
|
|
53
56
|
|
|
54
|
-
%{ ::Rabl::Engine.new(#{source.inspect}).
|
|
57
|
+
%{ ::Rabl::Engine.new(#{source.inspect}, :source_location => #{template.identifier.inspect}).
|
|
55
58
|
apply(self, assigns.merge(local_assigns)).
|
|
56
59
|
render }
|
|
57
60
|
end # call
|
|
@@ -71,7 +74,7 @@ if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rai
|
|
|
71
74
|
self.default_format = :json
|
|
72
75
|
|
|
73
76
|
def self.call(template, source)
|
|
74
|
-
%{ ::Rabl::Engine.new(#{source.inspect}).
|
|
77
|
+
%{ ::Rabl::Engine.new(#{source.inspect}, :source_location => #{template.identifier.inspect}).
|
|
75
78
|
apply(self, assigns.merge(local_assigns)).
|
|
76
79
|
render }
|
|
77
80
|
end # call
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest/sha1'
|
|
4
|
+
|
|
5
|
+
module Rabl
|
|
6
|
+
# Marks every cache key rabl reads or writes. Bump it when a release changes rendered output so
|
|
7
|
+
# that entries cached by earlier versions would be wrong: the keys change once, so no entry
|
|
8
|
+
# cached by an earlier version, which lacks that release's output fixes, is served.
|
|
9
|
+
# Keys before 0.18.0 had no marker ("rabl/", and "rabl_build/" in old releases); 2 is 0.18.0.
|
|
10
|
+
# The README's caching example shows the resulting prefix: update it when this changes.
|
|
11
|
+
CACHE_KEY_VERSION = 2
|
|
12
|
+
|
|
13
|
+
# Builds the parts of template cache keys. These are module functions rather than
|
|
14
|
+
# Engine methods so that they never shadow helpers a template calls.
|
|
15
|
+
module TemplateCacheKey
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Returns the key rabl reads and writes in the cache engine for the given parts. Earlier
|
|
19
|
+
# versions put their keys under "rabl/", so no key built here can be one of theirs.
|
|
20
|
+
# TemplateCacheKey.expand([user, "json"]) => "rabl-v2/users/1/json"
|
|
21
|
+
def expand(parts)
|
|
22
|
+
ActiveSupport::Cache.expand_cache_key(parts, "rabl-v#{CACHE_KEY_VERSION}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns a digest of the given parts
|
|
26
|
+
# TemplateCacheKey.digest(["app/views/show.rabl", "object @user"]) => "4a1b..."
|
|
27
|
+
def digest(parts)
|
|
28
|
+
digest = Digest::SHA1.new
|
|
29
|
+
Array(parts).flatten.each { |part| digest << part.to_s << "\0" }
|
|
30
|
+
digest.hexdigest
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns a path that stays the same when the application moves: relative to the
|
|
34
|
+
# working directory when inside it, and nil for "(eval)" locations (the identity of
|
|
35
|
+
# the parent template tells evaluated sources apart).
|
|
36
|
+
# ceiling: paths outside the working directory stay absolute, so moving them changes
|
|
37
|
+
# the keys; a configurable root would let those move too.
|
|
38
|
+
# TemplateCacheKey.path("/app/views/show.rabl", "/app/") => "views/show.rabl"
|
|
39
|
+
def path(path, root = self.root)
|
|
40
|
+
path = path.to_s.dup.force_encoding(Encoding::BINARY) # paths may differ in encoding
|
|
41
|
+
return if path.empty? || path.start_with?("(eval")
|
|
42
|
+
|
|
43
|
+
root && path.start_with?(root) ? path.byteslice(root.bytesize..-1) : path
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns the column a block starts at, which tells apart blocks on one line, or nil when
|
|
47
|
+
# the runtime doesn't expose it.
|
|
48
|
+
# ceiling: without RubyVM::InstructionSequence (e.g. on JRuby), blocks that start on the
|
|
49
|
+
# same line of a template share an identity; put them on separate lines, or give them
|
|
50
|
+
# distinct cache keys.
|
|
51
|
+
def column(block)
|
|
52
|
+
return unless defined?(RubyVM::InstructionSequence)
|
|
53
|
+
|
|
54
|
+
iseq = RubyVM::InstructionSequence.of(block)
|
|
55
|
+
iseq && iseq.to_a[4][:code_location][1]
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns the working directory with a trailing separator, as bytes, or nil if it is gone
|
|
61
|
+
def root
|
|
62
|
+
File.join(Dir.pwd, "").force_encoding(Encoding::BINARY)
|
|
63
|
+
rescue SystemCallError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns the options that change the shape of a rendering. A nested (hash format)
|
|
68
|
+
# rendering gets its root options from its parent; for other formats only root
|
|
69
|
+
# options given explicitly are added, so default keys have none. The format `extends`
|
|
70
|
+
# and `partial` look templates up in (lookup_format) is added when it isn't the one
|
|
71
|
+
# rendered, a hash counting as JSON: a nested rendering is a hash for every format.
|
|
72
|
+
# TemplateCacheKey.shape({ :format => "hash", :root => true }, "json") => ["root", "no_child_root"]
|
|
73
|
+
# TemplateCacheKey.shape({ :format => "hash", :child_root => true }, "xml") => ["lookup_format=xml"]
|
|
74
|
+
# TemplateCacheKey.shape({ :format => "json", :except => :b }, "json") => ["except=:b"]
|
|
75
|
+
def shape(options, lookup_format)
|
|
76
|
+
shape = []
|
|
77
|
+
format = options[:format].to_s
|
|
78
|
+
|
|
79
|
+
if format == "hash"
|
|
80
|
+
shape << "root" if options[:root]
|
|
81
|
+
shape << "no_child_root" unless options[:child_root]
|
|
82
|
+
shape << "object_root=#{options[:object_root_name].inspect}" if options[:object_root_name]
|
|
83
|
+
else
|
|
84
|
+
shape << "root=#{options[:root].inspect}" if options.key?(:root)
|
|
85
|
+
shape << "child_root=#{options[:child_root].inspect}" if options.key?(:child_root)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
shape << "except=#{Array(options[:except]).map { |name| name.inspect }.join(',')}" if options[:except]
|
|
89
|
+
shape << "lookup_format=#{lookup_format}" unless lookup_format == (format == "hash" ? "json" : format)
|
|
90
|
+
shape
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/rabl/tracker.rb
CHANGED
data/lib/rabl/version.rb
CHANGED
data/lib/rabl.rb
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_support'
|
|
4
|
+
require 'active_support/core_ext/array/extract_options'
|
|
2
5
|
require 'active_support/core_ext/string/inflections'
|
|
3
6
|
require 'active_support/core_ext/object/blank'
|
|
4
7
|
require 'active_support/core_ext/hash/reverse_merge'
|
|
@@ -15,6 +18,7 @@ require 'rabl/multi_builder'
|
|
|
15
18
|
require 'rabl/configuration'
|
|
16
19
|
require 'rabl/renderer'
|
|
17
20
|
require 'rabl/cache_engine'
|
|
21
|
+
require 'rabl/template_cache_key'
|
|
18
22
|
|
|
19
23
|
if defined?(Rails) && Rails.respond_to?(:version)
|
|
20
24
|
require 'rabl/tracker' if Rails.version =~ /^[45678]/
|
|
@@ -54,16 +58,18 @@ module Rabl
|
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
# Fetches from the source_cache, stores block result in cache if nil
|
|
57
|
-
# Used to cache the contents and paths to various rabl templates
|
|
58
|
-
#
|
|
59
|
-
|
|
61
|
+
# Used to cache the contents and paths to various rabl templates, keyed by
|
|
62
|
+
# everything the lookup depends on (the file, the view path and the lookup details)
|
|
63
|
+
# source_cache("users/index", ["path/to/view"], [:json]) { "/full/path/to/template/users/index" }
|
|
64
|
+
def source_cache(file, view_path, lookup = nil)
|
|
60
65
|
return yield unless Rabl.configuration.cache_sources
|
|
61
66
|
|
|
62
|
-
cache_key = [file, view_path].compact.join(":")
|
|
63
|
-
|
|
64
67
|
@_source_cache ||= {}
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
# stores a frozen copy of the key, so later changes to the caller's strings don't reach it
|
|
70
|
+
@_source_cache.fetch([file, view_path, lookup]) do
|
|
71
|
+
@_source_cache[frozen_copy([file, view_path, lookup])] = yield
|
|
72
|
+
end
|
|
67
73
|
end
|
|
68
74
|
|
|
69
75
|
# Resets the RABL source cache
|
|
@@ -71,6 +77,16 @@ module Rabl
|
|
|
71
77
|
@_source_cache = {}
|
|
72
78
|
end
|
|
73
79
|
|
|
80
|
+
# Returns a copy of value whose Strings and Arrays are frozen, for cache keys that
|
|
81
|
+
# must not change when the caller's objects do
|
|
82
|
+
def frozen_copy(value)
|
|
83
|
+
case value
|
|
84
|
+
when Array then value.map { |item| frozen_copy(item) }.freeze
|
|
85
|
+
when String then value.frozen? ? value : value.dup.freeze
|
|
86
|
+
else value
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
74
90
|
# Renders an object using a specified template within an application.
|
|
75
91
|
# render(@post, 'posts/show', :view_path => "/path/to/app/views")
|
|
76
92
|
def render(object, source, options = {})
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rabl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan Esquenazi
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-10-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -51,6 +51,7 @@ files:
|
|
|
51
51
|
- lib/rabl/renderer.rb
|
|
52
52
|
- lib/rabl/sources.rb
|
|
53
53
|
- lib/rabl/template.rb
|
|
54
|
+
- lib/rabl/template_cache_key.rb
|
|
54
55
|
- lib/rabl/tracker.rb
|
|
55
56
|
- lib/rabl/version.rb
|
|
56
57
|
homepage: https://github.com/nesquena/rabl
|
|
@@ -65,7 +66,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
65
66
|
requirements:
|
|
66
67
|
- - ">="
|
|
67
68
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
69
|
+
version: '2.7'
|
|
69
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
71
|
requirements:
|
|
71
72
|
- - ">="
|