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 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,3 @@
1
+ class TurboStreamer
2
+ VERSION = '1.0'
3
+ 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: []