rabl 0.16.1 → 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 +56 -1
- data/README.md +83 -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 +343 -27
- 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 +3 -1
- data/lib/rabl/renderer.rb +5 -2
- data/lib/rabl/sources.rb +261 -14
- data/lib/rabl/template.rb +8 -5
- 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 -9
- metadata +5 -116
data/lib/rabl/helpers.rb
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_support/inflector' # for the sake of pluralizing
|
|
4
|
+
require 'erb' # ERB::Util.h for escape_all_output
|
|
2
5
|
|
|
3
6
|
module Rabl
|
|
4
7
|
module Helpers
|
|
5
8
|
# data_object(data) => <AR Object>
|
|
6
9
|
# data_object(@user => :person) => @user
|
|
7
|
-
# data_object(:user => :person) => @_object.
|
|
10
|
+
# data_object(:user => :person) => @_object.public_send(:user)
|
|
8
11
|
def data_object(data)
|
|
12
|
+
return data if data_item?(data)
|
|
13
|
+
|
|
9
14
|
data = data.keys.first if data.is_a?(Hash) && data.keys.size == 1
|
|
10
|
-
data = @_object.
|
|
15
|
+
data = @_object.public_send(data) if data.is_a?(Symbol) && defined?(@_object) && @_object && @_object.respond_to?(data)
|
|
11
16
|
data
|
|
12
17
|
end
|
|
13
18
|
|
|
14
|
-
# data_object_attribute(data) => @_object.
|
|
19
|
+
# data_object_attribute(data) => @_object.public_send(data)
|
|
20
|
+
# public_send, so a name like :format reaches method_missing (Hashie::Mash) instead of private Kernel#format
|
|
15
21
|
def data_object_attribute(data)
|
|
16
|
-
attribute = @_object.
|
|
22
|
+
attribute = @_object.public_send(data)
|
|
17
23
|
attribute = attribute.as_json if
|
|
18
24
|
is_collection?(attribute, false) &&
|
|
19
25
|
attribute.respond_to?(:as_json)
|
|
@@ -34,6 +40,7 @@ module Rabl
|
|
|
34
40
|
|
|
35
41
|
if is_collection?(data) # data is a collection
|
|
36
42
|
object_name = collection_root_name if collection_root_name
|
|
43
|
+
object_name ||= data_token if data_token.is_a?(Symbol) # child :units => "units", whatever the table (#394)
|
|
37
44
|
object_name ||= data.table_name if data.respond_to?(:table_name)
|
|
38
45
|
|
|
39
46
|
if object_name.nil? && data.respond_to?(:first)
|
|
@@ -41,7 +48,6 @@ module Rabl
|
|
|
41
48
|
object_name = data_name(first).to_s.pluralize if first.present?
|
|
42
49
|
end
|
|
43
50
|
|
|
44
|
-
object_name ||= data_token if data_token.is_a?(Symbol)
|
|
45
51
|
object_name
|
|
46
52
|
elsif is_object?(data) # data is an object
|
|
47
53
|
object_name = object_root_name if object_root_name
|
|
@@ -82,6 +88,8 @@ module Rabl
|
|
|
82
88
|
# is_collection?(@user) => false
|
|
83
89
|
# is_collection?([]) => true
|
|
84
90
|
def is_collection?(obj, follow_symbols = true)
|
|
91
|
+
return false if data_item?(obj)
|
|
92
|
+
|
|
85
93
|
data_obj = follow_symbols ? data_object(obj) : obj
|
|
86
94
|
data_obj &&
|
|
87
95
|
data_obj.respond_to?(:map) && data_obj.respond_to?(:each) &&
|
|
@@ -89,6 +97,12 @@ module Rabl
|
|
|
89
97
|
defined?(Hashie::Mash) && data_obj.is_a?(Hashie::Mash))
|
|
90
98
|
end
|
|
91
99
|
|
|
100
|
+
# True for the Hash item that `extends` handed to this engine as its object (#396).
|
|
101
|
+
# It is data: never `@object => :name` alias syntax, and never a collection.
|
|
102
|
+
def data_item?(obj)
|
|
103
|
+
obj.is_a?(Hash) && defined?(@_data_item) && obj.equal?(@_data_item)
|
|
104
|
+
end
|
|
105
|
+
|
|
92
106
|
# Returns the context_scope wrapping this engine, used for retrieving data, invoking methods, etc
|
|
93
107
|
# In Rails, this is the controller and in Padrino this is the request context
|
|
94
108
|
def context_scope
|
|
@@ -128,10 +142,14 @@ module Rabl
|
|
|
128
142
|
return if object.nil?
|
|
129
143
|
|
|
130
144
|
options.reverse_merge!({
|
|
131
|
-
:format => "hash"
|
|
145
|
+
:format => "hash",
|
|
132
146
|
:view_path => view_path,
|
|
147
|
+
:nested => true,
|
|
133
148
|
:root => (options[:root] || false)
|
|
134
149
|
})
|
|
150
|
+
options[:parent_engine] = rabl_template_engine
|
|
151
|
+
# a nested template renders as a hash, but looks its templates up in the format being rendered
|
|
152
|
+
options[:source_format] ||= manual_source_format if respond_to?(:manual_source_format, true)
|
|
135
153
|
|
|
136
154
|
Engine.new(options[:source], options).apply(context_scope, :object => object, :locals => options[:locals], &block)
|
|
137
155
|
end
|
|
@@ -139,12 +157,12 @@ module Rabl
|
|
|
139
157
|
# Fetches a key from the cache and stores rabl template result otherwise
|
|
140
158
|
# fetch_from_cache('some_key') { ...rabl template result... }
|
|
141
159
|
def fetch_result_from_cache(cache_key, cache_options = {}, &block)
|
|
142
|
-
expanded_cache_key =
|
|
160
|
+
expanded_cache_key = TemplateCacheKey.expand(cache_key)
|
|
143
161
|
Rabl.configuration.cache_engine.fetch(expanded_cache_key, cache_options, &block)
|
|
144
162
|
end
|
|
145
163
|
|
|
146
164
|
def write_result_to_cache(cache_key, cache_options = {}, &block)
|
|
147
|
-
expanded_cache_key =
|
|
165
|
+
expanded_cache_key = TemplateCacheKey.expand(cache_key)
|
|
148
166
|
result = yield
|
|
149
167
|
Rabl.configuration.cache_engine.write(expanded_cache_key, result, cache_options)
|
|
150
168
|
result
|
|
@@ -159,13 +177,22 @@ module Rabl
|
|
|
159
177
|
end
|
|
160
178
|
end
|
|
161
179
|
|
|
180
|
+
protected
|
|
181
|
+
# Returns the engine of the template being rendered, if any (see Engine#rabl_template_identity)
|
|
182
|
+
def rabl_template_engine
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
162
186
|
module Escaper
|
|
187
|
+
# Returns escaped copies; the response may be shared with the cache, so it is never changed in place.
|
|
163
188
|
def escape_output(response)
|
|
164
189
|
case response
|
|
165
190
|
when Hash
|
|
166
|
-
|
|
191
|
+
escaped = response.dup
|
|
192
|
+
response.each { |k, v| escaped[k] = escape_value(v) }
|
|
193
|
+
escaped
|
|
167
194
|
when Array
|
|
168
|
-
response.map!{|v| escape_value(v) }
|
|
195
|
+
response.dup.map! { |v| escape_value(v) }
|
|
169
196
|
else
|
|
170
197
|
response
|
|
171
198
|
end
|
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
|
@@ -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|
|
|
@@ -22,7 +24,7 @@ module Rabl
|
|
|
22
24
|
Rabl.register!
|
|
23
25
|
|
|
24
26
|
# Inject dependency tracker for :rabl
|
|
25
|
-
if Rails.version =~ /^[
|
|
27
|
+
if Rails.version =~ /^[45678]/
|
|
26
28
|
require 'action_view/dependency_tracker'
|
|
27
29
|
ActionView::DependencyTracker.register_tracker :rabl, Rabl::Tracker
|
|
28
30
|
end
|
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
|
|
@@ -42,7 +44,7 @@ module Rabl
|
|
|
42
44
|
# - context_scope:
|
|
43
45
|
# Override the render context_scope to the 'context_scope' object. Defaults to self.
|
|
44
46
|
#
|
|
45
|
-
# Returns: And object representing the
|
|
47
|
+
# Returns: And object representing the transformed object in the requested format.
|
|
46
48
|
# e.g. json, xml, bson, plist
|
|
47
49
|
def render(context_scope = nil)
|
|
48
50
|
context_scope ||= options[:scope] || self
|
|
@@ -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
|
|
@@ -62,8 +65,8 @@ if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rai
|
|
|
62
65
|
ActionView::Template.register_template_handler :rabl, ActionView::Template::Handlers::Rabl
|
|
63
66
|
end
|
|
64
67
|
|
|
65
|
-
# Rails 6.X / 7.X Template
|
|
66
|
-
if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rails.version.to_s =~ /^[
|
|
68
|
+
# Rails 6.X / 7.X / 8.X Template
|
|
69
|
+
if defined?(ActionView) && defined?(Rails) && Rails.respond_to?(:version) && Rails.version.to_s =~ /^[678]/
|
|
67
70
|
module ActionView
|
|
68
71
|
module Template::Handlers
|
|
69
72
|
class Rabl
|
|
@@ -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