props_template 1.0.0.alpha.3 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47eaa94bedc38645ef945eb7308cdc257497b063b5a9df481d491ec3bb1ae3ca
4
- data.tar.gz: 48da4764807eb7924b224e98175f81c822a67e6ead927904df452d620d39e6e0
3
+ metadata.gz: 6e8ec3d3579ed65bb2a927d2bf7abb10c9e5f4ee4d5a5147da09e8efb1e6a030
4
+ data.tar.gz: f21d46d7b34d47ee191b0ddcfe68ceb506d15d316a4ad00e433ca3752d82045a
5
5
  SHA512:
6
- metadata.gz: 40c1e3bfc2c99a3b37834abec4e9b8463d4585203583f9f2f7ba710d775e714a7950bf1bf60f9d86b67c34de6742a5b5a0d249e0154fb50736288123e9ebb1e4
7
- data.tar.gz: 70953db156e838cf72b220f9549c3a4abc6684d5bf0b0ad8a6305b15abf56cd1bd876635248fa2c8f1e43f21241475716d1dca49605fe65279951a8f2e59ed84
6
+ metadata.gz: db8bb1f529058a5dc7f2be56fe29720f638194e756a229da90deb85b3b885cd8de56dab5609219c73df23d1fe4de2cb80a2edc8803bce6a550ee8a4f16c36f70
7
+ data.tar.gz: dc997a26a9e941c8468e8b1cf755cb3c81e6b541ca7c56661f3ecaab8a701fed67712fd368e1cea50eb8b3dba76238ae169b3b8abb6e2e07485b5a8349ba5673
data/README.md CHANGED
@@ -14,6 +14,49 @@ that other libraries perform by using Oj's `StringWriter` in `rails` mode.
14
14
 
15
15
  ![benchmarks](docs/benchmarks.png)
16
16
 
17
+ It's really fast.
18
+
19
+ `props_template` is second only to `panko` while being feature packed. The
20
+ `Props::Base` [class](./lib/props_template/base.rb) that be used standalone
21
+ is the fastest among all JSON serializers. Results derived from [alba benchmarks](https://github.com/thoughtbot/alba/tree/main/benchmark).
22
+
23
+ ```
24
+ props_base_class: 1439.9 i/s
25
+ panko: 1287.6 i/s - 1.12x slower
26
+ props_template: 998.8 i/s - 1.44x slower
27
+ turbostreamer: 912.9 i/s - 1.58x slower
28
+ alba: 871.0 i/s - 1.65x slower
29
+ jserializer: 668.7 i/s - 2.15x slower
30
+ alba_with_transformation: 604.2 i/s - 2.38x slower
31
+ barley: 452.2 i/s - 3.18x slower
32
+ barley_cache: 441.4 i/s - 3.26x slower
33
+ jbuilder: 432.6 i/s - 3.33x slower
34
+ fast_serializer: 390.1 i/s - 3.69x slower
35
+ rails: 374.1 i/s - 3.85x slower
36
+ rabl: 310.3 i/s - 4.64x slower
37
+ blueprinter: 268.4 i/s - 5.36x slower
38
+ representable: 187.2 i/s - 7.69x slower
39
+ simple_ams: 124.5 i/s - 11.57x slower
40
+ ams: 41.5 i/s - 34.67x slower
41
+ alba_inline: 10.9 i/s - 131.64x slower
42
+
43
+ Gem versions:
44
+ active_model_serializers: 0.10.16
45
+ alba: 3.10.0
46
+ barley: 0.9.0
47
+ blueprinter: 1.2.1
48
+ jbuilder: 2.14.1
49
+ jserializer: 0.2.1
50
+ panko_serializer: 0.8.4
51
+ rabl: 0.17.0
52
+ representable: 3.2.0
53
+ simple_ams: 0.2.6
54
+ turbostreamer: 1.11.0
55
+
56
+ Ruby version: 3.4.8
57
+ Apple M4 Pro
58
+ ```
59
+
17
60
  Caching is fast too.
18
61
 
19
62
  While other libraries spend time unmarshaling,
@@ -36,7 +79,7 @@ json.menu do
36
79
  end
37
80
  end
38
81
 
39
- json.dashboard(defer: :auto) do
82
+ json.dashboard(with.defer(:auto)) do
40
83
  sleep 5
41
84
  json.complexPostMetric 500
42
85
  end
@@ -46,7 +89,7 @@ json.posts do
46
89
  paged_posts = @posts.page(page_num).per(20)
47
90
 
48
91
  json.list do
49
- json.array! paged_posts, key: :id do |post|
92
+ json.array! paged_posts, with.id_key(:id) do |post|
50
93
  json.id post.id
51
94
  json.description post.description
52
95
  json.commentsCount post.comments.count
@@ -59,8 +102,7 @@ json.posts do
59
102
  json.total @posts.count
60
103
  end
61
104
 
62
- json.footer partial: 'shared/footer' do
63
- end
105
+ json.footer with.partial('shared/footer')
64
106
  ```
65
107
 
66
108
  ## Installation
@@ -96,12 +138,18 @@ You can also add a [layout](#layouts).
96
138
  Defines the attribute or structure. All keys are not formatted by default. See [Change Key Format](#change-key-format) to change this behavior.
97
139
 
98
140
  ```ruby
99
- json.set! :authorDetails, {...options} do
141
+ json.set! :authorDetails, with.some_option do
100
142
  json.set! :firstName, 'David'
101
143
  end
102
144
 
103
145
  # or
104
146
 
147
+ json.authorDetails, with.some_option do
148
+ json.firstName 'David'
149
+ end
150
+
151
+ # or with hash options
152
+
105
153
  json.authorDetails, {...options} do
106
154
  json.firstName 'David'
107
155
  end
@@ -115,7 +163,7 @@ The inline form defines key and value
115
163
  | Parameter | Notes |
116
164
  | :--- | :--- |
117
165
  | key | A json object key|
118
- | value | A value |
166
+ | value | A value or an [options][#options] object enabled with [partial](#partials)|
119
167
 
120
168
  ```ruby
121
169
 
@@ -126,6 +174,10 @@ json.set! :firstName, 'David'
126
174
  json.firstName 'David'
127
175
 
128
176
  # => { "firstName": "David" }
177
+
178
+ # or
179
+
180
+ json.postDetails with.partial("blog_post")
129
181
  ```
130
182
 
131
183
  The block form defines key and structure
@@ -133,7 +185,7 @@ The block form defines key and structure
133
185
  | Parameter | Notes |
134
186
  | :--- | :--- |
135
187
  | key | A json object key|
136
- | options | Additional [options](#options)|
188
+ | options | Additional hash [options](#options) or a [options](#options) object|
137
189
  | block | Additional `json.set!`s or `json.array!`s|
138
190
 
139
191
  ```ruby
@@ -149,11 +201,12 @@ end
149
201
  ```
150
202
 
151
203
  The difference between the block form and inline form is
152
- 1. The block form is an internal node. Functionality such as Partials,
153
- Deferment and other [options](#options) are only available on the
154
- block form.
155
- 2. The inline form is considered a leaf node, and you can only [dig](#digging)
156
- for internal nodes.
204
+
205
+ 1. Passing options as a [hash](#hash-options) is only available for the
206
+ block form (also called an internal node)
207
+ 2. The inline form is considered a leaf node, if you need features like
208
+ partials you may use [options](#options) objects only.
209
+ 3. Only internal nodes may dug for [digging](#digging)
157
210
 
158
211
  ### json.extract!
159
212
  Extracts attributes from object or hash in 1 line
@@ -189,7 +242,7 @@ Generates an array of json objects.
189
242
  collection = [ {name: 'john'}, {name: 'jim'} ]
190
243
 
191
244
  json.details do
192
- json.array! collection, {...options} do |person|
245
+ json.array! collection, with.some_option do |person|
193
246
  json.firstName person[:name]
194
247
  end
195
248
  end
@@ -306,11 +359,29 @@ option.
306
359
  `application.json.props` when first running `rails superglue:install:web`
307
360
 
308
361
  ## Options
309
- Options Functionality such as Partials, Deferments, and Caching can only be
310
- set on a block. It is normal to see empty blocks.
362
+
363
+ PropsTemplate provides a `with` helper to build a `Props::Option` object that enable
364
+ functionality such as Partials, Deferments, and Caching
365
+
366
+ The following are equivalent:
367
+
368
+ ```ruby
369
+ json.post(with.partial('blog_post')
370
+ ```
371
+
372
+ or
373
+
374
+ ```ruby
375
+ json.post(Props::Options.new.partial('blog_post')
376
+ ```
377
+ ### Hash options
378
+
379
+ Previously, a normal hash was supported for internal nodes as long as an empty
380
+ block accompanied the option. This option is still supported, but its
381
+ recommended to use the `with` option builder.
311
382
 
312
383
  ```ruby
313
- json.post(partial: 'blog_post') do
384
+ json.post(partial: 'blog_post', locals: {post: @post}) do
314
385
  end
315
386
  ```
316
387
 
@@ -321,6 +392,10 @@ Partials are supported. The following will render the file
321
392
  with @post, which you can use inside the partial.
322
393
 
323
394
  ```ruby
395
+ json.one_post(with.partial("posts/blog_post", locals: {post: @post}))
396
+
397
+ # or with hash options
398
+
324
399
  json.one_post partial: ["posts/blog_post", locals: {post: @post}] do
325
400
  end
326
401
  ```
@@ -332,8 +407,7 @@ Usage with arrays:
332
407
  # Without `as:` option you can use blog_post variable (name is based on partial's name) inside partial
333
408
 
334
409
  json.posts do
335
- json.array! @posts, partial: ["posts/blog_post", locals: {foo: 'bar'}, as: 'post'] do
336
- end
410
+ json.array! @posts, with.partial("posts/blog_post", locals: {foo: 'bar'}, as: 'post')
337
411
  end
338
412
  ```
339
413
 
@@ -344,16 +418,14 @@ cause performance problems. It's best used for things like a shared header or fo
344
418
  Do:
345
419
 
346
420
  ```ruby
347
- json.partial! partial: "header", locals: {user: @user} do
348
- end
421
+ json.partial! with.partial("header", locals: {user: @user})
349
422
  ```
350
423
 
351
424
  or
352
425
 
353
426
  ```ruby
354
427
  json.posts do
355
- json.array! @posts, partial: ["posts/blog_post", locals: {post: @post}] do
356
- end
428
+ json.array! @posts, with.partial("posts/blog_post", locals: {post: @post})
357
429
  end
358
430
  ```
359
431
 
@@ -374,8 +446,7 @@ update cross cutting concerns like a header bar.
374
446
 
375
447
  ```ruby
376
448
  # index.json.props
377
- json.header partial: ["profile", fragment: "header"] do
378
- end
449
+ json.header with.partial("profile").fragment("header")
379
450
 
380
451
  # _profile.json.props
381
452
  json.profile do
@@ -390,8 +461,7 @@ When using fragments with Arrays, the argument **MUST** be a lamda:
390
461
  ```ruby
391
462
  require 'props_template/core_ext'
392
463
 
393
- json.array! ['foo', 'bar'], partial: ["footer", fragment: ->(x){ x == 'foo'}] do
394
- end
464
+ json.array! ['foo', 'bar'], with.partial("footer").fragment(->(x){ x == 'foo'})
395
465
  ```
396
466
 
397
467
  ### Caching
@@ -402,19 +472,18 @@ use [push_json](http://www.ohler.com/oj/doc/Oj/StringWriter.html#push_json-insta
402
472
  Usage:
403
473
 
404
474
  ```ruby
405
- json.author(cache: "some_cache_key") do
475
+ json.author(with.cache("some_cache_key")) do
406
476
  json.firstName "tommy"
407
477
  end
408
478
 
409
479
  # or
410
480
 
411
- json.profile(cache: "cachekey", partial: ["profile", locals: {foo: 1}]) do
412
- end
481
+ json.profile(with.cache("cachekey").partial("profile", locals: {foo: 1}))
413
482
 
414
483
  # or nest it
415
484
 
416
- json.author(cache: "some_cache_key") do
417
- json.address(cache: "some_other_cache_key") do
485
+ json.author(with.cache("some_cache_key")) do
486
+ json.address(with.cache("some_other_cache_key")) do
418
487
  json.zip 11214
419
488
  end
420
489
  end
@@ -425,7 +494,7 @@ When used with arrays, PropsTemplate will use `Rails.cache.read_multi`.
425
494
  ```ruby
426
495
  require 'props_template/core_ext'
427
496
 
428
- opts = { cache: ->(i){ ['a', i] } }
497
+ opts = with.cache(->(i){ ['a', i] } )
429
498
 
430
499
  json.array! [4,5], opts do |x|
431
500
  json.top "hello" + x.to_s
@@ -433,7 +502,7 @@ end
433
502
 
434
503
  # or on arrays with partials
435
504
 
436
- opts = { cache: (->(d){ ['a', d.id] }), partial: ["blog_post", as: :blog_post] }
505
+ opts = with.cache(->(d){ ['a', d.id] }).partial("blog_post", as: :blog_post)
437
506
 
438
507
  json.array! @options, opts do
439
508
  end
@@ -457,7 +526,7 @@ store.
457
526
  Usage:
458
527
 
459
528
  ```ruby
460
- json.dashboard(defer: :manual) do
529
+ json.dashboard(with.defer(:manual)) do
461
530
  sleep 10
462
531
  json.someFancyMetric 42
463
532
  end
@@ -465,7 +534,7 @@ end
465
534
 
466
535
  # or you can explicitly pass a placeholder
467
536
 
468
- json.dashboard(defer: [:manual, placeholder: {}]) do
537
+ json.dashboard(with.defer(:manual, placeholder: {})) do
469
538
  sleep 10
470
539
  json.someFancyMetric 42
471
540
  end
@@ -476,7 +545,7 @@ A auto option is available:
476
545
  **Note** This is a [SuperglueJS][1] specific functionality.
477
546
 
478
547
  ```ruby
479
- json.dashboard(defer: :auto) do
548
+ json.dashboard(with.defer(:auto)) do
480
549
  sleep 10
481
550
  json.someFancyMetric 42
482
551
  end
@@ -514,11 +583,11 @@ data = [
514
583
  ]
515
584
 
516
585
  json.posts
517
- json.array! data, key: :some_id do |item|
586
+ json.array! data, with.id_key(:some_id) do |item|
518
587
  # By using :key, props_template will append `json.some_id item.some_id`
519
588
  # automatically
520
589
 
521
- json.contact(defer: :auto) do
590
+ json.contact(with.defer(:auto)) do
522
591
  json.address '123 example drive'
523
592
  end
524
593
  end
@@ -623,7 +692,10 @@ A single layout is supported. To use, create an `application.json.props` in
623
692
  ```ruby
624
693
  json.data do
625
694
  # template runs here.
626
- yield json
695
+ _.call(json)
696
+ # NOTE: you can also use `yield json` instead, but prism sees this as
697
+ # a syntax error, so the `_.call(json)` is a workaround.
698
+ # See: https://github.com/thoughtbot/props_template/issues/58
627
699
  end
628
700
 
629
701
  json.header do
@@ -9,13 +9,17 @@ module Props
9
9
  class InvalidScopeForChildError < StandardError; end
10
10
 
11
11
  class Base
12
+ attr_accessor :item_context
13
+
12
14
  def initialize(encoder = nil)
13
15
  @stream = Oj::StringWriter.new(mode: :rails)
14
16
  @scope = nil
17
+ @item_context = nil
15
18
  end
16
19
 
17
- def set_block_content!(options = {})
20
+ def set_content!(options = {})
18
21
  @scope = nil
22
+ @item_context = nil
19
23
  yield
20
24
  if @scope.nil?
21
25
  @stream.push_object
@@ -26,7 +30,7 @@ module Props
26
30
  def handle_set_block(key, options)
27
31
  key = format_key(key)
28
32
  @stream.push_key(key)
29
- set_block_content!(options) do
33
+ set_content!(options) do
30
34
  yield
31
35
  end
32
36
  end
@@ -59,30 +63,17 @@ module Props
59
63
  nil
60
64
  end
61
65
 
62
- def refine_item_options(item, options)
63
- options
64
- end
65
-
66
66
  def handle_collection_item(collection, item, index, options)
67
- set_block_content!(options) do
67
+ @item_context = item
68
+
69
+ set_content!(options) do
68
70
  yield
69
71
  end
70
72
  end
71
73
 
72
- def refine_all_item_options(all_options)
73
- all_options
74
- end
75
-
76
74
  def handle_collection(collection, options)
77
- all_opts = collection.map do |item|
78
- refine_item_options(item, options.clone)
79
- end
80
-
81
- all_opts = refine_all_item_options(all_opts)
82
-
83
75
  collection.each_with_index do |item, index|
84
- pass_opts = all_opts[index]
85
- handle_collection_item(collection, item, index, pass_opts) do
76
+ handle_collection_item(collection, item, index, options) do
86
77
  # todo: remove index?
87
78
  yield item, index
88
79
  end
@@ -97,13 +88,20 @@ module Props
97
88
  raise InvalidScopeForArrayError.new("array! expects exclusive use of this block")
98
89
  end
99
90
 
100
- if collection.nil?
101
- @child_index = nil
102
- yield
103
- else
104
- handle_collection(collection, options) do |item, index|
105
- yield item, index
91
+ if block_given?
92
+ if collection.nil?
93
+ @child_index = nil
94
+ yield
95
+ else
96
+ handle_collection(collection, options) do |item, index|
97
+ yield item, index
98
+ end
106
99
  end
100
+ elsif options.is_a?(Props::Options)
101
+ options.valid_for_set!
102
+ handle_collection(collection, options) {}
103
+ else
104
+ raise ArgumentError.new("array! requires a block when no Props::Options object is given")
107
105
  end
108
106
 
109
107
  @scope = :array
@@ -147,7 +145,7 @@ module Props
147
145
  child_index += 1
148
146
 
149
147
  # this changes the scope to nil so child in a child will break
150
- set_block_content!(options) do
148
+ set_content!(options) do
151
149
  yield
152
150
  end
153
151
 
@@ -26,10 +26,10 @@ module Props
26
26
  @traveled_path.join(".")
27
27
  end
28
28
 
29
- def set_block_content!(options = {})
29
+ def set_content!(options = {})
30
30
  return super if !@em.has_extensions(options)
31
31
 
32
- @em.handle(options) do
32
+ @em.handle(options, item_context) do
33
33
  yield
34
34
  end
35
35
  end
@@ -51,11 +51,12 @@ module Props
51
51
  end
52
52
 
53
53
  def set!(key, options = {}, &block)
54
- if block
55
- options = @em.refine_options(options)
54
+ if !block && options.is_a?(Props::Options)
55
+ options.valid_for_set!
56
+ super {}
57
+ else
58
+ super
56
59
  end
57
-
58
- super
59
60
  end
60
61
 
61
62
  def handle_set_block(key, options)
@@ -73,16 +74,37 @@ module Props
73
74
  nil
74
75
  end
75
76
 
77
+ def handle_collection(collection, options)
78
+ if options[:cache]
79
+ key, rest = [*options[:cache]]
80
+
81
+ if ::Proc === key
82
+ cache_keys = collection.map do |item|
83
+ key.call(item)
84
+ end
85
+ @em.load_cache(cache_keys, rest || {})
86
+ end
87
+ end
88
+
89
+ super
90
+ end
91
+
76
92
  def handle_collection_item(collection, item, index, options)
77
93
  if !options[:key]
78
94
  @traveled_path.push(index)
79
95
  else
80
- id, val = options[:key]
96
+ if (key = options[:key])
97
+ val = if item.respond_to? key
98
+ item.send(key)
99
+ elsif item.is_a? Hash
100
+ item[key] || item[key.to_sym]
101
+ end
102
+ end
81
103
 
82
- if id.nil?
104
+ if key.nil?
83
105
  @traveled_path.push(index)
84
106
  else
85
- @traveled_path.push("#{id}=#{val}")
107
+ @traveled_path.push("#{key}=#{val}")
86
108
  end
87
109
  end
88
110
 
@@ -91,25 +113,5 @@ module Props
91
113
  @traveled_path.pop
92
114
  nil
93
115
  end
94
-
95
- def refine_all_item_options(all_options)
96
- @em.refine_all_item_options(all_options)
97
- end
98
-
99
- def refine_item_options(item, options)
100
- return options if options.empty?
101
-
102
- if (key = options[:key])
103
- val = if item.respond_to? key
104
- item.send(key)
105
- elsif item.is_a? Hash
106
- item[key] || item[key.to_sym]
107
- end
108
-
109
- options[:key] = [options[:key], val]
110
- end
111
-
112
- @em.refine_options(options, item)
113
- end
114
116
  end
115
117
  end
@@ -1,6 +1,9 @@
1
1
  module Props
2
2
  class ExtensionManager
3
- attr_reader :base, :builder, :context
3
+ attr_reader :base, :builder, :context, :cache, :partialer
4
+
5
+ delegate :load_cache, to: :cache
6
+ delegate :find_and_add_template, to: :partialer
4
7
 
5
8
  def initialize(base, defered = [], fragments = [])
6
9
  @base = base
@@ -16,22 +19,6 @@ module Props
16
19
  @deferment.disable!
17
20
  end
18
21
 
19
- def refine_options(options, item = nil)
20
- options = @partialer.refine_options(options, item)
21
- if !@deferment.disabled
22
- options = @deferment.refine_options(options, item)
23
- end
24
-
25
- Cache.refine_options(options, item)
26
- end
27
-
28
- def refine_all_item_options(all_options)
29
- return all_options if all_options.empty?
30
-
31
- all_options = @partialer.find_and_add_template(all_options)
32
- @cache.multi_fetch_and_add_results(all_options)
33
- end
34
-
35
22
  def deferred
36
23
  @deferment.deferred
37
24
  end
@@ -44,26 +31,35 @@ module Props
44
31
  options[:defer] || options[:cache] || options[:partial] || options[:key]
45
32
  end
46
33
 
47
- def handle(options)
34
+ def handle(options, item_context = nil)
48
35
  return yield if !has_extensions(options)
49
36
 
50
- if options[:defer] && !@deferment.disabled
51
- placeholder = @deferment.handle(options)
37
+ if (key = options[:key]) && item_context
38
+ val = if item_context.respond_to? key
39
+ item_context.send(key)
40
+ elsif item_context.is_a? Hash
41
+ item_context[key] || item_context[key.to_sym]
42
+ end
43
+ end
44
+
45
+ deferment_type = @deferment.extract_deferment_type(options, item_context) if !@deferment.disabled
46
+
47
+ if deferment_type
48
+ placeholder = @deferment.handle(options, deferment_type, key, val)
52
49
  base.stream.push_value(placeholder)
53
- @fragment.handle(options)
50
+ @fragment.handle(options, item_context)
54
51
  else
55
- handle_cache(options) do
56
- base.set_block_content! do
52
+ handle_cache(options, item_context) do
53
+ base.set_content! do
57
54
  if options[:partial]
58
- @fragment.handle(options)
59
- @partialer.handle(options)
55
+ @fragment.handle(options, item_context)
56
+ @partialer.handle(options, item_context)
60
57
  else
61
58
  yield
62
59
  end
63
60
 
64
- if options[:key]
65
- id, val = options[:key]
66
- base.set!(id, val)
61
+ if key && val
62
+ base.set!(key, val)
67
63
  end
68
64
  end
69
65
  end
@@ -72,11 +68,13 @@ module Props
72
68
 
73
69
  private
74
70
 
75
- def handle_cache(options)
71
+ def handle_cache(options, item)
76
72
  if options[:cache]
77
73
  recently_cached = false
78
74
 
79
- state = @cache.cache(*options[:cache]) do
75
+ key, rest = Cache
76
+ .normalize_options(options[:cache], item)
77
+ state = @cache.cache(key, rest) do
80
78
  recently_cached = true
81
79
  result = nil
82
80
  start = base.stream.to_s.length
@@ -93,7 +91,7 @@ module Props
93
91
  result
94
92
  end
95
93
 
96
- meta, raw_json = state.split("\n")
94
+ meta, raw_json = state.split("\n", 2)
97
95
  next_deferred, next_fragments = Oj.load(meta)
98
96
  deferred.push(*next_deferred)
99
97
  fragments.push(*next_fragments)
@@ -2,23 +2,22 @@ module Props
2
2
  class Cache
3
3
  delegate :controller, :safe_concat, to: :@context
4
4
 
5
- def self.refine_options(options, item = nil)
6
- return options if !options[:cache]
5
+ attr_reader :results
7
6
 
8
- pass_opts = options.clone
9
- key, rest = [*options[:cache]]
7
+ def initialize(context)
8
+ @context = context
9
+ @results = {}
10
+ end
11
+
12
+ def self.normalize_options(options, item = nil)
13
+ key, rest = [*options]
10
14
  rest ||= {}
11
15
 
12
16
  if item && ::Proc === key
13
17
  key = key.call(item)
14
18
  end
15
19
 
16
- pass_opts[:cache] = [key, rest]
17
- pass_opts
18
- end
19
-
20
- def initialize(context)
21
- @context = context
20
+ [key, rest]
22
21
  end
23
22
 
24
23
  attr_reader :context
@@ -48,36 +47,21 @@ module Props
48
47
 
49
48
  keys.each do |k|
50
49
  ckey = key_to_ckey[k]
51
- result[k] = read_caches[ckey]
50
+
51
+ if read_caches[ckey]
52
+ result[k] = read_caches[ckey]
53
+ end
52
54
  end
53
55
 
54
56
  result
55
57
  end
56
58
 
57
- def multi_fetch_and_add_results(all_options)
58
- first_opts = all_options[0]
59
-
60
- if first_opts[:cache] && controller.perform_caching
61
- keys = all_options.map { |i| i[:cache][0] }
62
- c_opts = first_opts[:cache][1]
63
- result = multi_fetch(keys, c_opts)
64
-
65
- all_options.map do |opts|
66
- key = opts[:cache][0]
67
-
68
- if result.key? key
69
- opts[:cache][1][:result] = result[key]
70
- end
71
-
72
- opts
73
- end
74
- else
75
- all_options
76
- end
59
+ def load_cache(keys, options = {})
60
+ @results = results.merge multi_fetch(keys, options)
77
61
  end
78
62
 
79
- # Copied from jbuilder
80
- #
63
+ # The below was copied from the wonderful jbuilder library Its also MIT
64
+ # licensed, so no issues there. Thanks to the jbuilder authors!
81
65
 
82
66
  def cache(key = nil, options = {})
83
67
  if controller.perform_caching
@@ -90,10 +74,9 @@ module Props
90
74
  end
91
75
 
92
76
  def cache_fragment_for(key, options, &block)
93
- key = cache_key(key, options)
94
-
95
- return options[:result] if options[:result]
77
+ return results[key] if results[key]
96
78
 
79
+ key = cache_key(key, options)
97
80
  read_fragment_cache(key, options) || write_fragment_cache(key, options, &block)
98
81
  end
99
82
 
@@ -13,37 +13,27 @@ module Props
13
13
  end
14
14
 
15
15
  def refine_options(options, item = nil)
16
- return options if !options[:defer]
17
- pass_opts = options.clone
18
-
19
- type, rest = [*options[:defer]]
20
- rest ||= {
21
- placeholder: {}
22
- }
23
-
24
- if item
25
- type = (Proc === type) ? type.call(item) : type
26
- end
27
-
28
- if type
29
- pass_opts[:defer] = [type, rest]
30
- else
31
- pass_opts.delete(:defer)
32
- end
16
+ options.clone
17
+ end
33
18
 
34
- pass_opts
19
+ def extract_deferment_type(options, item)
20
+ type, _ = [*options[:defer]]
21
+ (Proc === type) ? type.call(item) : type
35
22
  end
36
23
 
37
- def handle(options)
24
+ def handle(options, type, key, val)
38
25
  return if !options[:defer]
39
26
 
40
- type, rest = options[:defer]
27
+ _, rest = [*options[:defer]]
28
+ rest ||= {
29
+ placeholder: {}
30
+ }
31
+
41
32
  placeholder = rest[:placeholder]
42
33
  success_action = rest[:success_action]
43
34
  fail_action = rest[:fail_action]
44
35
 
45
- if type.to_sym == :auto && options[:key]
46
- key, val = options[:key]
36
+ if type.to_sym == :auto && key && val
47
37
  placeholder = {}
48
38
  placeholder[key] = val
49
39
  end
@@ -7,7 +7,7 @@ module Props
7
7
  @fragments = fragments
8
8
  end
9
9
 
10
- def self.fragment_name_from_options(options)
10
+ def self.fragment_name_from_options(options, item = nil)
11
11
  return if !options[:partial]
12
12
 
13
13
  _, partial_opts = [*options[:partial]]
@@ -15,13 +15,17 @@ module Props
15
15
 
16
16
  fragment = partial_opts[:fragment]
17
17
 
18
+ if item && ::Proc === fragment
19
+ fragment = fragment.call(item)
20
+ end
21
+
18
22
  if String === fragment || Symbol === fragment
19
23
  fragment.to_s
20
24
  end
21
25
  end
22
26
 
23
- def handle(options)
24
- fragment_name = self.class.fragment_name_from_options(options)
27
+ def handle(options, item_context = nil)
28
+ fragment_name = self.class.fragment_name_from_options(options, item_context)
25
29
  path = @base.traveled_path
26
30
  .map { |item| item.is_a?(Array) ? item[0] : item }
27
31
  .join(".")
@@ -45,22 +45,6 @@ module Props
45
45
  end
46
46
  end
47
47
 
48
- def find_and_add_template(all_options)
49
- first_opts = all_options[0]
50
-
51
- if first_opts[:partial]
52
- partial_opts = block_opts_to_render_opts(@builder, first_opts)
53
- template = find_template(partial_opts)
54
-
55
- all_options.map do |opts|
56
- opts[:_template] = template
57
- opts
58
- end
59
- else
60
- all_options
61
- end
62
- end
63
-
64
48
  def find_template(partial_opts)
65
49
  partial = partial_opts[:partial]
66
50
  template_keys = retrieve_template_keys(partial_opts)
@@ -78,7 +62,7 @@ module Props
78
62
 
79
63
  def block_opts_to_render_opts(builder, options)
80
64
  partial, pass_opts = [*options[:partial]]
81
- pass_opts ||= {}
65
+ pass_opts = pass_opts&.clone || {}
82
66
  pass_opts[:locals] ||= {}
83
67
  pass_opts[:partial] = partial
84
68
  pass_opts[:formats] = [:json]
@@ -91,9 +75,17 @@ module Props
91
75
  pass_opts
92
76
  end
93
77
 
94
- def handle(options)
95
- partial_opts = block_opts_to_render_opts(@builder, options)
96
- template = options[:_template] || find_template(partial_opts)
78
+ def handle(options, item = nil)
79
+ return options if !options[:partial]
80
+
81
+ normalized_options = normalize_options(options, item)
82
+ partial_opts = block_opts_to_render_opts(@builder, normalized_options)
83
+ template = if options[:_template]
84
+ options[:_template]
85
+ else
86
+ # mutate the original options to bypass find_template a second time.
87
+ options[:_template] = find_template(partial_opts)
88
+ end
97
89
 
98
90
  render_partial(template, @context, partial_opts)
99
91
  end
@@ -133,8 +125,10 @@ module Props
133
125
  end
134
126
 
135
127
  def refine_options(options, item = nil)
136
- return options if !options[:partial]
128
+ options.clone
129
+ end
137
130
 
131
+ def normalize_options(options, item = nil)
138
132
  partial, rest = [*options[:partial]]
139
133
  rest = (rest || {}).clone
140
134
  locals = (rest[:locals] || {}).clone
@@ -150,21 +144,9 @@ module Props
150
144
  raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
151
145
 
152
146
  locals[as] = item
153
-
154
- if (fragment_name = rest[:fragment])
155
- if item && ::Proc === fragment_name
156
- fragment_name = fragment_name.call(item)
157
- end
158
-
159
- rest[:fragment] = fragment_name.to_s
160
- end
161
147
  end
162
148
 
163
- pass_opts = options.clone
164
- pass_opts[:partial] = [partial, rest]
165
-
166
- pass_opts
149
+ {partial: [partial, rest]}
167
150
  end
168
151
  end
169
152
  end
170
-
@@ -0,0 +1,7 @@
1
+ module Props
2
+ module Helper
3
+ def with
4
+ Props::Options.new
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,39 @@
1
+ module Props
2
+ class Options < Hash
3
+ class InvalidOptionError < StandardError; end
4
+
5
+ def partial(partial_name, opts = {})
6
+ self[:partial] = [partial_name, opts]
7
+ self
8
+ end
9
+
10
+ def defer(type, placeholder: {}, **opts)
11
+ self[:defer] = [type, {placeholder: placeholder}.merge(opts)]
12
+ self
13
+ end
14
+
15
+ def fragment(fragment)
16
+ raise "Fragment can't be defined without a partial. Please use `partial` first" if !self[:partial]
17
+
18
+ self[:partial][1][:fragment] = fragment
19
+ self
20
+ end
21
+
22
+ def cache(id_or_block)
23
+ return unless id_or_block
24
+
25
+ self[:cache] = id_or_block
26
+ self
27
+ end
28
+
29
+ def id_key(key_name)
30
+ self[:key] = key_name
31
+ self
32
+ end
33
+
34
+ def valid_for_set!
35
+ raise InvalidOptionError.new("Props::Options can't be empty") if empty?
36
+ raise InvalidOptionError.new("The partial option must be used with an inline `set!`") if !key?(:partial)
37
+ end
38
+ end
39
+ end
@@ -6,6 +6,7 @@ module Props
6
6
  initializer :props_template do
7
7
  ActiveSupport.on_load :action_view do
8
8
  ActionView::Template.register_template_handler :props, Props::Handler
9
+ ActionView::Base.include Props::Helper
9
10
  require "props_template/dependency_tracker"
10
11
  require "props_template/layout_patch"
11
12
  require "props_template/partial_patch"
@@ -12,6 +12,7 @@ module Props
12
12
  @traveled_path = []
13
13
  @partialer = Partialer.new(self, context, builder)
14
14
  @fragment_name = nil
15
+ @found_item = nil
15
16
  end
16
17
 
17
18
  def deferred!
@@ -30,7 +31,7 @@ module Props
30
31
  pass_opts[:path_suffix] = traveled_path
31
32
  end
32
33
 
33
- fragment_name = Fragment.fragment_name_from_options(pass_opts)
34
+ fragment_name = Fragment.fragment_name_from_options(pass_opts, @found_item)
34
35
  if fragment_name
35
36
  @fragment_name = fragment_name
36
37
  @fragment_path = @traveled_path.clone
@@ -38,15 +39,15 @@ module Props
38
39
 
39
40
  fragment_context = @fragment_name
40
41
 
41
- [@found_block, @traveled_path, pass_opts, @fragment_path, fragment_context]
42
+ [@found_block, @traveled_path, pass_opts, @fragment_path, fragment_context, @found_item]
42
43
  end
43
44
 
44
- def set_block_content!(*args)
45
+ def set_content!(*args)
45
46
  yield
46
47
  end
47
48
 
48
49
  def set!(key, options = {}, &block)
49
- return if @found_block || !block
50
+ return if @found_block || !block && !options.is_a?(Props::Options)
50
51
 
51
52
  if @search_path[@depth] == key.to_s
52
53
  @traveled_path.push(key)
@@ -97,21 +98,11 @@ module Props
97
98
 
98
99
  if item
99
100
  pass_opts = @partialer.refine_options(options, item)
100
-
101
- if (key = pass_opts[:key])
102
- val = if item.respond_to? key
103
- item.send(key)
104
- elsif item.is_a? Hash
105
- item[key] || item[key.to_sym]
106
- end
107
-
108
- pass_opts[:key] = [pass_opts[:key], val]
109
- end
110
-
111
101
  @traveled_path.push(key_index)
112
102
 
113
103
  if @depth == @search_path.size - 1
114
104
  @found_options = pass_opts
105
+ @found_item = item
115
106
  @found_block = proc {
116
107
  yield item, 0
117
108
  }
@@ -120,7 +111,7 @@ module Props
120
111
 
121
112
  @depth += 1
122
113
  if pass_opts[:partial]
123
- fragment_name = Fragment.fragment_name_from_options(pass_opts)
114
+ fragment_name = Fragment.fragment_name_from_options(pass_opts, item)
124
115
  if fragment_name
125
116
  @fragment_name = fragment_name
126
117
  @fragment_path = @traveled_path.clone
@@ -0,0 +1,28 @@
1
+ array
2
+ handle_collection
3
+ all_opts = collection.map do |item|
4
+ refine_item_options(item, options)
5
+ end
6
+ creates clone of the opts using
7
+ refine_item_options clones original options
8
+ then subsequently the ext refine_item_options
9
+ gets called first,
10
+
11
+ refine_all_item_options <- operates on the entire collection
12
+
13
+ handle_collection_item
14
+ set_content options
15
+
16
+
17
+
18
+ base with ExtensionManager
19
+ refine_all_item_options - overrided with ext manager
20
+ - which attahes a _template with partialer
21
+ to it AND multifetch and attaches it to cache
22
+ as [key, result]
23
+
24
+ refine_item_options
25
+ - transforms key: :id to key = [key, val]
26
+ - passes to refine_options
27
+ which is then asks the searcher the cache and
28
+ deferment to mutate the option
@@ -1,3 +1,3 @@
1
1
  module Props
2
- VERSION = "1.0.0.alpha.3".freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
@@ -9,6 +9,8 @@ require "active_support/core_ext/string/output_safety"
9
9
  require "active_support/core_ext/array"
10
10
  require "props_template/searcher"
11
11
  require "props_template/handler"
12
+ require "props_template/options"
13
+ require "props_template/helper"
12
14
  require "props_template/version"
13
15
 
14
16
  require "active_support"
@@ -28,7 +30,7 @@ module Props
28
30
  :deferred!,
29
31
  :fragments!,
30
32
  :disable_deferments!,
31
- :set_block_content!,
33
+ :set_content!,
32
34
  :traveled_path!,
33
35
  :fragment_context!,
34
36
  to: :builder!
@@ -51,13 +53,14 @@ module Props
51
53
  options.delete(:dig)
52
54
 
53
55
  @builder.set!(key, options, &block)
54
- found_block, found_path, found_options, fragment_path, fragment_context = @builder.found!
56
+ found_block, found_path, found_options, fragment_path, fragment_context, found_item = @builder.found!
55
57
  @found_path = found_path || []
56
58
  @fragment_context = fragment_context
57
59
  @builder = prev_builder
58
60
  @fragment_path = fragment_path
59
61
 
60
62
  if found_block
63
+ @builder.item_context = found_item
61
64
  set!(key, found_options, &found_block)
62
65
  end
63
66
  else
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: props_template
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.alpha.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johny Ho
@@ -83,10 +83,13 @@ files:
83
83
  - lib/props_template/extensions/fragment.rb
84
84
  - lib/props_template/extensions/partial_renderer.rb
85
85
  - lib/props_template/handler.rb
86
+ - lib/props_template/helper.rb
86
87
  - lib/props_template/layout_patch.rb
88
+ - lib/props_template/options.rb
87
89
  - lib/props_template/partial_patch.rb
88
90
  - lib/props_template/railtie.rb
89
91
  - lib/props_template/searcher.rb
92
+ - lib/props_template/test.md
90
93
  - lib/props_template/version.rb
91
94
  homepage: https://github.com/thoughtbot/props_template/
92
95
  licenses:
@@ -99,7 +102,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
99
102
  requirements:
100
103
  - - ">="
101
104
  - !ruby/object:Gem::Version
102
- version: '2.5'
105
+ version: '3.3'
103
106
  required_rubygems_version: !ruby/object:Gem::Requirement
104
107
  requirements:
105
108
  - - ">="