rabl 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/rabl/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.send(:user)
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.__send__(data) if data.is_a?(Symbol) && defined?(@_object) && @_object && @_object.respond_to?(data)
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.send(data)
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.__send__(data)
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".freeze,
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 = ActiveSupport::Cache.expand_cache_key(cache_key, :rabl)
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 = ActiveSupport::Cache.expand_cache_key(cache_key, :rabl)
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
- response.each{|k,v| response[k] = escape_value(v) }
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
@@ -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,20 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rabl
2
4
  class Railtie < Rails::Railtie
3
5
  initializer "rabl.initialize" do |app|
4
6
  # Force Rails to load view templates even in API mode
5
7
  # Stolen shamelessly from jbuilder: https://github.com/rails/jbuilder/blob/master/lib/jbuilder/railtie.rb
6
- if Rails::VERSION::MAJOR >= 5
7
- module ::ActionController
8
- module ApiRendering
9
- include ActionView::Rendering
10
- end
8
+ module ::ActionController
9
+ module ApiRendering
10
+ include ActionView::Rendering
11
11
  end
12
+ end
12
13
 
13
- ActiveSupport.on_load :action_controller do
14
- if self == ActionController::API
15
- include ActionController::Helpers
16
- include ActionController::ImplicitRender
17
- end
14
+ ActiveSupport.on_load :action_controller do
15
+ if self == ActionController::API
16
+ include ActionController::Helpers
17
+ include ActionController::ImplicitRender
18
18
  end
19
19
  end
20
20
 
@@ -22,10 +22,8 @@ module Rabl
22
22
  Rabl.register!
23
23
 
24
24
  # Inject dependency tracker for :rabl
25
- if Rails.version =~ /^[45678]/
26
- require 'action_view/dependency_tracker'
27
- ActionView::DependencyTracker.register_tracker :rabl, Rabl::Tracker
28
- end
25
+ require 'action_view/dependency_tracker'
26
+ ActionView::DependencyTracker.register_tracker :rabl, Rabl::Tracker
29
27
  end
30
28
  end
31
29
  end # Railtie
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
@@ -61,23 +63,26 @@ module Rabl
61
63
 
62
64
  # Returns the source given a relative template path
63
65
  def process_source(source)
64
- return source if source.is_a?(String) && source =~ /\n/
66
+ return source if source.is_a?(String) && source.include?("\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
 
70
- # Internal: Sets an instance variable named after the class of `object`
73
+ # Internal: Sets an instance variable named after the class of `object`,
74
+ # unless the class has no name (Struct.new or Class.new)
71
75
  #
72
76
  # Example:
73
77
  # object.class.name # => User
74
78
  # set_object_instance_variable # => @user == object
75
79
  #
76
80
  def set_object_instance_variable
77
- instance_variable_set(:"@#{object_model_name}", object)
81
+ name = object_model_name
82
+ instance_variable_set(:"@#{name}", object) if name
78
83
  end
79
84
 
80
- # Internal: Returns the model name for an object
85
+ # Internal: Returns the model name for an object, or nil when its class has no name
81
86
  #
82
87
  # Example:
83
88
  # object.class.name # => User
@@ -89,7 +94,10 @@ module Rabl
89
94
  is_collection = item.is_a?(Array)
90
95
  item = item.first if is_collection
91
96
 
92
- name = item.class.name.underscore
97
+ name = item.class.name
98
+ return unless name
99
+
100
+ name = name.underscore
93
101
 
94
102
  name = name.pluralize if is_collection
95
103
 
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 looks 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)
@@ -40,28 +252,18 @@ module Rabl
40
252
  File.join(view_path.first.to_s, (file_path.to_s + ".rabl"))
41
253
  end
42
254
 
43
- # Returns the rabl template path for Rails, including special lookups for Rails 2 and 3
255
+ # Returns the rabl template path for Rails, using Rails' own lookup
44
256
  def fetch_rails_source(file, options = {})
45
- # use Rails template resolution mechanism if possible (find_template)
46
257
  source_format = request_format if defined?(request_format)
47
258
 
48
- if source_format && context_scope.respond_to?(:lookup_context) # Rails 3
259
+ if source_format && context_scope.respond_to?(:lookup_context)
49
260
  lookup_proc = lambda do |partial|
50
- if ActionPack::VERSION::MAJOR == 3 && ActionPack::VERSION::MINOR < 2
51
- context_scope.lookup_context.find(file, [], partial)
52
- else # Rails 3.2 and higher
53
- # 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] })
57
- end
261
+ # pull format directly from rails unless it is html
262
+ context_scope.lookup_context.find(file, [], partial, [], { :formats => [rails_source_format] })
58
263
  end
59
264
  template = lookup_proc.call(false) rescue nil
60
265
  template ||= lookup_proc.call(true) rescue nil
61
266
  template.identifier if template
62
- elsif source_format && context_scope.respond_to?(:view_paths) # Rails 2
63
- template = context_scope.view_paths.find_template(file, source_format, false)
64
- template.filename if template
65
267
  end
66
268
  end
67
269
 
@@ -71,9 +273,46 @@ module Rabl
71
273
  fetch_manual_template(view_path, file)
72
274
  end
73
275
 
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
276
+ # Returns the rabl template by looking up files within the view_path and specified file path.
277
+ # Like Rails' lookup, it takes a template named for the format and locales it is given
278
+ # ("show.json.rabl", "show.fr.json.rabl", "show.fr.rabl" or "show.rabl"), the best named
279
+ # in the first view path, and the file before the partial, that has one; otherwise the
280
+ # first file found ("show.xml.rabl" for json), as it always has.
281
+ # ceiling: other details in a name, such as variants ("show.json+mobile.rabl"), never match
282
+ # here; Rails' own lookup handles those where it is used.
283
+ def fetch_manual_template(view_path, file, format = nil, locales = nil)
284
+ found = Dir[File.join("{#{view_path.join(",")}}", "{#{file},#{partialized(file)}}" + ".{*.,}rabl")]
285
+ return found.first if found.size < 2 # nothing to choose between, as for most templates
286
+
287
+ format ||= manual_source_format
288
+ locales ||= manual_source_locales
289
+ format = format.to_s
290
+ prefixes = [file, partialized(file)].map { |name| "#{File.basename(name)}." }
291
+ best = best_rank = best_group = nil
292
+
293
+ # Dir returns the files of each view path and name together, in the order given
294
+ found.each do |path|
295
+ dir, base = File.split(path)
296
+ prefix = prefixes.find { |name| base.start_with?(name) } or next
297
+ group = [dir, prefix]
298
+ break if best && group != best_group
299
+
300
+ rank = manual_template_rank(base[prefix.size...-4].split("."), format, locales)
301
+ best, best_rank, best_group = path, rank, group if rank && (!best || (rank <=> best_rank) < 0)
302
+ end
303
+
304
+ best || found.first
305
+ end
306
+
307
+ # Ranks the details in a template's name ("fr.json" => ["fr", "json"]) the way Rails
308
+ # sorts them, the locale first: a smaller rank is better, nil means they don't match
309
+ def manual_template_rank(details, format, locales)
310
+ format_rank = details.last == format ? 1 : 2
311
+ details = details[0...-1] if format_rank == 1
312
+ return [locales.size, format_rank] if details.empty?
313
+
314
+ locale_rank = locales.index(details.first)
315
+ [locale_rank, format_rank] if locale_rank && details.size == 1
77
316
  end
78
317
 
79
318
  # 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
@@ -19,51 +22,8 @@ if defined?(Tilt)
19
22
  Tilt.register 'rabl', RablTemplate
20
23
  end
21
24
 
22
- # Rails 2.X Template
23
- if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rails.version.to_s =~ /^2/
24
- require 'action_view/base'
25
- require 'action_view/template'
26
-
27
- module ActionView
28
- module TemplateHandlers
29
- class RablHandler < TemplateHandler
30
- include Compilable
31
-
32
- def compile(template) %{
33
- ::Rabl::Engine.new(#{template.source.inspect}, { :format => #{template.format.inspect} }).
34
- render(self, assigns.merge(local_assigns))
35
- } end
36
- end
37
- end
38
- end
39
-
40
- ActionView::Template.register_template_handler :rabl, ActionView::TemplateHandlers::RablHandler
41
- end
42
-
43
- # Rails 3.X / 4.X / 5.X Template
44
- if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rails.version.to_s =~ /^[345]/
45
- module ActionView
46
- module Template::Handlers
47
- class Rabl
48
- class_attribute :default_format
49
- self.default_format = Mime[:json]
50
-
51
- def self.call(template)
52
- source = template.source
53
-
54
- %{ ::Rabl::Engine.new(#{source.inspect}).
55
- apply(self, assigns.merge(local_assigns)).
56
- render }
57
- end # call
58
- end # rabl class
59
- end # handlers
60
- end
61
-
62
- ActionView::Template.register_template_handler :rabl, ActionView::Template::Handlers::Rabl
63
- end
64
-
65
- # Rails 6.X / 7.X / 8.X Template
66
- if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rails.version.to_s =~ /^[678]/
25
+ # Rails Template
26
+ if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version)
67
27
  module ActionView
68
28
  module Template::Handlers
69
29
  class Rabl
@@ -71,7 +31,7 @@ if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rai
71
31
  self.default_format = :json
72
32
 
73
33
  def self.call(template, source)
74
- %{ ::Rabl::Engine.new(#{source.inspect}).
34
+ %{ ::Rabl::Engine.new(#{source.inspect}, :source_location => #{template.identifier.inspect}).
75
35
  apply(self, assigns.merge(local_assigns)).
76
36
  render }
77
37
  end # call