turbostreamer 1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://api.travis-ci.org/malomalo/turbostreamer.svg)](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: []
|