turbostreamer 1.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 +7 -0
- data/README.md +305 -0
- data/ext/actionview/buffer.rb +25 -0
- data/ext/actionview/streaming_template_renderer.rb +40 -0
- data/lib/turbostreamer/dependency_tracker.rb +61 -0
- data/lib/turbostreamer/encoders/wankel.rb +82 -0
- data/lib/turbostreamer/handler.rb +22 -0
- data/lib/turbostreamer/key_formatter.rb +33 -0
- data/lib/turbostreamer/railtie.rb +17 -0
- data/lib/turbostreamer/template.rb +225 -0
- data/lib/turbostreamer/version.rb +3 -0
- data/lib/turbostreamer.rb +335 -0
- metadata +190 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0f57b4f2c7ce7f14ba08eaae9087d07e8d69ae6d
|
4
|
+
data.tar.gz: 7eb8046991d046191bc47378689739e971abdb80
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e3abb1caae47743829462991a5d2f10798bc45c07ee5b51154424f522a08766520b9638d81bdbe84fda5b28208c5f72e7af148d28242ed63fa410cd7ff4a76f8
|
7
|
+
data.tar.gz: 87118ef8372d08d696d2f9e2f3e703352ce04e3c9dbe5d30510a0e2edd05c3d8ab1f76ab8f66eda74007e1cd6de98e2d550f747b355a2cf65b5052444e8cd95c
|
data/README.md
ADDED
@@ -0,0 +1,305 @@
|
|
1
|
+
# TurboStreamer [](https://travis-ci.org/malomalo/turbostreamer)
|
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.
|
6
|
+
|
7
|
+
|
8
|
+
[Jbuilder](https://github.com/rails/jbuilder) builds a Hash as it renders the
|
9
|
+
template and once complete converts the Hash to JSON. TurboStreamer on the other
|
10
|
+
hand writes directly to the output as it is rendering the template. Because of
|
11
|
+
this some of the magic cannot be done and requires a little more verboseness.
|
12
|
+
|
13
|
+
Examples
|
14
|
+
--------
|
15
|
+
|
16
|
+
``` ruby
|
17
|
+
# app/views/message/show.json.streamer
|
18
|
+
|
19
|
+
json.object! do
|
20
|
+
json.content format_content(@message.content)
|
21
|
+
json.extract! @message, :created_at, :updated_at
|
22
|
+
|
23
|
+
json.author do
|
24
|
+
json.object! do
|
25
|
+
json.name @message.creator.name.familiar
|
26
|
+
json.email_address @message.creator.email_address_with_name
|
27
|
+
json.url url_for(@message.creator, format: :json)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if current_user.admin?
|
32
|
+
json.visitors calculate_visitors(@message)
|
33
|
+
end
|
34
|
+
|
35
|
+
json.tags do
|
36
|
+
json.array! do
|
37
|
+
@message.tags.each { |tag| json.child! tag }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
json.comments @message.comments, :content, :created_at
|
42
|
+
|
43
|
+
json.attachments @message.attachments do |attachment|
|
44
|
+
json.object! do
|
45
|
+
json.filename attachment.filename
|
46
|
+
json.url url_for(attachment)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
This will build the following structure:
|
53
|
+
|
54
|
+
``` javascript
|
55
|
+
{
|
56
|
+
"content": "<p>This is <i>serious</i> monkey business</p>",
|
57
|
+
"created_at": "2011-10-29T20:45:28-05:00",
|
58
|
+
"updated_at": "2011-10-29T20:45:28-05:00",
|
59
|
+
|
60
|
+
"author": {
|
61
|
+
"name": "David H.",
|
62
|
+
"email_address": "'David Heinemeier Hansson' <david@heinemeierhansson.com>",
|
63
|
+
"url": "http://example.com/users/1-david.json"
|
64
|
+
},
|
65
|
+
|
66
|
+
"visitors": 15,
|
67
|
+
|
68
|
+
"tags": ['public'],
|
69
|
+
|
70
|
+
"comments": [
|
71
|
+
{ "content": "Hello everyone!", "created_at": "2011-10-29T20:45:28-05:00" },
|
72
|
+
{ "content": "To you my good sir!", "created_at": "2011-10-29T20:47:28-05:00" }
|
73
|
+
],
|
74
|
+
|
75
|
+
"attachments": [
|
76
|
+
{ "filename": "forecast.xls", "url": "http://example.com/downloads/forecast.xls" },
|
77
|
+
{ "filename": "presentation.pdf", "url": "http://example.com/downloads/presentation.pdf" }
|
78
|
+
]
|
79
|
+
}
|
80
|
+
```
|
81
|
+
|
82
|
+
To define attribute and structure names dynamically, use the `set!` method:
|
83
|
+
|
84
|
+
``` ruby
|
85
|
+
json.object! do
|
86
|
+
json.set! :author do
|
87
|
+
json.object! do
|
88
|
+
json.set! :name, 'David'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# => { "author": { "name": "David" } }
|
94
|
+
```
|
95
|
+
|
96
|
+
Top level arrays can be handled directly. Useful for index and other collection
|
97
|
+
actions.
|
98
|
+
|
99
|
+
``` ruby
|
100
|
+
json.array! @comments do |comment|
|
101
|
+
next if comment.marked_as_spam_by?(current_user)
|
102
|
+
|
103
|
+
json.object! do
|
104
|
+
json.body comment.body
|
105
|
+
json.author do
|
106
|
+
json.first_name comment.author.first_name
|
107
|
+
json.last_name comment.author.last_name
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# => [ { "body": "great post...", "author": { "first_name": "Joe", "last_name": "Bloe" }} ]
|
113
|
+
```
|
114
|
+
|
115
|
+
You can also extract attributes from array directly.
|
116
|
+
|
117
|
+
``` ruby
|
118
|
+
# @people = People.all
|
119
|
+
|
120
|
+
json.array! @people, :id, :name
|
121
|
+
|
122
|
+
# => [ { "id": 1, "name": "David" }, { "id": 2, "name": "Jamie" } ]
|
123
|
+
```
|
124
|
+
|
125
|
+
You can either use TurboStreamer stand-alone or directly as an ActionView template
|
126
|
+
language. When required in Rails, you can create views ala show.json.streamer
|
127
|
+
(the json is already yielded):
|
128
|
+
|
129
|
+
``` ruby
|
130
|
+
# Any helpers available to views are available to the builder
|
131
|
+
json.object! do
|
132
|
+
json.content format_content(@message.content)
|
133
|
+
json.extract! @message, :created_at, :updated_at
|
134
|
+
|
135
|
+
json.author do
|
136
|
+
json.object! do
|
137
|
+
json.name @message.creator.name.familiar
|
138
|
+
json.email_address @message.creator.email_address_with_name
|
139
|
+
json.url url_for(@message.creator, format: :json)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
if current_user.admin?
|
144
|
+
json.visitors calculate_visitors(@message)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
You can use partials as well. The following will render the file
|
150
|
+
`views/comments/_comments.json.streamer`, and set a local variable
|
151
|
+
`comments` with all this message's comments, which you can use inside
|
152
|
+
the partial.
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
json.partial! 'comments/comments', comments: @message.comments
|
156
|
+
```
|
157
|
+
|
158
|
+
It's also possible to render collections of partials:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
json.array! @posts, partial: 'posts/post', as: :post
|
162
|
+
|
163
|
+
# or
|
164
|
+
|
165
|
+
json.partial! 'posts/post', collection: @posts, as: :post
|
166
|
+
|
167
|
+
# or
|
168
|
+
|
169
|
+
json.partial! partial: 'posts/post', collection: @posts, as: :post
|
170
|
+
|
171
|
+
# or
|
172
|
+
|
173
|
+
json.comments @post.comments, partial: 'comment/comment', as: :comment
|
174
|
+
```
|
175
|
+
|
176
|
+
You can explicitly make TurboStreamer object return null if you want:
|
177
|
+
|
178
|
+
``` ruby
|
179
|
+
json.extract! @post, :id, :title, :content, :published_at
|
180
|
+
json.author do
|
181
|
+
if @post.anonymous?
|
182
|
+
json.null! # or json.nil!
|
183
|
+
else
|
184
|
+
json.object! do
|
185
|
+
json.first_name @post.author_first_name
|
186
|
+
json.last_name @post.author_last_name
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
Fragment caching is supported, it uses `Rails.cache` and works like caching in
|
193
|
+
HTML templates:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
json.object! do
|
197
|
+
json.cache! ['v1', @person], expires_in: 10.minutes do
|
198
|
+
json.extract! @person, :name, :age
|
199
|
+
end
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
You can also conditionally cache a block by using `cache_if!` like this:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
json.object! do
|
207
|
+
json.cache_if! !admin?, ['v1', @person], expires_in: 10.minutes do
|
208
|
+
json.extract! @person, :name, :age
|
209
|
+
end
|
210
|
+
end
|
211
|
+
```
|
212
|
+
|
213
|
+
The only caveat with caching is inside and object you must cache both the key
|
214
|
+
and the value. You cannot just cache the value. For example:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
json.boject! do
|
218
|
+
json.key do
|
219
|
+
json.cache! :key do
|
220
|
+
json.value! 'Cache this.'
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
Will error out, but can easily be rewritten as:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
json.boject! do
|
230
|
+
json.cache! :key do
|
231
|
+
json.key do
|
232
|
+
json.value! 'Cache this.'
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
238
|
+
Keys can be auto formatted using `key_format!`, this can be used to convert
|
239
|
+
keynames from the standard ruby_format to camelCase:
|
240
|
+
|
241
|
+
``` ruby
|
242
|
+
json.key_format! camelize: :lower
|
243
|
+
json.object! do
|
244
|
+
json.first_name 'David'
|
245
|
+
end
|
246
|
+
|
247
|
+
# => { "firstName": "David" }
|
248
|
+
```
|
249
|
+
|
250
|
+
You can set this globally with the class method `key_format` (from inside your
|
251
|
+
environment.rb for example):
|
252
|
+
|
253
|
+
``` ruby
|
254
|
+
TurboStreamer.key_format camelize: :lower
|
255
|
+
```
|
256
|
+
|
257
|
+
Syntax Differences from Jbuilder
|
258
|
+
--------------------------------
|
259
|
+
|
260
|
+
- You must open JSON object or array if you want an object or array.
|
261
|
+
- You can directly output a value with `json.value! value`, this will
|
262
|
+
allow you to put a number, string, or other JSON value if you wish
|
263
|
+
to not have an object or array.
|
264
|
+
- The call syntax has been removed (eg. `json.(@person, :name, :age)`)
|
265
|
+
- Caching inside of a object must cache both the key and the value.
|
266
|
+
|
267
|
+
Backends
|
268
|
+
--------
|
269
|
+
|
270
|
+
Currently TurboStreamer only uses the [Wankel JSON backend](https://github.com/malomalo/wankel),
|
271
|
+
which supports streaming parsing and encoding.
|
272
|
+
|
273
|
+
The idea was to also support [Oj](https://github.com/ohler55/oj) and
|
274
|
+
[MessagePack](http://msgpack.org/).
|
275
|
+
|
276
|
+
Oj should be relatively easily to do, you just need to figure out how to switch
|
277
|
+
out the io so it can be captured for caching.
|
278
|
+
|
279
|
+
MessagePack would require a bit more work as you would need a change in the
|
280
|
+
protocol. We do not know how big an array or map/object will be when we
|
281
|
+
start emitting it and MessagePack require we know it. It seems like a relatively
|
282
|
+
small change, instead of a marker followed by number of elements there would be
|
283
|
+
a start marker followed by the elements and then an end marker.
|
284
|
+
|
285
|
+
All backends must have the following functions:
|
286
|
+
|
287
|
+
- `key(string)` Output a map key
|
288
|
+
- `value(value)` Output a value
|
289
|
+
- `map_open` Open a object/map
|
290
|
+
- `map_close` Close a object/map
|
291
|
+
- `array_open` Open an Array
|
292
|
+
- `array_close` Close an Array
|
293
|
+
- `flush` Flush any buffers
|
294
|
+
- `inject(string)` Inject a (usually cached) string into the output; instering any delimiters as needed.
|
295
|
+
- `capture(&block)` Capture the output of the block (w/o any delimiters)
|
296
|
+
|
297
|
+
Special Thanks & Contributors
|
298
|
+
-----------------------------
|
299
|
+
|
300
|
+
TurboStreamer is a fork of [Jbuilder](https://github.com/rails/jbuilder), built of
|
301
|
+
what they have accopmlished and with out Jbuilder TurboStreamer would not be here today.
|
302
|
+
Thanks to everyone who's been a part of Jbuilder!
|
303
|
+
|
304
|
+
* David Heinemeier Hansson - http://david.heinemeierhansson.com/ - for writing Jbuidler!!
|
305
|
+
* Pavel Pravosud - http://pavel.pravosud.com/ - for maintaing and pushing Jbuilder forward
|
@@ -0,0 +1,25 @@
|
|
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
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ActionView
|
2
|
+
class StreamingTemplateRenderer < TemplateRenderer
|
3
|
+
|
4
|
+
def render_template(template, layout_name = nil, locals = {}) #:nodoc:
|
5
|
+
return [super] unless layout_name && template.supports_streaming?
|
6
|
+
|
7
|
+
locals ||= {}
|
8
|
+
layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
|
9
|
+
|
10
|
+
Body.new do |buffer|
|
11
|
+
if template.handler == TurboStreamer::Handler
|
12
|
+
delayed_render_json(buffer, template, layout, @view, locals)
|
13
|
+
else
|
14
|
+
delayed_render(buffer, template, layout, @view, locals)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def delayed_render_json(buffer, template, layout, view, locals)
|
22
|
+
# Wrap the given buffer in the StreamingBuffer and pass it to the
|
23
|
+
# underlying template handler. Now, every time something is concatenated
|
24
|
+
# to the buffer, it is not appended to an array, but streamed straight
|
25
|
+
# to the client.
|
26
|
+
output = ActionView::JSONStreamingBuffer.new(buffer)
|
27
|
+
yielder = lambda { |*name| view._layout_for(*name) }
|
28
|
+
|
29
|
+
instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do
|
30
|
+
fiber = Fiber.new do
|
31
|
+
template.render(view, locals, output, &yielder)
|
32
|
+
end
|
33
|
+
|
34
|
+
fiber.resume
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,61 @@
|
|
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
|
16
|
+
|
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
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
::TurboStreamer::DependencyTracker = Class.new(dependency_tracker::ERBTracker)
|
59
|
+
::TurboStreamer::DependencyTracker.send :include, ::TurboStreamer::DependencyTrackerMethods
|
60
|
+
dependency_tracker.register_tracker :streamer, ::TurboStreamer::DependencyTracker
|
61
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'wankel'
|
2
|
+
|
3
|
+
class TurboStreamer
|
4
|
+
class WankelEncoder < ::Wankel::StreamEncoder
|
5
|
+
|
6
|
+
def initialize(io, options={})
|
7
|
+
@stack = []
|
8
|
+
@indexes = []
|
9
|
+
|
10
|
+
super(io, {mode: :as_json}.merge(options))
|
11
|
+
end
|
12
|
+
|
13
|
+
def key(k)
|
14
|
+
string(k)
|
15
|
+
end
|
16
|
+
|
17
|
+
def value(v)
|
18
|
+
if @stack.last == :array || @stack.last == :map
|
19
|
+
@indexes[-1] += 1
|
20
|
+
end
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def map_open
|
25
|
+
@stack << :map
|
26
|
+
@indexes << 0
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def map_close
|
31
|
+
@indexes.pop
|
32
|
+
@stack.pop
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def array_open
|
37
|
+
@stack << :array
|
38
|
+
@indexes << 0
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
def array_close
|
43
|
+
@indexes.pop
|
44
|
+
@stack.pop
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
def inject(string)
|
49
|
+
flush
|
50
|
+
|
51
|
+
if @stack.last == :array
|
52
|
+
self.output.write(',') if @indexes.last > 0
|
53
|
+
@indexes[-1] += 1
|
54
|
+
elsif @stack.last == :map
|
55
|
+
self.output.write(',') if @indexes.last > 0
|
56
|
+
capture do
|
57
|
+
string("")
|
58
|
+
string("")
|
59
|
+
end
|
60
|
+
@indexes[-1] += 1
|
61
|
+
end
|
62
|
+
|
63
|
+
self.output.write(string)
|
64
|
+
end
|
65
|
+
|
66
|
+
def capture(to=nil)
|
67
|
+
flush
|
68
|
+
old, to = self.output, to || ::StringIO.new
|
69
|
+
@indexes << 0
|
70
|
+
self.output = to
|
71
|
+
|
72
|
+
yield
|
73
|
+
|
74
|
+
flush
|
75
|
+
to.string.gsub(/\A,|,\Z/, '')
|
76
|
+
ensure
|
77
|
+
@indexes.pop
|
78
|
+
self.output = old
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "turbostreamer"
|
2
|
+
require "active_support/core_ext"
|
3
|
+
# This module makes TurboStreamer work with Rails using the template handler API.
|
4
|
+
|
5
|
+
class TurboStreamer
|
6
|
+
class Handler
|
7
|
+
|
8
|
+
class_attribute :default_format
|
9
|
+
self.default_format = :json
|
10
|
+
|
11
|
+
def self.supports_streaming?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.call(template)
|
16
|
+
# this juggling is required to keep line numbers right in the error
|
17
|
+
%{__already_defined = defined?(json); json||=TurboStreamer::Template.new(self, output_buffer: output_buffer || ActionView::OutputBuffer.new); #{template.source}
|
18
|
+
json.target! unless (__already_defined && __already_defined != "method")}
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'active_support/core_ext/array'
|
2
|
+
|
3
|
+
class TurboStreamer
|
4
|
+
class KeyFormatter
|
5
|
+
def initialize(*args)
|
6
|
+
@format = {}
|
7
|
+
@cache = {}
|
8
|
+
|
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
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize_copy(original)
|
19
|
+
@cache = {}
|
20
|
+
end
|
21
|
+
|
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
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rails/railtie'
|
2
|
+
require 'turbostreamer/handler'
|
3
|
+
require 'turbostreamer/template'
|
4
|
+
|
5
|
+
require File.expand_path('../../../ext/actionview/buffer', __FILE__)
|
6
|
+
require File.expand_path('../../../ext/actionview/streaming_template_renderer', __FILE__)
|
7
|
+
|
8
|
+
class TurboStreamer
|
9
|
+
class Railtie < ::Rails::Railtie
|
10
|
+
initializer :turbostreamer do
|
11
|
+
ActiveSupport.on_load :action_view do
|
12
|
+
ActionView::Template.register_template_handler :streamer, TurboStreamer::Handler
|
13
|
+
require 'turbostreamer/dependency_tracker'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'turbostreamer'
|
2
|
+
|
3
|
+
class TurboStreamer::Template < TurboStreamer
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :template_lookup_options
|
7
|
+
end
|
8
|
+
|
9
|
+
self.template_lookup_options = { handlers: [:streamer] }
|
10
|
+
|
11
|
+
def initialize(context, *args, &block)
|
12
|
+
@context = context
|
13
|
+
super(*args, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def partial!(name_or_options, locals = {})
|
17
|
+
if name_or_options.class.respond_to?(:model_name) && name_or_options.respond_to?(:to_partial_path)
|
18
|
+
@context.render(name_or_options, json: self)
|
19
|
+
else
|
20
|
+
if name_or_options.is_a?(Hash)
|
21
|
+
options = name_or_options
|
22
|
+
else
|
23
|
+
if locals.one? && (locals.keys.first == :locals)
|
24
|
+
options = locals.merge(partial: name_or_options)
|
25
|
+
else
|
26
|
+
options = { partial: name_or_options, locals: locals }
|
27
|
+
end
|
28
|
+
# partial! 'name', foo: 'bar'
|
29
|
+
as = locals.delete(:as)
|
30
|
+
options[:as] = as if as.present?
|
31
|
+
options[:collection] = locals[:collection] if locals.key?(:collection)
|
32
|
+
end
|
33
|
+
|
34
|
+
_render_partial_with_options options
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def array!(collection = BLANK, *attributes, &block)
|
39
|
+
options = attributes.extract_options!
|
40
|
+
|
41
|
+
if options.key?(:partial)
|
42
|
+
partial! options.merge(collection: collection)
|
43
|
+
else
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Caches the json constructed within the block passed. Has the same signature
|
49
|
+
# as the `cache` helper method in `ActionView::Helpers::CacheHelper` and so
|
50
|
+
# can be used in the same way.
|
51
|
+
#
|
52
|
+
# Example:
|
53
|
+
#
|
54
|
+
# json.cache! ['v1', @person], expires_in: 10.minutes do
|
55
|
+
# json.extract! @person, :name, :age
|
56
|
+
# end
|
57
|
+
def cache!(key=nil, options={})
|
58
|
+
if @context.controller.perform_caching
|
59
|
+
value = _cache_fragment_for(key, options) do
|
60
|
+
_capture { _scope { yield self }; }
|
61
|
+
end
|
62
|
+
|
63
|
+
inject!(value)
|
64
|
+
else
|
65
|
+
yield
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Caches a collection of objects using fetch_multi, if supported.
|
70
|
+
# Requires a block for each item in the array. Accepts optional 'key' attribute
|
71
|
+
# in options (e.g. key: 'v1').
|
72
|
+
#
|
73
|
+
# Example:
|
74
|
+
#
|
75
|
+
# json.cache_collection! @people, expires_in: 10.minutes do |person|
|
76
|
+
# json.partial! 'person', :person => person
|
77
|
+
# end
|
78
|
+
def cache_collection!(collection, options = {}, &block)
|
79
|
+
if @context.controller.perform_caching
|
80
|
+
keys_to_collection_map = _keys_to_collection_map(collection, options)
|
81
|
+
results = _read_multi_fragment_cache(keys_to_collection_map.keys, options)
|
82
|
+
|
83
|
+
array! do
|
84
|
+
keys_to_collection_map.each_key do |key|
|
85
|
+
if results[key]
|
86
|
+
inject!(results[key])
|
87
|
+
else
|
88
|
+
value = _write_fragment_cache(key, options) do
|
89
|
+
_capture { _scope { yield keys_to_collection_map[key] } }
|
90
|
+
end
|
91
|
+
inject!(value)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
else
|
96
|
+
array! collection, options, &block
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Conditionally catches the json depending in the condition given as first
|
101
|
+
# parameter. Has the same signature as the `cache` helper method in
|
102
|
+
# `ActionView::Helpers::CacheHelper` and so can be used in the same way.
|
103
|
+
#
|
104
|
+
# Example:
|
105
|
+
#
|
106
|
+
# json.cache_if! !admin?, @person, expires_in: 10.minutes do
|
107
|
+
# json.extract! @person, :name, :age
|
108
|
+
# end
|
109
|
+
def cache_if!(condition, *args)
|
110
|
+
condition ? cache!(*args, &::Proc.new) : yield
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def _render_partial_with_options(options)
|
116
|
+
options.reverse_merge! locals: {}
|
117
|
+
options.reverse_merge! ::TurboStreamer::Template.template_lookup_options
|
118
|
+
as = options[:as]
|
119
|
+
|
120
|
+
if as && options.key?(:collection)
|
121
|
+
as = as.to_sym
|
122
|
+
collection = options.delete(:collection)
|
123
|
+
locals = options.delete(:locals)
|
124
|
+
array! collection do |member|
|
125
|
+
member_locals = locals.clone
|
126
|
+
member_locals.merge! collection: collection
|
127
|
+
member_locals.merge! as => member
|
128
|
+
_render_partial options.merge(locals: member_locals)
|
129
|
+
end
|
130
|
+
else
|
131
|
+
_render_partial options
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def _render_partial(options)
|
136
|
+
options[:locals].merge! json: self
|
137
|
+
@context.render options
|
138
|
+
end
|
139
|
+
|
140
|
+
def _keys_to_collection_map(collection, options)
|
141
|
+
key = options.delete(:key)
|
142
|
+
|
143
|
+
collection.inject({}) do |result, item|
|
144
|
+
key = key.respond_to?(:call) ? key.call(item) : key
|
145
|
+
cache_key = key ? [key, item] : item
|
146
|
+
result[_cache_key(cache_key, options)] = item
|
147
|
+
result
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def _cache_fragment_for(key, options, &block)
|
152
|
+
key = _cache_key(key, options)
|
153
|
+
_read_fragment_cache(key, options) || _write_fragment_cache(key, options, &block)
|
154
|
+
end
|
155
|
+
|
156
|
+
def _read_multi_fragment_cache(keys, options = nil)
|
157
|
+
@context.controller.instrument_fragment_cache :read_multi_fragment, keys do
|
158
|
+
::Rails.cache.read_multi(*keys, options)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def _read_fragment_cache(key, options = nil)
|
163
|
+
@context.controller.instrument_fragment_cache :read_fragment, key do
|
164
|
+
::Rails.cache.read(key, options)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def _write_fragment_cache(key, options = nil)
|
169
|
+
@context.controller.instrument_fragment_cache :write_fragment, key do
|
170
|
+
yield.tap do |value|
|
171
|
+
::Rails.cache.write(key, value, options)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def _cache_key(key, options)
|
177
|
+
name_options = options.slice(:skip_digest, :virtual_path)
|
178
|
+
key = _fragment_name_with_digest(key, name_options)
|
179
|
+
|
180
|
+
if @context.respond_to?(:fragment_cache_key)
|
181
|
+
key = @context.fragment_cache_key(key)
|
182
|
+
elsif ::Hash === key
|
183
|
+
key = url_for(key).split('://', 2).last
|
184
|
+
end
|
185
|
+
|
186
|
+
::ActiveSupport::Cache.expand_cache_key(key, :streamer)
|
187
|
+
end
|
188
|
+
|
189
|
+
def _fragment_name_with_digest(key, options)
|
190
|
+
if @context.respond_to?(:cache_fragment_name)
|
191
|
+
# Current compatibility, fragment_name_with_digest is private again and cache_fragment_name
|
192
|
+
# should be used instead.
|
193
|
+
@context.cache_fragment_name(key, options)
|
194
|
+
elsif @context.respond_to?(:fragment_name_with_digest)
|
195
|
+
# Backwards compatibility for period of time when fragment_name_with_digest was made public.
|
196
|
+
@context.fragment_name_with_digest(key)
|
197
|
+
else
|
198
|
+
key
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def _fragment_name_with_digest(key, options)
|
203
|
+
if @context.respond_to?(:cache_fragment_name)
|
204
|
+
# Current compatibility, fragment_name_with_digest is private again and cache_fragment_name
|
205
|
+
# should be used instead.
|
206
|
+
@context.cache_fragment_name(key, options)
|
207
|
+
else
|
208
|
+
key
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def _partial_options?(options)
|
213
|
+
::Hash === options && options.key?(:as) && options.key?(:partial)
|
214
|
+
end
|
215
|
+
|
216
|
+
def _is_active_model?(object)
|
217
|
+
object.class.respond_to?(:model_name) && object.respond_to?(:to_partial_path)
|
218
|
+
end
|
219
|
+
|
220
|
+
def _eachable_arguments?(value, *args)
|
221
|
+
return true if super
|
222
|
+
options = args.last
|
223
|
+
::Hash === options && options.key?(:as)
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,335 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'turbostreamer/key_formatter'
|
3
|
+
|
4
|
+
class TurboStreamer
|
5
|
+
|
6
|
+
BLANK = ::Object.new
|
7
|
+
|
8
|
+
|
9
|
+
ENCODERS = {
|
10
|
+
json: {oj: 'Oj', wankel: 'Wankel'},
|
11
|
+
msgpack: {msgpack: 'MessagePack'}
|
12
|
+
}
|
13
|
+
|
14
|
+
@@default_encoders = {}
|
15
|
+
@@key_formatter = nil
|
16
|
+
|
17
|
+
undef_method :==
|
18
|
+
undef_method :equal?
|
19
|
+
|
20
|
+
def self.encode(options = {}, &block)
|
21
|
+
new(options, &block).target!
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(options = {})
|
25
|
+
@output_buffer = options[:output_buffer] || ::StringIO.new
|
26
|
+
@encoder = options[:encoder] || TurboStreamer.default_encoder_for(options[:mime] || :json).new(@output_buffer)
|
27
|
+
|
28
|
+
@key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil }
|
29
|
+
|
30
|
+
yield self if ::Kernel.block_given?
|
31
|
+
end
|
32
|
+
|
33
|
+
def key!(key)
|
34
|
+
@encoder.key(_key(key))
|
35
|
+
end
|
36
|
+
|
37
|
+
def value!(value)
|
38
|
+
@encoder.value(value)
|
39
|
+
end
|
40
|
+
|
41
|
+
def object!(&block)
|
42
|
+
@encoder.map_open
|
43
|
+
_scope { block.call } if block
|
44
|
+
@encoder.map_close
|
45
|
+
end
|
46
|
+
|
47
|
+
# Extracts the mentioned attributes or hash elements from the passed object
|
48
|
+
# and turns them into a JSON object.
|
49
|
+
#
|
50
|
+
# Example:
|
51
|
+
#
|
52
|
+
# @person = Struct.new(:name, :age).new('David', 32)
|
53
|
+
#
|
54
|
+
# or you can utilize a Hash
|
55
|
+
#
|
56
|
+
# @person = { name: 'David', age: 32 }
|
57
|
+
#
|
58
|
+
# json.pluck! @person, :name, :age
|
59
|
+
#
|
60
|
+
# { "name": David", "age": 32 }
|
61
|
+
def pluck!(object, *attributes)
|
62
|
+
object! do
|
63
|
+
extract!(object, *attributes)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Extracts the mentioned attributes or hash elements from the passed object
|
68
|
+
# and turns them into attributes of the JSON.
|
69
|
+
#
|
70
|
+
# Example:
|
71
|
+
#
|
72
|
+
# @person = Struct.new(:name, :age).new('David', 32)
|
73
|
+
#
|
74
|
+
# or you can utilize a Hash
|
75
|
+
#
|
76
|
+
# @person = { name: 'David', age: 32 }
|
77
|
+
#
|
78
|
+
# json.extract! @person, :name, :age
|
79
|
+
#
|
80
|
+
# { "name": David", "age": 32 }, { "name": Jamie", "age": 31 }
|
81
|
+
def extract!(object, *attributes)
|
82
|
+
if ::Hash === object
|
83
|
+
attributes.each{ |key| _set_value key, object.fetch(key) }
|
84
|
+
else
|
85
|
+
attributes.each{ |key| _set_value key, object.public_send(key) }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Turns the current element into an array and iterates over the passed
|
90
|
+
# collection, adding each iteration as an element of the resulting array.
|
91
|
+
#
|
92
|
+
# Example:
|
93
|
+
#
|
94
|
+
# json.array!(@people) do |person|
|
95
|
+
# json.name person.name
|
96
|
+
# json.age calculate_age(person.birthday)
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ]
|
100
|
+
#
|
101
|
+
# It's generally only needed to use this method for top-level arrays. If you
|
102
|
+
# have named arrays, you can do:
|
103
|
+
#
|
104
|
+
# json.people(@people) do |person|
|
105
|
+
# json.name person.name
|
106
|
+
# json.age calculate_age(person.birthday)
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# { "people": [ { "name": David", "age": 32 }, { "name": Jamie", "age": 31 } ] }
|
110
|
+
#
|
111
|
+
# If you omit the block then you can set the top level array directly:
|
112
|
+
#
|
113
|
+
# json.array! [1, 2, 3]
|
114
|
+
#
|
115
|
+
# [1,2,3]
|
116
|
+
def array!(collection = BLANK, *attributes, &block)
|
117
|
+
@encoder.array_open
|
118
|
+
|
119
|
+
if _blank?(collection)
|
120
|
+
_scope(&block) if block
|
121
|
+
else
|
122
|
+
_extract_collection(collection, *attributes, &block)
|
123
|
+
end
|
124
|
+
|
125
|
+
@encoder.array_close
|
126
|
+
end
|
127
|
+
|
128
|
+
def set!(key, value = BLANK, *args, &block)
|
129
|
+
key!(key)
|
130
|
+
|
131
|
+
if block
|
132
|
+
if !_blank?(value)
|
133
|
+
# json.comments @post.comments { |comment| ... }
|
134
|
+
# { "comments": [ { ... }, { ... } ] }
|
135
|
+
_scope { array!(value, &block) }
|
136
|
+
else
|
137
|
+
# json.comments { ... }
|
138
|
+
# { "comments": ... }
|
139
|
+
_scope(&block)
|
140
|
+
end
|
141
|
+
elsif args.empty?
|
142
|
+
# json.age 32
|
143
|
+
# { "age": 32 }
|
144
|
+
value!(value)
|
145
|
+
elsif _eachable_arguments?(value, *args)
|
146
|
+
# json.comments @post.comments, :content, :created_at
|
147
|
+
# { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
|
148
|
+
_scope{ array!(value, *args) }
|
149
|
+
else
|
150
|
+
# json.author @post.creator, :name, :email_address
|
151
|
+
# { "author": { "name": "David", "email_address": "david@thinking.com" } }
|
152
|
+
object!{ extract!(value, *args) }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
alias_method :method_missing, :set!
|
157
|
+
private :method_missing
|
158
|
+
|
159
|
+
# Specifies formatting to be applied to the key. Passing in a name of a
|
160
|
+
# function will cause that function to be called on the key. So :upcase
|
161
|
+
# will upper case the key. You can also pass in lambdas for more complex
|
162
|
+
# transformations.
|
163
|
+
#
|
164
|
+
# Example:
|
165
|
+
#
|
166
|
+
# json.key_format! :upcase
|
167
|
+
# json.author do
|
168
|
+
# json.name "David"
|
169
|
+
# json.age 32
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# { "AUTHOR": { "NAME": "David", "AGE": 32 } }
|
173
|
+
#
|
174
|
+
# You can pass parameters to the method using a hash pair.
|
175
|
+
#
|
176
|
+
# json.key_format! camelize: :lower
|
177
|
+
# json.first_name "David"
|
178
|
+
#
|
179
|
+
# { "firstName": "David" }
|
180
|
+
#
|
181
|
+
# Lambdas can also be used.
|
182
|
+
#
|
183
|
+
# json.key_format! ->(key){ "_" + key }
|
184
|
+
# json.first_name "David"
|
185
|
+
#
|
186
|
+
# { "_first_name": "David" }
|
187
|
+
#
|
188
|
+
def key_format!(*args)
|
189
|
+
@key_formatter = KeyFormatter.new(*args)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Same as the instance method key_format! except sets the default.
|
193
|
+
def self.key_format(*args)
|
194
|
+
@@key_formatter = KeyFormatter.new(*args)
|
195
|
+
end
|
196
|
+
|
197
|
+
def self.key_formatter=(formatter)
|
198
|
+
@@key_formatter = formatter
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.set_default_encoder(mime, encoder)
|
202
|
+
@@default_encoders[mime] = encoder
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.get_encoder(mime, key)
|
206
|
+
require "turbostreamer/encoders/#{key}"
|
207
|
+
Object.const_get("TurboStreamer::#{ENCODERS[mime][key]}Encoder")
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.default_encoder_for(mime)
|
211
|
+
if @@default_encoders[mime]
|
212
|
+
@@default_encoders[mime]
|
213
|
+
else
|
214
|
+
ENCODERS[mime].to_a.find do |key, class_name|
|
215
|
+
next if !const_defined?(class_name)
|
216
|
+
return get_encoder(mime, key)
|
217
|
+
end
|
218
|
+
|
219
|
+
ENCODERS[mime].to_a.find do |key, class_name|
|
220
|
+
begin
|
221
|
+
return get_encoder(mime, key)
|
222
|
+
rescue ::LoadError
|
223
|
+
next
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
raise ArgumentError, "Could not find an adapter to use"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def _extract_collection(collection, *attributes, &block)
|
232
|
+
if collection.nil?
|
233
|
+
# noop
|
234
|
+
elsif block
|
235
|
+
collection.each do |element|
|
236
|
+
_scope{ yield element }
|
237
|
+
end
|
238
|
+
elsif attributes.any?
|
239
|
+
collection.each { |element| pluck!(element, *attributes) }
|
240
|
+
else
|
241
|
+
collection.each { |element| value!(element) }
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Inject a valid JSON string into the current
|
246
|
+
def inject!(json_text)
|
247
|
+
@encoder.inject(json_text)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Turns the current element into an array and yields a builder to add a hash.
|
251
|
+
#
|
252
|
+
# Example:
|
253
|
+
#
|
254
|
+
# json.comments do
|
255
|
+
# json.child! { json.content "hello" }
|
256
|
+
# json.child! { json.content "world" }
|
257
|
+
# end
|
258
|
+
#
|
259
|
+
# { "comments": [ { "content": "hello" }, { "content": "world" } ]}
|
260
|
+
#
|
261
|
+
# More commonly, you'd use the combined iterator, though:
|
262
|
+
#
|
263
|
+
# json.comments(@post.comments) do |comment|
|
264
|
+
# json.content comment.formatted_content
|
265
|
+
# end
|
266
|
+
def child!(value = BLANK, *args, &block)
|
267
|
+
if block
|
268
|
+
if _eachable_arguments?(value, *args)
|
269
|
+
# json.child! comments { |c| ... }
|
270
|
+
_scope { array!(value, &block) }
|
271
|
+
else
|
272
|
+
# json.child! { ... }
|
273
|
+
# [...]
|
274
|
+
_scope(&block)
|
275
|
+
end
|
276
|
+
elsif args.empty?
|
277
|
+
value!(value)
|
278
|
+
elsif _eachable_arguments?(value, *args)
|
279
|
+
_scope{ array!(value, *args) }
|
280
|
+
else
|
281
|
+
object!{ extract!(value, *args) }
|
282
|
+
end
|
283
|
+
|
284
|
+
end
|
285
|
+
|
286
|
+
# Encodes the current builder as JSON.
|
287
|
+
def target!
|
288
|
+
@encoder.flush
|
289
|
+
|
290
|
+
if @encoder.output.is_a?(::StringIO)
|
291
|
+
@encoder.output.string
|
292
|
+
else
|
293
|
+
@encoder.output
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
private
|
298
|
+
|
299
|
+
def _write(key, value)
|
300
|
+
@encoder.key(_key(key))
|
301
|
+
@encoder.value(value)
|
302
|
+
end
|
303
|
+
|
304
|
+
def _key(key)
|
305
|
+
@key_formatter ? @key_formatter.format(key) : key.to_s
|
306
|
+
end
|
307
|
+
|
308
|
+
def _set_value(key, value)
|
309
|
+
return if _blank?(value)
|
310
|
+
_write key, value
|
311
|
+
end
|
312
|
+
|
313
|
+
def _capture(to=nil, &block)
|
314
|
+
@encoder.capture(to, &block)
|
315
|
+
end
|
316
|
+
|
317
|
+
def _scope
|
318
|
+
parent_formatter = @key_formatter
|
319
|
+
yield
|
320
|
+
ensure
|
321
|
+
@key_formatter = parent_formatter
|
322
|
+
end
|
323
|
+
|
324
|
+
def _eachable_arguments?(value, *args)
|
325
|
+
value.respond_to?(:each) && !value.is_a?(Hash)
|
326
|
+
end
|
327
|
+
|
328
|
+
def _blank?(value=@attributes)
|
329
|
+
BLANK == value
|
330
|
+
end
|
331
|
+
|
332
|
+
end
|
333
|
+
|
334
|
+
|
335
|
+
require 'turbostreamer/railtie' if defined?(Rails)
|
metadata
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: turbostreamer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.0'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jon Bracy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: wankel
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.11'
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 1.11.2
|
65
|
+
type: :development
|
66
|
+
prerelease: false
|
67
|
+
version_requirements: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - "~>"
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '1.11'
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 1.11.2
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: mocha
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: simplecov
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: byebug
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: actionview
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: actionpack
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
description:
|
146
|
+
email:
|
147
|
+
- jonbracy@gmail.com
|
148
|
+
executables: []
|
149
|
+
extensions: []
|
150
|
+
extra_rdoc_files:
|
151
|
+
- README.md
|
152
|
+
files:
|
153
|
+
- README.md
|
154
|
+
- ext/actionview/buffer.rb
|
155
|
+
- ext/actionview/streaming_template_renderer.rb
|
156
|
+
- lib/turbostreamer.rb
|
157
|
+
- lib/turbostreamer/dependency_tracker.rb
|
158
|
+
- lib/turbostreamer/encoders/wankel.rb
|
159
|
+
- lib/turbostreamer/handler.rb
|
160
|
+
- lib/turbostreamer/key_formatter.rb
|
161
|
+
- lib/turbostreamer/railtie.rb
|
162
|
+
- lib/turbostreamer/template.rb
|
163
|
+
- lib/turbostreamer/version.rb
|
164
|
+
homepage: https://github.com/malomalo/turbostreamer
|
165
|
+
licenses:
|
166
|
+
- MIT
|
167
|
+
metadata: {}
|
168
|
+
post_install_message:
|
169
|
+
rdoc_options:
|
170
|
+
- "--main"
|
171
|
+
- README.md
|
172
|
+
require_paths:
|
173
|
+
- lib
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
175
|
+
requirements:
|
176
|
+
- - ">="
|
177
|
+
- !ruby/object:Gem::Version
|
178
|
+
version: '0'
|
179
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
180
|
+
requirements:
|
181
|
+
- - ">="
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: '0'
|
184
|
+
requirements: []
|
185
|
+
rubyforge_project:
|
186
|
+
rubygems_version: 2.6.11
|
187
|
+
signing_key:
|
188
|
+
specification_version: 4
|
189
|
+
summary: Stream JSON via a Builder-style DSL
|
190
|
+
test_files: []
|