aws-eventstream 1.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0bd89e470bb670750b5db3fa159c05c47f6ae848
4
+ data.tar.gz: 0e6a31068166775645f803c36f2aa27c7b7341ce
5
+ SHA512:
6
+ metadata.gz: 475cdba6288c5ba3224a65d18f9b3d16feba99d2b9868043af4c01194adeeb55f686200cac511e3067465ffe8b7cfa93dbe306751f31f6ce93966d5b052f4b6c
7
+ data.tar.gz: b96b77164bc0e6e1f9095de872400f9a589f912d450f6cdb0c40c3b769888f38f90be9b572b09f2715f1358ba43b71d06842d95c628108e57bdd1c5201aba3be
@@ -0,0 +1,8 @@
1
+ require_relative 'aws-eventstream/decoder'
2
+ require_relative 'aws-eventstream/encoder'
3
+
4
+ require_relative 'aws-eventstream/bytes_buffer'
5
+ require_relative 'aws-eventstream/message'
6
+ require_relative 'aws-eventstream/header_value'
7
+ require_relative 'aws-eventstream/types'
8
+ require_relative 'aws-eventstream/errors'
@@ -0,0 +1,66 @@
1
+ module Aws
2
+ module EventStream
3
+
4
+ # @api private
5
+ class BytesBuffer
6
+
7
+ # This Util class is for Decoder/Encoder usage only
8
+ # Not for public common bytes buffer usage
9
+ def initialize(data)
10
+ @data = data
11
+ @pos = 0
12
+ end
13
+
14
+ def read(len = nil, offset = 0)
15
+ return '' if len == 0 || bytesize == 0
16
+ unless eof?
17
+ start_byte = @pos + offset
18
+ end_byte = len ?
19
+ start_byte + len - 1 :
20
+ bytesize - 1
21
+
22
+ error = Errors::ReadBytesExceedLengthError.new(end_byte, bytesize)
23
+ raise error if end_byte >= bytesize
24
+
25
+ @pos = end_byte + 1
26
+ @data[start_byte..end_byte]
27
+ end
28
+ end
29
+
30
+ def readbyte
31
+ unless eof?
32
+ @pos += 1
33
+ @data[@pos - 1]
34
+ end
35
+ end
36
+
37
+ def write(bytes)
38
+ @data <<= bytes
39
+ bytes.bytesize
40
+ end
41
+ alias_method :<<, :write
42
+
43
+ def rewind
44
+ @pos = 0
45
+ end
46
+
47
+ def eof?
48
+ @pos == bytesize
49
+ end
50
+
51
+ def bytesize
52
+ @data.bytesize
53
+ end
54
+
55
+ def tell
56
+ @pos
57
+ end
58
+
59
+ def clear!
60
+ @data = ''
61
+ @pos = 0
62
+ end
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,247 @@
1
+ require 'stringio'
2
+ require 'tempfile'
3
+ require 'zlib'
4
+
5
+ module Aws
6
+ module EventStream
7
+
8
+ # This class provides method for decoding binary inputs into
9
+ # single or multiple messages (Aws::EventStream::Message).
10
+ #
11
+ # * {#decode} - decodes messages from an IO like object responds
12
+ # to #read that containing binary data, returning decoded
13
+ # Aws::EventStream::Message along the way or wrapped in an enumerator
14
+ #
15
+ # ## Examples
16
+ #
17
+ # decoder = Aws::EventStream::Decoder.new
18
+ #
19
+ # # decoding from IO
20
+ # decoder.decode(io) do |message|
21
+ # message.headers
22
+ # # => { ... }
23
+ # message.payload
24
+ # # => StringIO / Tempfile
25
+ # end
26
+ #
27
+ # # alternatively
28
+ # message_pool = decoder.decode(io)
29
+ # message_pool.next
30
+ # # => Aws::EventStream::Message
31
+ #
32
+ # * {#decode_chunk} - decodes a single message from a chunk of data,
33
+ # returning message object followed by boolean(indicating eof status
34
+ # of data) in an array object
35
+ #
36
+ # ## Examples
37
+ #
38
+ # # chunk containing exactly one message data
39
+ # message, chunk_eof = decoder.decode_chunk(chunk_str)
40
+ # message
41
+ # # => Aws::EventStream::Message
42
+ # chunk_eof
43
+ # # => true
44
+ #
45
+ # # chunk containing a partial message
46
+ # message, chunk_eof = decoder.decode_chunk(chunk_str)
47
+ # message
48
+ # # => nil
49
+ # chunk_eof
50
+ # # => true
51
+ # # chunk data is saved at decoder's message_buffer
52
+ #
53
+ # # chunk containing more that one data message
54
+ # message, chunk_eof = decoder.decode_chunk(chunk_str)
55
+ # message
56
+ # # => Aws::EventStream::Message
57
+ # chunk_eof
58
+ # # => false
59
+ # # extra chunk data is saved at message_buffer of the decoder
60
+ #
61
+ class Decoder
62
+
63
+ include Enumerable
64
+
65
+ ONE_MEGABYTE = 1024 * 1024
66
+
67
+ # bytes of prelude part, including 4 bytes of
68
+ # total message length, headers length and crc checksum of prelude
69
+ PRELUDE_LENGTH = 12
70
+
71
+ # bytes of total overhead in a message, including prelude
72
+ # and 4 bytes total message crc checksum
73
+ OVERHEAD_LENGTH = 16
74
+
75
+ # @options options [Boolean] format (true) When `false`
76
+ # disable user-friendly formatting for message header values
77
+ # including timestamp and uuid etc.
78
+ #
79
+ def initialize(options = {})
80
+ @format = options.fetch(:format, true)
81
+ @message_buffer = BytesBuffer.new('')
82
+ end
83
+
84
+ # @returns [BytesBuffer]
85
+ attr_reader :message_buffer
86
+
87
+ # Decodes messages from a binary stream
88
+ #
89
+ # @param [IO#read] io An IO-like object
90
+ # that responds to `#read`
91
+ #
92
+ # @yieldparam [Message] message
93
+ # @return [Enumerable<Message>, nil] Returns a new Enumerable
94
+ # containing decoded messages if no block is given
95
+ def decode(io, &block)
96
+ io = BytesBuffer.new(io.read)
97
+ return decode_io(io) unless block_given?
98
+ until io.eof?
99
+ # fetch message only
100
+ yield(decode_message(io).first)
101
+ end
102
+ end
103
+
104
+ # Decodes a single message from a chunk of string
105
+ #
106
+ # @param [String] chunk A chunk of string to be decoded,
107
+ # chunk can contain partial event message to multiple event messages
108
+ # When not provided, decode data from #message_buffer
109
+ #
110
+ # @return [Array<Message|nil, Boolean>] Returns single decoded message
111
+ # and boolean pair, the boolean flag indicates whether this chunk
112
+ # has been fully consumed, unused data is tracked at #message_buffer
113
+ def decode_chunk(chunk = nil)
114
+ @message_buffer.write(chunk) if chunk
115
+ @message_buffer.rewind
116
+ decode_message(@message_buffer)
117
+ end
118
+
119
+ private
120
+
121
+ def decode_io(io)
122
+ ::Enumerator.new {|e| e << decode_message(io) unless io.eof? }
123
+ end
124
+
125
+ def decode_message(io)
126
+ # decode prelude
127
+ total_len, headers_len, prelude_buffer = prelude(io)
128
+
129
+ # incomplete message received, leave it in the buffer
130
+ return [nil, true] if io.bytesize < total_len
131
+
132
+ # decode headers and payload
133
+ headers, payload = context(io, total_len, headers_len, prelude_buffer)
134
+
135
+ # track extra message data in the buffer if exists
136
+ # for #decode_chunk, io is @message_buffer
137
+ if eof = io.eof?
138
+ @message_buffer.clear!
139
+ else
140
+ @message_buffer = BytesBuffer.new(@message_buffer.read)
141
+ end
142
+
143
+ [Message.new(headers: headers, payload: payload), eof]
144
+ end
145
+
146
+ def prelude(io)
147
+ # buffer prelude into bytes buffer
148
+ # prelude contains length of message and headers,
149
+ # followed with CRC checksum of itself
150
+ buffer = BytesBuffer.new(io.read(PRELUDE_LENGTH))
151
+
152
+ # prelude checksum takes last 4 bytes
153
+ checksum = Zlib.crc32(buffer.read(PRELUDE_LENGTH - 4))
154
+ unless checksum == unpack_uint32(buffer)
155
+ raise Errors::PreludeChecksumError
156
+ end
157
+
158
+ buffer.rewind
159
+ total_len, headers_len, _ = buffer.read.unpack("N*")
160
+ [total_len, headers_len, buffer]
161
+ end
162
+
163
+ def context(io, total_len, headers_len, prelude_buffer)
164
+ # buffer rest of the message except prelude length
165
+ # including context and total message checksum
166
+ buffer = BytesBuffer.new(io.read(total_len - PRELUDE_LENGTH))
167
+ context_len = total_len - OVERHEAD_LENGTH
168
+
169
+ prelude_buffer.rewind
170
+ checksum = Zlib.crc32(prelude_buffer.read << buffer.read(context_len))
171
+ unless checksum == unpack_uint32(buffer)
172
+ raise Errors::MessageChecksumError
173
+ end
174
+
175
+ buffer.rewind
176
+ [
177
+ extract_headers(BytesBuffer.new(buffer.read(headers_len))),
178
+ extract_payload(BytesBuffer.new(buffer.read(context_len - headers_len)))
179
+ ]
180
+ end
181
+
182
+ def extract_headers(buffer)
183
+ headers = {}
184
+ until buffer.eof?
185
+ # header key
186
+ key_len = unpack_uint8(buffer)
187
+ key = buffer.read(key_len)
188
+
189
+ # header value
190
+ value_type = Types.types[unpack_uint8(buffer)]
191
+ unpack_pattern, value_len, _ = Types.pattern[value_type]
192
+ if !!unpack_pattern == unpack_pattern
193
+ # boolean types won't have value specified
194
+ value = unpack_pattern
195
+ else
196
+ value_len = unpack_uint16(buffer) unless value_len
197
+ value = unpack_pattern ?
198
+ buffer.read(value_len).unpack(unpack_pattern)[0] :
199
+ buffer.read(value_len)
200
+ end
201
+
202
+ headers[key] = HeaderValue.new(
203
+ format: @format,
204
+ value: value,
205
+ type: value_type
206
+ )
207
+ end
208
+ headers
209
+ end
210
+
211
+ def extract_payload(buffer)
212
+ buffer.bytesize <= ONE_MEGABYTE ?
213
+ payload_stringio(buffer) :
214
+ payload_tempfile(buffer)
215
+ end
216
+
217
+ def payload_stringio(buffer)
218
+ StringIO.new(buffer.read)
219
+ end
220
+
221
+ def payload_tempfile(buffer)
222
+ payload = Tempfile.new
223
+ payload.binmode
224
+ until buffer.eof?
225
+ payload.write(buffer.read(ONE_MEGABYTE))
226
+ end
227
+ payload.rewind
228
+ payload
229
+ end
230
+
231
+ # overhead decode helpers
232
+
233
+ def unpack_uint32(buffer)
234
+ buffer.read(4).unpack("N")[0]
235
+ end
236
+
237
+ def unpack_uint16(buffer)
238
+ buffer.read(2).unpack("S>")[0]
239
+ end
240
+
241
+ def unpack_uint8(buffer)
242
+ buffer.readbyte.unpack("C")[0]
243
+ end
244
+ end
245
+
246
+ end
247
+ end
@@ -0,0 +1,136 @@
1
+ require 'zlib'
2
+
3
+ module Aws
4
+ module EventStream
5
+
6
+ # This class provides #encode method for encoding
7
+ # Aws::EventStream::Message into binary.
8
+ #
9
+ # * {#encode} - encode Aws::EventStream::Message into binary
10
+ # when output IO-like object is provided, binary string
11
+ # would be written to IO. If not, the encoded binary string
12
+ # would be returned directly
13
+ #
14
+ # ## Examples
15
+ #
16
+ # message = Aws::EventStream::Message.new(
17
+ # headers: {
18
+ # "foo" => Aws::EventStream::HeaderValue.new(
19
+ # value: "bar", type: "string"
20
+ # )
21
+ # },
22
+ # payload: "payload"
23
+ # )
24
+ # encoder = Aws::EventsStream::Encoder.new
25
+ # file = Tempfile.new
26
+ #
27
+ # # encode into IO ouput
28
+ # encoder.encode(message, file)
29
+ #
30
+ # # get encoded binary string
31
+ # encoded_message = encoder.encode(message)
32
+ #
33
+ # file.read == encoded_message
34
+ # # => true
35
+ #
36
+ class Encoder
37
+
38
+ # bytes of total overhead in a message, including prelude
39
+ # and 4 bytes total message crc checksum
40
+ OVERHEAD_LENGTH = 16
41
+
42
+ # Encodes Aws::EventStream::Message to output IO when
43
+ # provided, else return the encoded binary string
44
+ #
45
+ # @param [Aws::EventStream::Message] message
46
+ #
47
+ # @param [IO#write, nil] io An IO-like object that
48
+ # responds to `#write`, encoded message will be
49
+ # written to this IO when provided
50
+ #
51
+ # @return [nil, String] when output IO is provided,
52
+ # encoded message will be written to that IO, nil
53
+ # will be returned. Else, encoded binary string is
54
+ # returned.
55
+ def encode(message, io = nil)
56
+ encoded = encode_message(message).read
57
+ if io
58
+ io.write(encoded)
59
+ io.close
60
+ else
61
+ encoded
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def encode_message(message)
68
+ # create context buffer with encode headers
69
+ ctx_buffer = encode_headers(message)
70
+ headers_len = ctx_buffer.bytesize
71
+ # encode payload
72
+ ctx_buffer << message.payload.read
73
+ total_len = ctx_buffer.bytesize + OVERHEAD_LENGTH
74
+
75
+ # create message buffer with prelude section
76
+ buffer = prelude(total_len, headers_len)
77
+
78
+ # append message context (headers, payload)
79
+ buffer << ctx_buffer.read
80
+ # append message checksum
81
+ buffer << pack_uint32(Zlib.crc32(buffer.read))
82
+
83
+ # write buffered message to io
84
+ buffer.rewind
85
+ buffer
86
+ end
87
+
88
+ def encode_headers(msg)
89
+ buffer = BytesBuffer.new('')
90
+ msg.headers.each do |k, v|
91
+ # header key
92
+ buffer << pack_uint8(k.bytesize)
93
+ buffer << k
94
+
95
+ # header value
96
+ pattern, val_len, idx = Types.pattern[v.type]
97
+ buffer << pack_uint8(idx)
98
+ # boolean types doesn't need to specify value
99
+ next if !!pattern == pattern
100
+ buffer << pack_uint16(v.value.bytesize) unless val_len
101
+ pattern ? buffer << [v.value].pack(pattern) :
102
+ buffer << v.value
103
+ end
104
+ buffer
105
+ end
106
+
107
+ def prelude(total_len, headers_len)
108
+ BytesBuffer.new(pack_uint32([
109
+ total_len,
110
+ headers_len,
111
+ Zlib.crc32(pack_uint32([total_len, headers_len]))
112
+ ]))
113
+ end
114
+
115
+ # overhead encode helpers
116
+
117
+ def pack_uint8(val)
118
+ [val].pack("C")
119
+ end
120
+
121
+ def pack_uint16(val)
122
+ [val].pack("S>")
123
+ end
124
+
125
+ def pack_uint32(val)
126
+ if val.respond_to?(:each)
127
+ val.pack("N*")
128
+ else
129
+ [val].pack("N")
130
+ end
131
+ end
132
+
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,35 @@
1
+ module Aws
2
+ module EventStream
3
+ module Errors
4
+
5
+ # Raised when reading bytes exceed buffer total bytes
6
+ class ReadBytesExceedLengthError < RuntimeError
7
+ def initialize(target_byte, total_len)
8
+ msg = "Attempting reading bytes to offset #{target_byte} exceeds"\
9
+ " buffer length of #{total_len}"
10
+ super(msg)
11
+ end
12
+ end
13
+
14
+ # Raise when insufficient bytes of a message is received
15
+ class IncompleteMessageError < RuntimeError
16
+ def initialize(*args)
17
+ super("Not enough bytes for event message")
18
+ end
19
+ end
20
+
21
+ class PreludeChecksumError < RuntimeError
22
+ def initialize(*args)
23
+ super("Prelude checksum mismatch")
24
+ end
25
+ end
26
+
27
+ class MessageChecksumError < RuntimeError
28
+ def initialize(*args)
29
+ super("Message checksum mismatch")
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,46 @@
1
+ module Aws
2
+ module EventStream
3
+
4
+ class HeaderValue
5
+
6
+ def initialize(options)
7
+ @type = options.fetch(:type)
8
+ @value = options[:format] ?
9
+ format_value(options.fetch(:value)) :
10
+ options.fetch(:value)
11
+ end
12
+
13
+ attr_reader :value
14
+
15
+ # @return [String] type of the header value
16
+ # complete type list see Aws::EventStream::Types
17
+ attr_reader :type
18
+
19
+ private
20
+
21
+ def format_value(value)
22
+ case @type
23
+ when "timestamp" then format_timestamp(value)
24
+ when "uuid" then format_uuid(value)
25
+ else
26
+ value
27
+ end
28
+ end
29
+
30
+ def format_uuid(value)
31
+ bytes = value.bytes
32
+ # For user-friendly uuid representation,
33
+ # format binary bytes into uuid string format
34
+ uuid_pattern = [ [ 3, 2, 1, 0 ], [ 5, 4 ], [ 7, 6 ], [ 8, 9 ], 10..15 ]
35
+ uuid_pattern.map {|p| p.map {|n| "%02x" % bytes.to_a[n] }.join }.join("-")
36
+ end
37
+
38
+ def format_timestamp(value)
39
+ # millis_since_epoch to sec_since_epoch
40
+ Time.at(value / 1000.0)
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ module Aws
2
+ module EventStream
3
+ class Message
4
+
5
+ def initialize(options)
6
+ @headers = options[:headers] || {}
7
+ @payload = options[:payload] || StringIO.new
8
+ end
9
+
10
+ # @return [Hash] headers of a message
11
+ attr_reader :headers
12
+
13
+ # @return [IO] payload of a message, size not exceed 16MB.
14
+ # StringIO is returned for <= 1MB payload
15
+ # Tempfile is returned for > 1MB payload
16
+ attr_reader :payload
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ module Aws
2
+ module EventStream
3
+
4
+ # Message Header Value Types
5
+ module Types
6
+
7
+ def self.types
8
+ [
9
+ "bool_true",
10
+ "bool_false",
11
+ "byte",
12
+ "short",
13
+ "integer",
14
+ "long",
15
+ "bytes",
16
+ "string",
17
+ "timestamp",
18
+ "uuid"
19
+ ]
20
+ end
21
+
22
+ # pack/unpack pattern, byte size, type idx
23
+ def self.pattern
24
+ {
25
+ "bool_true" => [true, 0, 0],
26
+ "bool_false" => [false, 0, 1],
27
+ "byte" => ["c", 1, 2],
28
+ "short" => ["s>", 2, 3],
29
+ "integer" => ["l>", 4, 4],
30
+ "long" => ["q>", 8, 5],
31
+ "bytes" => [nil, nil, 6],
32
+ "string" => [nil, nil, 7],
33
+ "timestamp" => ["q>", 8, 8],
34
+ "uuid" => [nil, 16, 9]
35
+ }
36
+ end
37
+
38
+ end
39
+ end
40
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aws-eventstream
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Amazon Web Services
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-05-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Amazon Web Services event stream library. Decodes and encodes binary
14
+ stream under `vnd.amazon.event-stream` content-type
15
+ email:
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/aws-eventstream.rb
21
+ - lib/aws-eventstream/bytes_buffer.rb
22
+ - lib/aws-eventstream/decoder.rb
23
+ - lib/aws-eventstream/encoder.rb
24
+ - lib/aws-eventstream/errors.rb
25
+ - lib/aws-eventstream/header_value.rb
26
+ - lib/aws-eventstream/message.rb
27
+ - lib/aws-eventstream/types.rb
28
+ homepage: http://github.com/aws/aws-sdk-ruby
29
+ licenses:
30
+ - Apache-2.0
31
+ metadata:
32
+ source_code_uri: https://github.com/aws/aws-sdk-ruby/tree/master/gems/aws-eventstream
33
+ changelog_uri: https://github.com/aws/aws-sdk-ruby/tree/master/gems/aws-eventstream/CHANGELOG.md
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 2.5.2.3
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: AWS Event Stream Library
54
+ test_files: []