event_stream_parser 0.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
+ SHA256:
3
+ metadata.gz: 498814773bcffd664024e918c1f94e32393e6b8af266fe88084cadb4cd8c86d7
4
+ data.tar.gz: da45b31afcb41e23bb2fb4dbde9d0c5ed173d8fb8ac0759464129528b19e49bd
5
+ SHA512:
6
+ metadata.gz: 2123d8df807cb4cb1a3f1981f3bd3d59eec2393579d4c41bc7a4f597be6f827e285ef8cffb37abfd12d9266e69febe5be09ad79f7dff53ef288c950ffc077603
7
+ data.tar.gz: dbadc0f0bc184c0d08f8e6466f4384ce51baf25da1d97bbc90ba75a0d92f3a48468dc82882015f82d17d5f047d64625ddffe8897a51dd7b8e972a19b3d585fbb
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023-present, Shopify Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # event_stream_parser
2
+
3
+ A lightweight, fully spec-compliant parser for the
4
+ [event stream](https://www.w3.org/TR/eventsource/) format.
5
+
6
+ It only deals with the parsing of events and not any of the client/transport
7
+ aspects. This is not a Server-sent Events (SSE) client.
8
+
9
+ Under the hood, it's a stateful parser that receives chunks (that are received
10
+ from an HTTP client, for example) and emits events as it parses them. But it
11
+ remembers the last event id and reconnection time and keeps emitting them as
12
+ long as they are not overwritten by new ones.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'event_stream_parser'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```sh
25
+ bundle
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```sh
31
+ gem install event_stream_parser
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ Create a new Parser and give it a block to receive events:
37
+
38
+ ```rb
39
+ parser = EventStreamParser::Parser.new
40
+
41
+ parser.feed do |type, data, id, reconnection_time|
42
+ puts "Event type: #{type}"
43
+ puts "Event data: #{data}"
44
+ puts "Event id: #{id}"
45
+ puts "Reconnection time: #{reconnection_time}"
46
+ end
47
+ ```
48
+
49
+ Then, feed it chunks as they come in:
50
+
51
+ ```rb
52
+ do_something_that_yields_chunks do { |chunk| parser.feed(chunk) }
53
+ ```
54
+
55
+ Or use the `stream` method to generate a proc that you can pass to a chunk
56
+ producer:
57
+
58
+ ```rb
59
+ parser_stream = parser.stream do |type, data, id, reconnection_time|
60
+ puts "Event type: #{type}"
61
+ puts "Event data: #{data}"
62
+ puts "Event id: #{id}"
63
+ puts "Reconnection time: #{reconnection_time}"
64
+ end
65
+
66
+ do_something_that_yields_chunks(&parser_stream)
67
+ ```
68
+
69
+ ## Development
70
+
71
+ After checking out the repo:
72
+
73
+ 1. Run `bundle` to install dependencies.
74
+ 2. Run `rake test` to run the tests.
75
+ 3. Run `rubocop` to run Rubocop.
76
+
77
+ To install this gem onto your local machine, run `bundle exec rake install`. To
78
+ release a new version, update the version number in `version.rb`, and then run
79
+ `bundle exec rake release`, which will create a git tag for the version, push
80
+ git commits and tags, and push the `.gem` file to
81
+ [rubygems.org](https://rubygems.org).
82
+
83
+ ## Contributing
84
+
85
+ Bug reports and pull requests are welcome on GitHub at
86
+ https://github.com/Shopify/event_stream_parser. This project is intended to be a
87
+ safe, welcoming space for collaboration, and contributors are expected to adhere
88
+ to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. Read more about contributing [here](https://github.com/Shopify/event_stream_parser/blob/main/CONTRIBUTING.md).
89
+
90
+ ## License
91
+
92
+ The gem is available as open source under the terms of the
93
+ [MIT License](https://opensource.org/licenses/MIT).
94
+
95
+ ## Code of Conduct
96
+
97
+ Everyone interacting in this repository is expected to follow the
98
+ [Code of Conduct](https://github.com/Shopify/event_stream_parser/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventStreamParser
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "event_stream_parser/version"
4
+
5
+ module EventStreamParser
6
+ ##
7
+ # Implements a spec-compliant event stream parser, following:
8
+ #
9
+ # https://html.spec.whatwg.org/multipage/server-sent-events.html
10
+ # Section: 9.2.6 Interpreting an event stream
11
+ #
12
+ # Code comments are copied from the spec.
13
+ #
14
+ class Parser
15
+ UTF_8_BOM = [0xEF, 0xBB, 0xBF].pack("C*").force_encoding("UTF-8").freeze
16
+
17
+ def initialize
18
+ ##
19
+ # When a stream is parsed, a data buffer, an event type buffer, and a last
20
+ # event ID buffer must be associated with it. They must be initialized to
21
+ # the empty string.
22
+ #
23
+ @data_buffer = +""
24
+ @event_type_buffer = +""
25
+ @last_event_id_buffer = +""
26
+
27
+ @reconnection_time = nil
28
+ @buffer = +""
29
+ @first_chunk = true
30
+ @last_delimiter = nil
31
+ end
32
+
33
+ def feed(chunk, &proc)
34
+ ##
35
+ # The UTF-8 decode algorithm strips one leading UTF-8 Byte Order Mark (BOM),
36
+ # if any.
37
+ #
38
+ if @first_chunk
39
+ chunk = chunk.delete_prefix(UTF_8_BOM)
40
+ @first_chunk = false
41
+ end
42
+
43
+ @buffer << chunk
44
+
45
+ ##
46
+ # The stream must then be parsed by reading everything line by line, with a
47
+ # U+000D CARRIAGE RETURN U+000A LINE FEED (CRLF) character pair, a single
48
+ # U+000A LINE FEED (LF) character not preceded by a U+000D CARRIAGE RETURN
49
+ # (CR) character, and a single U+000D CARRIAGE RETURN (CR) character not
50
+ # followed by a U+000A LINE FEED (LF) character being the ways in which a
51
+ # line can end.
52
+ #
53
+ if @last_delimiter == "\r"
54
+ @buffer.delete_prefix!("\n")
55
+ end
56
+
57
+ while (line = @buffer.slice!(/.*?(?<delim>\r\n|\r|\n)/))
58
+ line.chomp!
59
+ @last_delimiter = $~[:delim]
60
+ process_line(line, &proc)
61
+ end
62
+ ##
63
+ # Once the end of the file is reached, any pending data must be discarded.
64
+ # (If the file ends in the middle of an event, before the final empty line,
65
+ # the incomplete event is not dispatched.)
66
+ #
67
+ end
68
+
69
+ def stream
70
+ proc { |chunk| feed(chunk) { |*args| yield(*args) } }
71
+ end
72
+
73
+ private
74
+
75
+ def process_line(line, &proc)
76
+ ##
77
+ # Lines must be processed, in the order they are received, as follows:
78
+ #
79
+ case line
80
+ ##
81
+ # If the line is empty (a blank line)
82
+ #
83
+ when ""
84
+ ##
85
+ # Dispatch the event, as defined below.
86
+ #
87
+ dispatch_event(&proc)
88
+ ##
89
+ # If the line starts with a U+003A COLON character (:)
90
+ #
91
+ when /^:/
92
+ ##
93
+ # Ignore the line.
94
+ #
95
+ ignore
96
+ ##
97
+ # If the line contains a U+003A COLON character (:)
98
+ #
99
+ # Collect the characters on the line before the first U+003A COLON character
100
+ # (:), and let field be that string.
101
+ #
102
+ # Collect the characters on the line after the first U+003A COLON character
103
+ # (:), and let value be that string. If value starts with a U+0020 SPACE
104
+ # character, remove it from value.
105
+ #
106
+ when /\A(?<field>[^:]+):\s?(?<value>.*)\z/
107
+ ##
108
+ # Process the field using the steps described below, using field as the
109
+ # field name and value as the field value.
110
+ #
111
+ process_field($~[:field], $~[:value])
112
+ ##
113
+ # Otherwise, the string is not empty but does not contain a U+003A COLON
114
+ # character (:)
115
+ #
116
+ else
117
+ ##
118
+ # Process the field using the steps described below, using the whole line
119
+ # as the field name, and the empty string as the field value.
120
+ #
121
+ process_field(line, "")
122
+ end
123
+ end
124
+
125
+ def process_field(field, value)
126
+ ##
127
+ # The steps to process the field given a field name and a field value depend
128
+ # on the field name, as given in the following list. Field names must be
129
+ # compared literally, with no case folding performed.
130
+ #
131
+ case field
132
+ ##
133
+ # If the field name is "event"
134
+ #
135
+ when "event"
136
+ ##
137
+ # Set the event type buffer to field value.
138
+ #
139
+ @event_type_buffer = value
140
+ ##
141
+ # If the field name is "data"
142
+ #
143
+ when "data"
144
+ ##
145
+ # Append the field value to the data buffer, then append a single U+000A
146
+ # LINE FEED (LF) character to the data buffer.
147
+ #
148
+ @data_buffer << value << "\n"
149
+ ##
150
+ # If the field name is "id"
151
+ #
152
+ when "id"
153
+ ##
154
+ # If the field value does not contain U+0000 NULL, then set the last event
155
+ # ID buffer to the field value. Otherwise, ignore the field.
156
+ #
157
+ @last_event_id_buffer = value unless value.include?("\u0000")
158
+ ##
159
+ # If the field name is "retry"
160
+ #
161
+ when "retry"
162
+ ##
163
+ # If the field value consists of only ASCII digits, then interpret the
164
+ # field value as an integer in base ten, and set the event stream's
165
+ # reconnection time to that integer. Otherwise, ignore the field.
166
+ #
167
+ @reconnection_time = value.to_i if /\A\d+\z/.match?(value)
168
+ ##
169
+ # Otherwise
170
+ #
171
+ else
172
+ ##
173
+ # The field is ignored.
174
+ #
175
+ ignore
176
+ end
177
+ end
178
+
179
+ def dispatch_event
180
+ ##
181
+ # When the user agent is required to dispatch the event, the user agent must
182
+ # process the data buffer, the event type buffer, and the last event ID
183
+ # buffer using steps appropriate for the user agent.
184
+ #
185
+ # For web browsers, the appropriate steps to dispatch the event are as
186
+ # follows:
187
+ #
188
+ # 1. Set the last event ID string of the event source to the value of the
189
+ # last event ID buffer. The buffer does not get reset, so the last event
190
+ # ID string of the event source remains set to this value until the next
191
+ # time it is set by the server.
192
+ #
193
+ # Note: If an event doesn't have an "id" field, but an earlier event did set
194
+ # the event source's last event ID string, then the event's lastEventId
195
+ # field will be set to the value of whatever the last seen "id" field was.
196
+ #
197
+ id = @last_event_id_buffer
198
+ ##
199
+ # 2. If the data buffer is an empty string, set the data buffer and the
200
+ # event type buffer to the empty string and return.
201
+ #
202
+ if @data_buffer.empty?
203
+ @data_buffer = +""
204
+ @event_type_buffer = +""
205
+ return
206
+ end
207
+ ##
208
+ # 3. If the data buffer's last character is a U+000A LINE FEED (LF)
209
+ # character, then remove the last character from the data buffer.
210
+ #
211
+ @data_buffer.chomp!
212
+ ##
213
+ # 5. Initialize event's type attribute to "message", its data attribute to
214
+ # data, ...
215
+ # 6. If the event type buffer has a value other than the empty string,
216
+ # change the type of the newly created event to equal the value of the
217
+ # event type buffer.
218
+ #
219
+ # For other user agents, the appropriate steps to dispatch the event are
220
+ # implementation dependent, but at a minimum they must set the data and
221
+ # event type buffers to the empty string before returning.
222
+ #
223
+ type = @event_type_buffer
224
+ data = @data_buffer
225
+ ##
226
+ # 7. Set the data buffer and the event type buffer to the empty string.
227
+ #
228
+ @data_buffer = +""
229
+ @event_type_buffer = +""
230
+
231
+ yield type, data, id, @reconnection_time
232
+ end
233
+
234
+ def ignore; end
235
+ end
236
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: event_stream_parser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ates Goral
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-10-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.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: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ description:
42
+ email:
43
+ - ates.goral@shopify.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files:
47
+ - README.md
48
+ files:
49
+ - LICENSE.md
50
+ - README.md
51
+ - lib/event_stream_parser.rb
52
+ - lib/event_stream_parser/version.rb
53
+ homepage: https://github.com/Shopify/event_stream_parser
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ allowed_push_host: https://rubygems.org
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 2.7.0
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 1.3.7
72
+ requirements: []
73
+ rubygems_version: 3.4.20
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: A spec-compliant event stream parser
77
+ test_files: []