message-driver 0.1.0 → 0.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.travis.yml +18 -7
  4. data/CHANGELOG.md +12 -2
  5. data/Gemfile +17 -0
  6. data/Guardfile +8 -4
  7. data/README.md +14 -5
  8. data/Rakefile +44 -11
  9. data/examples/basic_producer_and_consumer/Gemfile +5 -0
  10. data/examples/basic_producer_and_consumer/common.rb +17 -0
  11. data/examples/basic_producer_and_consumer/consumer.rb +24 -0
  12. data/examples/basic_producer_and_consumer/producer.rb +33 -0
  13. data/features/.nav +8 -0
  14. data/features/CHANGELOG.md +12 -2
  15. data/features/amqp_specific_features/binding_amqp_destinations.feature +7 -7
  16. data/features/amqp_specific_features/declaring_amqp_exchanges.feature +3 -3
  17. data/features/amqp_specific_features/nack_redelivered_messages.feature +92 -0
  18. data/features/amqp_specific_features/requeueing_on_nack.feature +44 -0
  19. data/features/amqp_specific_features/server_named_destinations.feature +5 -5
  20. data/features/client_acks.feature +92 -0
  21. data/features/destination_metadata.feature +9 -11
  22. data/features/dynamic_destinations.feature +7 -7
  23. data/features/error_handling.feature +11 -9
  24. data/features/logging.feature +14 -0
  25. data/features/message_consumers/auto_ack_consumers.feature +79 -0
  26. data/features/message_consumers/manual_ack_consumers.feature +95 -0
  27. data/features/message_consumers/transactional_ack_consumers.feature +77 -0
  28. data/features/message_consumers.feature +54 -0
  29. data/features/publishing_a_message.feature +6 -10
  30. data/features/publishing_with_transactions.feature +10 -14
  31. data/features/rabbitmq_specific_features/dead_letter_queueing.feature +116 -0
  32. data/features/step_definitions/dynamic_destinations_steps.rb +3 -3
  33. data/features/step_definitions/error_handling_steps.rb +4 -2
  34. data/features/step_definitions/logging_steps.rb +28 -0
  35. data/features/step_definitions/message_consumers_steps.rb +29 -0
  36. data/features/step_definitions/steps.rb +60 -9
  37. data/features/support/broker_config_helper.rb +19 -0
  38. data/features/support/env.rb +1 -0
  39. data/features/support/firewall_helper.rb +8 -11
  40. data/features/support/message_table_matcher.rb +21 -5
  41. data/features/support/test_runner.rb +39 -16
  42. data/lib/message_driver/adapters/base.rb +51 -4
  43. data/lib/message_driver/adapters/bunny_adapter.rb +251 -127
  44. data/lib/message_driver/adapters/in_memory_adapter.rb +97 -18
  45. data/lib/message_driver/adapters/stomp_adapter.rb +127 -0
  46. data/lib/message_driver/broker.rb +23 -24
  47. data/lib/message_driver/client.rb +157 -0
  48. data/lib/message_driver/destination.rb +7 -4
  49. data/lib/message_driver/errors.rb +27 -0
  50. data/lib/message_driver/logging.rb +11 -0
  51. data/lib/message_driver/message.rb +8 -0
  52. data/lib/message_driver/subscription.rb +18 -0
  53. data/lib/message_driver/vendor/.document +0 -0
  54. data/lib/message_driver/vendor/nesty/nested_error.rb +26 -0
  55. data/lib/message_driver/vendor/nesty.rb +1 -0
  56. data/lib/message_driver/version.rb +1 -1
  57. data/lib/message_driver.rb +4 -2
  58. data/message-driver.gemspec +4 -4
  59. data/spec/integration/{amqp_integration_spec.rb → bunny/amqp_integration_spec.rb} +29 -28
  60. data/spec/integration/bunny/bunny_adapter_spec.rb +339 -0
  61. data/spec/integration/in_memory/in_memory_adapter_spec.rb +126 -0
  62. data/spec/integration/stomp/stomp_adapter_spec.rb +142 -0
  63. data/spec/spec_helper.rb +5 -2
  64. data/spec/support/shared/adapter_examples.rb +17 -0
  65. data/spec/support/shared/client_ack_examples.rb +18 -0
  66. data/spec/support/shared/context_examples.rb +14 -0
  67. data/spec/support/shared/destination_examples.rb +4 -5
  68. data/spec/support/shared/subscription_examples.rb +146 -0
  69. data/spec/support/shared/transaction_examples.rb +43 -0
  70. data/spec/support/utils.rb +14 -0
  71. data/spec/units/message_driver/adapters/base_spec.rb +38 -19
  72. data/spec/units/message_driver/broker_spec.rb +71 -18
  73. data/spec/units/message_driver/client_spec.rb +375 -0
  74. data/spec/units/message_driver/destination_spec.rb +9 -0
  75. data/spec/units/message_driver/logging_spec.rb +18 -0
  76. data/spec/units/message_driver/message_spec.rb +36 -0
  77. data/spec/units/message_driver/subscription_spec.rb +24 -0
  78. data/test_lib/broker_config.rb +50 -20
  79. metadata +83 -45
  80. data/.rbenv-version +0 -1
  81. data/lib/message_driver/exceptions.rb +0 -18
  82. data/lib/message_driver/message_publisher.rb +0 -15
  83. data/spec/integration/message_driver/adapters/bunny_adapter_spec.rb +0 -301
  84. data/spec/units/message_driver/adapters/in_memory_adapter_spec.rb +0 -43
  85. data/spec/units/message_driver/message_publisher_spec.rb +0 -65
@@ -0,0 +1,375 @@
1
+ require 'spec_helper'
2
+
3
+ require 'message_driver/adapters/in_memory_adapter'
4
+
5
+ module MessageDriver
6
+ describe Client do
7
+ class TestPublisher
8
+ include Client
9
+ end
10
+
11
+ let(:adapter) { Adapters::InMemoryAdapter.new({}) }
12
+ let(:adapter_context) { adapter.new_context }
13
+ let(:logger) { double(Logger).as_null_object }
14
+
15
+ before do
16
+ MessageDriver.configure(adapter: adapter, logger: logger)
17
+ end
18
+
19
+ shared_examples "a Client" do
20
+ describe "#current_adapter_context" do
21
+ before { subject.clear_context }
22
+
23
+ it "returns an adapter_context" do
24
+ expect(subject.current_adapter_context).to be_a Adapters::ContextBase
25
+ end
26
+
27
+ it "returns the same adapter context on the second call" do
28
+ ctx = subject.current_adapter_context
29
+ expect(subject.current_adapter_context).to be ctx
30
+ end
31
+
32
+ context "when called with false" do
33
+ it "doesn't initialize the adapter context if there isn't one" do
34
+ expect(subject.current_adapter_context(false)).to be_nil
35
+ end
36
+ end
37
+ end
38
+
39
+ context "with a given adapter_context" do
40
+ around do |example|
41
+ subject.with_adapter_context(adapter_context, &example)
42
+ end
43
+
44
+ describe "#dynamic_destination" do
45
+ let(:dest_name) { "my_new_queue" }
46
+ let(:dest_options) { {type: 2} }
47
+ let(:message_props) { {expires: "soon"} }
48
+ let(:created_dest) { double("created destination") }
49
+ before do
50
+ adapter_context.stub(:create_destination) { created_dest }
51
+ end
52
+
53
+ it "delegates to the adapter_context" do
54
+ result = subject.dynamic_destination(dest_name, dest_options, message_props)
55
+ expect(result).to be(created_dest)
56
+
57
+ adapter_context.should have_received(:create_destination).with(dest_name, dest_options, message_props)
58
+ end
59
+
60
+ it "only requires destination name" do
61
+ result = subject.dynamic_destination(dest_name)
62
+ expect(result).to be(created_dest)
63
+
64
+ adapter_context.should have_received(:create_destination).with(dest_name, {}, {})
65
+ end
66
+ end
67
+
68
+ describe "#publish" do
69
+ let(:destination) { Broker.destination(:my_queue, "my_queue", exclusive: true) }
70
+ let(:body) { "my message" }
71
+ let(:headers) { {foo: :bar} }
72
+ let(:properties) { {bar: :baz} }
73
+ before do
74
+ adapter_context.stub(:publish)
75
+ end
76
+
77
+ it "delegates to the adapter_context" do
78
+ subject.publish(destination, body, headers, properties)
79
+ adapter_context.should have_received(:publish).with(destination, body, headers, properties)
80
+ end
81
+
82
+ it "only requires destination and body" do
83
+ subject.publish(destination, body)
84
+ adapter_context.should have_received(:publish).with(destination, body, {}, {})
85
+ end
86
+
87
+ it "looks up the destination if necessary" do
88
+ destination
89
+ subject.publish(:my_queue, body, headers, properties)
90
+ adapter_context.should have_received(:publish).with(destination, body, headers, properties)
91
+ end
92
+
93
+ context "when the destination can't be found" do
94
+ let(:bad_dest_name) { :not_a_queue }
95
+ it "raises a MessageDriver:NoSuchDestinationError" do
96
+ expect {
97
+ subject.publish(bad_dest_name, body, headers, properties)
98
+ }.to raise_error(MessageDriver::NoSuchDestinationError, /#{bad_dest_name}/)
99
+ adapter_context.should_not have_received(:publish)
100
+ end
101
+ end
102
+ end
103
+
104
+ describe "#pop_message" do
105
+ let(:expected) { double(MessageDriver::Message) }
106
+ let(:destination) { Broker.destination(:my_queue, "my_queue", exclusive: true) }
107
+ let(:options) { {foo: :bar} }
108
+ before do
109
+ adapter_context.stub(:pop_message)
110
+ end
111
+
112
+ it "delegates to the adapter_context" do
113
+ subject.pop_message(destination, options)
114
+ adapter_context.should have_received(:pop_message).with(destination, options)
115
+ end
116
+
117
+ it "looks up the destination if necessary" do
118
+ destination
119
+ subject.pop_message(:my_queue, options)
120
+ adapter_context.should have_received(:pop_message).with(destination, options)
121
+ end
122
+
123
+ context "when the destination can't be found" do
124
+ let(:bad_dest_name) { :not_a_queue }
125
+ it "raises a MessageDriver:NoSuchDestinationError" do
126
+ expect {
127
+ subject.pop_message(bad_dest_name, options)
128
+ }.to raise_error(MessageDriver::NoSuchDestinationError, /#{bad_dest_name}/)
129
+ adapter_context.should_not have_received(:pop_message)
130
+ end
131
+ end
132
+
133
+ it "requires the destination and returns the message" do
134
+ adapter_context.should_receive(:pop_message).with(destination, {}).and_return(expected)
135
+
136
+ actual = subject.pop_message(destination)
137
+
138
+ expect(actual).to be expected
139
+ end
140
+
141
+ it "passes the options through and returns the message" do
142
+ adapter_context.should_receive(:pop_message).with(destination, options).and_return(expected)
143
+
144
+ actual = subject.pop_message(destination, options)
145
+
146
+ expect(actual).to be expected
147
+ end
148
+ end
149
+
150
+ describe "#with_message_transaction" do
151
+ before do
152
+ adapter_context.stub(:begin_transaction)
153
+ adapter_context.stub(:commit_transaction)
154
+ adapter_context.stub(:rollback_transaction)
155
+ end
156
+
157
+ context "when the adapter supports transactions" do
158
+ before do
159
+ adapter_context.stub(:supports_transactions?) { true }
160
+ end
161
+ it "delegates to the adapter context" do
162
+ expect { |blk|
163
+ subject.with_message_transaction(&blk)
164
+ }.to yield_control
165
+ adapter_context.should have_received(:begin_transaction)
166
+ adapter_context.should have_received(:commit_transaction)
167
+ end
168
+
169
+ context "when the block raises an error" do
170
+ it "calls rollback instead of commit and raises the error" do
171
+ expect {
172
+ subject.with_message_transaction do
173
+ raise "having a tough time"
174
+ end
175
+ }.to raise_error "having a tough time"
176
+ adapter_context.should have_received(:begin_transaction)
177
+ adapter_context.should_not have_received(:commit_transaction)
178
+ adapter_context.should have_received(:rollback_transaction)
179
+ end
180
+
181
+ context "and the the rollback raises an error" do
182
+ it "logs the error from the rollback and raises the original error" do
183
+ adapter_context.stub(:rollback_transaction).and_raise("rollback failed!")
184
+ expect {
185
+ subject.with_message_transaction do
186
+ raise "having a tough time"
187
+ end
188
+ }.to raise_error "having a tough time"
189
+ expect(logger).to have_received(:error).with(match("rollback failed!"))
190
+ end
191
+ end
192
+ end
193
+
194
+ context "when the transactions are nested" do
195
+ it "only starts and commits once" do
196
+ expect { |blk|
197
+ subject.with_message_transaction do
198
+ subject.with_message_transaction(&blk)
199
+ end
200
+ }.to yield_control
201
+ adapter_context.should have_received(:begin_transaction).once
202
+ adapter_context.should have_received(:commit_transaction).once
203
+ end
204
+
205
+ context "when the block raises an error" do
206
+ it "calls rollback instead of commit and raises the error" do
207
+ expect {
208
+ subject.with_message_transaction do
209
+ subject.with_message_transaction do
210
+ raise "having a tough time"
211
+ end
212
+ end
213
+ }.to raise_error "having a tough time"
214
+ adapter_context.should have_received(:begin_transaction).once
215
+ adapter_context.should_not have_received(:commit_transaction)
216
+ adapter_context.should have_received(:rollback_transaction).once
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ context "when the adapter doesn't support transactions" do
223
+ before do
224
+ adapter_context.stub(:supports_transactions?) { false }
225
+ end
226
+ it "run the block on it's own" do
227
+ expect { |blk|
228
+ subject.with_message_transaction(&blk)
229
+ }.to yield_control
230
+ adapter_context.should_not have_received(:begin_transaction)
231
+ adapter_context.should_not have_received(:commit_transaction)
232
+ adapter_context.should_not have_received(:rollback_transaction)
233
+ end
234
+ it "logs a warning" do
235
+ expect { |blk|
236
+ subject.with_message_transaction(&blk)
237
+ }.to yield_control
238
+ expect(logger).to have_received(:debug).with("this adapter does not support transactions")
239
+ end
240
+ end
241
+ end
242
+
243
+ describe "#ack_message" do
244
+ let(:message) { double("message") }
245
+ let(:options) { {foo: :bar} }
246
+ before do
247
+ adapter_context.stub(:ack_message)
248
+ end
249
+ context "when the adapter supports client acks" do
250
+ before do
251
+ adapter_context.stub(:supports_client_acks?) { true }
252
+ end
253
+ it "calls #ack_message with the message" do
254
+ subject.ack_message(message)
255
+ adapter_context.should have_received(:ack_message).with(message, {})
256
+ end
257
+ it "passes the supplied options to ack_message" do
258
+ subject.ack_message(message, options)
259
+ adapter_context.should have_received(:ack_message).with(message, options)
260
+ end
261
+ end
262
+ context "when the adapter doesn't support client acks" do
263
+ before do
264
+ adapter_context.stub(:supports_client_acks?) { false }
265
+ end
266
+ it "doesn't call #ack_message" do
267
+ subject.ack_message(message)
268
+ adapter_context.should_not have_received(:ack_message)
269
+ end
270
+ it "logs a warning" do
271
+ subject.ack_message(message)
272
+ expect(logger).to have_received(:debug).with("this adapter does not support client acks")
273
+ end
274
+ end
275
+ end
276
+
277
+ describe "#nack_message" do
278
+ let(:message) { double("message") }
279
+ let(:options) { {foo: :bar} }
280
+ before do
281
+ adapter_context.stub(:nack_message)
282
+ end
283
+ context "when the adapter supports client acks" do
284
+ before do
285
+ adapter_context.stub(:supports_client_acks?) { true }
286
+ end
287
+ it "calls #nack_message with the message" do
288
+ subject.nack_message(message)
289
+ adapter_context.should have_received(:nack_message).with(message, {})
290
+ end
291
+ it "passes the supplied options to nack_message" do
292
+ subject.nack_message(message, options)
293
+ adapter_context.should have_received(:nack_message).with(message, options)
294
+ end
295
+ end
296
+ context "when the adapter doesn't support client acks" do
297
+ before do
298
+ adapter_context.stub(:supports_client_acks?) { false }
299
+ end
300
+ it "doesn't call #nack_message" do
301
+ subject.nack_message(message)
302
+ adapter_context.should_not have_received(:nack_message)
303
+ end
304
+ it "logs a warning" do
305
+ subject.nack_message(message)
306
+ expect(logger).to have_received(:debug).with("this adapter does not support client acks")
307
+ end
308
+ end
309
+ end
310
+
311
+ describe "#subscribe" do
312
+ let(:destination) { Broker.destination(:my_queue, "my_queue", exclusive: true) }
313
+ let(:consumer_double) { lambda do |m| end }
314
+
315
+ before do
316
+ adapter_context.stub(:subscribe)
317
+ Broker.consumer(:my_consumer, &consumer_double)
318
+ end
319
+
320
+ it "delegates to the adapter_context" do
321
+ adapter_context.should_receive(:subscribe).with(destination, {}) do |&blk|
322
+ expect(blk).to be(consumer_double)
323
+ end
324
+ subject.subscribe(destination, :my_consumer)
325
+ end
326
+
327
+ it "passes the options through" do
328
+ options = {foo: :bar}
329
+ adapter_context.should_receive(:subscribe).with(destination, options) do |&blk|
330
+ expect(blk).to be(consumer_double)
331
+ end
332
+ subject.subscribe(destination, :my_consumer, options)
333
+ end
334
+
335
+ it "looks up the destination" do
336
+ adapter_context.should_receive(:subscribe).with(destination, {}) do |&blk|
337
+ expect(blk).to be(consumer_double)
338
+ end
339
+ subject.subscribe(:my_queue, :my_consumer)
340
+ end
341
+
342
+ context "when the destination can't be found" do
343
+ let(:bad_dest_name) { :not_a_queue }
344
+ it "raises a MessageDriver:NoSuchDestinationError" do
345
+ expect {
346
+ subject.subscribe(bad_dest_name, :my_consumer)
347
+ }.to raise_error(MessageDriver::NoSuchDestinationError, /#{bad_dest_name}/)
348
+ adapter_context.should_not have_received(:subscribe)
349
+ end
350
+ end
351
+
352
+ context "when the consumer can't be found" do
353
+ let(:bad_consumer_name) { :not_a_consumer }
354
+ it "raises a MessageDriver:NoSuchConsumerError" do
355
+ expect {
356
+ subject.subscribe(destination, bad_consumer_name)
357
+ }.to raise_error(MessageDriver::NoSuchConsumerError, /#{bad_consumer_name}/)
358
+ adapter_context.should_not have_received(:subscribe)
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end
364
+
365
+ context "when used as an included module" do
366
+ subject { TestPublisher.new }
367
+ it_behaves_like "a Client"
368
+ end
369
+
370
+ context "when the module is used directly" do
371
+ subject { described_class }
372
+ it_behaves_like "a Client"
373
+ end
374
+ end
375
+ end
@@ -7,5 +7,14 @@ module MessageDriver::Destination
7
7
  it "needs some real tests"
8
8
 
9
9
  include_examples "doesn't support #message_count"
10
+
11
+ describe "#subscribe" do
12
+ it "raises an error" do
13
+ expect {
14
+ consumer = lambda do |m| end
15
+ destination.subscribe(&consumer)
16
+ }.to raise_error "#subscribe is not supported by #{destination.class}"
17
+ end
18
+ end
10
19
  end
11
20
  end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ module MessageDriver
4
+ describe Logging do
5
+ class TestLogger
6
+ include Logging
7
+ end
8
+ subject { TestLogger.new }
9
+
10
+ describe "#logger" do
11
+ let(:logger) { double(Logger) }
12
+ it "returns the broker logger" do
13
+ allow(MessageDriver::Broker).to receive(:logger).and_return(logger)
14
+ expect(subject.logger).to be logger
15
+ end
16
+ end
17
+ end
18
+ end
@@ -15,5 +15,41 @@ module MessageDriver::Message
15
15
  its(:properties) { should eq(properties) }
16
16
  end
17
17
  end
18
+
19
+ subject(:message) { described_class.new("body", {}, {}) }
20
+
21
+ describe "#ack" do
22
+ let(:options) { {foo: :bar} }
23
+
24
+ before do
25
+ MessageDriver::Client.stub(:ack_message)
26
+ end
27
+ it "passes itself to Client.ack_message" do
28
+ subject.ack
29
+ expect(MessageDriver::Client).to have_received(:ack_message).with(subject, {})
30
+ end
31
+
32
+ it "passes the options to Client.ack_message" do
33
+ subject.ack(options)
34
+ expect(MessageDriver::Client).to have_received(:ack_message).with(subject, options)
35
+ end
36
+ end
37
+
38
+ describe "#nack" do
39
+ let(:options) { {foo: :bar} }
40
+
41
+ before do
42
+ MessageDriver::Client.stub(:nack_message)
43
+ end
44
+ it "passes itself to Client.nack_message" do
45
+ subject.nack
46
+ expect(MessageDriver::Client).to have_received(:nack_message).with(subject, {})
47
+ end
48
+
49
+ it "passes the options to Client.nack_message" do
50
+ subject.nack(options)
51
+ expect(MessageDriver::Client).to have_received(:nack_message).with(subject, options)
52
+ end
53
+ end
18
54
  end
19
55
  end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ module MessageDriver::Subscription
4
+ describe Base do
5
+ let(:adapter) { double(MessageDriver::Adapters::Base) }
6
+ let(:destination) { double(MessageDriver::Destination::Base) }
7
+ let(:consumer) { double("a consumer") }
8
+ subject(:subscription) { Base.new(adapter, destination, consumer) }
9
+
10
+ it "sets it's adapter, destination and consumer on instansiation" do
11
+ expect(subscription.adapter).to eq(adapter)
12
+ expect(subscription.destination).to eq(destination)
13
+ expect(subscription.consumer).to eq(consumer)
14
+ end
15
+
16
+ describe "#unsubscribe" do
17
+ it "raises an error" do
18
+ expect {
19
+ subscription.unsubscribe
20
+ }.to raise_error("must be implemented in subclass")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,25 +1,55 @@
1
- module BrokerConfig
2
- def self.config
3
- adapter_file = File.expand_path("../../.adapter_under_test", __FILE__)
4
- adapter = ENV['ADAPTER'] || (File.exist?(adapter_file) && File.read(adapter_file).chomp)
5
- case adapter
6
- when 'bunny'
7
- {
8
- adapter: :bunny,
9
- vhost: 'message-driver-test'
10
- }
11
- when 'in_memory'
12
- {adapter: :in_memory}
13
- else
14
- {adapter: :in_memory}
1
+ class BrokerConfig
2
+ class << self
3
+ def config
4
+ adapter_file = File.expand_path("../../.adapter_under_test", __FILE__)
5
+ @adapter, @version = (ENV['ADAPTER'] || (File.exist?(adapter_file) && File.read(adapter_file).chomp)).split(":")
6
+ case @adapter
7
+ when 'bunny'
8
+ {
9
+ adapter: :bunny,
10
+ vhost: 'message-driver-test'
11
+ }
12
+ when 'in_memory'
13
+ {adapter: :in_memory}
14
+ when 'stomp'
15
+ {
16
+ adapter: :stomp,
17
+ vhost: 'message-driver-test',
18
+ hosts: [{host: 'localhost', login: 'guest', passcode: 'guest'}],
19
+ reliable: false,
20
+ max_reconnect_attempts: 1
21
+ }
22
+ else
23
+ {adapter: :in_memory}
24
+ end
15
25
  end
16
- end
17
26
 
18
- def self.current_adapter
19
- config[:adapter]
20
- end
27
+ def all_adapters
28
+ %w(in_memory bunny stomp)
29
+ end
30
+
31
+ def current_adapter
32
+ config[:adapter]
33
+ end
21
34
 
22
- def self.unconfigured_adapters
23
- %w(bunny in_memory) - current_adapter
35
+ def adapter_version
36
+ config unless @version
37
+ @version
38
+ end
39
+
40
+ def unconfigured_adapters
41
+ all_adapters - [current_adapter]
42
+ end
43
+
44
+ def current_adapter_port
45
+ case current_adapter
46
+ when :bunny
47
+ 5672
48
+ when :stomp
49
+ 61613
50
+ else
51
+ nil
52
+ end
53
+ end
24
54
  end
25
55
  end