amqp-client 1.0.1 → 1.0.2

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: 89ea2743bb81c4615d1445ee6ce25a5234a95f9f859ca4926ff5437228af1e02
4
- data.tar.gz: 4739cd429d9cf55a631c36c9100f78c6d2da65d4aa18535297d69b8ec0ca6e56
3
+ metadata.gz: 59f619f7aec324221f4c5dd66a10e5e30b1904a224d34702f94f9f75ac19f1c3
4
+ data.tar.gz: 4d3e3fc1234f44ab741b15b61156617a7f8f181ce5fdc2aacbbc4c07a787129e
5
5
  SHA512:
6
- metadata.gz: 12d2650fbf4be1f3d1449c4c035e8e12be88a64f3e4eeb2f4588558d294816e36510f53cb37f8217e2754ca3fb6b1acb021df25b63f34c7710f267de6186123c
7
- data.tar.gz: 4b44974681cacc7956c73983cebd61315684c35adb5d98851469eaa7677e4a5149cc582cfeb3ec6561f1a036b0b80d7623197e8e1881400f6e00fff1305a4362
6
+ metadata.gz: 6ade93665b42b3c64ebed869be2af64cde53b259a6f376483c8c4d7e9de6432e2453be88caddf58655c249ae0d531a924348eb97c92b0a6a21b13a3276e3019b
7
+ data.tar.gz: '0479fe0b0c6f120e4bf0c5b72b905d7946037f8b5e1d83f462c18c2df790c8ed67ff5e32be693deac570d7d82dc9411875f252384364feea3b6e8b85c6abd27d'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.2] - 2021-09-07
4
+
5
+ - Changed: Raise ConnectionClosed and ChannelClosed correctly (previous always ChannelClosed)
6
+ - Fixed: Respect Connection#blocked sent by the broker, will block all writes/requests
7
+
3
8
  ## [1.0.1] - 2021-09-06
4
9
 
5
10
  - The API is fully documented! https://cloudamqp.github.io/amqp-client.rb/
data/README.md CHANGED
@@ -38,7 +38,7 @@ msg = ch.basic_get q[:queue_name]
38
38
  puts msg.body
39
39
  ```
40
40
 
41
- High level API, is an easier and safer API, that only deal with durable queues and persisted messages. All methods are blocking in the case of connection loss etc. It's also fully thread-safe. Don't expect it to have extreme throughput, but expect 100% delivery guarantees (messages might be delivered twice, in the unlikely event of connection loss between message publish and message confirmation by the server).
41
+ High level API, is an easier and safer API, that only deal with durable queues and persisted messages. All methods are blocking in the case of connection loss etc. It's also fully thread-safe. Don't expect it to have extreme throughput, but expect 100% delivery guarantees (messages might be delivered twice, in the unlikely event of connection loss between message publish and message confirmation by the broker).
42
42
 
43
43
  ```ruby
44
44
  amqp = AMQP::Client.new("amqp://localhost")
@@ -50,7 +50,7 @@ q = amqp.queue("myqueue")
50
50
  # Bind the queue to any exchange, with any binding key
51
51
  q.bind("amq.topic", "my.events.*")
52
52
 
53
- # The message will be reprocessed if the client loses connection to the server
53
+ # The message will be reprocessed if the client loses connection to the broker
54
54
  # between message arrival and when the message was supposed to be ack'ed.
55
55
  q.subscribe(prefetch: 20) do |msg|
56
56
  process(JSON.parse(msg.body))
@@ -55,7 +55,7 @@ module AMQP
55
55
  return if @closed
56
56
 
57
57
  write_bytes FrameBytes.channel_close(@id, reason, code)
58
- @closed = [code, reason]
58
+ @closed = [:channel, code, reason]
59
59
  expect :channel_close_ok
60
60
  @replies.close
61
61
  @basic_gets.close
@@ -64,11 +64,12 @@ module AMQP
64
64
  nil
65
65
  end
66
66
 
67
- # Called when channel is closed by server
67
+ # Called when channel is closed by broker
68
+ # @param level [Symbol] :connection or :channel
68
69
  # @return [nil]
69
70
  # @api private
70
- def closed!(code, reason, classid, methodid)
71
- @closed = [code, reason, classid, methodid]
71
+ def closed!(level, code, reason, classid, methodid)
72
+ @closed = [level, code, reason, classid, methodid]
72
73
  @replies.close
73
74
  @basic_gets.close
74
75
  @unconfirmed_empty.close
@@ -77,7 +78,7 @@ module AMQP
77
78
  end
78
79
 
79
80
  # Handle returned messages in this block. If not set the message will just be logged to STDERR
80
- # @yield [ReturnMessage] Messages returned by the server when a publish has failed
81
+ # @yield [ReturnMessage] Messages returned by the broker when a publish has failed
81
82
  # @return nil
82
83
  def on_return(&block)
83
84
  @on_return = block
@@ -105,7 +106,7 @@ module AMQP
105
106
  # Delete an exchange
106
107
  # @param name [String] Name of the exchange
107
108
  # @param if_unused [Boolean] If true raise an exception if queues/exchanges is bound to this exchange
108
- # @param no_wait [Boolean] If true don't wait for a server confirmation
109
+ # @param no_wait [Boolean] If true don't wait for a broker confirmation
109
110
  # @return [nil]
110
111
  def exchange_delete(name, if_unused: false, no_wait: false)
111
112
  write_bytes FrameBytes.exchange_delete(@id, name, if_unused, no_wait)
@@ -174,7 +175,7 @@ module AMQP
174
175
  # @param name [String] Name of the queue
175
176
  # @param if_unused [Boolean] Only delete if the queue doesn't have consumers, raises a ChannelClosed error otherwise
176
177
  # @param if_empty [Boolean] Only delete if the queue is empty, raises a ChannelClosed error otherwise
177
- # @param no_wait [Boolean] Don't wait for a server confirmation if true
178
+ # @param no_wait [Boolean] Don't wait for a broker confirmation if true
178
179
  # @return [Integer] Number of messages in queue when deleted
179
180
  # @return [nil] If no_wait was set true
180
181
  def queue_delete(name, if_unused: false, if_empty: false, no_wait: false)
@@ -197,7 +198,7 @@ module AMQP
197
198
 
198
199
  # Purge a queue
199
200
  # @param name [String] Name of the queue
200
- # @param no_wait [Boolean] Don't wait for a server confirmation if true
201
+ # @param no_wait [Boolean] Don't wait for a broker confirmation if true
201
202
  # @return [nil]
202
203
  def queue_purge(name, no_wait: false)
203
204
  write_bytes FrameBytes.queue_purge(@id, name, no_wait)
@@ -229,7 +230,7 @@ module AMQP
229
230
  case (msg = @basic_gets.pop)
230
231
  when Message then msg
231
232
  when :basic_get_empty then nil
232
- when nil then raise Error::ChannelClosed.new(@id, *@closed)
233
+ when nil then raise Error::Closed.new(@id, *@closed)
233
234
  end
234
235
  end
235
236
 
@@ -238,6 +239,8 @@ module AMQP
238
239
  # @param exchange [String] Name of the exchange to publish to
239
240
  # @param routing_key [String] The routing key that the exchange might use to route the message to a queue
240
241
  # @param properties [Properties]
242
+ # @option properties [Boolean] mandatory The message will be returned if the message can't be routed to a queue
243
+ # @option properties [Boolean] persistent Same as delivery_mode: 2
241
244
  # @option properties [String] content_type Content type of the message body
242
245
  # @option properties [String] content_encoding Content encoding of the body
243
246
  # @option properties [Hash<String, Object>] headers Custom headers
@@ -253,7 +256,7 @@ module AMQP
253
256
  # @option properties [String] app_id Can be used to indicates which app that generated the message
254
257
  # @return [nil]
255
258
  def basic_publish(body, exchange, routing_key, **properties)
256
- frame_max = @connection.frame_max - 8
259
+ body_max = @connection.frame_max - 8
257
260
  id = @id
258
261
  mandatory = properties.delete(:mandatory) || false
259
262
  case properties.delete(:persistent)
@@ -261,7 +264,7 @@ module AMQP
261
264
  when false then properties[:delivery_mode] = 1
262
265
  end
263
266
 
264
- if body.bytesize.between?(1, frame_max)
267
+ if body.bytesize.between?(1, body_max)
265
268
  write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
266
269
  FrameBytes.header(id, body.bytesize, properties),
267
270
  FrameBytes.body(id, body)
@@ -273,7 +276,7 @@ module AMQP
273
276
  FrameBytes.header(id, body.bytesize, properties)
274
277
  pos = 0
275
278
  while pos < body.bytesize # split body into multiple frame_max frames
276
- len = [frame_max, body.bytesize - pos].min
279
+ len = [body_max, body.bytesize - pos].min
277
280
  body_part = body.byteslice(pos, len)
278
281
  write_bytes FrameBytes.body(id, body_part)
279
282
  pos += len
@@ -284,7 +287,9 @@ module AMQP
284
287
 
285
288
  # Publish a message and block until the message has confirmed it has received it
286
289
  # @param (see #basic_publish)
290
+ # @option (see #basic_publish)
287
291
  # @return [Boolean] True if the message was successfully published
292
+ # @raise (see #basic_publish)
288
293
  def basic_publish_confirm(body, exchange, routing_key, **properties)
289
294
  confirm_select(no_wait: true)
290
295
  basic_publish(body, exchange, routing_key, **properties)
@@ -293,12 +298,13 @@ module AMQP
293
298
 
294
299
  # Consume messages from a queue
295
300
  # @param queue [String] Name of the queue to subscribe to
296
- # @param tag [String] Custom consumer tag, will be auto assigned by the server if empty
301
+ # @param tag [String] Custom consumer tag, will be auto assigned by the broker if empty.
302
+ # Has to be uniqe among this channel's consumers only
297
303
  # @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected)
298
304
  # @param exclusive [Boolean] When true only a single consumer can consume from the queue at a time
299
- # @param arguments [Hash] Custom arguments to the consumer
305
+ # @param arguments [Hash] Custom arguments for the consumer
300
306
  # @param worker_threads [Integer] Number of threads processing messages,
301
- # 0 means that the thread calling this method will be blocked
307
+ # 0 means that the thread calling this method will process the messages and thus this method will block
302
308
  # @yield [Message] Delivered message from the queue
303
309
  # @return [Array<(String, Array<Thread>)>] Returns consumer_tag and an array of worker threads
304
310
  # @return [nil] When `worker_threads` is 0 the method will return when the consumer is cancelled
@@ -325,7 +331,7 @@ module AMQP
325
331
 
326
332
  # Cancel/abort/stop a consumer
327
333
  # @param consumer_tag [String] Tag of the consumer to cancel
328
- # @param no_wait [Boolean] Will wait for a confirmation from the server that the consumer is cancelled
334
+ # @param no_wait [Boolean] Will wait for a confirmation from the broker that the consumer is cancelled
329
335
  # @return [nil]
330
336
  def basic_cancel(consumer_tag, no_wait: false)
331
337
  consumer = @consumers.fetch(consumer_tag)
@@ -377,7 +383,7 @@ module AMQP
377
383
 
378
384
  # Recover all the unacknowledge messages
379
385
  # @param requeue [Boolean] If false the currently unack:ed messages will be deliviered to this consumer again,
380
- # if false to any consumer
386
+ # if true to any consumer
381
387
  # @return [nil]
382
388
  def basic_recover(requeue: false)
383
389
  write_bytes FrameBytes.basic_recover(@id, requeue: requeue)
@@ -388,8 +394,8 @@ module AMQP
388
394
  # @!endgroup
389
395
  # @!group Confirm
390
396
 
391
- # Put the channel in confirm mode, each published message will then be confirmed by the server
392
- # @param no_wait [Boolean] If false the method will block until the server has confirmed the request
397
+ # Put the channel in confirm mode, each published message will then be confirmed by the broker
398
+ # @param no_wait [Boolean] If false the method will block until the broker has confirmed the request
393
399
  # @return [nil]
394
400
  def confirm_select(no_wait: false)
395
401
  return if @confirm
@@ -408,11 +414,11 @@ module AMQP
408
414
  case @unconfirmed_empty.pop
409
415
  when true then true
410
416
  when false then false
411
- else raise Error::ChannelClosed.new(@id, *@closed)
417
+ else raise Error::Closed.new(@id, *@closed)
412
418
  end
413
419
  end
414
420
 
415
- # Called by Connection when received ack/nack from server
421
+ # Called by Connection when received ack/nack from broker
416
422
  # @api private
417
423
  def confirm(args)
418
424
  ack_or_nack, delivery_tag, multiple = *args
@@ -527,14 +533,14 @@ module AMQP
527
533
  end
528
534
 
529
535
  def write_bytes(*bytes)
530
- raise Error::ChannelClosed.new(@id, *@closed) if @closed
536
+ raise Error::Closed.new(@id, *@closed) if @closed
531
537
 
532
538
  @connection.write_bytes(*bytes)
533
539
  end
534
540
 
535
541
  def expect(expected_frame_type)
536
542
  frame_type, *args = @replies.pop
537
- raise Error::ChannelClosed.new(@id, *@closed) if frame_type.nil?
543
+ raise Error::Closed.new(@id, *@closed) if frame_type.nil?
538
544
  raise Error::UnexpectedFrame.new(expected_frame_type, frame_type) unless frame_type == expected_frame_type
539
545
 
540
546
  args
@@ -59,9 +59,10 @@ module AMQP
59
59
  @frame_max = frame_max
60
60
  @heartbeat = heartbeat
61
61
  @channels = {}
62
- @closed = false
62
+ @closed = nil
63
63
  @replies = ::Queue.new
64
64
  @write_lock = Mutex.new
65
+ @blocked = nil
65
66
  Thread.new { read_loop } if read_loop_thread
66
67
  end
67
68
 
@@ -115,17 +116,21 @@ module AMQP
115
116
  def close(reason: "", code: 200)
116
117
  return if @closed
117
118
 
118
- @closed = true
119
- write_bytes FrameBytes.connection_close(code, reason)
120
- @channels.each_value { |ch| ch.closed!(code, reason, 0, 0) }
121
- expect(:close_ok)
119
+ @closed = [code, reason]
120
+ @channels.each_value { |ch| ch.closed!(:connection, code, reason, 0, 0) }
121
+ if @blocked
122
+ @socket.close
123
+ else
124
+ write_bytes FrameBytes.connection_close(code, reason)
125
+ expect(:close_ok)
126
+ end
122
127
  nil
123
128
  end
124
129
 
125
130
  # True if the connection is closed
126
131
  # @return [Boolean]
127
132
  def closed?
128
- @closed
133
+ !@closed.nil?
129
134
  end
130
135
 
131
136
  # Write byte array(s) directly to the socket (thread-safe)
@@ -133,10 +138,15 @@ module AMQP
133
138
  # @return [Integer] number of bytes written
134
139
  # @api private
135
140
  def write_bytes(*bytes)
141
+ blocked = @blocked
142
+ warn "AMQP-Client blocked by broker: #{blocked}" if blocked
136
143
  @write_lock.synchronize do
144
+ warn "AMQP-Client unblocked by broker" if blocked
137
145
  @socket.write(*bytes)
138
146
  end
139
147
  rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
148
+ raise Error::ConnectionClosed.new(*@closed) if @closed
149
+
140
150
  raise Error, "Could not write to socket, #{e.message}"
141
151
  end
142
152
 
@@ -167,10 +177,10 @@ module AMQP
167
177
  end
168
178
  nil
169
179
  rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
170
- warn "AMQP-Client read error: #{e.inspect}"
180
+ @closed ||= [400, "read error: #{e.message}"]
171
181
  nil # ignore read errors
172
182
  ensure
173
- @closed = true
183
+ @closed ||= [400, "unknown"]
174
184
  @replies.close
175
185
  begin
176
186
  @socket.close
@@ -191,11 +201,11 @@ module AMQP
191
201
 
192
202
  case method_id
193
203
  when 50 # connection#close
194
- @closed = true
195
204
  code, text_len = buf.unpack("@4 S> C")
196
205
  text = buf.byteslice(7, text_len).force_encoding("utf-8")
197
206
  error_class_id, error_method_id = buf.byteslice(7 + text_len, 4).unpack("S> S>")
198
- @channels.each_value { |ch| ch.closed!(code, text, error_class_id, error_method_id) }
207
+ @closed = [code, text, error_class_id, error_method_id]
208
+ @channels.each_value { |ch| ch.closed!(:connection, code, text, error_class_id, error_method_id) }
199
209
  begin
200
210
  write_bytes FrameBytes.connection_close_ok
201
211
  rescue Error
@@ -203,9 +213,16 @@ module AMQP
203
213
  end
204
214
  return false
205
215
  when 51 # connection#close-ok
206
- @closed = true
207
216
  @replies.push [:close_ok]
208
217
  return false
218
+ when 60 # connection#blocked
219
+ reason_len = buf.unpack1("@4 C")
220
+ reason = buf.byteslice(5, reason_len).force_encoding("utf-8")
221
+ @blocked = reason
222
+ @write_lock.lock
223
+ when 61 # connection#unblocked
224
+ @blocked = nil
225
+ @write_lock.unlock
209
226
  else raise Error::UnsupportedMethodFrame, class_id, method_id
210
227
  end
211
228
  when 20 # channel
@@ -217,7 +234,7 @@ module AMQP
217
234
  reply_text = buf.byteslice(7, reply_text_len).force_encoding("utf-8")
218
235
  classid, methodid = buf.byteslice(7 + reply_text_len, 4).unpack("S> S>")
219
236
  channel = @channels.delete(channel_id)
220
- channel.closed!(reply_code, reply_text, classid, methodid)
237
+ channel.closed!(:channel, reply_code, reply_text, classid, methodid)
221
238
  write_bytes FrameBytes.channel_close_ok(channel_id)
222
239
  when 41 # channel#close-ok
223
240
  channel = @channels.delete(channel_id)
@@ -32,6 +32,19 @@ module AMQP
32
32
  end
33
33
  end
34
34
 
35
+ # Depending on close level a ConnectionClosed or ChannelClosed error is returned
36
+ class Closed < Error
37
+ def self.new(id, level, code, reason, classid = 0, methodid = 0)
38
+ case level
39
+ when :connection
40
+ ConnectionClosed.new(code, reason, classid, methodid)
41
+ when :channel
42
+ ChannelClosed.new(id, code, reason, classid, methodid)
43
+ else raise ArgumentError, "invalid level '#{level}'"
44
+ end
45
+ end
46
+ end
47
+
35
48
  # Raised if channel is already closed
36
49
  class ChannelClosed < Error
37
50
  def initialize(id, code, reason, classid = 0, methodid = 0)
@@ -40,7 +53,11 @@ module AMQP
40
53
  end
41
54
 
42
55
  # Raised if connection is unexpectedly closed
43
- class ConnectionClosed < Error; end
56
+ class ConnectionClosed < Error
57
+ def initialize(code, reason, classid = 0, methodid = 0)
58
+ super "Connection closed (#{code}) #{reason} (#{classid}/#{methodid})"
59
+ end
60
+ end
44
61
  end
45
62
  end
46
63
  end
@@ -11,30 +11,24 @@ module AMQP
11
11
  @name = name
12
12
  end
13
13
 
14
- # Publish to the queue
15
- # @param body [String] The message body
16
- # @param properties [Properties]
17
- # @option properties [String] content_type Content type of the message body
18
- # @option properties [String] content_encoding Content encoding of the body
19
- # @option properties [Hash<String, Object>] headers Custom headers
20
- # @option properties [Integer] delivery_mode 2 for persisted message, transient messages for all other values
21
- # @option properties [Integer] priority A priority of the message (between 0 and 255)
22
- # @option properties [Integer] correlation_id A correlation id, most often used used for RPC communication
23
- # @option properties [String] reply_to Queue to reply RPC responses to
24
- # @option properties [Integer, String] expiration Number of seconds the message will stay in the queue
25
- # @option properties [String] message_id Can be used to uniquely identify the message, e.g. for deduplication
26
- # @option properties [Date] timestamp Often used for the time the message was originally generated
27
- # @option properties [String] type Can indicate what kind of message this is
28
- # @option properties [String] user_id Can be used to verify that this is the user that published the message
29
- # @option properties [String] app_id Can be used to indicates which app that generated the message
30
- # @return [Queue] self
14
+ # Publish to the queue, wait for confirm
15
+ # @param (see Client#publish)
16
+ # @option (see Client#publish)
17
+ # @raise (see Client#publish)
18
+ # @return [self]
31
19
  def publish(body, **properties)
32
20
  @client.publish(body, "", @name, **properties)
33
21
  self
34
22
  end
35
23
 
36
24
  # Subscribe/consume from the queue
37
- # @return [Queue] self
25
+ # @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected)
26
+ # @param prefetch [Integer] Specify how many messages to prefetch for consumers with no_ack is false
27
+ # @param worker_threads [Integer] Number of threads processing messages,
28
+ # 0 means that the thread calling this method will be blocked
29
+ # @param arguments [Hash] Custom arguments to the consumer
30
+ # @yield [Message] Delivered message from the queue
31
+ # @return [self]
38
32
  def subscribe(no_ack: false, prefetch: 1, worker_threads: 1, arguments: {}, &blk)
39
33
  @client.subscribe(@name, no_ack: no_ack, prefetch: prefetch, worker_threads: worker_threads, arguments: arguments, &blk)
40
34
  self
@@ -44,7 +38,7 @@ module AMQP
44
38
  # @param exchange [String] Name of the exchange to bind to
45
39
  # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
46
40
  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
47
- # @return [Queue] self
41
+ # @return [self]
48
42
  def bind(exchange, binding_key, arguments: {})
49
43
  @client.bind(@name, exchange, binding_key, arguments: arguments)
50
44
  self
@@ -54,14 +48,14 @@ module AMQP
54
48
  # @param exchange [String] Name of the exchange to unbind from
55
49
  # @param binding_key [String] Binding key which the queue is bound to the exchange with
56
50
  # @param arguments [Hash] Arguments matching the binding that's being removed
57
- # @return [Queue] self
51
+ # @return [self]
58
52
  def unbind(exchange, binding_key, arguments: {})
59
53
  @client.unbind(@name, exchange, binding_key, arguments: arguments)
60
54
  self
61
55
  end
62
56
 
63
57
  # Purge/empty the queue
64
- # @return [Queue] self
58
+ # @return [self]
65
59
  def purge
66
60
  @client.purge(@name)
67
61
  self
@@ -3,6 +3,6 @@
3
3
  module AMQP
4
4
  class Client
5
5
  # Version of the client library
6
- VERSION = "1.0.1"
6
+ VERSION = "1.0.2"
7
7
  end
8
8
  end
data/lib/amqp/client.rb CHANGED
@@ -122,7 +122,10 @@ module AMQP
122
122
  # @!group Publish
123
123
 
124
124
  # Publish a (persistent) message and wait for confirmation
125
- # @return [nil]
125
+ # @param (see Connection::Channel#basic_publish_confirm)
126
+ # @option (see Connection::Channel#basic_publish_confirm)
127
+ # @return (see Connection::Channel#basic_publish_confirm)
128
+ # @raise (see Connection::Channel#basic_publish_confirm)
126
129
  def publish(body, exchange, routing_key, **properties)
127
130
  with_connection do |conn|
128
131
  properties = { delivery_mode: 2 }.merge!(properties)
@@ -131,7 +134,10 @@ module AMQP
131
134
  end
132
135
 
133
136
  # Publish a (persistent) message but don't wait for a confirmation
134
- # @return [nil]
137
+ # @param (see Connection::Channel#basic_publish)
138
+ # @option (see Connection::Channel#basic_publish)
139
+ # @return (see Connection::Channel#basic_publish)
140
+ # @raise (see Connection::Channel#basic_publish)
135
141
  def publish_and_forget(body, exchange, routing_key, **properties)
136
142
  with_connection do |conn|
137
143
  properties = { delivery_mode: 2 }.merge!(properties)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amqp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Hörberg
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-06 00:00:00.000000000 Z
11
+ date: 2021-09-07 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Work in progress
14
14
  email: