aws-eventstream 1.0.0

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