props_template 0.16.0 → 0.21.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/README.md +211 -145
- data/lib/props_template/base_with_extensions.rb +0 -4
- data/lib/props_template/extension_manager.rb +2 -15
- data/lib/props_template/extensions/cache.rb +2 -21
- data/lib/props_template/extensions/deferment.rb +13 -5
- data/lib/props_template/extensions/fragment.rb +4 -28
- data/lib/props_template/extensions/partial_renderer.rb +79 -31
- data/lib/props_template/searcher.rb +0 -3
- data/lib/props_template/version.rb +3 -0
- data/lib/props_template.rb +1 -1
- data/spec/layout_spec.rb +18 -10
- metadata +14 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23e014144b689057bd6e6cc96a007b17263a2eab2a84890dd822056c2a4e258d
|
4
|
+
data.tar.gz: bba7578e983913d2eeeb15fc12026bb66b25c6da5a8df52c4d2003e8615a2eba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1adee8973b67e46c8c2169a7664ec92c0ee52e21451108b449888b8a8dd89221b46d2918da2ace75e016530a7654ce03a513f4f761cb2f6fa938dd76d4a18ed
|
7
|
+
data.tar.gz: 2bc87b68c92e21f4755bf4a41c8ea1290bbfcd94f570fa2cf98b86fc2eecaa9ae83eae0f58042c3ae18cfd9c9f0d0c54caa23b5cff4c92814dfd81b3e306baa0
|
data/README.md
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
# PropsTemplate
|
2
2
|
|
3
|
-
PropsTemplate is a direct-to-Oj, JBuilder-like DSL for building JSON. It has
|
3
|
+
PropsTemplate is a direct-to-Oj, JBuilder-like DSL for building JSON. It has
|
4
|
+
support for Russian-Doll caching, layouts, and can be queried by giving the
|
5
|
+
root a key path.
|
4
6
|
|
5
|
-
|
7
|
+
[](https://circleci.com/gh/thoughtbot/props_template)
|
6
9
|
|
7
|
-
|
10
|
+
It's fast.
|
8
11
|
|
9
|
-
PropsTemplate
|
12
|
+
PropsTemplate bypasses the steps of hash building and serializing
|
13
|
+
that other libraries perform by using Oj's `StringWriter` in `rails` mode.
|
10
14
|
|
15
|
+

|
11
16
|
|
12
|
-
|
17
|
+
Caching is fast too.
|
18
|
+
|
19
|
+
While other libraries spend time unmarshaling,
|
20
|
+
merging hashes, and serializing to JSON; PropsTemplate simply takes
|
21
|
+
the cached string and uses Oj's [push_json](http://www.ohler.com/oj/doc/Oj/StringWriter.html#push_json-instance_method).
|
22
|
+
|
23
|
+
## Example:
|
24
|
+
|
25
|
+
PropsTemplate is very similar to JBuilder, and selectively retains some
|
26
|
+
conveniences and magic.
|
13
27
|
|
14
28
|
```ruby
|
15
29
|
json.flash flash.to_h
|
@@ -47,34 +61,50 @@ json.posts do
|
|
47
61
|
json.total @posts.count
|
48
62
|
end
|
49
63
|
|
50
|
-
|
51
64
|
json.footer partial: 'shared/footer' do
|
52
65
|
end
|
53
66
|
```
|
54
67
|
|
55
68
|
## Installation
|
56
|
-
If you plan to use PropsTemplate alone just add it to your Gemfile.
|
57
69
|
|
58
70
|
```
|
59
71
|
gem 'props_template'
|
60
72
|
```
|
61
73
|
|
62
|
-
and run `bundle
|
74
|
+
and run `bundle`.
|
75
|
+
|
76
|
+
Add the [core ext](#array-core-extension) to an initializer.
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
require 'props_template/core_ext'
|
80
|
+
```
|
81
|
+
|
82
|
+
|
83
|
+
And create a file in your `app/views` folder like so:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# app/views/posts/index.json.props
|
87
|
+
|
88
|
+
json.greetings "hello world"
|
89
|
+
```
|
90
|
+
|
91
|
+
You can also add a [layout](#layouts).
|
63
92
|
|
64
93
|
## API
|
65
94
|
|
66
|
-
### json.set! or json
|
67
|
-
|
95
|
+
### json.set! or json.\<your key here\>
|
96
|
+
|
97
|
+
Defines the attribute or structure. All keys are automatically camelized lower.
|
68
98
|
|
69
99
|
```ruby
|
70
|
-
json.set! :author_details, {
|
100
|
+
json.set! :author_details, {...options} do
|
71
101
|
json.set! :first_name, 'David'
|
72
102
|
end
|
73
103
|
|
74
104
|
or
|
75
105
|
|
76
|
-
json.author_details, {
|
77
|
-
json.first_name
|
106
|
+
json.author_details, {...options} do
|
107
|
+
json.first_name 'David'
|
78
108
|
end
|
79
109
|
|
80
110
|
|
@@ -89,6 +119,7 @@ The inline form defines key and value
|
|
89
119
|
| value | A value |
|
90
120
|
|
91
121
|
```ruby
|
122
|
+
|
92
123
|
json.set! :first_name, 'David'
|
93
124
|
|
94
125
|
or
|
@@ -108,39 +139,36 @@ The block form defines key and structure
|
|
108
139
|
|
109
140
|
```ruby
|
110
141
|
json.set! :details do
|
111
|
-
|
142
|
+
...
|
112
143
|
end
|
113
144
|
|
114
145
|
or
|
115
146
|
|
116
147
|
json.details do
|
117
|
-
|
148
|
+
...
|
118
149
|
end
|
119
150
|
```
|
120
151
|
|
121
152
|
The difference between the block form and inline form is
|
122
|
-
1. The block form is an internal node.
|
123
|
-
|
153
|
+
1. The block form is an internal node. Functionality such as Partials,
|
154
|
+
Deferment and other [options](#options) are only available on the
|
155
|
+
block form.
|
156
|
+
2. The inline form is considered a leaf node, and you can only [search](#traversing)
|
157
|
+
for internal nodes.
|
124
158
|
|
125
159
|
### json.array!
|
126
160
|
Generates an array of json objects.
|
127
161
|
|
128
162
|
```ruby
|
129
|
-
collection = [
|
130
|
-
{name: 'john'},
|
131
|
-
{name: 'jim'}
|
132
|
-
]
|
163
|
+
collection = [ {name: 'john'}, {name: 'jim'} ]
|
133
164
|
|
134
165
|
json.details do
|
135
|
-
json.array! collection, {
|
166
|
+
json.array! collection, {...options} do |person|
|
136
167
|
json.first_name person[:name]
|
137
168
|
end
|
138
169
|
end
|
139
170
|
|
140
|
-
# => {"details": [
|
141
|
-
{"firstName": 'john'},
|
142
|
-
{"firstName": 'jim'}
|
143
|
-
]}
|
171
|
+
# => {"details": [{"firstName": 'john'}, {"firstName": 'jim'} ]}
|
144
172
|
```
|
145
173
|
|
146
174
|
| Parameter | Notes |
|
@@ -148,7 +176,8 @@ end
|
|
148
176
|
| collection | A collection that responds to `member_at` and `member_by` |
|
149
177
|
| options | Additional [options](#options)|
|
150
178
|
|
151
|
-
To support [traversing nodes](
|
179
|
+
To support [traversing nodes](#traversing), any list passed
|
180
|
+
to `array!` MUST implement `member_at(index)` and `member_by(attr, value)`.
|
152
181
|
|
153
182
|
For example, if you were using a delegate:
|
154
183
|
|
@@ -169,7 +198,10 @@ end
|
|
169
198
|
Then in your template:
|
170
199
|
|
171
200
|
```ruby
|
172
|
-
data = ObjectCollection.new([
|
201
|
+
data = ObjectCollection.new([
|
202
|
+
{id: 1, name: 'foo'},
|
203
|
+
{id: 2, name: 'bar'}
|
204
|
+
])
|
173
205
|
|
174
206
|
json.array! data do
|
175
207
|
...
|
@@ -200,11 +232,15 @@ end
|
|
200
232
|
|
201
233
|
#### **Array core extension**
|
202
234
|
|
203
|
-
For convenience, PropsTemplate includes a core\_ext that adds these methods to
|
235
|
+
For convenience, PropsTemplate includes a core\_ext that adds these methods to
|
236
|
+
`Array`. For example:
|
204
237
|
|
205
238
|
```ruby
|
206
239
|
require 'props_template/core_ext'
|
207
|
-
data = [
|
240
|
+
data = [
|
241
|
+
{id: 1, name: 'foo'},
|
242
|
+
{id: 2, name: 'bar'}
|
243
|
+
]
|
208
244
|
|
209
245
|
json.posts
|
210
246
|
json.array! data do
|
@@ -213,38 +249,39 @@ json.posts
|
|
213
249
|
end
|
214
250
|
```
|
215
251
|
|
216
|
-
PropsTemplate does not know what the elements are in your collection. The
|
252
|
+
PropsTemplate does not know what the elements are in your collection. The
|
253
|
+
example above will be fine for [traversing](#traversing)
|
254
|
+
by index, but will raise a `NotImplementedError` if you query by attribute. You
|
255
|
+
may still need to implement `member_by`.
|
217
256
|
|
218
257
|
### json.deferred!
|
219
|
-
Returns all deferred nodes used by the [
|
258
|
+
Returns all deferred nodes used by the [deferment](#deferment) option.
|
220
259
|
|
221
|
-
|
222
|
-
json.
|
223
|
-
```
|
260
|
+
**Note** This is a [BreezyJS][1] specific functionality and is used in
|
261
|
+
`application.json.props` when first running `rails breezy:install:web`
|
224
262
|
|
225
|
-
This method is normally used in `application.json.props` when first generated by `rails breezy:install:web`
|
226
|
-
|
227
|
-
### json.fragments!
|
228
|
-
Returns all fragment nodes used by the [partial fragments](#partial-fragments) option.
|
229
263
|
|
230
264
|
```ruby
|
231
|
-
json.
|
232
|
-
```
|
265
|
+
json.deferred json.deferred!
|
233
266
|
|
234
|
-
|
267
|
+
# => [{url: '/some_url?props_at=outer.inner', path: 'outer.inner', type: 'auto'}]
|
268
|
+
```
|
235
269
|
|
236
|
-
|
270
|
+
This method provides metadata about deferred nodes to the frontend ([BreezyJS][1])
|
271
|
+
to fetch missing data in a second round trip.
|
237
272
|
|
238
|
-
|
273
|
+
### json.fragments!
|
274
|
+
Returns all fragment nodes used by the [partial fragments](#partial-fragments)
|
275
|
+
option.
|
239
276
|
|
240
|
-
```ruby
|
241
|
-
# _some_partial.json.props
|
277
|
+
```ruby json.fragments json.fragments! ```
|
242
278
|
|
243
|
-
|
244
|
-
|
279
|
+
**Note** This is a [BreezyJS][1] specific functionality and is used in
|
280
|
+
`application.json.props` when first running `rails breezy:install:web`
|
245
281
|
|
246
282
|
## Options
|
247
|
-
Functionality such as Partials, Deferements, and Caching can only be
|
283
|
+
Options Functionality such as Partials, Deferements, and Caching can only be
|
284
|
+
set on a block. It is normal to see empty blocks.
|
248
285
|
|
249
286
|
```ruby
|
250
287
|
json.post(partial: 'blog_post') do
|
@@ -253,7 +290,9 @@ end
|
|
253
290
|
|
254
291
|
### Partials
|
255
292
|
|
256
|
-
Partials are supported. The following will render the file
|
293
|
+
Partials are supported. The following will render the file
|
294
|
+
`views/posts/_blog_posts.json.props`, and set a local variable `foo` assigned
|
295
|
+
with @post, which you can use inside the partial.
|
257
296
|
|
258
297
|
```ruby
|
259
298
|
json.one_post partial: ["posts/blog_post", locals: {post: @post}] do
|
@@ -263,7 +302,8 @@ end
|
|
263
302
|
Usage with arrays:
|
264
303
|
|
265
304
|
```ruby
|
266
|
-
#
|
305
|
+
# The `as:` option is supported when using `array!`
|
306
|
+
|
267
307
|
json.posts do
|
268
308
|
json.array! @posts, partial: ["posts/blog_post", locals: {foo: 'bar'}, as: 'post'] do
|
269
309
|
end
|
@@ -271,14 +311,14 @@ end
|
|
271
311
|
```
|
272
312
|
|
273
313
|
### Partial Fragments
|
314
|
+
**Note** This is a [BreezyJS][1] specific functionality.
|
274
315
|
|
275
|
-
A fragment
|
276
|
-
|
277
|
-
You would need use partials and add the option `fragment: true`.
|
316
|
+
A fragment identifies a partial output across multiple pages. It can be used to
|
317
|
+
update cross cutting concerns like a header bar.
|
278
318
|
|
279
319
|
```ruby
|
280
320
|
# index.json.props
|
281
|
-
json.header partial: ["profile", fragment:
|
321
|
+
json.header partial: ["profile", fragment: "header"] do
|
282
322
|
end
|
283
323
|
|
284
324
|
# _profile.json.props
|
@@ -292,57 +332,16 @@ end
|
|
292
332
|
When using fragments with Arrays, the argument **MUST** be a lamda:
|
293
333
|
|
294
334
|
```ruby
|
295
|
-
require 'props_template/core_ext'
|
296
|
-
|
297
|
-
json.array! ['foo', 'bar'], partial: ["footer", fragment: ->(x){ x == 'foo'}]
|
298
|
-
```
|
299
|
-
|
300
|
-
PropsTemplate creates a name for the partial using a digest of your locals, partial name, and globalId (to_json as fallback if there is no globalId) on objects that you pass. You may override this behavior and use a custom identifier:
|
335
|
+
require 'props_template/core_ext'
|
301
336
|
|
302
|
-
|
303
|
-
# index.js.breezy
|
304
|
-
json.header partial: ["profile", fragment: 'me_header'] do
|
337
|
+
json.array! ['foo', 'bar'], partial: ["footer", fragment: ->(x){ x == 'foo'}] do
|
305
338
|
end
|
306
339
|
```
|
307
340
|
|
308
|
-
#### Optimisitc Updates
|
309
|
-
Breezy uses the digest generated by `fragment: true` to uniquely identify a partial across the redux state. If you need to optimistically update a fragment, use `json.fragment_digest!` to obtain the identifier in your partial, and `updateFragments` from BreezyJS.
|
310
|
-
|
311
|
-
For example:
|
312
|
-
|
313
|
-
```ruby
|
314
|
-
# _header.js.props
|
315
|
-
json.fragment_digest json.fragment_digest!
|
316
|
-
```
|
317
|
-
|
318
|
-
And in your reducer
|
319
|
-
|
320
|
-
```javacript
|
321
|
-
import {updateFragments} from 'jho406/Breezy';
|
322
|
-
|
323
|
-
switch(action.type) {
|
324
|
-
case SOME_ACTION: {
|
325
|
-
const {
|
326
|
-
fragmentDigest,
|
327
|
-
prevNode // <- the content of the _header.js.props
|
328
|
-
} = action.payload
|
329
|
-
|
330
|
-
const nextNode = {
|
331
|
-
...prevNode,
|
332
|
-
foo: 'bar'
|
333
|
-
}
|
334
|
-
|
335
|
-
return updateFragments(state, {
|
336
|
-
[fragmentDigest]: nextNode
|
337
|
-
})
|
338
|
-
}
|
339
|
-
default:
|
340
|
-
return state
|
341
|
-
}
|
342
|
-
```
|
343
|
-
|
344
341
|
### Caching
|
345
|
-
Caching is supported on
|
342
|
+
Caching is supported on internal nodes only. This limitation is what makes it
|
343
|
+
possible to for props_template to forgo marshalling/unmarshalling and simply
|
344
|
+
use [push_json](http://www.ohler.com/oj/doc/Oj/StringWriter.html#push_json-instance_method).
|
346
345
|
|
347
346
|
Usage:
|
348
347
|
|
@@ -368,44 +367,60 @@ end
|
|
368
367
|
When used with arrays, PropsTemplate will use `Rails.cache.read_multi`.
|
369
368
|
|
370
369
|
```ruby
|
371
|
-
require 'props_template/core_ext'
|
370
|
+
require 'props_template/core_ext'
|
371
|
+
|
372
|
+
opts = { cache: ->(i){ ['a', i] } }
|
372
373
|
|
373
|
-
opts = {
|
374
|
-
cache: ->(i){ ['a', i] }
|
375
|
-
}
|
376
374
|
json.array! [4,5], opts do |x|
|
377
375
|
json.top "hello" + x.to_s
|
378
376
|
end
|
379
377
|
|
380
378
|
#or on arrays with partials
|
381
379
|
|
382
|
-
opts = {
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
json.array! @options, opts
|
380
|
+
opts = { cache: (->(d){ ['a', d.id] }), partial: ["blog_post", as: :blog_post] }
|
381
|
+
|
382
|
+
json.array! @options, opts do
|
383
|
+
end
|
387
384
|
```
|
388
385
|
|
389
386
|
### Deferment
|
390
387
|
|
391
|
-
You can defer rendering of expensive nodes in your content tree using the
|
392
|
-
|
388
|
+
You can defer rendering of expensive nodes in your content tree using the
|
389
|
+
`defer: :manual` option. Behind the scenes PropsTemplates will no-op the block
|
390
|
+
entirely and replace the value with a placeholder. A common use case would be
|
391
|
+
tabbed content that does not load until you click the tab.
|
392
|
+
|
393
|
+
When your client receives the payload, you may issue a second request to the
|
394
|
+
same endpoint to fetch any missing nodes. See [traversing nodes](#traversing)
|
393
395
|
|
394
|
-
|
396
|
+
There is also an `defer: :auto` option that you can use with [BreezyJS][1]. [BreezyJS][1]
|
397
|
+
will use the metadata from `json.deferred!` to issue a `remote` dispatch to fetch
|
398
|
+
the missing node and immutably graft it at the appropriate keypath in your Redux
|
399
|
+
store.
|
395
400
|
|
396
401
|
Usage:
|
397
402
|
|
398
403
|
```ruby
|
399
|
-
json.dashboard(defer: :
|
404
|
+
json.dashboard(defer: :manual) do
|
405
|
+
sleep 10
|
406
|
+
json.some_fancy_metric 42
|
407
|
+
end
|
408
|
+
|
409
|
+
|
410
|
+
# or you can explicitly pass a placeholder
|
411
|
+
|
412
|
+
json.dashboard(defer: [:manual, placeholder: {}]) do
|
400
413
|
sleep 10
|
401
414
|
json.some_fancy_metric 42
|
402
415
|
end
|
403
416
|
```
|
404
417
|
|
405
|
-
A
|
418
|
+
A auto option is available:
|
419
|
+
|
420
|
+
**Note** This is a [BreezyJS][1] specific functionality.
|
406
421
|
|
407
422
|
```ruby
|
408
|
-
json.dashboard(defer: :
|
423
|
+
json.dashboard(defer: :auto) do
|
409
424
|
sleep 10
|
410
425
|
json.some_fancy_metric 42
|
411
426
|
end
|
@@ -417,39 +432,51 @@ Finally in your `application.json.props`:
|
|
417
432
|
json.defers json.deferred!
|
418
433
|
```
|
419
434
|
|
420
|
-
|
421
|
-
If `:manual` is used, PropsTemplate will no-op the block and will not populate `json.deferred!`. Its up to you to [traverse](props-template.md#traversing_nodes) to fetch the node seperately. A common usecase would be tab content that does not load until you click the tab.
|
422
|
-
|
423
435
|
#### Working with arrays
|
424
|
-
The default behavior for deferements is to use the index of the collection to
|
436
|
+
The default behavior for deferements is to use the index of the collection to
|
437
|
+
identify an element.
|
438
|
+
|
439
|
+
**Note** If you are using this library with [BreezyJS][1], the `:auto` options will
|
440
|
+
generate `?props_at=a.b.c.0.title` for `json.deferred!`.
|
425
441
|
|
426
442
|
If you wish to use an attribute to identify the element. You must:
|
427
|
-
|
428
|
-
|
443
|
+
|
444
|
+
1. Use the `:key` option on `json.array!`. This key refers to an attribute on
|
445
|
+
your collection item, and is used for `defer: :auto` to generate a keypath for
|
446
|
+
[BreezyJS][1]. If you are NOT using BreezyJS, you do not need to do this.
|
447
|
+
|
448
|
+
2. Implement `member_at`, on the [collection](#jsonarray). This will be called
|
449
|
+
by PropsTemplate to when [searching nodes](#traversing)
|
429
450
|
|
430
451
|
For example:
|
431
452
|
|
432
453
|
```ruby
|
433
|
-
require 'props_template/core_ext'
|
434
|
-
|
435
|
-
|
454
|
+
require 'props_template/core_ext'
|
455
|
+
data = [
|
456
|
+
{id: 1, name: 'foo'},
|
457
|
+
{id: 2, name: 'bar'}
|
458
|
+
]
|
436
459
|
|
437
460
|
json.posts
|
438
461
|
json.array! data, key: :some_id do |item|
|
462
|
+
# By using :key, props_template will append `json.some_id item.some_id`
|
463
|
+
# automatically
|
464
|
+
|
439
465
|
json.contact(defer: :auto) do
|
440
466
|
json.address '123 example drive'
|
441
467
|
end
|
442
|
-
|
443
|
-
# json.some_id item.some_id will be appended automatically to the end of the block
|
444
468
|
end
|
445
469
|
end
|
446
470
|
```
|
447
471
|
|
448
|
-
|
472
|
+
If you are using [BreezyJS][1], BreezyJS will, it will automatically kick off
|
473
|
+
`remote(?props_at=posts.some_id=1.contact)` and `remote(?props_at=posts.some_id=2.contact)`.
|
449
474
|
|
450
|
-
|
475
|
+
## Traversing
|
451
476
|
|
452
|
-
PropsTemplate has the ability to walk the tree you build, skipping execution of
|
477
|
+
PropsTemplate has the ability to walk the tree you build, skipping execution of
|
478
|
+
untargeted nodes. This feature is useful for selectively updating your frontend
|
479
|
+
state.
|
453
480
|
|
454
481
|
```ruby
|
455
482
|
traversal_path = ['data', 'details', 'personal']
|
@@ -457,7 +484,7 @@ traversal_path = ['data', 'details', 'personal']
|
|
457
484
|
json.data(search: traversal_path) do
|
458
485
|
json.details do
|
459
486
|
json.employment do
|
460
|
-
...more stuff
|
487
|
+
...more stuff
|
461
488
|
end
|
462
489
|
|
463
490
|
json.personal do
|
@@ -468,25 +495,28 @@ json.data(search: traversal_path) do
|
|
468
495
|
end
|
469
496
|
|
470
497
|
json.footer do
|
471
|
-
|
498
|
+
...
|
472
499
|
end
|
473
500
|
```
|
474
501
|
|
475
|
-
PropsTemplate will
|
502
|
+
PropsTemplate will walk depth first, walking only when it finds a matching key,
|
503
|
+
then executes the associated block, and repeats until it the node is found.
|
504
|
+
The above will output:
|
476
505
|
|
477
506
|
```json
|
478
507
|
{
|
479
|
-
data: {
|
480
|
-
name: 'james',
|
481
|
-
zipCode: 91210
|
508
|
+
"data": {
|
509
|
+
"name": 'james',
|
510
|
+
"zipCode": 91210
|
482
511
|
},
|
483
|
-
footer: {
|
484
|
-
|
512
|
+
"footer": {
|
513
|
+
...
|
485
514
|
}
|
486
515
|
}
|
487
516
|
```
|
488
517
|
|
489
|
-
|
518
|
+
Searching only works with blocks, and will NOT work with Scalars
|
519
|
+
("leaf" values). For example:
|
490
520
|
|
491
521
|
```ruby
|
492
522
|
traversal_path = ['data', 'details', 'personal', 'name'] <- not found
|
@@ -498,12 +528,11 @@ json.data(search: traversal_path) do
|
|
498
528
|
end
|
499
529
|
end
|
500
530
|
end
|
501
|
-
|
502
531
|
```
|
503
532
|
|
504
533
|
## Nodes that do not exist
|
505
534
|
|
506
|
-
Nodes that are not found will
|
535
|
+
Nodes that are not found will remove the branch where search was enabled on.
|
507
536
|
|
508
537
|
```ruby
|
509
538
|
traversal_path = ['data', 'details', 'does_not_exist']
|
@@ -517,17 +546,54 @@ json.data(search: traversal_path) do
|
|
517
546
|
end
|
518
547
|
|
519
548
|
json.footer do
|
520
|
-
|
549
|
+
...
|
521
550
|
end
|
522
|
-
|
523
551
|
```
|
524
552
|
|
525
553
|
The above will render:
|
526
554
|
|
527
|
-
```
|
555
|
+
```json
|
528
556
|
{
|
529
|
-
footer: {
|
557
|
+
"footer": {
|
530
558
|
...
|
531
559
|
}
|
532
560
|
}
|
533
561
|
```
|
562
|
+
|
563
|
+
## Layouts
|
564
|
+
A single layout is supported. To use, create an `application.json.props` in
|
565
|
+
`app/views/layouts`. Here's an example:
|
566
|
+
|
567
|
+
```ruby
|
568
|
+
json.data do
|
569
|
+
# template runs here.
|
570
|
+
yield json
|
571
|
+
end
|
572
|
+
|
573
|
+
json.header do
|
574
|
+
json.greeting "Hello"
|
575
|
+
end
|
576
|
+
|
577
|
+
json.footer do
|
578
|
+
json.greeting "Hello"
|
579
|
+
end
|
580
|
+
|
581
|
+
json.flash flash.to_h
|
582
|
+
```
|
583
|
+
|
584
|
+
**NOTE** PropsTemplate inverts the usual Rails rendering flow. PropsTemplate
|
585
|
+
will render Layout first, then the template when `yield json` is used.
|
586
|
+
|
587
|
+
## Contributing
|
588
|
+
|
589
|
+
See the [CONTRIBUTING] document. Thank you, [contributors]!
|
590
|
+
|
591
|
+
[CONTRIBUTING]: CONTRIBUTING.md
|
592
|
+
[contributors]: https://github.com/thoughtbot/props_template/graphs/contributors
|
593
|
+
|
594
|
+
## Special Thanks
|
595
|
+
|
596
|
+
Thanks to [turbostreamer](https://github.com/malomalo/turbostreamer) for the
|
597
|
+
inspiration.
|
598
|
+
|
599
|
+
[1]: https://github.com/thoughtbot/breezy
|
@@ -7,7 +7,7 @@ module Props
|
|
7
7
|
class ExtensionManager
|
8
8
|
attr_reader :base, :builder, :context
|
9
9
|
|
10
|
-
def initialize(base, defered=[], fragments=
|
10
|
+
def initialize(base, defered=[], fragments=[])
|
11
11
|
@base = base
|
12
12
|
@context = base.context
|
13
13
|
@builder = base.builder
|
@@ -36,10 +36,6 @@ module Props
|
|
36
36
|
@deferment.deferred
|
37
37
|
end
|
38
38
|
|
39
|
-
def fragment_digest
|
40
|
-
@fragment.name
|
41
|
-
end
|
42
|
-
|
43
39
|
def fragments
|
44
40
|
@fragment.fragments
|
45
41
|
end
|
@@ -59,10 +55,8 @@ module Props
|
|
59
55
|
handle_cache(options) do
|
60
56
|
base.set_block_content! do
|
61
57
|
if options[:partial]
|
62
|
-
current_digest = @fragment.name
|
63
58
|
@fragment.handle(options)
|
64
59
|
@partialer.handle(options)
|
65
|
-
@fragment.name = current_digest
|
66
60
|
else
|
67
61
|
yield
|
68
62
|
end
|
@@ -104,14 +98,7 @@ module Props
|
|
104
98
|
next_deferred, next_fragments = Oj.load(meta)
|
105
99
|
base.stream.push_json(raw_json)
|
106
100
|
deferred.push(*next_deferred)
|
107
|
-
|
108
|
-
next_fragments.each do |k, v|
|
109
|
-
if fragments[k]
|
110
|
-
fragments[k].push(*v)
|
111
|
-
else
|
112
|
-
fragments[k] = v
|
113
|
-
end
|
114
|
-
end
|
101
|
+
fragments.push(*next_fragments)
|
115
102
|
end
|
116
103
|
else
|
117
104
|
yield
|
@@ -25,12 +25,6 @@ module Props
|
|
25
25
|
@context
|
26
26
|
end
|
27
27
|
|
28
|
-
def instrument(name, **options)
|
29
|
-
ActiveSupport::Notifications.instrument(name, options) do |payload|
|
30
|
-
yield payload
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
28
|
def multi_fetch(keys, options = {})
|
35
29
|
result = {}
|
36
30
|
key_to_ckey = {}
|
@@ -49,7 +43,7 @@ module Props
|
|
49
43
|
|
50
44
|
read_caches = {}
|
51
45
|
|
52
|
-
instrument('read_multi_fragments.action_view', payload) do |payload|
|
46
|
+
ActiveSupport::Notifications.instrument('read_multi_fragments.action_view', payload) do |payload|
|
53
47
|
read_caches = ::Rails.cache.read_multi(*ckeys, options)
|
54
48
|
payload[:read_caches] = read_caches
|
55
49
|
end
|
@@ -122,7 +116,7 @@ module Props
|
|
122
116
|
|
123
117
|
def cache_key(key, options)
|
124
118
|
name_options = options.slice(:skip_digest, :virtual_path)
|
125
|
-
key =
|
119
|
+
key = @context.cache_fragment_name(key, **name_options)
|
126
120
|
|
127
121
|
if @context.respond_to?(:combined_fragment_cache_key)
|
128
122
|
key = @context.combined_fragment_cache_key(key)
|
@@ -132,19 +126,6 @@ module Props
|
|
132
126
|
|
133
127
|
::ActiveSupport::Cache.expand_cache_key(key, :props)
|
134
128
|
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
129
|
end
|
149
130
|
end
|
150
131
|
|
@@ -34,6 +34,8 @@ module Props
|
|
34
34
|
|
35
35
|
type, rest = options[:defer]
|
36
36
|
placeholder = rest[:placeholder]
|
37
|
+
success_action = rest[:success_action]
|
38
|
+
fail_action = rest[:fail_action]
|
37
39
|
|
38
40
|
if type.to_sym == :auto && options[:key]
|
39
41
|
key, val = options[:key]
|
@@ -45,16 +47,22 @@ module Props
|
|
45
47
|
path = @base.traveled_path.join('.')
|
46
48
|
uri = ::URI.parse(request_path)
|
47
49
|
qry = ::URI.decode_www_form(uri.query || '')
|
48
|
-
.reject{|x| x[0] == '
|
49
|
-
.push(["
|
50
|
+
.reject{|x| x[0] == 'props_at' }
|
51
|
+
.push(["props_at", path])
|
50
52
|
|
51
53
|
uri.query = ::URI.encode_www_form(qry)
|
52
54
|
|
53
|
-
|
55
|
+
deferral = {
|
54
56
|
url: uri.to_s,
|
55
57
|
path: path,
|
56
|
-
type: type.to_s
|
57
|
-
|
58
|
+
type: type.to_s,
|
59
|
+
}
|
60
|
+
|
61
|
+
# camelize for JS land
|
62
|
+
deferral[:successAction] = success_action if success_action
|
63
|
+
deferral[:failAction] = fail_action if fail_action
|
64
|
+
|
65
|
+
@deferred.push(deferral)
|
58
66
|
|
59
67
|
placeholder
|
60
68
|
end
|
@@ -1,14 +1,10 @@
|
|
1
|
-
require 'digest'
|
2
|
-
|
3
1
|
module Props
|
4
2
|
class Fragment
|
5
3
|
attr_reader :fragments
|
6
|
-
attr_accessor :name
|
7
4
|
|
8
|
-
def initialize(base, fragments=
|
5
|
+
def initialize(base, fragments=[])
|
9
6
|
@base = base
|
10
7
|
@fragments = fragments
|
11
|
-
@digest = Digest::SHA2.new(256)
|
12
8
|
end
|
13
9
|
|
14
10
|
def handle(options)
|
@@ -20,30 +16,10 @@ module Props
|
|
20
16
|
fragment_name = fragment.to_s
|
21
17
|
path = @base.traveled_path.join('.')
|
22
18
|
@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
19
|
|
30
|
-
|
31
|
-
|
32
|
-
|
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)
|
20
|
+
@fragments.push(
|
21
|
+
{ type: fragment_name, partial: partial_name, path: path }
|
22
|
+
)
|
47
23
|
end
|
48
24
|
end
|
49
25
|
end
|
@@ -1,22 +1,56 @@
|
|
1
1
|
require 'action_view'
|
2
2
|
|
3
3
|
module Props
|
4
|
+
class RenderedTemplate
|
5
|
+
attr_reader :body, :layout, :template
|
6
|
+
|
7
|
+
def initialize(body, layout, template)
|
8
|
+
@body = body
|
9
|
+
@layout = layout
|
10
|
+
@template = template
|
11
|
+
end
|
12
|
+
|
13
|
+
def format
|
14
|
+
template.format
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
4
18
|
class Partialer
|
19
|
+
INVALID_PARTIAL_MESSAGE = "The partial name must be a string, but received (%s)."
|
20
|
+
|
5
21
|
def initialize(base, context, builder)
|
6
22
|
@context = context
|
7
23
|
@builder = builder
|
8
24
|
@base = base
|
9
25
|
end
|
10
26
|
|
27
|
+
def extract_details(options) # :doc:
|
28
|
+
@context.lookup_context.registered_details.each_with_object({}) do |key, details|
|
29
|
+
value = options[key]
|
30
|
+
|
31
|
+
details[key] = Array(value) if value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
11
35
|
def find_and_add_template(all_options)
|
12
36
|
first_opts = all_options[0]
|
13
37
|
|
14
38
|
if first_opts[:partial]
|
15
39
|
partial_opts = block_opts_to_render_opts(@builder, first_opts)
|
16
|
-
|
40
|
+
.merge(formats: [:json])
|
41
|
+
partial_opts.delete(:handlers)
|
42
|
+
partial = partial_opts[:partial]
|
43
|
+
|
44
|
+
if !(String === partial)
|
45
|
+
raise ArgumentError.new(INVALID_PARTIAL_MESSAGE % (partial.inspect))
|
46
|
+
end
|
47
|
+
|
48
|
+
template_keys = retrieve_template_keys(partial_opts)
|
49
|
+
details = extract_details(partial_opts)
|
50
|
+
template = find_template(partial, template_keys, details)
|
17
51
|
|
18
52
|
all_options.map do |opts|
|
19
|
-
opts[:_template] =
|
53
|
+
opts[:_template] = template
|
20
54
|
opts
|
21
55
|
end
|
22
56
|
else
|
@@ -24,6 +58,17 @@ module Props
|
|
24
58
|
end
|
25
59
|
end
|
26
60
|
|
61
|
+
def find_template(path, locals, details)
|
62
|
+
prefixes = path.include?(?/) ? [] : @context.lookup_context.prefixes
|
63
|
+
@context.lookup_context.find_template(path, prefixes, true, locals, details)
|
64
|
+
end
|
65
|
+
|
66
|
+
def retrieve_template_keys(options)
|
67
|
+
template_keys = options[:locals].keys
|
68
|
+
template_keys << options[:as] if options[:as]
|
69
|
+
template_keys
|
70
|
+
end
|
71
|
+
|
27
72
|
def block_opts_to_render_opts(builder, options)
|
28
73
|
partial, pass_opts = [*options[:partial]]
|
29
74
|
pass_opts ||= {}
|
@@ -45,9 +90,20 @@ module Props
|
|
45
90
|
|
46
91
|
renderer.render(template, pass_opts)
|
47
92
|
end
|
93
|
+
|
94
|
+
def render(template, options)
|
95
|
+
view = @context
|
96
|
+
instrument(:partial, identifier: template.identifier) do |payload|
|
97
|
+
locals = options[:locals]
|
98
|
+
content = template.render(view, locals)
|
99
|
+
|
100
|
+
payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
|
101
|
+
build_rendered_template(content, template)
|
102
|
+
end
|
103
|
+
end
|
48
104
|
end
|
49
105
|
|
50
|
-
class PartialRenderer
|
106
|
+
class PartialRenderer
|
51
107
|
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
|
52
108
|
"make sure it starts with lowercase letter, " \
|
53
109
|
"and is followed by any combination of letters, numbers and underscores."
|
@@ -56,21 +112,6 @@ module Props
|
|
56
112
|
|
57
113
|
INVALID_PARTIAL_MESSAGE = "The partial name must be a string, but received (%s)."
|
58
114
|
|
59
|
-
def self.find_and_add_template(builder, context, all_options)
|
60
|
-
first_opts = all_options[0]
|
61
|
-
|
62
|
-
if first_opts[:partial]
|
63
|
-
partial_opts = block_opts_to_render_opts(builder, first_opts)
|
64
|
-
renderer = new(context, partial_opts)
|
65
|
-
|
66
|
-
all_options.map do |opts|
|
67
|
-
opts[:_template] = renderer.template
|
68
|
-
opts
|
69
|
-
end
|
70
|
-
else
|
71
|
-
all_options
|
72
|
-
end
|
73
|
-
end
|
74
115
|
|
75
116
|
def self.raise_invalid_option_as(as)
|
76
117
|
raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
|
@@ -106,8 +147,7 @@ module Props
|
|
106
147
|
locals[as] = item
|
107
148
|
|
108
149
|
if fragment_name = rest[:fragment]
|
109
|
-
|
110
|
-
rest[:fragment] = fragment_name
|
150
|
+
rest[:fragment] = fragment_name.to_s
|
111
151
|
end
|
112
152
|
end
|
113
153
|
|
@@ -121,7 +161,6 @@ module Props
|
|
121
161
|
|
122
162
|
def initialize(context, options)
|
123
163
|
@context = context
|
124
|
-
super(@context.lookup_context)
|
125
164
|
@options = options.merge(formats: [:json])
|
126
165
|
@options.delete(:handlers)
|
127
166
|
@details = extract_details(@options)
|
@@ -133,7 +172,7 @@ module Props
|
|
133
172
|
end
|
134
173
|
|
135
174
|
@path = partial
|
136
|
-
|
175
|
+
|
137
176
|
template_keys = retrieve_template_keys(@options)
|
138
177
|
@template = find_template(@path, template_keys)
|
139
178
|
end
|
@@ -145,6 +184,19 @@ module Props
|
|
145
184
|
end
|
146
185
|
|
147
186
|
private
|
187
|
+
def extract_details(options) # :doc:
|
188
|
+
@context.lookup_context.registered_details.each_with_object({}) do |key, details|
|
189
|
+
value = options[key]
|
190
|
+
|
191
|
+
details[key] = Array(value) if value
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def instrument(name, **options) # :doc:
|
196
|
+
ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
|
197
|
+
yield payload
|
198
|
+
end
|
199
|
+
end
|
148
200
|
|
149
201
|
def render_partial(template, view, options)
|
150
202
|
template ||= @template
|
@@ -159,17 +211,13 @@ module Props
|
|
159
211
|
end
|
160
212
|
end
|
161
213
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
#
|
166
|
-
# If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
|
167
|
-
# set to that string. Otherwise, the +options[:partial]+ object must
|
168
|
-
# respond to +to_partial_path+ in order to setup the path.
|
214
|
+
def build_rendered_template(content, template, layout = nil)
|
215
|
+
RenderedTemplate.new content, layout, template
|
216
|
+
end
|
169
217
|
|
170
218
|
def find_template(path, locals)
|
171
|
-
prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
|
172
|
-
@lookup_context.find_template(path, prefixes, true, locals, @details)
|
219
|
+
prefixes = path.include?(?/) ? [] : @context.lookup_context.prefixes
|
220
|
+
@context.lookup_context.find_template(path, prefixes, true, locals, @details)
|
173
221
|
end
|
174
222
|
|
175
223
|
def retrieve_template_keys(options)
|
data/lib/props_template.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'props_template/base_with_extensions'
|
2
2
|
require 'props_template/searcher'
|
3
3
|
require 'props_template/handler'
|
4
|
+
require 'props_template/version'
|
4
5
|
|
5
6
|
require 'active_support'
|
6
7
|
|
@@ -16,7 +17,6 @@ module Props
|
|
16
17
|
:deferred!,
|
17
18
|
:fragments!,
|
18
19
|
:set_block_content!,
|
19
|
-
:fragment_digest!,
|
20
20
|
to: :builder!
|
21
21
|
|
22
22
|
def initialize(context = nil, options = {})
|
data/spec/layout_spec.rb
CHANGED
@@ -1,16 +1,24 @@
|
|
1
|
-
require_relative
|
2
|
-
require_relative
|
3
|
-
require
|
1
|
+
require_relative "./support/helper"
|
2
|
+
require_relative "./support/rails_helper"
|
3
|
+
require "props_template/layout_patch"
|
4
|
+
require "action_controller"
|
4
5
|
|
5
|
-
RSpec.describe
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
RSpec.describe "Props::Template" do
|
7
|
+
class TestController < ActionController::Base
|
8
|
+
protect_from_forgery
|
9
|
+
|
10
|
+
def self.controller_path
|
11
|
+
""
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it "uses a layout to render" do
|
16
|
+
view_path = File.join(File.dirname(__FILE__), "./fixtures")
|
17
|
+
controller = TestController.new
|
9
18
|
controller.prepend_view_path(view_path)
|
10
|
-
controller.response.headers['Content-Type']='application/json'
|
11
|
-
controller.request.path = '/some_url'
|
12
19
|
|
13
|
-
json = controller.
|
20
|
+
json = controller.render_to_string("200", layout: "application")
|
21
|
+
|
14
22
|
expect(json.strip).to eql('{"data":{"success":"ok"}}')
|
15
23
|
end
|
16
24
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: props_template
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.21.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Johny Ho
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -52,8 +52,10 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.9'
|
55
|
-
description:
|
56
|
-
|
55
|
+
description: PropsTemplate is a direct-to-Oj, JBuilder-like DSL for building JSON.
|
56
|
+
It has support for Russian-Doll caching, layouts, and can be queried by giving the
|
57
|
+
root a key path.
|
58
|
+
email: johny@thoughtbot.com
|
57
59
|
executables: []
|
58
60
|
extensions: []
|
59
61
|
extra_rdoc_files: []
|
@@ -74,14 +76,15 @@ files:
|
|
74
76
|
- lib/props_template/layout_patch.rb
|
75
77
|
- lib/props_template/railtie.rb
|
76
78
|
- lib/props_template/searcher.rb
|
79
|
+
- lib/props_template/version.rb
|
77
80
|
- spec/layout_spec.rb
|
78
81
|
- spec/props_template_spec.rb
|
79
82
|
- spec/searcher_spec.rb
|
80
|
-
homepage: https://github.com/
|
83
|
+
homepage: https://github.com/thoughtbot/props_template/
|
81
84
|
licenses:
|
82
85
|
- MIT
|
83
86
|
metadata: {}
|
84
|
-
post_install_message:
|
87
|
+
post_install_message:
|
85
88
|
rdoc_options: []
|
86
89
|
require_paths:
|
87
90
|
- lib
|
@@ -89,17 +92,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
89
92
|
requirements:
|
90
93
|
- - ">="
|
91
94
|
- !ruby/object:Gem::Version
|
92
|
-
version: '2.
|
95
|
+
version: '2.5'
|
93
96
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
97
|
requirements:
|
95
98
|
- - ">="
|
96
99
|
- !ruby/object:Gem::Version
|
97
100
|
version: '0'
|
98
101
|
requirements: []
|
99
|
-
rubygems_version: 3.
|
100
|
-
signing_key:
|
102
|
+
rubygems_version: 3.1.4
|
103
|
+
signing_key:
|
101
104
|
specification_version: 4
|
102
|
-
summary: A JSON builder
|
105
|
+
summary: A fast JSON builder
|
103
106
|
test_files:
|
104
107
|
- spec/searcher_spec.rb
|
105
108
|
- spec/layout_spec.rb
|