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.
@@ -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 = ActiveSupport::Cache.expand_cache_key(cache_key, :rabl)
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
- builder.replace_engine(engine, value) if value
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rabl
2
4
  module Partials
3
5
  include Helpers
data/lib/rabl/railtie.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rabl
2
4
  class Railtie < Rails::Railtie
3
5
  initializer "rabl.initialize" do |app|
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, _ = engine.fetch_source(source, { :view_path => options[:view_path] })
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
- view_paths = Array(options[:view_path]) + Array(Rabl.configuration.view_paths)
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
- if defined?(Padrino) && context_scope.respond_to?(:settings) && context_scope.respond_to?(:resolve_template)
46
+ case resolver
47
+ when :padrino
13
48
  fetch_padrino_source(file, options)
14
- elsif defined?(Rails) && context_scope.respond_to?(:view_paths)
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
- elsif defined?(Sinatra) && context_scope.respond_to?(:settings) && context_scope.settings.respond_to?(:views)
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
- private
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
- request_format = context_scope.request.format.to_sym
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
- def fetch_manual_template(view_path, file)
76
- Dir[File.join("{#{view_path.join(",")}}", "{#{file},#{partialized(file)}}" + ".{*.,}rabl")].first
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
- options = @options.merge(:source_location => file)
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rabl
2
4
  # DependencyTracker for ActionView to support cache digest
3
5
  class Tracker
data/lib/rabl/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rabl
2
- VERSION = "0.17.0"
4
+ VERSION = "0.18.0"
3
5
  end
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
- # source_cache("users/index", "path/to/view") { "/full/path/to/template/users/index" }
59
- def source_cache(file, view_path, &block)
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
- @_source_cache[cache_key] ||= yield
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.17.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: 2024-11-26 00:00:00.000000000 Z
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: '0'
69
+ version: '2.7'
69
70
  required_rubygems_version: !ruby/object:Gem::Requirement
70
71
  requirements:
71
72
  - - ">="