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,33 @@
|
|
1
|
+
require 'active_support/core_ext/array'
|
2
|
+
|
3
|
+
module Props
|
4
|
+
class KeyFormatter
|
5
|
+
def initialize(*args)
|
6
|
+
@format = {}
|
7
|
+
@cache = {}
|
8
|
+
|
9
|
+
options = args.extract_options!
|
10
|
+
args.each do |name|
|
11
|
+
@format[name] = []
|
12
|
+
end
|
13
|
+
options.each do |name, parameters|
|
14
|
+
@format[name] = parameters
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize_copy(original)
|
19
|
+
@cache = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def format(key)
|
23
|
+
@cache[key] ||= @format.inject(key.to_s) do |result, args|
|
24
|
+
func, args = args
|
25
|
+
if ::Proc === func
|
26
|
+
func.call result, *args
|
27
|
+
else
|
28
|
+
result.send func, *args
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Props
|
2
|
+
module LayoutPatch
|
3
|
+
def render(context, options)
|
4
|
+
options[:locals] ||= {}
|
5
|
+
options[:locals][:json] = nil
|
6
|
+
|
7
|
+
@details = extract_details(options)
|
8
|
+
template = determine_template(options)
|
9
|
+
|
10
|
+
if template.handler == Props::Handler && options[:layout]
|
11
|
+
prepend_formats(template.format)
|
12
|
+
render_props_template(context, template, options[:layout], options[:locals])
|
13
|
+
else
|
14
|
+
super(context, options)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def render_props_template(view, template, path, locals)
|
19
|
+
layout_locals = locals.dup
|
20
|
+
layout_locals.delete(:json)
|
21
|
+
|
22
|
+
layout = resolve_props_layout(path, layout_locals, [formats.first])
|
23
|
+
|
24
|
+
body = layout.render(view, layout_locals) do |json|
|
25
|
+
locals[:json] = json
|
26
|
+
template.render(view, locals)
|
27
|
+
end
|
28
|
+
|
29
|
+
build_rendered_template(body, template)
|
30
|
+
end
|
31
|
+
|
32
|
+
def resolve_props_layout(layout, keys, formats)
|
33
|
+
details = @details.dup
|
34
|
+
details[:formats] = formats
|
35
|
+
|
36
|
+
case layout
|
37
|
+
when String
|
38
|
+
begin
|
39
|
+
if layout.start_with?("/")
|
40
|
+
ActiveSupport::Deprecation.warn "Rendering layouts from an absolute path is deprecated."
|
41
|
+
@lookup_context.with_fallbacks.find_template(layout, nil, false, [], details)
|
42
|
+
else
|
43
|
+
@lookup_context.find_template(layout, nil, false, [], details)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
when Proc
|
47
|
+
resolve_layout(layout.call(@lookup_context, formats), keys, formats)
|
48
|
+
else
|
49
|
+
layout
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
ActionView::TemplateRenderer.prepend(Props::LayoutPatch)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rails/railtie'
|
2
|
+
require 'props_template'
|
3
|
+
|
4
|
+
module Props
|
5
|
+
class Railtie < ::Rails::Railtie
|
6
|
+
initializer :props_template do
|
7
|
+
ActiveSupport.on_load :action_view do
|
8
|
+
ActionView::Template.register_template_handler :props, Props::Handler
|
9
|
+
require 'props_template/dependency_tracker'
|
10
|
+
require 'props_template/layout_patch'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# if Rails::VERSION::MAJOR >= 4
|
15
|
+
# generators do |app|
|
16
|
+
# Rails::Generators.configure! app.config.generators
|
17
|
+
# Rails::Generators.hidden_namespaces.uniq!
|
18
|
+
# require 'generators/rails/scaffold_controller_generator'
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'props_template/extensions/partial_renderer'
|
2
|
+
|
3
|
+
module Props
|
4
|
+
class Searcher
|
5
|
+
attr_reader :builder, :context, :fragments, :traveled_path
|
6
|
+
|
7
|
+
def initialize(builder, path=[], context = nil)
|
8
|
+
@search_path = path
|
9
|
+
@depth = 0
|
10
|
+
@context = context
|
11
|
+
@found_block = nil
|
12
|
+
@found_options = nil
|
13
|
+
@builder = builder
|
14
|
+
@traveled_path = []
|
15
|
+
@partialer = Partialer.new(self, context, builder)
|
16
|
+
end
|
17
|
+
|
18
|
+
def deferred!
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
|
22
|
+
def fragments!
|
23
|
+
[]
|
24
|
+
end
|
25
|
+
|
26
|
+
def fragment_digest!
|
27
|
+
end
|
28
|
+
|
29
|
+
def found!
|
30
|
+
pass_opts = @found_options.clone || {}
|
31
|
+
pass_opts.delete(:defer)
|
32
|
+
traveled_path = @traveled_path[1..-1] || []
|
33
|
+
if !traveled_path.empty?
|
34
|
+
pass_opts[:path_suffix] = traveled_path
|
35
|
+
end
|
36
|
+
|
37
|
+
[@found_block, pass_opts]
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_block_content!(*args)
|
41
|
+
yield
|
42
|
+
end
|
43
|
+
|
44
|
+
def set!(key, options={}, &block)
|
45
|
+
return if @found_block || !block_given?
|
46
|
+
|
47
|
+
if @search_path[@depth] == key.to_s
|
48
|
+
@traveled_path.push(key)
|
49
|
+
|
50
|
+
if @depth == @search_path.size - 1
|
51
|
+
@found_options = options
|
52
|
+
@found_block = block
|
53
|
+
return
|
54
|
+
end
|
55
|
+
|
56
|
+
@depth += 1
|
57
|
+
if options[:partial]
|
58
|
+
@partialer.handle(options)
|
59
|
+
else
|
60
|
+
yield
|
61
|
+
end
|
62
|
+
@depth -= 1
|
63
|
+
end
|
64
|
+
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def array!(collection, options = {}, &block)
|
69
|
+
return if @found_block
|
70
|
+
|
71
|
+
key_index = @search_path[@depth]
|
72
|
+
id_name, id_val = key_index.to_s.split('=')
|
73
|
+
|
74
|
+
if id_val
|
75
|
+
id_val = id_val.to_i
|
76
|
+
item = collection.member_by(id_name, id_val)
|
77
|
+
else
|
78
|
+
index = id_name.to_i
|
79
|
+
item = collection.member_at(index)
|
80
|
+
end
|
81
|
+
|
82
|
+
if item
|
83
|
+
pass_opts = @partialer.refine_options(options, item)
|
84
|
+
@traveled_path.push(key_index)
|
85
|
+
|
86
|
+
if @depth == @search_path.size - 1
|
87
|
+
@found_options = pass_opts
|
88
|
+
@found_block = Proc.new {
|
89
|
+
yield item, 0
|
90
|
+
}
|
91
|
+
return
|
92
|
+
end
|
93
|
+
|
94
|
+
@depth += 1
|
95
|
+
if pass_opts[:partial]
|
96
|
+
# todo: what happens when cached: true is passed?
|
97
|
+
# would there be any problems with not using the collection_rende?
|
98
|
+
@partialer.handle(pass_opts)
|
99
|
+
else
|
100
|
+
yield item, 0
|
101
|
+
end
|
102
|
+
@depth -= 1
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/spec/layout_spec.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative './support/helper'
|
2
|
+
require_relative './support/rails_helper'
|
3
|
+
require 'props_template/layout_patch'
|
4
|
+
|
5
|
+
RSpec.describe 'Props::Template' do
|
6
|
+
it 'uses a layout to render' do
|
7
|
+
view_path = File.join(File.dirname(__FILE__),'./fixtures')
|
8
|
+
controller = ActionView::TestCase::TestController.new
|
9
|
+
controller.prepend_view_path(view_path)
|
10
|
+
controller.response.headers['Content-Type']='application/json'
|
11
|
+
controller.request.path = '/some_url'
|
12
|
+
|
13
|
+
json = controller.render('200', layout: 'application')
|
14
|
+
expect(json.strip).to eql('{"data":{"success":"ok"}}')
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,282 @@
|
|
1
|
+
require_relative './support/helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Props::Base' do
|
4
|
+
it 'initializes' do
|
5
|
+
expect {
|
6
|
+
Props::Base.new
|
7
|
+
}.to_not raise_error
|
8
|
+
end
|
9
|
+
|
10
|
+
context 'result!' do
|
11
|
+
it 'returns {} when empty' do
|
12
|
+
json = Props::Base.new
|
13
|
+
expect(json.result!.strip).to eql('{}')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'set!' do
|
18
|
+
it 'sets a value' do
|
19
|
+
json = Props::Base.new
|
20
|
+
json.set! :foo, 'bar'
|
21
|
+
attrs = json.result!.strip
|
22
|
+
|
23
|
+
expect(attrs).to eql_json({
|
24
|
+
foo: 'bar'
|
25
|
+
})
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'sets a empty obj when block is empty' do
|
29
|
+
json = Props::Base.new
|
30
|
+
json.set! :foo do
|
31
|
+
end
|
32
|
+
attrs = json.result!.strip
|
33
|
+
|
34
|
+
expect(attrs).to eql_json({
|
35
|
+
foo: {}
|
36
|
+
})
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'sets a empty obj when nested block is empty' do
|
40
|
+
json = Props::Base.new
|
41
|
+
json.set! :foo do
|
42
|
+
json.set! :bar do
|
43
|
+
end
|
44
|
+
end
|
45
|
+
attrs = json.result!.strip
|
46
|
+
|
47
|
+
expect(attrs).to eql_json({
|
48
|
+
foo: {
|
49
|
+
bar: {}
|
50
|
+
}
|
51
|
+
})
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'sets a null value' do
|
55
|
+
json = Props::Base.new
|
56
|
+
json.set! :foo, nil
|
57
|
+
attrs = json.result!.strip
|
58
|
+
|
59
|
+
expect(attrs).to eql_json({
|
60
|
+
foo: nil
|
61
|
+
})
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'sets multiple values' do
|
65
|
+
json = Props::Base.new
|
66
|
+
json.set! :foo, 'bar'
|
67
|
+
json.set! :steve, 'cool'
|
68
|
+
|
69
|
+
attrs = json.result!.strip
|
70
|
+
|
71
|
+
expect(attrs).to eql_json({
|
72
|
+
foo: 'bar',
|
73
|
+
steve: 'cool'
|
74
|
+
})
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'sets multiple values with the same key, the last one wins' do
|
78
|
+
json = Props::Base.new
|
79
|
+
json.set! :foo, 'bar'
|
80
|
+
json.set! :foo, 'cool'
|
81
|
+
|
82
|
+
attrs = JSON.parse(json.result!)
|
83
|
+
|
84
|
+
expect(attrs).to eql({
|
85
|
+
'foo' => 'cool'
|
86
|
+
})
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'throws InvalidScopeForObjError when the current scope is an array' do
|
90
|
+
json = Props::Base.new
|
91
|
+
json.array! [1, 2] do |item|
|
92
|
+
json.set! :foo, item
|
93
|
+
end
|
94
|
+
expect {
|
95
|
+
json.set! :bar, 'world'
|
96
|
+
}.to raise_error(Props::InvalidScopeForObjError)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'set! with a block' do
|
101
|
+
it 'creates a new nested object' do
|
102
|
+
json = Props::Base.new
|
103
|
+
json.set! :outer do
|
104
|
+
json.set! :inner, 'baz'
|
105
|
+
end
|
106
|
+
|
107
|
+
attrs = json.result!.strip
|
108
|
+
|
109
|
+
expect(attrs).to eql_json({
|
110
|
+
outer: {
|
111
|
+
inner: 'baz'
|
112
|
+
}
|
113
|
+
})
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'creates a nested object' do
|
117
|
+
json = Props::Base.new
|
118
|
+
json.set! :outer do
|
119
|
+
json.set! :inner, 'baz'
|
120
|
+
end
|
121
|
+
|
122
|
+
attrs = json.result!.strip
|
123
|
+
|
124
|
+
expect(attrs).to eql_json({
|
125
|
+
outer: {
|
126
|
+
inner: 'baz'
|
127
|
+
}
|
128
|
+
})
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'creates a nested array of objects' do
|
132
|
+
json = Props::Base.new
|
133
|
+
json.set! :outer do
|
134
|
+
json.array! [1, 2] do |item|
|
135
|
+
json.set! :foo, item
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
attrs = json.result!.strip
|
140
|
+
|
141
|
+
expect(attrs).to eql_json({
|
142
|
+
outer: [
|
143
|
+
{foo: 1},
|
144
|
+
{foo: 2}
|
145
|
+
]
|
146
|
+
})
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'treats the second argument as an options arg' do
|
150
|
+
json = Props::Base.new
|
151
|
+
json.set! :outer, {some: 'setting'} do
|
152
|
+
json.set! :inner, 'baz'
|
153
|
+
end
|
154
|
+
|
155
|
+
attrs = json.result!.strip
|
156
|
+
|
157
|
+
expect(attrs).to eql_json({
|
158
|
+
outer: {
|
159
|
+
inner: 'baz'
|
160
|
+
}
|
161
|
+
})
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context 'array!' do
|
166
|
+
it 'creates an array of 1 object' do
|
167
|
+
json = Props::Base.new
|
168
|
+
json.array! [1] do |num|
|
169
|
+
json.set! :foo, num
|
170
|
+
end
|
171
|
+
|
172
|
+
attrs = json.result!.strip
|
173
|
+
|
174
|
+
expect(attrs).to eql_json([
|
175
|
+
{foo: 1}
|
176
|
+
])
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'passes the index as the second argument of yield' do
|
180
|
+
json = Props::Base.new
|
181
|
+
json.array! ['a', 'b'] do |item, index|
|
182
|
+
json.set! :foo, [item, index]
|
183
|
+
end
|
184
|
+
|
185
|
+
attrs = json.result!.strip
|
186
|
+
|
187
|
+
expect(attrs).to eql_json([
|
188
|
+
{foo: ['a', 0]},
|
189
|
+
{foo: ['b', 1]}
|
190
|
+
])
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'creates an empty array when passed an empty collection' do
|
194
|
+
json = Props::Base.new
|
195
|
+
json.array! [] do |num|
|
196
|
+
end
|
197
|
+
|
198
|
+
attrs = json.result!.strip
|
199
|
+
|
200
|
+
expect(attrs).to eql_json([
|
201
|
+
])
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'creates an array of empty arrays when passed an empty collection' do
|
205
|
+
json = Props::Base.new
|
206
|
+
json.array! [1] do |num|
|
207
|
+
json.array! [] do
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
attrs = json.result!.strip
|
212
|
+
expect(attrs).to eql_json([[]])
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'creates an array of empties if set did not get called in array' do
|
216
|
+
json = Props::Base.new
|
217
|
+
json.array! [1, 2, 3] do |num|
|
218
|
+
end
|
219
|
+
|
220
|
+
attrs = json.result!.strip
|
221
|
+
|
222
|
+
expect(attrs).to eql_json([
|
223
|
+
{},
|
224
|
+
{},
|
225
|
+
{}
|
226
|
+
])
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'throws InvalidScopeForArray error when array is used twice' do
|
230
|
+
json = Props::Base.new
|
231
|
+
json.array! [1] do
|
232
|
+
json.set! :foo, 'first'
|
233
|
+
end
|
234
|
+
|
235
|
+
expect {
|
236
|
+
json.array! [2] do |num|
|
237
|
+
json.set! :foo, 'second'
|
238
|
+
end
|
239
|
+
}.to raise_error(Props::InvalidScopeForArrayError)
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'creates an array of multiple objects' do
|
243
|
+
json = Props::Base.new
|
244
|
+
json.array! [1, 2] do |num|
|
245
|
+
json.set! :foo, num
|
246
|
+
end
|
247
|
+
|
248
|
+
attrs = json.result!.strip
|
249
|
+
|
250
|
+
expect(attrs).to eql_json([
|
251
|
+
{foo: 1},
|
252
|
+
{foo: 2}
|
253
|
+
])
|
254
|
+
end
|
255
|
+
|
256
|
+
it 'creates an array of arrays' do
|
257
|
+
json = Props::Base.new
|
258
|
+
json.array! [[1, 2], [3, 4]] do |part|
|
259
|
+
json.array! part do |num|
|
260
|
+
json.set! :foo, num
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
attrs = json.result!.strip
|
265
|
+
|
266
|
+
expect(attrs).to eql_json([
|
267
|
+
[{foo: 1}, {foo: 2}],
|
268
|
+
[{foo: 3}, {foo: 4}]
|
269
|
+
])
|
270
|
+
end
|
271
|
+
|
272
|
+
it 'throws InvalidScopeForArrayError when the current scope is already an object' do
|
273
|
+
json = Props::Base.new
|
274
|
+
json.set! :foo, 'bar'
|
275
|
+
expect {
|
276
|
+
json.array! [1, 2] do |num|
|
277
|
+
json.set! :foo, 'noop'
|
278
|
+
end
|
279
|
+
}.to raise_error(Props::InvalidScopeForArrayError)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|