json-emitter 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d39d9a362efddd2cb39412926290328feb042228099e2b737d6d8450fad28cdd
4
- data.tar.gz: ff8a418af3119910c46d78320e8b4dcac3cd6c0282202e1bae970c56fadf3e12
3
+ metadata.gz: 5ffd5b387bbbf0ce7f40a9b725dbaaea83a479ac53ada2ede1f01231c5be28e1
4
+ data.tar.gz: c0af5bb60b80c67da129f2ba14fea69a99c97ba69a47e508de165ebb12bf11e7
5
5
  SHA512:
6
- metadata.gz: b5cc83b5e2018d7f9b05f98f3cbfa9ab3f45176ff96e121f4336303466abdfb1836db673c5b567ae3579de7cdf9bf3d95b7879488ef35a25d34422d11177c1ed
7
- data.tar.gz: 33cc0cf57bedd84fda214c19859a32289c31d08e5e1d587d55485e73b5dab81030cd4fc12b14ed0c6e8234238821492fc694e603ad4d7ee8193f479de867ad6c
6
+ metadata.gz: 6ffb66b5cae058582b8d0904b6ae55272b7065ea6f412a2ddc7ab6db89a030f1e13c7801894e672f182c2da0b72d9e7211b9751f500f05fa91c8356679416403
7
+ data.tar.gz: e7a855f40029fbd7343cd4bfebf0e59594d81257fd2d0a232b8bf9cee4d0ac82210c8fa8907f6c5ae0eeaf4ee3acb68ca5b58adaa9a7525c1c094ac8f67e6d35
data/README.md CHANGED
@@ -1,106 +1,99 @@
1
- ## JsonEmitter
1
+ # JsonEmitter
2
2
 
3
3
  JsonEmitter is a library for efficiently generating very large bits of JSON in Ruby. Need to generate a JSON array of 10,000 database records without eating up all your RAM? No problem! Objects? Nested structures? JsonEmitter has you covered.
4
4
 
5
5
  Use JsonEmitter in your Rack/Rails/Sinatra/Grape API to stream large JSON responses without worrying about RAM or HTTP timeouts. Use it to write large JSON objects to your filesystem, S3, or ~~a 3D printer~~ anywhere else!
6
6
 
7
- **Stream a JSON array from ActiveRecord**
7
+ # HTTP Chunked Responses
8
+
9
+ These examples will use the Order enumerator to generate chunks of JSON and send them to the client as more chunks are generated. No more than 500 orders will be in memory at a time, regardless of how many orders there are. And only small portions of the JSON will be in memory at once, no matter how much we're generating.
8
10
 
9
11
  ```ruby
10
- order_query = Order.limit(10_000).find_each(batch_size: 500)
11
- stream = JsonEmitter.array(order_query) { |order|
12
- {
13
- number: order.id,
14
- desc: order.description,
15
- ...
16
- }
17
- }
12
+ enumerator = Order.
13
+ where("created_at >= ?", 1.year.ago).
14
+ find_each(batch_size: 500)
18
15
  ```
19
16
 
20
- **Stream a JSON object**
17
+ **Rails**
21
18
 
22
19
  ```ruby
23
- order_query = Order.limit(10_000).find_each(batch_size: 500)
24
- stream = JsonEmitter.object({
25
- tuesday: false,
26
-
27
- orders: order_query.lazy.map { |order|
28
- {id: order.id, desc: order.description}
29
- }
30
-
31
- big_text_1: ->() {
32
- load_tons_of_text
33
- },
34
-
35
- big_text_2: ->() {
36
- load_tons_of_text
37
- },
38
- })
20
+ class OrdersController < ApplicationController
21
+ def index
22
+ headers["Content-Type"] = "application/json"
23
+ headers["Last-Modified"] = Time.now.ctime.to_s
24
+ self.response_body = JsonEmitter.array(enumerator) { |order|
25
+ order.to_h
26
+ }
27
+ end
28
+ end
39
29
  ```
40
30
 
41
- **Generate the JSON and put it somewhere**
31
+ **Sinatra**
42
32
 
43
33
  ```ruby
44
- # write to a file or any IO
45
- File.open("/tmp/foo.json", "w+") { |file|
46
- stream.write file
47
- }
48
-
49
- # get chunks and do something with them
50
- stream.each { |json_chunk|
51
- ...
52
- }
34
+ get "/orders" do
35
+ content_type :json
36
+ JsonEmitter.array(enumerator) { |order|
37
+ order.to_h
38
+ }
39
+ end
53
40
  ```
54
41
 
55
- # HTTP Chunked Transfer (a.k.a streaming)
56
-
57
- In HTTP 1.0 the entire response is normally sent all at once. Usually this is fine, but it can cause problems when very large responses must be generated and sent. These problems usually manifest as spikes in memory usage and/or responses that take so long to send that the client (or an in-between proxy) times out the request.
58
-
59
- The solution to this in HTTP 1.1 is chunked transfer encoding. The response body can be split up and sent in a series of separate "chunks" for the client to receive and automatically put back together. Ruby's Rack specification supports chunking, as do most frameworks based on it (e.g. Rails, Sinatra, Grape, etc).
60
-
61
- The following examples all show the same streaming API in various Rack-based frameworks. Without streaming, the examples could eat up tons of memory, take too long, and time out on the client. With streaming, the following improvements are possible without your client-side code needing any changes:
62
-
63
- 1. Only 500 orders will ever be in memory at once.
64
- 2. Only one `ApiV1::Entities::Order` will ever be in memory at once.
65
- 3. Only 16kb (roughly) of JSON will ever be in memory at once.
66
- 5. That 16kb of JSON will be sent to the client while the next 16kb of JSON is generating.
67
-
68
- **IMPORTANT** Not every Ruby application server supports HTTP chunking. Puma definitely supports it and WEBrick definitely does not. Phusion Passenger claims to but I have not tried it.
69
-
70
- ## Rails
71
-
72
- TODO
73
-
74
- ## Sinatra
75
-
76
- TODO
77
-
78
- ## Grape
42
+ **Grape**
79
43
 
80
44
  ```ruby
81
45
  get :orders do
82
- enumerator = Order.
83
- where("created_at >= ?", 1.year.ago).
84
- find_each(batch_size: 500)
85
-
86
46
  stream JsonEmitter.array(enumerator) { |order|
87
47
  ApiV1::Entities::Order.new(order)
88
48
  }
89
49
  end
90
50
  ```
91
51
 
92
- ## Rack
52
+ **Rack**
93
53
 
94
54
  ```ruby
95
55
  app = ->(env) {
96
- enumerator = Order.
97
- where("created_at >= ?", 1.year.ago).
98
- find_each(batch_size: 500)
99
-
100
56
  stream = JsonEmitter.array(enumerator) { |order|
101
57
  order.to_h
102
58
  }
103
-
104
59
  [200, {"Content-Type" => "application/json"}, stream]
105
60
  }
106
61
  ```
62
+
63
+ # Other uses
64
+
65
+ `JsonEmitter.array` takes an `Enumerable` and returns a stream that generates chunks of JSON.
66
+
67
+ ```ruby
68
+ JsonEmitter.array(enumerator).each { |json_chunk|
69
+ # write json_chunk somewhere
70
+ }
71
+ ```
72
+
73
+ `JsonEmitter.object` takes a `Hash` and returns a stream that generates chunks of JSON.
74
+
75
+ ```ruby
76
+ JsonEmitter.object({
77
+ orders: Order.find_each.lazy.map { |order|
78
+ {id: order.id, desc: order.description}
79
+ },
80
+
81
+ big_text_1: ->() {
82
+ load_tons_of_text
83
+ },
84
+
85
+ big_text_2: ->() {
86
+ load_tons_of_text
87
+ },
88
+ }).each { |json_chunk|
89
+ # write json_chunk somewhere
90
+ }
91
+ ```
92
+
93
+ Streams have a `#write` method for writing directly to a `File` or `IO` object.
94
+
95
+ ```ruby
96
+ File.open("~/out.json", "w+") { |f|
97
+ JsonEmitter.array(enumerator).write f
98
+ }
99
+ ```
data/lib/json-emitter.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'multi_json'
2
2
 
3
3
  require 'json-emitter/version'
4
+ require 'json-emitter/context'
4
5
  require 'json-emitter/emitter'
5
6
  require 'json-emitter/stream'
6
7
  require 'json-emitter/buffered_stream'
@@ -24,7 +25,7 @@ module JsonEmitter
24
25
  @error_handlers = []
25
26
 
26
27
  #
27
- # Generates an stream that will output a JSON array. The input can be any Enumerable, such as an Array or an Enumerator.
28
+ # Generates a stream that will output a JSON array. The input can be any Enumerable, such as an Array or an Enumerator.
28
29
  #
29
30
  # The following example uses minumum memory to genrate a very large JSON array string from an ActiveRecord query.
30
31
  # Only 500 Order records will ever by in memory at once. The JSON will be generated in small chunks so that
@@ -49,7 +50,7 @@ module JsonEmitter
49
50
  #
50
51
  # @param enum [Enumerable] Something that can be enumerated over, like an Array or Enumerator. Each element should be something that can be rendered as JSON (e.g. a number, string, boolean, Array, or Hash).
51
52
  # @param buffer_size [Integer] The buffer size in kb. This is a size *hint*, not a hard limit.
52
- # @param unit [Symbol] :bytes | :kb (default) | :mb
53
+ # @param buffer_unit [Symbol] :bytes | :kb (default) | :mb
53
54
  # @yield If a block is given, it will be yielded each value in the array. The return value from the block will be converted to JSON instead of the original value.
54
55
  # @return [JsonEmitter::BufferedStream]
55
56
  #
@@ -59,7 +60,7 @@ module JsonEmitter
59
60
  end
60
61
 
61
62
  #
62
- # Generates an stream that will output a JSON object.
63
+ # Generates a stream that will output a JSON object.
63
64
  #
64
65
  # If some of the values will be large arrays, use Enumerators or lazy Enumerators to build each element on demand
65
66
  # (to potentially save lots of RAM).
@@ -93,7 +94,7 @@ module JsonEmitter
93
94
  #
94
95
  # @param hash [Hash] Keys should be Strings or Symbols and values should be any JSON-compatible value like a number, string, boolean, Array, or Hash.
95
96
  # @param buffer_size [Integer] The buffer size in kb. This is a size *hint*, not a hard limit.
96
- # @param unit [Symbol] :bytes | :kb (default) | :mb
97
+ # @param buffer_unit [Symbol] :bytes | :kb (default) | :mb
97
98
  # @return [JsonEmitter::BufferedStream]
98
99
  #
99
100
  def self.object(hash, buffer_size: 16, buffer_unit: :kb)
@@ -0,0 +1,45 @@
1
+ module JsonEmitter
2
+ #
3
+ # By using JsonEmitter.wrap and JsonEmitter.error you can wrap your streams within a special "context".
4
+ #
5
+ # This is probably most useful when your stream is being consumed by Rack/Rails/Sinatra/Grape/etc. Your
6
+ # app is probably depending on certain Rack middlewars to provide before/after behavior and error handling.
7
+ # All those middlewares will over by the time Rack consumes your stream, but you can use JsonEmitter.wrap
8
+ # and JsonEmitter.error to add critical behavior back in.
9
+ #
10
+ class Context
11
+ def initialize
12
+ @wrappers = JsonEmitter.wrappers.map(&:call).compact
13
+ @error_handlers = JsonEmitter.error_handlers
14
+ @pass_through_errors = []
15
+ @pass_through_errors << Puma::ConnectionError if defined? Puma::ConnectionError
16
+ end
17
+
18
+ # Wrap the enumeration in a block. It will be passed a callback which it must call to continue.
19
+ # TODO better docs and examples.
20
+ def wrap(&block)
21
+ if (wrapper = block.call)
22
+ @wrappers.unshift wrapper
23
+ end
24
+ end
25
+
26
+ # Add an error handler.
27
+ # TODO better docs and examples.
28
+ def error(&handler)
29
+ @error_handlers += [handler]
30
+ end
31
+
32
+ # Execute a block within this context.
33
+ def execute(&inner)
34
+ @wrappers.reduce(inner) { |f, outer_wrapper|
35
+ ->() { outer_wrapper.call(f) }
36
+ }.call
37
+
38
+ rescue *@pass_through_errors => e
39
+ raise e
40
+ rescue => e
41
+ @error_handlers.each { |h| h.call(e) }
42
+ raise e
43
+ end
44
+ end
45
+ end
@@ -3,11 +3,11 @@ module JsonEmitter
3
3
  # Builds Enumerators that yield JSON from Ruby Arrays or Hashes.
4
4
  #
5
5
  class Emitter
6
+ # @return [JsonEmitter::Context]
7
+ attr_reader :context
8
+
6
9
  def initialize
7
- @wrappers = JsonEmitter.wrappers.map(&:call).compact
8
- @error_handlers = JsonEmitter.error_handlers
9
- @pass_through_errors = []
10
- @pass_through_errors << Puma::ConnectionError if defined? Puma::ConnectionError
10
+ @context = Context.new
11
11
  end
12
12
 
13
13
  #
@@ -19,7 +19,7 @@ module JsonEmitter
19
19
  #
20
20
  def array(enum, &mapper)
21
21
  Enumerator.new { |y|
22
- wrapped {
22
+ context.execute {
23
23
  array_generator(enum, &mapper).each { |json_val|
24
24
  y << json_val
25
25
  }
@@ -35,7 +35,7 @@ module JsonEmitter
35
35
  #
36
36
  def object(hash)
37
37
  Enumerator.new { |y|
38
- wrapped {
38
+ context.execute {
39
39
  object_generator(hash).each { |json_val|
40
40
  y << json_val
41
41
  }
@@ -43,20 +43,6 @@ module JsonEmitter
43
43
  }
44
44
  end
45
45
 
46
- # Wrap the enumeration in a block. It will be passed a callback which it must call to continue.
47
- # TODO better docs and examples.
48
- def wrap(&block)
49
- if (wrapper = block.call)
50
- @wrappers.unshift wrapper
51
- end
52
- end
53
-
54
- # Add an error handler.
55
- # TODO better docs and examples.
56
- def error(&handler)
57
- @error_handlers += [handler]
58
- end
59
-
60
46
  private
61
47
 
62
48
  def array_generator(enum, &mapper)
@@ -112,17 +98,5 @@ module JsonEmitter
112
98
  [MultiJson.dump(x)]
113
99
  end
114
100
  end
115
-
116
- def wrapped(&final)
117
- @wrappers.reduce(final) { |f, outer_wrapper|
118
- ->() { outer_wrapper.call(f) }
119
- }.call
120
-
121
- rescue *@pass_through_errors => e
122
- raise e
123
- rescue => e
124
- @error_handlers.each { |h| h.call(e) }
125
- raise e
126
- end
127
101
  end
128
102
  end
@@ -1,4 +1,4 @@
1
1
  module JsonEmitter
2
2
  # Library version
3
- VERSION = "0.0.3".freeze
3
+ VERSION = "0.0.4".freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json-emitter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Hollinger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-30 00:00:00.000000000 Z
11
+ date: 2019-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json
@@ -33,6 +33,7 @@ files:
33
33
  - README.md
34
34
  - lib/json-emitter.rb
35
35
  - lib/json-emitter/buffered_stream.rb
36
+ - lib/json-emitter/context.rb
36
37
  - lib/json-emitter/emitter.rb
37
38
  - lib/json-emitter/stream.rb
38
39
  - lib/json-emitter/version.rb