turbostreamer 1.11.0 → 2.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 +4 -4
- data/CHANGELOG.md +86 -0
- data/LICENSE +22 -0
- data/README.md +79 -0
- data/lib/action_view/streaming_turbo_buffer.rb +25 -0
- data/lib/action_view/streaming_turbo_template_renderer.rb +45 -0
- data/lib/action_view/turbo_buffer.rb +44 -0
- data/lib/action_view/turbo_template_renderer.rb +83 -0
- data/lib/turbostreamer/dependency_tracker.rb +45 -55
- data/lib/turbostreamer/encoders/oj.rb +2 -0
- data/lib/turbostreamer/encoders/wankel.rb +2 -0
- data/lib/turbostreamer/errors.rb +3 -2
- data/lib/turbostreamer/handler.rb +4 -2
- data/lib/turbostreamer/key_formatter.rb +2 -0
- data/lib/turbostreamer/railtie.rb +29 -8
- data/lib/turbostreamer/template.rb +42 -12
- data/lib/turbostreamer/version.rb +3 -1
- data/lib/turbostreamer.rb +55 -17
- metadata +21 -100
- data/ext/actionview/buffer.rb +0 -25
- data/ext/actionview/streaming_template_renderer.rb +0 -41
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0304ed518c7b493ea45ca44b3b3165d88a8f8171b7a3492b86c95a0c7b856617
|
|
4
|
+
data.tar.gz: e8b9e0d3335c16375794e0de491aa34ef176e06f2d49754e918f28ea492fe305
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a61cc82094a231be929bd06358d64566364ac587741af0de0764b5d1db4279dfdcb14e7b4244fc249f447d70ca5f75a58dcbc8d72c244fbd02edde1b75f000c2
|
|
7
|
+
data.tar.gz: b6e7abe601616b9fdd57e51acb9f4c24d34d7571c85c51e1a655c2d89cdc70849f62ef7bfeb8513078439a84ee6db6e5f84a9434cc75d97a5bb8d2008be1f757
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
2.0.0
|
|
4
|
+
-----
|
|
5
|
+
|
|
6
|
+
Breaking:
|
|
7
|
+
|
|
8
|
+
* Requires Rails 8.0+ and Ruby 3.3+.
|
|
9
|
+
* `write` is no longer aliased onto `ActionView::OutputBuffer` and
|
|
10
|
+
`ActionView::StreamingBuffer`. Installing the gem used to add a non-escaping
|
|
11
|
+
append to every buffer in the application; the encoders now write into
|
|
12
|
+
`ActionView::TurboBuffer` instead. Code outside TurboStreamer that called
|
|
13
|
+
`output_buffer.write` was relying on that alias and will need `safe_concat`.
|
|
14
|
+
* `ActionView::JSONStreamingBuffer` is now `ActionView::StreamingTurboBuffer`.
|
|
15
|
+
|
|
16
|
+
* Optimize internal `extract!` calls to save on memory allocation [PR #25](https://github.com/malomalo/turbostreamer/pull/25)
|
|
17
|
+
* Add `frozen_string_literal` magic comments
|
|
18
|
+
* Remove some old Rails code
|
|
19
|
+
* Add Rails 8.0 & 8.1 to CI; drop support for Rails < 8.0 and Ruby < 3.3
|
|
20
|
+
* Package `LICENSE` and `CHANGELOG.md` with the gem, and fix `spec.files` dropping
|
|
21
|
+
everything but `README.md` when the gem is built on a shell without brace expansion
|
|
22
|
+
* Layouts now work. A `.json.streamer` layout places the template with
|
|
23
|
+
`json.yield!`. Layout and template share one builder, so the template writes
|
|
24
|
+
into the same stream rather than being buffered and spliced. The layout
|
|
25
|
+
renders first and yields to the template, the reverse of an ERB layout.
|
|
26
|
+
Applies both to `render stream: true` and to ordinary rendering; previously a
|
|
27
|
+
streamed layout was resolved and then discarded, and an unstreamed one had no
|
|
28
|
+
way to place the template's JSON at all.
|
|
29
|
+
* Fix streaming JSON raising `NoMethodError: undefined method 'instrument'`.
|
|
30
|
+
`AbstractRenderer#instrument` was removed in Rails 6.1, so every streamed
|
|
31
|
+
render failed -- silently, since `Body#each` rescues and substitutes an error
|
|
32
|
+
page. It now notifies `render_template.action_view` directly.
|
|
33
|
+
* Stop aliasing `write` onto `ActionView::OutputBuffer` and
|
|
34
|
+
`ActionView::StreamingBuffer`. The encoders now stream into `TurboStreamer::Buffer`,
|
|
35
|
+
which wraps the ActionView buffer instead of patching it, so the non-escaping
|
|
36
|
+
`write` is no longer added to every buffer in the application.
|
|
37
|
+
`ActionView::JSONStreamingBuffer` moves to `TurboStreamer::StreamingBuffer`.
|
|
38
|
+
|
|
39
|
+
1.11.0 - 2024-04-29
|
|
40
|
+
-----
|
|
41
|
+
* Fix timestamp precision for Rails [PR #24](https://github.com/malomalo/turbostreamer/pull/24)
|
|
42
|
+
* Fix CI yajl archive download URL [PR #22](https://github.com/malomalo/turbostreamer/pull/22)
|
|
43
|
+
|
|
44
|
+
1.10.0
|
|
45
|
+
-----
|
|
46
|
+
* Fixed Rails 6.1 & Ruby 3.0 Compatibility
|
|
47
|
+
|
|
48
|
+
1.9.0
|
|
49
|
+
-----
|
|
50
|
+
* Fixed deprecation of using `Proc.new` to capture block; replaced with `&block`
|
|
51
|
+
|
|
52
|
+
1.8.0
|
|
53
|
+
-----
|
|
54
|
+
* Make the StreamingRenderer Rails 6 compatible [PR #15](https://github.com/malomalo/turbostreamer/issues/15)
|
|
55
|
+
* Update gemspec to require Ruby 2.5+ [PR #14](https://github.com/malomalo/turbostreamer/issues/14)
|
|
56
|
+
|
|
57
|
+
1.7.0
|
|
58
|
+
-----
|
|
59
|
+
* Add the ability to set default options for encoders
|
|
60
|
+
* Allow setting the `buffer_size` on the OJ encode
|
|
61
|
+
* Reduce find_template calls [PR #11](https://github.com/malomalo/turbostreamer/pull/1)
|
|
62
|
+
* Don't require a layout to stream template in Rails
|
|
63
|
+
|
|
64
|
+
1.5.0
|
|
65
|
+
-----
|
|
66
|
+
* Add Rails 6.0 support
|
|
67
|
+
* Drop Rails 4.2 support
|
|
68
|
+
|
|
69
|
+
1.4.0
|
|
70
|
+
-----
|
|
71
|
+
* Replace deprecated fragment_cache_key for Rails 5.2 support
|
|
72
|
+
|
|
73
|
+
1.3.0
|
|
74
|
+
-----
|
|
75
|
+
* Bump version and update bundler
|
|
76
|
+
|
|
77
|
+
1.2.0
|
|
78
|
+
-----
|
|
79
|
+
* Add `TurboStreamer#merge!` to merge a hash or array into the current json stream.
|
|
80
|
+
|
|
81
|
+
1.1.0
|
|
82
|
+
-----
|
|
83
|
+
* Add `Oj` as an encoder option
|
|
84
|
+
* Add ability to pass encoder as an option to `TurboStreamer#new` (symbol or class)
|
|
85
|
+
* Ability to set default encoder for mime type with `TurboStreamer#set_default_encoder`
|
|
86
|
+
* Add some performance test
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2011-2017 David Heinemeier Hansson, 37signals
|
|
4
|
+
Copyright (c) 2017 Jonathan Bracy
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -191,6 +191,85 @@ json.partial! partial: 'posts/post', collection: @posts, as: :post
|
|
|
191
191
|
json.comments @post.comments, partial: 'comment/comment', as: :comment
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
+
### Layouts
|
|
195
|
+
|
|
196
|
+
A `.json.streamer` layout can wrap the template. Call `json.yield!` where the
|
|
197
|
+
template's JSON should go:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
# app/views/layouts/application.json.streamer
|
|
201
|
+
json.object! do
|
|
202
|
+
json.meta do
|
|
203
|
+
json.object! { json.version 1 }
|
|
204
|
+
end
|
|
205
|
+
json.key! :data
|
|
206
|
+
json.yield!
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# app/views/posts/index.json.streamer
|
|
210
|
+
json.array! @posts, :id, :title
|
|
211
|
+
|
|
212
|
+
# => { "meta": { "version": 1 }, "data": [ { "id": 1, "title": "..." } ] }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The layout and the template share one builder, so the template writes straight
|
|
216
|
+
into the same stream at the point it is yielded — nothing is buffered into a
|
|
217
|
+
string and spliced back in. This works whether or not the response is streamed
|
|
218
|
+
with `render stream: true`; when it is, a large response still arrives in
|
|
219
|
+
chunks.
|
|
220
|
+
|
|
221
|
+
`json.yield!` goes anywhere a value goes, including inside an array:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
json.array! do
|
|
225
|
+
json.child! { json.object! { json.first true } }
|
|
226
|
+
json.child! { json.yield! }
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
A layout that only wraps is just the one call:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
json.yield!
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
A layout may yield more than once, and one that never yields renders without the
|
|
237
|
+
template — both as an ERB layout does. The difference is that ERB replays a
|
|
238
|
+
buffered string, whereas each `json.yield!` renders the template again, so
|
|
239
|
+
anything it does happens again too:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
json.array! do
|
|
243
|
+
json.child! { json.yield! }
|
|
244
|
+
json.child! { json.yield! } # the template is rendered a second time
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Why `json.yield!` and not `yield`
|
|
249
|
+
|
|
250
|
+
`yield` is a Ruby keyword that returns a value, and the template's JSON is never
|
|
251
|
+
a value here — it is written into the stream at the position the layout has
|
|
252
|
+
reached. Placing it therefore has to go through the builder, and `json.yield!`
|
|
253
|
+
matches the rest of the DSL, where the methods that write something end in `!`:
|
|
254
|
+
`object!`, `array!`, `child!`, `partial!`, `merge!`, `cache!`. A bare `yield` in
|
|
255
|
+
a `.json.streamer` layout raises `LocalJumpError` naming `json.yield!`.
|
|
256
|
+
|
|
257
|
+
#### Layouts are not ERB layouts
|
|
258
|
+
|
|
259
|
+
Two differences worth knowing:
|
|
260
|
+
|
|
261
|
+
* **The layout renders first**, and `json.yield!` renders the template it wraps.
|
|
262
|
+
An ERB layout is the other way around: the template is rendered up front and
|
|
263
|
+
the layout concatenates the resulting string. Reversing it is what lets the
|
|
264
|
+
template write into the layout's builder instead of being encoded to a string
|
|
265
|
+
and spliced back in — which neither encoder can do into a keyed slot.
|
|
266
|
+
* **There is one yield, and it has no name.** `content_for` / `provide` have no
|
|
267
|
+
analogue, and a layout cannot ask for content the template defines later. In
|
|
268
|
+
ERB that works because the layout runs in a Fiber and suspends until the
|
|
269
|
+
template provides the key; here the layout calls the template directly, so
|
|
270
|
+
there is nothing to suspend. A JSON document's shape is positional, so one
|
|
271
|
+
yield in one place is generally what you want.
|
|
272
|
+
|
|
194
273
|
You can explicitly make TurboStreamer object return null if you want:
|
|
195
274
|
|
|
196
275
|
``` ruby
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Hands each chunk to a block as it is encoded rather than accumulating it,
|
|
4
|
+
# so StreamingTemplateRenderer can push straight to the client instead of
|
|
5
|
+
# buffering the whole response.
|
|
6
|
+
#
|
|
7
|
+
# Like TurboBuffer, it subclasses its ActionView counterpart and takes over
|
|
8
|
+
# the block that one was writing to, so the inherited methods stay consistent
|
|
9
|
+
# with what has been written.
|
|
10
|
+
class ActionView::StreamingTurboBuffer < ActionView::StreamingBuffer
|
|
11
|
+
|
|
12
|
+
def initialize(block)
|
|
13
|
+
@block = block
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The encoders -- and the C extensions behind them, Oj::StreamWriter and
|
|
17
|
+
# Wankel::StreamEncoder -- write their output with the IO-style `write`,
|
|
18
|
+
# which ActionView's buffers do not have.
|
|
19
|
+
def write(value)
|
|
20
|
+
string = value.to_s
|
|
21
|
+
@block.call(string)
|
|
22
|
+
string.bytesize
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Prepended to StreamingTemplateRenderer by the railtie.
|
|
4
|
+
#
|
|
5
|
+
# ActionView's own streaming renders the *layout* and buffers the template
|
|
6
|
+
# into a string for it, which would defeat the point here -- the whole JSON
|
|
7
|
+
# document would be built in memory before a byte was written. So streamer
|
|
8
|
+
# templates take their own path, writing to the client as the encoder
|
|
9
|
+
# produces bytes.
|
|
10
|
+
module ActionView::StreamingTurboTemplateRenderer
|
|
11
|
+
|
|
12
|
+
def render_template(view, template, layout_name = nil, locals = {})
|
|
13
|
+
return super unless template.handler == TurboStreamer::Handler
|
|
14
|
+
|
|
15
|
+
locals ||= {}
|
|
16
|
+
layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
|
|
17
|
+
log_skipped_layout(layout_name) if layout_name && layout.nil?
|
|
18
|
+
|
|
19
|
+
ActionView::StreamingTemplateRenderer::Body.new do |buffer|
|
|
20
|
+
delayed_render_json(buffer, template, layout, view, locals)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def delayed_render_json(buffer, template, layout, view, locals)
|
|
27
|
+
output = ActionView::StreamingTurboBuffer.new(buffer)
|
|
28
|
+
|
|
29
|
+
ActiveSupport::Notifications.instrument(
|
|
30
|
+
"render_template.action_view",
|
|
31
|
+
identifier: template.identifier,
|
|
32
|
+
layout: layout && layout.virtual_path,
|
|
33
|
+
locals: locals
|
|
34
|
+
) do
|
|
35
|
+
if layout
|
|
36
|
+
render_json_layout(view, layout, locals, output) do |json|
|
|
37
|
+
template.render(view, locals.merge(json: json), output)
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
template.render(view, locals, output)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The encoders -- and the C extensions behind them, Oj::StreamWriter and
|
|
4
|
+
# Wankel::StreamEncoder -- write their output with the IO-style `write`.
|
|
5
|
+
# ActionView's buffers only expose `<<` / `concat` / `safe_concat`, so they
|
|
6
|
+
# have to be adapted before an encoder can stream into one.
|
|
7
|
+
#
|
|
8
|
+
# Subclassing OutputBuffer means this *is* one: Template#render finishes with
|
|
9
|
+
# `result.is_a?(OutputBuffer) ? result.to_s : result`, so target! can hand it
|
|
10
|
+
# back directly instead of unwrapping. It takes over the String the given
|
|
11
|
+
# buffer was writing to rather than keeping one of its own, so the inherited
|
|
12
|
+
# methods all report on the same content and there is nothing to delegate.
|
|
13
|
+
#
|
|
14
|
+
# See StreamingTurboBuffer for the streaming counterpart.
|
|
15
|
+
class ActionView::TurboBuffer < ActionView::OutputBuffer
|
|
16
|
+
|
|
17
|
+
# instance_of? rather than is_a?, since this class is itself an
|
|
18
|
+
# OutputBuffer and must not be wrapped a second time.
|
|
19
|
+
def self.wrap(buffer)
|
|
20
|
+
if buffer.instance_of?(ActionView::OutputBuffer)
|
|
21
|
+
ActionView::TurboBuffer::new(buffer.raw_buffer)
|
|
22
|
+
elsif buffer.instance_of?(ActionView::StreamingBuffer)
|
|
23
|
+
ActionView::StreamingTurboBuffer.new(buffer.block)
|
|
24
|
+
elsif buffer.respond_to?(:write)
|
|
25
|
+
buffer
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, "#{buffer.class} has no #write, so an encoder cannot stream into it"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(raw_buffer)
|
|
32
|
+
@raw_buffer = raw_buffer
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Whatever reaches here is already encoded JSON, so it is appended verbatim
|
|
36
|
+
# instead of being HTML-escaped -- the same thing the old alias did by
|
|
37
|
+
# pointing `write` at `safe_concat`.
|
|
38
|
+
def write(value)
|
|
39
|
+
string = value.to_s
|
|
40
|
+
@raw_buffer << string
|
|
41
|
+
string.bytesize
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Prepended to TemplateRenderer by the railtie.
|
|
4
|
+
module ActionView::TurboTemplateRenderer
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# ActionView renders the template into a string first and then hands that
|
|
9
|
+
# string to the layout. That works for ERB, where the layout concatenates
|
|
10
|
+
# text.
|
|
11
|
+
#
|
|
12
|
+
# For streamer templates the order is reversed. The layout renders first
|
|
13
|
+
# and its `yield` renders the template into the layout's own
|
|
14
|
+
# builder, which is what the streaming renderer does too.
|
|
15
|
+
def render_template(view, template, layout_name, locals)
|
|
16
|
+
return super unless template.handler == TurboStreamer::Handler
|
|
17
|
+
|
|
18
|
+
# find_layout returns nil when the layout exists in another format but
|
|
19
|
+
# not this one -- an app with layouts/application.html.erb and no JSON
|
|
20
|
+
# layout. It only raises MissingTemplate when there is no layout by
|
|
21
|
+
# that name in any format.
|
|
22
|
+
layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
|
|
23
|
+
if layout.nil?
|
|
24
|
+
log_skipped_layout(layout_name) if layout_name
|
|
25
|
+
return super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
body = ActiveSupport::Notifications.instrument(
|
|
29
|
+
"render_layout.action_view",
|
|
30
|
+
identifier: layout.identifier
|
|
31
|
+
) do
|
|
32
|
+
render_json_layout(view, layout, locals) do |json|
|
|
33
|
+
ActiveSupport::Notifications.instrument(
|
|
34
|
+
"render_template.action_view",
|
|
35
|
+
identifier: template.identifier,
|
|
36
|
+
layout: layout.virtual_path,
|
|
37
|
+
locals: locals
|
|
38
|
+
) do
|
|
39
|
+
template.render(view, locals.merge(json: json))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# render_json_layout hands back its buffer; the caller wants a String.
|
|
45
|
+
build_rendered_template(body.to_s, template)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Renders a layout with a builder of our own, so `json.yield!` has something
|
|
49
|
+
# to place. Layout and template write to one encoder, so the yielded JSON
|
|
50
|
+
# lands where it is placed with its commas and nesting intact.
|
|
51
|
+
#
|
|
52
|
+
# `render_inner` renders the template the layout wraps, taking the builder
|
|
53
|
+
# to render into; the buffer, when given, is the streaming one both renders
|
|
54
|
+
# share. Shared with StreamingTurboTemplateRenderer, which reaches it
|
|
55
|
+
# through StreamingTemplateRenderer's superclass.
|
|
56
|
+
#
|
|
57
|
+
# The block exists only to give a bare `yield` a useful failure. Without one
|
|
58
|
+
# it would be `no block given (yield)`, which says nothing about layouts.
|
|
59
|
+
def render_json_layout(view, layout, locals, buffer = nil, &render_inner)
|
|
60
|
+
# ActionView::TurboBuffer in full: the compact `module
|
|
61
|
+
# ActionView::TurboTemplateRenderer` above does not nest ActionView
|
|
62
|
+
# lexically, so a bare TurboBuffer would not resolve.
|
|
63
|
+
json = TurboStreamer::Template.new(view, output_buffer: buffer || ActionView::TurboBuffer.new(String.new))
|
|
64
|
+
json.yield_content = render_inner
|
|
65
|
+
|
|
66
|
+
layout.render(view, locals.merge(json: json)) do |*|
|
|
67
|
+
raise ::LocalJumpError, '`yield` is not supported in a .json.streamer layout, use `json.yield!`'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
json.target!
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def log_skipped_layout(layout_name)
|
|
74
|
+
logger = ActionView::Base.logger
|
|
75
|
+
return unless logger
|
|
76
|
+
|
|
77
|
+
logger.debug do
|
|
78
|
+
" Skipped layout #{layout_name} -- it does not exist for " \
|
|
79
|
+
"#{formats.first.inspect}; rendering without a layout"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end
|
|
@@ -1,59 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# TODO: maybe remove reliance on ERBTracker ala: https://github.com/rails/jbuilder/commit/710979958aa012f4ff21800abf5bd50b717df0a6
|
|
4
|
+
require 'action_view'
|
|
5
|
+
require 'action_view/dependency_tracker'
|
|
6
|
+
|
|
7
|
+
class TurboStreamer
|
|
8
|
+
module DependencyTrackerMethods
|
|
9
|
+
# Matches:
|
|
10
|
+
# json.partial! "messages/message"
|
|
11
|
+
# json.partial!('messages/message')
|
|
12
|
+
#
|
|
13
|
+
DIRECT_RENDERS = /
|
|
14
|
+
\w+\.partial! # json.partial!
|
|
15
|
+
\(?\s* # optional parenthesis
|
|
16
|
+
(['"])([^'"]+)\1 # quoted value
|
|
17
|
+
/x
|
|
18
|
+
|
|
19
|
+
# Matches:
|
|
20
|
+
# json.partial! partial: "comments/comment"
|
|
21
|
+
# json.comments @post.comments, partial: "comments/comment", as: :comment
|
|
22
|
+
# json.array! @posts, partial: "posts/post", as: :post
|
|
23
|
+
# = render partial: "account"
|
|
24
|
+
#
|
|
25
|
+
INDIRECT_RENDERS = /
|
|
26
|
+
(?::partial\s*=>|partial:) # partial: or :partial =>
|
|
27
|
+
\s* # optional whitespace
|
|
28
|
+
(['"])([^'"]+)\1 # quoted value
|
|
29
|
+
/x
|
|
30
|
+
|
|
31
|
+
def dependencies
|
|
32
|
+
direct_dependencies + indirect_dependencies + explicit_dependencies
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
14
36
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
module DependencyTrackerMethods
|
|
18
|
-
# Matches:
|
|
19
|
-
# json.partial! "messages/message"
|
|
20
|
-
# json.partial!('messages/message')
|
|
21
|
-
#
|
|
22
|
-
DIRECT_RENDERS = /
|
|
23
|
-
\w+\.partial! # json.partial!
|
|
24
|
-
\(?\s* # optional parenthesis
|
|
25
|
-
(['"])([^'"]+)\1 # quoted value
|
|
26
|
-
/x
|
|
27
|
-
|
|
28
|
-
# Matches:
|
|
29
|
-
# json.partial! partial: "comments/comment"
|
|
30
|
-
# json.comments @post.comments, partial: "comments/comment", as: :comment
|
|
31
|
-
# json.array! @posts, partial: "posts/post", as: :post
|
|
32
|
-
# = render partial: "account"
|
|
33
|
-
#
|
|
34
|
-
INDIRECT_RENDERS = /
|
|
35
|
-
(?::partial\s*=>|partial:) # partial: or :partial =>
|
|
36
|
-
\s* # optional whitespace
|
|
37
|
-
(['"])([^'"]+)\1 # quoted value
|
|
38
|
-
/x
|
|
39
|
-
|
|
40
|
-
def dependencies
|
|
41
|
-
direct_dependencies + indirect_dependencies + explicit_dependencies
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
def direct_dependencies
|
|
47
|
-
source.scan(DIRECT_RENDERS).map(&:second)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def indirect_dependencies
|
|
51
|
-
source.scan(INDIRECT_RENDERS).map(&:second)
|
|
52
|
-
end
|
|
37
|
+
def direct_dependencies
|
|
38
|
+
source.scan(DIRECT_RENDERS).map(&:second)
|
|
53
39
|
end
|
|
54
|
-
end
|
|
55
40
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
41
|
+
def indirect_dependencies
|
|
42
|
+
source.scan(INDIRECT_RENDERS).map(&:second)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
59
45
|
end
|
|
46
|
+
|
|
47
|
+
::TurboStreamer::DependencyTracker = Class.new(::ActionView::DependencyTracker::ERBTracker)
|
|
48
|
+
::TurboStreamer::DependencyTracker.send :include, ::TurboStreamer::DependencyTrackerMethods
|
|
49
|
+
::ActionView::DependencyTracker.register_tracker :streamer, ::TurboStreamer::DependencyTracker
|
data/lib/turbostreamer/errors.rb
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
class TurboStreamer
|
|
2
4
|
module Errors
|
|
3
5
|
class MergeError < ::StandardError
|
|
4
6
|
def self.build(updates)
|
|
5
|
-
|
|
6
|
-
new(message)
|
|
7
|
+
new("Can't merge #{updates.inspect} which isn't Hash or Array")
|
|
7
8
|
end
|
|
8
9
|
end
|
|
9
10
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "turbostreamer"
|
|
2
4
|
require "active_support/core_ext"
|
|
3
5
|
|
|
@@ -15,8 +17,8 @@ class TurboStreamer
|
|
|
15
17
|
|
|
16
18
|
def self.call(template, source)
|
|
17
19
|
# this juggling is required to keep line numbers right in the error
|
|
18
|
-
%{
|
|
19
|
-
json.target! unless
|
|
20
|
+
%{__parent_json = local_assigns[:json]; json = __parent_json || TurboStreamer::Template.new(self, output_buffer: ActionView::TurboBuffer.wrap(output_buffer)); #{source}
|
|
21
|
+
json.target! unless __parent_json}
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
end
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rails. 8.1 requires this not sure why we have to load it and the railtie doesn't
|
|
4
|
+
require "active_support/core_ext/module/delegation"
|
|
5
|
+
|
|
1
6
|
require 'rails/railtie'
|
|
2
7
|
|
|
3
8
|
class TurboStreamer
|
|
@@ -6,18 +11,34 @@ class TurboStreamer
|
|
|
6
11
|
ActiveSupport.on_load :action_view do
|
|
7
12
|
# Require turbostreamer in here so it's only loaded if needed
|
|
8
13
|
require 'turbostreamer'
|
|
9
|
-
require
|
|
10
|
-
require
|
|
14
|
+
require 'action_view/turbo_buffer'
|
|
15
|
+
require 'action_view/streaming_turbo_buffer'
|
|
16
|
+
require 'action_view/turbo_template_renderer'
|
|
17
|
+
require 'action_view/streaming_turbo_template_renderer'
|
|
18
|
+
|
|
19
|
+
ActionView::TemplateRenderer.prepend(ActionView::TurboTemplateRenderer)
|
|
20
|
+
ActionView::StreamingTemplateRenderer.prepend(ActionView::StreamingTurboTemplateRenderer)
|
|
11
21
|
|
|
12
22
|
# Register Turbostreamer with Rails
|
|
13
23
|
ActionView::Template.register_template_handler :streamer, TurboStreamer::Handler
|
|
14
24
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
# Resolve the encoder once, here, the way an app resolves its cache
|
|
26
|
+
# store at boot. Left unset, default_encoder_for falls through to
|
|
27
|
+
# get_encoder on every render, and the `require` in there re-scans
|
|
28
|
+
# $LOAD_PATH each time.
|
|
29
|
+
encoder = TurboStreamer.default_encoder_for(:json)
|
|
30
|
+
TurboStreamer.set_default_encoder(:json, encoder)
|
|
31
|
+
|
|
32
|
+
# Oj's :rails mode is what escapes HTML entities the way
|
|
33
|
+
# ActiveSupport::JSON does, so a payload embedded in a <script> tag is
|
|
34
|
+
# safe. Merge rather than skip when options are already set: an app
|
|
35
|
+
# setting an unrelated one -- buffer_size, say -- would otherwise lose
|
|
36
|
+
# the mode along with the escaping, silently. Anything the app set
|
|
37
|
+
# explicitly still wins.
|
|
38
|
+
if encoder.name == 'TurboStreamer::OjEncoder'
|
|
39
|
+
TurboStreamer.set_default_encoder_options(
|
|
40
|
+
:oj, { mode: :rails }.merge(TurboStreamer.default_encoder_options(:oj))
|
|
41
|
+
)
|
|
21
42
|
end
|
|
22
43
|
|
|
23
44
|
require 'turbostreamer/dependency_tracker'
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'turbostreamer'
|
|
2
4
|
|
|
3
5
|
class TurboStreamer::Template < TurboStreamer
|
|
@@ -12,6 +14,10 @@ class TurboStreamer::Template < TurboStreamer
|
|
|
12
14
|
@context = context
|
|
13
15
|
super(*args, &block)
|
|
14
16
|
end
|
|
17
|
+
|
|
18
|
+
# The proc that renders the template this layout wraps, for json.yield! to
|
|
19
|
+
# place.
|
|
20
|
+
attr_accessor :yield_content
|
|
15
21
|
|
|
16
22
|
def partial!(name_or_options, locals = {})
|
|
17
23
|
if name_or_options.class.respond_to?(:model_name) && name_or_options.respond_to?(:to_partial_path)
|
|
@@ -35,6 +41,29 @@ class TurboStreamer::Template < TurboStreamer
|
|
|
35
41
|
end
|
|
36
42
|
end
|
|
37
43
|
|
|
44
|
+
# The same thing as a statement rather than a value, for a layout that would
|
|
45
|
+
# rather write the key itself:
|
|
46
|
+
#
|
|
47
|
+
# json.key! :data
|
|
48
|
+
# json.yield!
|
|
49
|
+
def yield!
|
|
50
|
+
content = @yield_content
|
|
51
|
+
# The same thing Ruby says for a `yield` with no block behind it, because
|
|
52
|
+
# that is what this is.
|
|
53
|
+
raise ::LocalJumpError, 'no block given (yield)' if content.nil?
|
|
54
|
+
|
|
55
|
+
# The template renders through this same builder, so it would otherwise
|
|
56
|
+
# still see the content and yield straight back into itself. Clear it while
|
|
57
|
+
# it renders -- a template has nothing to yield -- and put it back, so a
|
|
58
|
+
# layout can go on to yield again.
|
|
59
|
+
begin
|
|
60
|
+
@yield_content = nil
|
|
61
|
+
content.call(self)
|
|
62
|
+
ensure
|
|
63
|
+
@yield_content = content
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
38
67
|
def array!(collection = BLANK, *attributes, &block)
|
|
39
68
|
options = attributes.extract_options!
|
|
40
69
|
|
|
@@ -201,9 +230,6 @@ class TurboStreamer::Template < TurboStreamer
|
|
|
201
230
|
|
|
202
231
|
if @context.respond_to?(:combined_fragment_cache_key)
|
|
203
232
|
key = @context.combined_fragment_cache_key(key)
|
|
204
|
-
elsif @context.respond_to?(:fragment_cache_key)
|
|
205
|
-
# TODO: remove after droping rails 5.1 support
|
|
206
|
-
key = @context.fragment_cache_key(key)
|
|
207
233
|
elsif ::Hash === key
|
|
208
234
|
key = url_for(key).split('://', 2).last
|
|
209
235
|
end
|
|
@@ -224,17 +250,21 @@ class TurboStreamer::Template < TurboStreamer
|
|
|
224
250
|
end
|
|
225
251
|
end
|
|
226
252
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
253
|
+
# The base rule, plus the case only Rails has: a nil collection rendered
|
|
254
|
+
# through a partial -- `json.comments nil, partial: 'comment/comment', as:
|
|
255
|
+
# :comment` -- has to reach array! to come out as [] rather than being
|
|
256
|
+
# treated as a single object to extract from.
|
|
257
|
+
#
|
|
258
|
+
# Written out rather than calling super, because child! runs this on every
|
|
259
|
+
# element and the second dispatch showed up. It stays here rather than moving
|
|
260
|
+
# into TurboStreamer because partial! is a Template method: in a plain
|
|
261
|
+
# builder the `:as` clause has nothing to route to, and would only turn
|
|
262
|
+
# `json.foo nil, as: :x` from a TypeError into [].
|
|
235
263
|
def _eachable_arguments?(value, *args)
|
|
236
|
-
return true if
|
|
264
|
+
return true if value.respond_to?(:each) && !value.is_a?(Hash)
|
|
265
|
+
|
|
237
266
|
options = args.last
|
|
238
267
|
::Hash === options && options.key?(:as)
|
|
239
268
|
end
|
|
269
|
+
|
|
240
270
|
end
|
data/lib/turbostreamer.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'stringio'
|
|
2
4
|
|
|
3
5
|
class TurboStreamer
|
|
@@ -6,7 +8,7 @@ class TurboStreamer
|
|
|
6
8
|
autoload :Template, 'turbostreamer/template'
|
|
7
9
|
autoload :KeyFormatter, 'turbostreamer/key_formatter'
|
|
8
10
|
autoload :Errors, 'turbostreamer/errors'
|
|
9
|
-
|
|
11
|
+
|
|
10
12
|
BLANK = ::Object.new
|
|
11
13
|
|
|
12
14
|
ENCODERS = {
|
|
@@ -32,7 +34,7 @@ class TurboStreamer
|
|
|
32
34
|
@encoder_options = @@encoder_options[options[:encoder]]
|
|
33
35
|
elsif options[:encoder].nil?
|
|
34
36
|
@encoder = TurboStreamer.default_encoder_for(options[:mime] || :json)
|
|
35
|
-
if encoder_symbol =
|
|
37
|
+
if encoder_symbol = TurboStreamer.encoder_symbol_for(options[:mime] || :json, @encoder)
|
|
36
38
|
@encoder_options = @@encoder_options[encoder_symbol]
|
|
37
39
|
else
|
|
38
40
|
@encoder_options = {}
|
|
@@ -97,11 +99,7 @@ class TurboStreamer
|
|
|
97
99
|
#
|
|
98
100
|
# { "name": David", "age": 32 }, { "name": Jamie", "age": 31 }
|
|
99
101
|
def extract!(object, *attributes)
|
|
100
|
-
|
|
101
|
-
attributes.each{ |key| _set_value key, object.fetch(key) }
|
|
102
|
-
else
|
|
103
|
-
attributes.each{ |key| _set_value key, object.public_send(key) }
|
|
104
|
-
end
|
|
102
|
+
_extract(object, attributes)
|
|
105
103
|
end
|
|
106
104
|
|
|
107
105
|
# Turns the current element into an array and iterates over the passed
|
|
@@ -167,7 +165,7 @@ class TurboStreamer
|
|
|
167
165
|
else
|
|
168
166
|
# json.author @post.creator, :name, :email_address
|
|
169
167
|
# { "author": { "name": "David", "email_address": "david@thinking.com" } }
|
|
170
|
-
object!{
|
|
168
|
+
object!{ _extract(value, args) }
|
|
171
169
|
end
|
|
172
170
|
end
|
|
173
171
|
|
|
@@ -231,6 +229,15 @@ class TurboStreamer
|
|
|
231
229
|
@@key_formatter = formatter
|
|
232
230
|
end
|
|
233
231
|
|
|
232
|
+
# The symbol an encoder is known by, given either the symbol itself or the
|
|
233
|
+
# class. Options are always keyed by symbol, since that is what a builder has
|
|
234
|
+
# to look them up with.
|
|
235
|
+
def self.encoder_symbol_for(mime, encoder)
|
|
236
|
+
return encoder if encoder.is_a?(Symbol)
|
|
237
|
+
|
|
238
|
+
ENCODERS[mime]&.key(encoder.name.delete_prefix('TurboStreamer::').delete_suffix('Encoder'))
|
|
239
|
+
end
|
|
240
|
+
|
|
234
241
|
def self.set_default_encoder(mime, encoder, default_options=nil)
|
|
235
242
|
@@default_encoders[mime] = if encoder.is_a?(Symbol)
|
|
236
243
|
get_encoder(mime, encoder)
|
|
@@ -238,20 +245,41 @@ class TurboStreamer
|
|
|
238
245
|
encoder
|
|
239
246
|
end
|
|
240
247
|
|
|
241
|
-
|
|
248
|
+
# Keyed by symbol even when handed a class -- keying by the class stored
|
|
249
|
+
# options a builder would never find, dropping them silently.
|
|
250
|
+
if default_options
|
|
251
|
+
@@encoder_options[encoder_symbol_for(mime, encoder) || encoder] = default_options
|
|
252
|
+
end
|
|
242
253
|
end
|
|
243
|
-
|
|
254
|
+
|
|
244
255
|
def self.set_default_encoder_options(encoder, options)
|
|
245
256
|
@@encoder_options[encoder] = options
|
|
246
257
|
end
|
|
247
|
-
|
|
258
|
+
|
|
259
|
+
# Does not use [] -- @@encoder_options defaults new keys to {} *and assigns
|
|
260
|
+
# them*, so reading through it would make has_default_encoder_options? true.
|
|
261
|
+
def self.default_encoder_options(encoder)
|
|
262
|
+
@@encoder_options.fetch(encoder, {})
|
|
263
|
+
end
|
|
264
|
+
|
|
248
265
|
def self.has_default_encoder_options?(encoder)
|
|
249
266
|
@@encoder_options.has_key?(encoder)
|
|
250
267
|
end
|
|
251
268
|
|
|
269
|
+
# Memoized because `require` with a relative feature name re-scans $LOAD_PATH
|
|
270
|
+
# on every call, stat-ing each entry, even once the file is loaded. This runs
|
|
271
|
+
# per render whenever no default encoder has been set -- which is the case in
|
|
272
|
+
# a Rails app, since the railtie only sets encoder options -- and the stat
|
|
273
|
+
# storm dwarfed the encoding itself.
|
|
274
|
+
#
|
|
275
|
+
# A [mime, key] pair always names the same class, so caching it does not
|
|
276
|
+
# affect set_default_encoder: default_encoder_for still reads @@default_encoders.
|
|
252
277
|
def self.get_encoder(mime, key)
|
|
253
|
-
|
|
254
|
-
|
|
278
|
+
@encoders ||= {}
|
|
279
|
+
@encoders[[mime, key]] ||= begin
|
|
280
|
+
require "turbostreamer/encoders/#{key}"
|
|
281
|
+
Object.const_get("TurboStreamer::#{ENCODERS[mime][key]}Encoder")
|
|
282
|
+
end
|
|
255
283
|
end
|
|
256
284
|
|
|
257
285
|
def self.default_encoder_for(mime)
|
|
@@ -325,7 +353,7 @@ class TurboStreamer
|
|
|
325
353
|
elsif _eachable_arguments?(value, *args)
|
|
326
354
|
_scope{ array!(value, *args) }
|
|
327
355
|
else
|
|
328
|
-
object!{
|
|
356
|
+
object!{ _extract(value, args) }
|
|
329
357
|
end
|
|
330
358
|
|
|
331
359
|
end
|
|
@@ -334,15 +362,25 @@ class TurboStreamer
|
|
|
334
362
|
def target!
|
|
335
363
|
@encoder.flush
|
|
336
364
|
|
|
337
|
-
|
|
338
|
-
|
|
365
|
+
output = @encoder.output
|
|
366
|
+
|
|
367
|
+
if output.is_a?(::StringIO)
|
|
368
|
+
output.string
|
|
339
369
|
else
|
|
340
|
-
|
|
370
|
+
output
|
|
341
371
|
end
|
|
342
372
|
end
|
|
343
373
|
|
|
344
374
|
private
|
|
345
375
|
|
|
376
|
+
def _extract(object, attributes)
|
|
377
|
+
if ::Hash === object
|
|
378
|
+
attributes.each{ |key| _set_value key, object.fetch(key) }
|
|
379
|
+
else
|
|
380
|
+
attributes.each{ |key| _set_value key, object.public_send(key) }
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
346
384
|
def _write(key, value)
|
|
347
385
|
@encoder.key(_key(key))
|
|
348
386
|
@encoder.value(value)
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: turbostreamer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jon Bracy
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activesupport
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
18
|
+
version: 8.0.0
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
25
|
+
version: 8.0.0
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: rake
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -80,20 +79,6 @@ dependencies:
|
|
|
80
79
|
- - ">="
|
|
81
80
|
- !ruby/object:Gem::Version
|
|
82
81
|
version: '0'
|
|
83
|
-
- !ruby/object:Gem::Dependency
|
|
84
|
-
name: bundler
|
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
|
86
|
-
requirements:
|
|
87
|
-
- - ">="
|
|
88
|
-
- !ruby/object:Gem::Version
|
|
89
|
-
version: '0'
|
|
90
|
-
type: :development
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - ">="
|
|
95
|
-
- !ruby/object:Gem::Version
|
|
96
|
-
version: '0'
|
|
97
82
|
- !ruby/object:Gem::Dependency
|
|
98
83
|
name: mocha
|
|
99
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -123,7 +108,7 @@ dependencies:
|
|
|
123
108
|
- !ruby/object:Gem::Version
|
|
124
109
|
version: '0'
|
|
125
110
|
- !ruby/object:Gem::Dependency
|
|
126
|
-
name:
|
|
111
|
+
name: debug
|
|
127
112
|
requirement: !ruby/object:Gem::Requirement
|
|
128
113
|
requirements:
|
|
129
114
|
- - ">="
|
|
@@ -164,62 +149,6 @@ dependencies:
|
|
|
164
149
|
- - ">="
|
|
165
150
|
- !ruby/object:Gem::Version
|
|
166
151
|
version: '0'
|
|
167
|
-
- !ruby/object:Gem::Dependency
|
|
168
|
-
name: analyzer
|
|
169
|
-
requirement: !ruby/object:Gem::Requirement
|
|
170
|
-
requirements:
|
|
171
|
-
- - ">="
|
|
172
|
-
- !ruby/object:Gem::Version
|
|
173
|
-
version: '0'
|
|
174
|
-
type: :development
|
|
175
|
-
prerelease: false
|
|
176
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
177
|
-
requirements:
|
|
178
|
-
- - ">="
|
|
179
|
-
- !ruby/object:Gem::Version
|
|
180
|
-
version: '0'
|
|
181
|
-
- !ruby/object:Gem::Dependency
|
|
182
|
-
name: jbuilder
|
|
183
|
-
requirement: !ruby/object:Gem::Requirement
|
|
184
|
-
requirements:
|
|
185
|
-
- - ">="
|
|
186
|
-
- !ruby/object:Gem::Version
|
|
187
|
-
version: '0'
|
|
188
|
-
type: :development
|
|
189
|
-
prerelease: false
|
|
190
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
191
|
-
requirements:
|
|
192
|
-
- - ">="
|
|
193
|
-
- !ruby/object:Gem::Version
|
|
194
|
-
version: '0'
|
|
195
|
-
- !ruby/object:Gem::Dependency
|
|
196
|
-
name: rabl
|
|
197
|
-
requirement: !ruby/object:Gem::Requirement
|
|
198
|
-
requirements:
|
|
199
|
-
- - ">="
|
|
200
|
-
- !ruby/object:Gem::Version
|
|
201
|
-
version: '0'
|
|
202
|
-
type: :development
|
|
203
|
-
prerelease: false
|
|
204
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
205
|
-
requirements:
|
|
206
|
-
- - ">="
|
|
207
|
-
- !ruby/object:Gem::Version
|
|
208
|
-
version: '0'
|
|
209
|
-
- !ruby/object:Gem::Dependency
|
|
210
|
-
name: appraisal
|
|
211
|
-
requirement: !ruby/object:Gem::Requirement
|
|
212
|
-
requirements:
|
|
213
|
-
- - "~>"
|
|
214
|
-
- !ruby/object:Gem::Version
|
|
215
|
-
version: '2.0'
|
|
216
|
-
type: :development
|
|
217
|
-
prerelease: false
|
|
218
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
219
|
-
requirements:
|
|
220
|
-
- - "~>"
|
|
221
|
-
- !ruby/object:Gem::Version
|
|
222
|
-
version: '2.0'
|
|
223
152
|
- !ruby/object:Gem::Dependency
|
|
224
153
|
name: railties
|
|
225
154
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -234,23 +163,9 @@ dependencies:
|
|
|
234
163
|
- - ">="
|
|
235
164
|
- !ruby/object:Gem::Version
|
|
236
165
|
version: '0'
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
requirements:
|
|
241
|
-
- - ">="
|
|
242
|
-
- !ruby/object:Gem::Version
|
|
243
|
-
version: '0'
|
|
244
|
-
type: :development
|
|
245
|
-
prerelease: false
|
|
246
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
247
|
-
requirements:
|
|
248
|
-
- - ">="
|
|
249
|
-
- !ruby/object:Gem::Version
|
|
250
|
-
version: '0'
|
|
251
|
-
description: |2
|
|
252
|
-
TurboStreamer is a JBuilder-like DSL for building JSON that streams directly
|
|
253
|
-
to a string or IO
|
|
166
|
+
description: |
|
|
167
|
+
TurboStreamer is a JBuilder-like DSL for building
|
|
168
|
+
JSON that streams directly to a string or IO
|
|
254
169
|
email:
|
|
255
170
|
- jonbracy@gmail.com
|
|
256
171
|
executables: []
|
|
@@ -258,9 +173,13 @@ extensions: []
|
|
|
258
173
|
extra_rdoc_files:
|
|
259
174
|
- README.md
|
|
260
175
|
files:
|
|
176
|
+
- CHANGELOG.md
|
|
177
|
+
- LICENSE
|
|
261
178
|
- README.md
|
|
262
|
-
-
|
|
263
|
-
-
|
|
179
|
+
- lib/action_view/streaming_turbo_buffer.rb
|
|
180
|
+
- lib/action_view/streaming_turbo_template_renderer.rb
|
|
181
|
+
- lib/action_view/turbo_buffer.rb
|
|
182
|
+
- lib/action_view/turbo_template_renderer.rb
|
|
264
183
|
- lib/turbostreamer.rb
|
|
265
184
|
- lib/turbostreamer/dependency_tracker.rb
|
|
266
185
|
- lib/turbostreamer/encoders/oj.rb
|
|
@@ -274,8 +193,11 @@ files:
|
|
|
274
193
|
homepage: https://github.com/malomalo/turbostreamer
|
|
275
194
|
licenses:
|
|
276
195
|
- MIT
|
|
277
|
-
metadata:
|
|
278
|
-
|
|
196
|
+
metadata:
|
|
197
|
+
source_code_uri: https://github.com/malomalo/turbostreamer
|
|
198
|
+
bug_tracker_uri: https://github.com/malomalo/turbostreamer/issues
|
|
199
|
+
changelog_uri: https://github.com/malomalo/turbostreamer/blob/master/CHANGELOG.md
|
|
200
|
+
rubygems_mfa_required: 'true'
|
|
279
201
|
rdoc_options:
|
|
280
202
|
- "--main"
|
|
281
203
|
- README.md
|
|
@@ -285,15 +207,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
285
207
|
requirements:
|
|
286
208
|
- - ">="
|
|
287
209
|
- !ruby/object:Gem::Version
|
|
288
|
-
version:
|
|
210
|
+
version: 3.3.0
|
|
289
211
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
290
212
|
requirements:
|
|
291
213
|
- - ">="
|
|
292
214
|
- !ruby/object:Gem::Version
|
|
293
215
|
version: '0'
|
|
294
216
|
requirements: []
|
|
295
|
-
rubygems_version:
|
|
296
|
-
signing_key:
|
|
217
|
+
rubygems_version: 4.0.11
|
|
297
218
|
specification_version: 4
|
|
298
219
|
summary: Stream JSON via a Builder-style DSL
|
|
299
220
|
test_files: []
|
data/ext/actionview/buffer.rb
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
module ActionView
|
|
2
|
-
class OutputBuffer
|
|
3
|
-
alias :write :safe_concat
|
|
4
|
-
end
|
|
5
|
-
|
|
6
|
-
class StreamingBuffer #:nodoc:
|
|
7
|
-
alias :write :safe_concat
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
class JSONStreamingBuffer #:nodoc:
|
|
11
|
-
def initialize(block)
|
|
12
|
-
@block = block
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def <<(value)
|
|
16
|
-
@block.call(value.to_s)
|
|
17
|
-
end
|
|
18
|
-
alias :write :<<
|
|
19
|
-
alias :concat :<<
|
|
20
|
-
alias :append= :<<
|
|
21
|
-
alias :safe_concat :<<
|
|
22
|
-
alias :safe_append= :<<
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
end
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
module ActionView
|
|
2
|
-
class StreamingTemplateRenderer < TemplateRenderer
|
|
3
|
-
|
|
4
|
-
def render_template(view, template, layout_name = nil, locals = {}) #:nodoc:
|
|
5
|
-
template_supports_streaming = (layout_name && template.supports_streaming?) || template.handler == TurboStreamer::Handler
|
|
6
|
-
return [super.body] unless layout_name && template_supports_streaming
|
|
7
|
-
|
|
8
|
-
locals ||= {}
|
|
9
|
-
layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
|
|
10
|
-
|
|
11
|
-
Body.new do |buffer|
|
|
12
|
-
if template.handler == TurboStreamer::Handler
|
|
13
|
-
delayed_render_json(buffer, template, layout, view, locals)
|
|
14
|
-
else
|
|
15
|
-
delayed_render(buffer, template, layout, view, locals)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def delayed_render_json(buffer, template, layout, view, locals)
|
|
23
|
-
# Wrap the given buffer in the StreamingBuffer and pass it to the
|
|
24
|
-
# underlying template handler. Now, every time something is concatenated
|
|
25
|
-
# to the buffer, it is not appended to an array, but streamed straight
|
|
26
|
-
# to the client.
|
|
27
|
-
output = ActionView::JSONStreamingBuffer.new(buffer)
|
|
28
|
-
yielder = lambda { |*name| view._layout_for(*name) }
|
|
29
|
-
|
|
30
|
-
instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do
|
|
31
|
-
fiber = Fiber.new do
|
|
32
|
-
template.render(view, locals, output, &yielder)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
fiber.resume
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
end
|
|
41
|
-
end
|