props_template 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/props_template.rb +60 -0
- data/lib/props_template/base.rb +113 -0
- data/lib/props_template/base_with_extensions.rb +117 -0
- data/lib/props_template/core_ext.rb +19 -0
- data/lib/props_template/dependency_tracker.rb +50 -0
- data/lib/props_template/extension_manager.rb +107 -0
- data/lib/props_template/extensions/cache.rb +150 -0
- data/lib/props_template/extensions/deferment.rb +56 -0
- data/lib/props_template/extensions/fragment.rb +50 -0
- data/lib/props_template/extensions/partial_renderer.rb +187 -0
- data/lib/props_template/handler.rb +16 -0
- data/lib/props_template/key_formatter.rb +33 -0
- data/lib/props_template/layout_patch.rb +55 -0
- data/lib/props_template/railtie.rb +22 -0
- data/lib/props_template/searcher.rb +106 -0
- data/spec/layout_spec.rb +16 -0
- data/spec/props_template_spec.rb +282 -0
- data/spec/searcher_spec.rb +209 -0
- metadata +119 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
module Props
|
2
|
+
class Cache
|
3
|
+
delegate :controller, :safe_concat, to: :@context
|
4
|
+
|
5
|
+
def self.refine_options(options, item = nil)
|
6
|
+
return options if !options[:cache]
|
7
|
+
|
8
|
+
pass_opts = options.clone
|
9
|
+
key, rest = [*options[:cache]]
|
10
|
+
rest ||= {}
|
11
|
+
|
12
|
+
if item && ::Proc === key
|
13
|
+
key = key.call(item)
|
14
|
+
end
|
15
|
+
|
16
|
+
pass_opts[:cache] = [key, rest]
|
17
|
+
pass_opts
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(context)
|
21
|
+
@context = context
|
22
|
+
end
|
23
|
+
|
24
|
+
def context
|
25
|
+
@context
|
26
|
+
end
|
27
|
+
|
28
|
+
def instrument(name, **options)
|
29
|
+
ActiveSupport::Notifications.instrument(name, options) do |payload|
|
30
|
+
yield payload
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def multi_fetch(keys, options = {})
|
35
|
+
result = {}
|
36
|
+
key_to_ckey = {}
|
37
|
+
ckeys = []
|
38
|
+
|
39
|
+
keys.each do |k|
|
40
|
+
ckey = cache_key(k, options)
|
41
|
+
ckeys.push(ckey)
|
42
|
+
key_to_ckey[k] = ckey
|
43
|
+
end
|
44
|
+
|
45
|
+
payload = {
|
46
|
+
controller_name: controller.controller_name,
|
47
|
+
action_name: controller.action_name,
|
48
|
+
}
|
49
|
+
|
50
|
+
read_caches = {}
|
51
|
+
|
52
|
+
instrument('read_multi_fragments.action_view', payload) do |payload|
|
53
|
+
read_caches = ::Rails.cache.read_multi(*ckeys, options)
|
54
|
+
payload[:read_caches] = read_caches
|
55
|
+
end
|
56
|
+
|
57
|
+
keys.each do |k|
|
58
|
+
ckey = key_to_ckey[k]
|
59
|
+
result[k] = read_caches[ckey]
|
60
|
+
end
|
61
|
+
|
62
|
+
result
|
63
|
+
end
|
64
|
+
|
65
|
+
def multi_fetch_and_add_results(all_options)
|
66
|
+
first_opts = all_options[0]
|
67
|
+
|
68
|
+
if first_opts[:cache] && controller.perform_caching
|
69
|
+
keys = all_options.map{|i| i[:cache][0]}
|
70
|
+
c_opts = first_opts[:cache][1]
|
71
|
+
result = multi_fetch(keys, c_opts)
|
72
|
+
|
73
|
+
all_options.map do |opts|
|
74
|
+
key = opts[:cache][0]
|
75
|
+
|
76
|
+
if result.key? key
|
77
|
+
opts[:cache][1][:result] = result[key]
|
78
|
+
opts
|
79
|
+
else
|
80
|
+
opts
|
81
|
+
end
|
82
|
+
end
|
83
|
+
else
|
84
|
+
all_options
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
#Copied from jbuilder
|
89
|
+
#
|
90
|
+
|
91
|
+
def cache(key=nil, options={})
|
92
|
+
if controller.perform_caching
|
93
|
+
value = cache_fragment_for(key, options) do
|
94
|
+
yield
|
95
|
+
end
|
96
|
+
else
|
97
|
+
yield
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def cache_fragment_for(key, options, &block)
|
102
|
+
key = cache_key(key, options)
|
103
|
+
|
104
|
+
return options[:result] if options[:result]
|
105
|
+
|
106
|
+
read_fragment_cache(key, options) || write_fragment_cache(key, options, &block)
|
107
|
+
end
|
108
|
+
|
109
|
+
def read_fragment_cache(key, options = nil)
|
110
|
+
controller.instrument_fragment_cache :read_fragment, key do
|
111
|
+
::Rails.cache.read(key, options)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def write_fragment_cache(key, options = nil)
|
116
|
+
controller.instrument_fragment_cache :write_fragment, key do
|
117
|
+
yield.tap do |value|
|
118
|
+
::Rails.cache.write(key, value, options)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def cache_key(key, options)
|
124
|
+
name_options = options.slice(:skip_digest, :virtual_path)
|
125
|
+
key = fragment_name_with_digest(key, name_options)
|
126
|
+
|
127
|
+
if @context.respond_to?(:combined_fragment_cache_key)
|
128
|
+
key = @context.combined_fragment_cache_key(key)
|
129
|
+
else
|
130
|
+
key = url_for(key).split('://', 2).last if ::Hash === key
|
131
|
+
end
|
132
|
+
|
133
|
+
::ActiveSupport::Cache.expand_cache_key(key, :props)
|
134
|
+
end
|
135
|
+
|
136
|
+
def fragment_name_with_digest(key, options)
|
137
|
+
if @context.respond_to?(:cache_fragment_name)
|
138
|
+
# Current compatibility, fragment_name_with_digest is private again and cache_fragment_name
|
139
|
+
# should be used instead.
|
140
|
+
@context.cache_fragment_name(key, options)
|
141
|
+
elsif @context.respond_to?(:fragment_name_with_digest)
|
142
|
+
# Backwards compatibility for period of time when fragment_name_with_digest was made public.
|
143
|
+
@context.fragment_name_with_digest(key)
|
144
|
+
else
|
145
|
+
key
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Props
|
2
|
+
class Deferment
|
3
|
+
attr_reader :deferred
|
4
|
+
|
5
|
+
def initialize(base, deferred = [])
|
6
|
+
@deferred = deferred
|
7
|
+
@base = base
|
8
|
+
end
|
9
|
+
|
10
|
+
def refine_options(options, item = nil)
|
11
|
+
return options if !options[:defer]
|
12
|
+
pass_opts = options.clone
|
13
|
+
|
14
|
+
type, rest = [*options[:defer]]
|
15
|
+
rest ||= {
|
16
|
+
placeholder: {}
|
17
|
+
}
|
18
|
+
|
19
|
+
if item
|
20
|
+
type = Proc === type ? type.call(item) : type
|
21
|
+
end
|
22
|
+
|
23
|
+
if type
|
24
|
+
pass_opts[:defer] = [type, rest]
|
25
|
+
else
|
26
|
+
pass_opts.delete(:defer)
|
27
|
+
end
|
28
|
+
|
29
|
+
pass_opts
|
30
|
+
end
|
31
|
+
|
32
|
+
def handle(options)
|
33
|
+
return if !options[:defer]
|
34
|
+
|
35
|
+
type, rest = options[:defer]
|
36
|
+
placeholder = rest[:placeholder]
|
37
|
+
|
38
|
+
request_path = @base.context.controller.request.fullpath
|
39
|
+
path = @base.traveled_path.join('.')
|
40
|
+
uri = ::URI.parse(request_path)
|
41
|
+
qry = ::URI.decode_www_form(uri.query || '')
|
42
|
+
.reject{|x| x[0] == 'bzq' }
|
43
|
+
.push(["bzq", path])
|
44
|
+
|
45
|
+
uri.query = ::URI.encode_www_form(qry)
|
46
|
+
|
47
|
+
@deferred.push(
|
48
|
+
url: uri.to_s,
|
49
|
+
path: path,
|
50
|
+
type: type.to_s
|
51
|
+
)
|
52
|
+
|
53
|
+
placeholder
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module Props
|
4
|
+
class Fragment
|
5
|
+
attr_reader :fragments
|
6
|
+
attr_accessor :name
|
7
|
+
|
8
|
+
def initialize(base, fragments={})
|
9
|
+
@base = base
|
10
|
+
@fragments = fragments
|
11
|
+
@digest = Digest::SHA2.new(256)
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle(options)
|
15
|
+
return if !options[:partial]
|
16
|
+
partial_name, partial_opts = options[:partial]
|
17
|
+
fragment = partial_opts[:fragment]
|
18
|
+
|
19
|
+
if String === fragment || Symbol === fragment
|
20
|
+
fragment_name = fragment.to_s
|
21
|
+
path = @base.traveled_path.join('.')
|
22
|
+
@name = fragment_name
|
23
|
+
@fragments[fragment_name] ||= []
|
24
|
+
@fragments[fragment_name].push(path)
|
25
|
+
end
|
26
|
+
|
27
|
+
if fragment == true
|
28
|
+
locals = partial_opts[:locals]
|
29
|
+
|
30
|
+
identity = {}
|
31
|
+
locals
|
32
|
+
.clone
|
33
|
+
.tap{|h| h.delete(:json)}
|
34
|
+
.each do |key, value|
|
35
|
+
if value.respond_to?(:to_global_id)
|
36
|
+
identity[key] = value.to_global_id.to_s
|
37
|
+
else
|
38
|
+
identity[key] = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
path = @base.traveled_path.join('.')
|
43
|
+
fragment_name = @digest.hexdigest("#{partial_name}#{identity.to_json}")
|
44
|
+
@name = fragment_name
|
45
|
+
@fragments[fragment_name] ||= []
|
46
|
+
@fragments[fragment_name].push(path)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require "concurrent/map"
|
2
|
+
require 'action_view'
|
3
|
+
|
4
|
+
module Props
|
5
|
+
class Partialer
|
6
|
+
def initialize(base, context, builder)
|
7
|
+
@context = context
|
8
|
+
@builder = builder
|
9
|
+
@base = base
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_and_add_template(all_options)
|
13
|
+
first_opts = all_options[0]
|
14
|
+
|
15
|
+
if first_opts[:partial]
|
16
|
+
partial_opts = block_opts_to_render_opts(@builder, first_opts)
|
17
|
+
renderer = PartialRenderer.new(@context, partial_opts)
|
18
|
+
|
19
|
+
all_options.map do |opts|
|
20
|
+
opts[:_template] = renderer.template
|
21
|
+
opts
|
22
|
+
end
|
23
|
+
else
|
24
|
+
all_options
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def block_opts_to_render_opts(builder, options)
|
29
|
+
partial, pass_opts = [*options[:partial]]
|
30
|
+
pass_opts ||= {}
|
31
|
+
pass_opts[:locals] ||= {}
|
32
|
+
pass_opts[:locals][:json] = @builder
|
33
|
+
pass_opts[:partial] = partial
|
34
|
+
|
35
|
+
pass_opts
|
36
|
+
end
|
37
|
+
|
38
|
+
def refine_options(options, item = nil)
|
39
|
+
PartialRenderer.refine_options(options, item)
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle(options)
|
43
|
+
pass_opts = block_opts_to_render_opts(@builder, options)
|
44
|
+
renderer = PartialRenderer.new(@context, pass_opts)
|
45
|
+
template = options[:_template] || renderer.template
|
46
|
+
|
47
|
+
renderer.render(template, pass_opts)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class PartialRenderer < ActionView::AbstractRenderer
|
52
|
+
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
|
53
|
+
"make sure it starts with lowercase letter, " \
|
54
|
+
"and is followed by any combination of letters, numbers and underscores."
|
55
|
+
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
|
56
|
+
"make sure your partial name starts with underscore."
|
57
|
+
|
58
|
+
INVALID_PARTIAL_MESSAGE = "The partial name must be a string, but received (%s)."
|
59
|
+
|
60
|
+
def self.find_and_add_template(builder, context, all_options)
|
61
|
+
first_opts = all_options[0]
|
62
|
+
|
63
|
+
if first_opts[:partial]
|
64
|
+
partial_opts = block_opts_to_render_opts(builder, first_opts)
|
65
|
+
renderer = new(context, partial_opts)
|
66
|
+
|
67
|
+
all_options.map do |opts|
|
68
|
+
opts[:_template] = renderer.template
|
69
|
+
opts
|
70
|
+
end
|
71
|
+
else
|
72
|
+
all_options
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.raise_invalid_option_as(as)
|
77
|
+
raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.raise_invalid_identifier(path)
|
81
|
+
raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
def self.retrieve_variable(path)
|
86
|
+
base = path[-1] == "/" ? "" : File.basename(path)
|
87
|
+
raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
|
88
|
+
$1.to_sym
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.refine_options(options, item = nil)
|
92
|
+
return options if !options[:partial]
|
93
|
+
|
94
|
+
partial, rest = [*options[:partial]]
|
95
|
+
rest = (rest || {}).clone
|
96
|
+
locals = rest[:locals] || {}
|
97
|
+
rest[:locals] = locals
|
98
|
+
|
99
|
+
if item
|
100
|
+
as = if !rest[:as]
|
101
|
+
retrieve_variable(partial)
|
102
|
+
else
|
103
|
+
rest[:as].to_sym
|
104
|
+
end
|
105
|
+
|
106
|
+
raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
|
107
|
+
|
108
|
+
locals[as] = item
|
109
|
+
|
110
|
+
if fragment_name = rest[:fragment]
|
111
|
+
fragment_name = Proc === fragment_name ? fragment_name.call(item) : fragment_name.to_s
|
112
|
+
rest[:fragment] = fragment_name
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
pass_opts = options.clone
|
117
|
+
pass_opts[:partial] = [partial, rest]
|
118
|
+
|
119
|
+
pass_opts
|
120
|
+
end
|
121
|
+
|
122
|
+
attr_reader :template
|
123
|
+
|
124
|
+
def initialize(context, options)
|
125
|
+
@context = context
|
126
|
+
super(@context.lookup_context)
|
127
|
+
@options = options.merge(formats: [:json])
|
128
|
+
@options.delete(:handlers)
|
129
|
+
@details = extract_details(@options)
|
130
|
+
|
131
|
+
partial = @options[:partial]
|
132
|
+
|
133
|
+
if !(String === partial)
|
134
|
+
raise_invalid_partial(partial.inspect)
|
135
|
+
end
|
136
|
+
|
137
|
+
@path = partial
|
138
|
+
@context_prefix = @lookup_context.prefixes.first
|
139
|
+
template_keys = retrieve_template_keys(@options)
|
140
|
+
@template = find_template(@path, template_keys)
|
141
|
+
end
|
142
|
+
|
143
|
+
def render(template, options)
|
144
|
+
#remove this later
|
145
|
+
|
146
|
+
render_partial(template, @context, @options)
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def render_partial(template, view, options)
|
152
|
+
template ||= @template
|
153
|
+
# @variable ||= template.variable
|
154
|
+
|
155
|
+
instrument(:partial, identifier: @template.identifier) do |payload|
|
156
|
+
locals = options[:locals]
|
157
|
+
content = template.render(view, locals)
|
158
|
+
|
159
|
+
payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
|
160
|
+
build_rendered_template(content, template)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Sets up instance variables needed for rendering a partial. This method
|
165
|
+
# finds the options and details and extracts them. The method also contains
|
166
|
+
# logic that handles the type of object passed in as the partial.
|
167
|
+
#
|
168
|
+
# If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
|
169
|
+
# set to that string. Otherwise, the +options[:partial]+ object must
|
170
|
+
# respond to +to_partial_path+ in order to setup the path.
|
171
|
+
|
172
|
+
def find_template(path, locals)
|
173
|
+
prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
|
174
|
+
@lookup_context.find_template(path, prefixes, true, locals, @details)
|
175
|
+
end
|
176
|
+
|
177
|
+
def retrieve_template_keys(options)
|
178
|
+
template_keys = options[:locals].keys
|
179
|
+
template_keys << options[:as] if options[:as]
|
180
|
+
template_keys
|
181
|
+
end
|
182
|
+
|
183
|
+
def raise_invalid_partial(path)
|
184
|
+
raise ArgumentError.new(INVALID_PARTIAL_MESSAGE % (path))
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module Props
|
4
|
+
class Handler
|
5
|
+
cattr_accessor :default_format
|
6
|
+
self.default_format = :json
|
7
|
+
|
8
|
+
def self.call(template, source = nil)
|
9
|
+
source ||= template.source
|
10
|
+
# this juggling is required to keep line numbers right in the error
|
11
|
+
%{__already_defined = defined?(json); json||=Props::Template.new(self); #{source};
|
12
|
+
json.result! unless (__already_defined && __already_defined != "method")
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|