turbostreamer 1.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63f1e21caf6c87b095cbd4528226ebbc0489dc7f58ef6de192ed222b1920863e
4
- data.tar.gz: d7e9e7237f6fe49b46c114a39da1b6983db8cdc758a4b0d10cb9ce97d65a7ad4
3
+ metadata.gz: 0304ed518c7b493ea45ca44b3b3165d88a8f8171b7a3492b86c95a0c7b856617
4
+ data.tar.gz: e8b9e0d3335c16375794e0de491aa34ef176e06f2d49754e918f28ea492fe305
5
5
  SHA512:
6
- metadata.gz: 69a4204a0598eb92fae7a08c1385241b0f648998eb5f6762c42bd61c8ded3689078ed1930b06e15383fc5ab744f5e1f8b85b6efd2eda58cbc125831683a8ca30
7
- data.tar.gz: e8002ae1e4369b59071dff8a74f3f57d2e51d63c242c86023e174c6fb00b74b9777defa68e5e1241ace35bbf24e4a92d6dd275870155b18e4438d732e5807896
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
@@ -1,17 +1,23 @@
1
1
  # TurboStreamer
2
2
 
3
- TurboStreamer gives you a simple DSL for generating JSON that beats massaging giant
4
- hash structures. This is particularly helpful when the generation process is
5
- fraught with conditionals and loops.
3
+ [![GitHub Build Status](https://img.shields.io/github/workflow/status/malomalo/turbostreamer/CI?style=flat-square)](https://github.com/malomalo/turbostreamer/actions?query=workflow%3ACI)
6
4
 
5
+ [![Gem Version](http://img.shields.io/gem/v/turbostreamer.svg?style=flat-square)](http://badge.fury.io/rb/turbostreamer)
6
+ [![License](https://img.shields.io/github/license/malomalo/turbostreamer.svg?style=flat-square)](http://badge.fury.io/rb/turbostreamer)
7
+
8
+ TurboStreamer gives you a simple DSL like jBuilder for generating JSON that
9
+ streams directly to a String or IO object.
7
10
 
8
11
  [Jbuilder](https://github.com/rails/jbuilder) builds a Hash as it renders the
9
12
  template and once complete converts the Hash to JSON. TurboStreamer on the other
10
13
  hand writes directly to the output as it is rendering the template. Because of
11
14
  this some of the magic cannot be done and requires a little more verboseness.
12
15
 
13
- Examples
14
- --------
16
+ Because no time is spent creating a hash caching is also fast. No time is spent
17
+ marshaling and unmarshaling from the cache, instead the string is cached and
18
+ directly inserted into the stream skipping any unmarshaling.
19
+
20
+ ## Examples
15
21
 
16
22
  ``` ruby
17
23
  # app/views/message/show.json.streamer
@@ -31,7 +37,7 @@ json.object! do
31
37
  if current_user.admin?
32
38
  json.visitors calculate_visitors(@message)
33
39
  end
34
-
40
+
35
41
  json.tags do
36
42
  json.array! do
37
43
  @message.tags.each { |tag| json.child! tag }
@@ -66,7 +72,7 @@ This will build the following structure:
66
72
  "visitors": 15,
67
73
 
68
74
  "tags": ['public'],
69
-
75
+
70
76
  "comments": [
71
77
  { "content": "Hello everyone!", "created_at": "2011-10-29T20:45:28-05:00" },
72
78
  { "content": "To you my good sir!", "created_at": "2011-10-29T20:47:28-05:00" }
@@ -151,7 +157,7 @@ json.object! do
151
157
  json.url url_for(@message.creator, format: :json)
152
158
  end
153
159
  end
154
-
160
+
155
161
  if current_user.admin?
156
162
  json.visitors calculate_visitors(@message)
157
163
  end
@@ -185,6 +191,85 @@ json.partial! partial: 'posts/post', collection: @posts, as: :post
185
191
  json.comments @post.comments, partial: 'comment/comment', as: :comment
186
192
  ```
187
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
+
188
273
  You can explicitly make TurboStreamer object return null if you want:
189
274
 
190
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,61 +1,49 @@
1
- require 'turbostreamer'
2
-
3
- dependency_tracker = false
4
-
5
- begin
6
- require 'action_view'
7
- require 'action_view/dependency_tracker'
8
- dependency_tracker = ::ActionView::DependencyTracker
9
- rescue LoadError
10
- begin
11
- require 'cache_digests'
12
- dependency_tracker = ::CacheDigests::DependencyTracker
13
- rescue LoadError
14
- end
15
- end
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
16
36
 
17
- if dependency_tracker
18
- class TurboStreamer
19
- module DependencyTrackerMethods
20
- # Matches:
21
- # json.partial! "messages/message"
22
- # json.partial!('messages/message')
23
- #
24
- DIRECT_RENDERS = /
25
- \w+\.partial! # json.partial!
26
- \(?\s* # optional parenthesis
27
- (['"])([^'"]+)\1 # quoted value
28
- /x
29
-
30
- # Matches:
31
- # json.partial! partial: "comments/comment"
32
- # json.comments @post.comments, partial: "comments/comment", as: :comment
33
- # json.array! @posts, partial: "posts/post", as: :post
34
- # = render partial: "account"
35
- #
36
- INDIRECT_RENDERS = /
37
- (?::partial\s*=>|partial:) # partial: or :partial =>
38
- \s* # optional whitespace
39
- (['"])([^'"]+)\1 # quoted value
40
- /x
41
-
42
- def dependencies
43
- direct_dependencies + indirect_dependencies + explicit_dependencies
44
- end
45
-
46
- private
47
-
48
- def direct_dependencies
49
- source.scan(DIRECT_RENDERS).map(&:second)
50
- end
51
-
52
- def indirect_dependencies
53
- source.scan(INDIRECT_RENDERS).map(&:second)
54
- end
37
+ def direct_dependencies
38
+ source.scan(DIRECT_RENDERS).map(&:second)
55
39
  end
56
- end
57
40
 
58
- ::TurboStreamer::DependencyTracker = Class.new(dependency_tracker::ERBTracker)
59
- ::TurboStreamer::DependencyTracker.send :include, ::TurboStreamer::DependencyTrackerMethods
60
- dependency_tracker.register_tracker :streamer, ::TurboStreamer::DependencyTracker
41
+ def indirect_dependencies
42
+ source.scan(INDIRECT_RENDERS).map(&:second)
43
+ end
44
+ end
61
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "oj"
2
4
 
3
5
  class TurboStreamer
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'wankel'
2
4
 
3
5
  class TurboStreamer
@@ -1,11 +1,10 @@
1
-
1
+ # frozen_string_literal: true
2
2
 
3
3
  class TurboStreamer
4
4
  module Errors
5
5
  class MergeError < ::StandardError
6
6
  def self.build(updates)
7
- message = "Can't merge #{updates.inspect} which isn't Hash or Array"
8
- new(message)
7
+ new("Can't merge #{updates.inspect} which isn't Hash or Array")
9
8
  end
10
9
  end
11
10
  end
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "turbostreamer"
2
4
  require "active_support/core_ext"
3
- # This module makes TurboStreamer work with Rails using the template handler API.
4
5
 
6
+ # This module makes TurboStreamer work with Rails using the template handler
7
+ # API.
5
8
  class TurboStreamer
6
9
  class Handler
7
10
 
@@ -12,14 +15,10 @@ class TurboStreamer
12
15
  true
13
16
  end
14
17
 
15
- # TODO: setting source=nil is for rails 5.x compatability, once unsppored
16
- # source can be a required param and
17
- # `source = template.source if source.nil?` can be removed
18
- def self.call(template, source=nil)
19
- source = template.source if source.nil?
18
+ def self.call(template, source)
20
19
  # this juggling is required to keep line numbers right in the error
21
- %{__already_defined = defined?(json); json||=TurboStreamer::Template.new(self, output_buffer: output_buffer || ActionView::OutputBuffer.new); #{source}
22
- json.target! unless (__already_defined && __already_defined != "method")}
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}
23
22
  end
24
23
 
25
24
  end
@@ -1,33 +1,31 @@
1
- require 'active_support/core_ext/array'
1
+ # frozen_string_literal: true
2
2
 
3
- class TurboStreamer
4
- class KeyFormatter
5
- def initialize(*args)
6
- @format = {}
7
- @cache = {}
3
+ class TurboStreamer::KeyFormatter
4
+ def initialize(*args)
5
+ @format = {}
6
+ @cache = {}
8
7
 
9
- options = args.extract_options!
10
- args.each do |name|
11
- @format[name] = []
12
- end
13
- options.each do |name, paramaters|
14
- @format[name] = paramaters
15
- end
8
+ options = args.extract_options!
9
+ args.each do |name|
10
+ @format[name] = []
16
11
  end
17
-
18
- def initialize_copy(original)
19
- @cache = {}
12
+ options.each do |name, paramaters|
13
+ @format[name] = paramaters
20
14
  end
15
+ end
16
+
17
+ def initialize_copy(original)
18
+ @cache = {}
19
+ end
21
20
 
22
- def format(key)
23
- @cache[key] ||= @format.inject(key.to_s) do |result, args|
24
- func, args = args
25
- if ::Proc === func
26
- func.call result, *args
27
- else
28
- result.send func, *args
29
- end
21
+ def format(key)
22
+ @cache[key] ||= @format.inject(key.to_s) do |result, args|
23
+ func, args = args
24
+ if ::Proc === func
25
+ func.call result, *args
26
+ else
27
+ result.send func, *args
30
28
  end
31
29
  end
32
30
  end
33
- end
31
+ end
@@ -1,15 +1,46 @@
1
- require 'rails/railtie'
2
- require 'turbostreamer/handler'
3
- require 'turbostreamer/template'
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"
4
5
 
5
- require File.expand_path('../../../ext/actionview/buffer', __FILE__)
6
- require File.expand_path('../../../ext/actionview/streaming_template_renderer', __FILE__)
6
+ require 'rails/railtie'
7
7
 
8
8
  class TurboStreamer
9
9
  class Railtie < ::Rails::Railtie
10
10
  initializer :turbostreamer do
11
11
  ActiveSupport.on_load :action_view do
12
+ # Require turbostreamer in here so it's only loaded if needed
13
+ require 'turbostreamer'
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)
21
+
22
+ # Register Turbostreamer with Rails
12
23
  ActionView::Template.register_template_handler :streamer, TurboStreamer::Handler
24
+
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
+ )
42
+ end
43
+
13
44
  require 'turbostreamer/dependency_tracker'
14
45
  end
15
46
  end
@@ -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
- def _partial_options?(options)
228
- ::Hash === options && options.key?(:as) && options.key?(:partial)
229
- end
230
-
231
- def _is_active_model?(object)
232
- object.class.respond_to?(:model_name) && object.respond_to?(:to_partial_path)
233
- end
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 super
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class TurboStreamer
2
- VERSION = '1.10.0'
4
+ VERSION = '2.0.0'
3
5
  end
data/lib/turbostreamer.rb CHANGED
@@ -1,9 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'stringio'
2
- require 'turbostreamer/key_formatter'
3
- require 'turbostreamer/errors'
4
4
 
5
5
  class TurboStreamer
6
6
 
7
+ autoload :Handler, 'turbostreamer/handler'
8
+ autoload :Template, 'turbostreamer/template'
9
+ autoload :KeyFormatter, 'turbostreamer/key_formatter'
10
+ autoload :Errors, 'turbostreamer/errors'
11
+
7
12
  BLANK = ::Object.new
8
13
 
9
14
  ENCODERS = {
@@ -29,7 +34,7 @@ class TurboStreamer
29
34
  @encoder_options = @@encoder_options[options[:encoder]]
30
35
  elsif options[:encoder].nil?
31
36
  @encoder = TurboStreamer.default_encoder_for(options[:mime] || :json)
32
- if encoder_symbol = ENCODERS[options[:mime] || :json].find { |k, v| v == @encoder.name.delete_prefix('TurboStreamer::').delete_suffix('Encoder') }&.first
37
+ if encoder_symbol = TurboStreamer.encoder_symbol_for(options[:mime] || :json, @encoder)
33
38
  @encoder_options = @@encoder_options[encoder_symbol]
34
39
  else
35
40
  @encoder_options = {}
@@ -94,11 +99,7 @@ class TurboStreamer
94
99
  #
95
100
  # { "name": David", "age": 32 }, { "name": Jamie", "age": 31 }
96
101
  def extract!(object, *attributes)
97
- if ::Hash === object
98
- attributes.each{ |key| _set_value key, object.fetch(key) }
99
- else
100
- attributes.each{ |key| _set_value key, object.public_send(key) }
101
- end
102
+ _extract(object, attributes)
102
103
  end
103
104
 
104
105
  # Turns the current element into an array and iterates over the passed
@@ -164,7 +165,7 @@ class TurboStreamer
164
165
  else
165
166
  # json.author @post.creator, :name, :email_address
166
167
  # { "author": { "name": "David", "email_address": "david@thinking.com" } }
167
- object!{ extract!(value, *args) }
168
+ object!{ _extract(value, args) }
168
169
  end
169
170
  end
170
171
 
@@ -228,22 +229,57 @@ class TurboStreamer
228
229
  @@key_formatter = formatter
229
230
  end
230
231
 
231
- def self.set_default_encoder(mime, encoder, default_options={})
232
- if encoder.is_a?(Symbol)
233
- @@default_encoders[mime] = get_encoder(mime, encoder)
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
+
241
+ def self.set_default_encoder(mime, encoder, default_options=nil)
242
+ @@default_encoders[mime] = if encoder.is_a?(Symbol)
243
+ get_encoder(mime, encoder)
234
244
  else
235
- @@default_encoders[mime] = encoder
245
+ encoder
246
+ end
247
+
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
236
252
  end
237
- @@encoder_options[encoder] = default_options
238
253
  end
239
-
254
+
240
255
  def self.set_default_encoder_options(encoder, options)
241
256
  @@encoder_options[encoder] = options
242
257
  end
243
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
+
265
+ def self.has_default_encoder_options?(encoder)
266
+ @@encoder_options.has_key?(encoder)
267
+ end
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.
244
277
  def self.get_encoder(mime, key)
245
- require "turbostreamer/encoders/#{key}"
246
- Object.const_get("TurboStreamer::#{ENCODERS[mime][key]}Encoder")
278
+ @encoders ||= {}
279
+ @encoders[[mime, key]] ||= begin
280
+ require "turbostreamer/encoders/#{key}"
281
+ Object.const_get("TurboStreamer::#{ENCODERS[mime][key]}Encoder")
282
+ end
247
283
  end
248
284
 
249
285
  def self.default_encoder_for(mime)
@@ -317,7 +353,7 @@ class TurboStreamer
317
353
  elsif _eachable_arguments?(value, *args)
318
354
  _scope{ array!(value, *args) }
319
355
  else
320
- object!{ extract!(value, *args) }
356
+ object!{ _extract(value, args) }
321
357
  end
322
358
 
323
359
  end
@@ -326,15 +362,25 @@ class TurboStreamer
326
362
  def target!
327
363
  @encoder.flush
328
364
 
329
- if @encoder.output.is_a?(::StringIO)
330
- @encoder.output.string
365
+ output = @encoder.output
366
+
367
+ if output.is_a?(::StringIO)
368
+ output.string
331
369
  else
332
- @encoder.output
370
+ output
333
371
  end
334
372
  end
335
373
 
336
374
  private
337
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
+
338
384
  def _write(key, value)
339
385
  @encoder.key(_key(key))
340
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: 1.10.0
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: 2022-09-08 00:00:00.000000000 Z
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: 5.0.0
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: 5.0.0
25
+ version: 8.0.0
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: rake
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -53,7 +52,7 @@ dependencies:
53
52
  - !ruby/object:Gem::Version
54
53
  version: '0'
55
54
  - !ruby/object:Gem::Dependency
56
- name: oj
55
+ name: minitest-reporters
57
56
  requirement: !ruby/object:Gem::Requirement
58
57
  requirements:
59
58
  - - ">="
@@ -67,7 +66,7 @@ dependencies:
67
66
  - !ruby/object:Gem::Version
68
67
  version: '0'
69
68
  - !ruby/object:Gem::Dependency
70
- name: bundler
69
+ name: oj
71
70
  requirement: !ruby/object:Gem::Requirement
72
71
  requirements:
73
72
  - - ">="
@@ -109,7 +108,7 @@ dependencies:
109
108
  - !ruby/object:Gem::Version
110
109
  version: '0'
111
110
  - !ruby/object:Gem::Dependency
112
- name: byebug
111
+ name: debug
113
112
  requirement: !ruby/object:Gem::Requirement
114
113
  requirements:
115
114
  - - ">="
@@ -151,49 +150,7 @@ dependencies:
151
150
  - !ruby/object:Gem::Version
152
151
  version: '0'
153
152
  - !ruby/object:Gem::Dependency
154
- name: analyzer
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - ">="
158
- - !ruby/object:Gem::Version
159
- version: '0'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: '0'
167
- - !ruby/object:Gem::Dependency
168
- name: jbuilder
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: rabl
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: multi_json
153
+ name: railties
197
154
  requirement: !ruby/object:Gem::Requirement
198
155
  requirements:
199
156
  - - ">="
@@ -206,7 +163,9 @@ dependencies:
206
163
  - - ">="
207
164
  - !ruby/object:Gem::Version
208
165
  version: '0'
209
- description:
166
+ description: |
167
+ TurboStreamer is a JBuilder-like DSL for building
168
+ JSON that streams directly to a string or IO
210
169
  email:
211
170
  - jonbracy@gmail.com
212
171
  executables: []
@@ -214,9 +173,13 @@ extensions: []
214
173
  extra_rdoc_files:
215
174
  - README.md
216
175
  files:
176
+ - CHANGELOG.md
177
+ - LICENSE
217
178
  - README.md
218
- - ext/actionview/buffer.rb
219
- - ext/actionview/streaming_template_renderer.rb
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
220
183
  - lib/turbostreamer.rb
221
184
  - lib/turbostreamer/dependency_tracker.rb
222
185
  - lib/turbostreamer/encoders/oj.rb
@@ -230,8 +193,11 @@ files:
230
193
  homepage: https://github.com/malomalo/turbostreamer
231
194
  licenses:
232
195
  - MIT
233
- metadata: {}
234
- post_install_message:
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'
235
201
  rdoc_options:
236
202
  - "--main"
237
203
  - README.md
@@ -241,15 +207,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
241
207
  requirements:
242
208
  - - ">="
243
209
  - !ruby/object:Gem::Version
244
- version: '2.5'
210
+ version: 3.3.0
245
211
  required_rubygems_version: !ruby/object:Gem::Requirement
246
212
  requirements:
247
213
  - - ">="
248
214
  - !ruby/object:Gem::Version
249
215
  version: '0'
250
216
  requirements: []
251
- rubygems_version: 3.2.22
252
- signing_key:
217
+ rubygems_version: 4.0.11
253
218
  specification_version: 4
254
219
  summary: Stream JSON via a Builder-style DSL
255
220
  test_files: []
@@ -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