props_template 0.16.0 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build
|
8
|
+
Status](https://circleci.com/gh/thoughtbot/props_template.svg?style=shield)](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
|
+
![benchmarks](docs/benchmarks.png)
|
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
|