props_template 0.13.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 +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
|