turbostreamer 1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []