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,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
|