natsy 0.3.0 → 0.4.3

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: a037edf2d9a4b510d45ac57b9a891ccaf5c8b14a82ad359b5ef24974f119342f
4
- data.tar.gz: 7351f54131880e05d0ddf5955a8bc48af0635abf53de8fcd13a06f8ef9587e72
3
+ metadata.gz: e8c3433d746ea11eacc51f7e602ab6a0d0816a69c5ef7db7e802847cf5b931fd
4
+ data.tar.gz: 79dc340ffefbe6371558fa1cd901977e55215f8cbda71024fcd930dda94eab2c
5
5
  SHA512:
6
- metadata.gz: 071a63d868fbd6441c0b33f459c643d0d23f471c63317274dc5ae45ddf896fd69cee84701da5c1cd8ccb86ff1b8cc90e8c9cc9f98ced72508b48879a93e8b572
7
- data.tar.gz: 13345e7ab11e6f770310d27d8dbd2cffa64b0e27dd04b63096bce5827562361b261b81f7e762c328f7a01d6d971399cb8f0c39d61efd370d69fc7aee9a3cd031
6
+ metadata.gz: e7c12f04962acfb2e36bfd1ad580da552678dd26a6c5c51b7b373ef69f816a2018bc72a1c2ad2278b394f0e9e7d18f3d942dd46883a01630b1e30384e21fcc9a
7
+ data.tar.gz: af46e5498949a3806972d9a5a26fdadfc275e3d8914338c634a3cc01ab33c45b26fb48849103ec8a786903d580685912b418df099ace08f9aa5fbdcc9b05dfbb
@@ -18,6 +18,14 @@ jobs:
18
18
 
19
19
  runs-on: ubuntu-latest
20
20
 
21
+ services:
22
+ nats:
23
+ image: nats:latest
24
+ ports:
25
+ - 4222:4222
26
+ - 8222:8222
27
+ - 6222:6222
28
+
21
29
  strategy:
22
30
  matrix:
23
31
  ruby-version:
data/.rubocop.yml CHANGED
@@ -26,13 +26,20 @@ Layout/FirstArrayElementIndentation:
26
26
  # Metrics
27
27
 
28
28
  Metrics/AbcSize:
29
- Max: 30
29
+ Max: 35
30
30
  CountRepeatedAttributes: false
31
31
  Exclude:
32
32
  - 'spec/**/*_spec.rb'
33
33
  - '*.gemspec'
34
34
 
35
35
  Metrics/BlockLength:
36
+ Max: 50
37
+ Exclude:
38
+ - 'spec/**/*_spec.rb'
39
+ - '*.gemspec'
40
+
41
+ Metrics/CyclomaticComplexity:
42
+ Max: 15
36
43
  Exclude:
37
44
  - 'spec/**/*_spec.rb'
38
45
  - '*.gemspec'
@@ -55,6 +62,9 @@ Metrics/MethodLength:
55
62
  - array
56
63
  - hash
57
64
  - heredoc
65
+ Exclude:
66
+ - 'spec/**/*_spec.rb'
67
+ - '*.gemspec'
58
68
 
59
69
  Metrics/ModuleLength:
60
70
  Max: 150
@@ -67,6 +77,12 @@ Metrics/ModuleLength:
67
77
  - 'spec/**/*_spec.rb'
68
78
  - '*.gemspec'
69
79
 
80
+ Metrics/PerceivedComplexity:
81
+ Max: 20
82
+ Exclude:
83
+ - 'spec/**/*_spec.rb'
84
+ - '*.gemspec'
85
+
70
86
  # Rspec
71
87
 
72
88
  RSpec/ExampleLength:
data/README.md CHANGED
@@ -5,14 +5,15 @@ The `natsy` gem allows you to listen for (and reply to) NATS messages asynchrono
5
5
  ## TODO
6
6
 
7
7
  - [x] docs
8
- - [ ] tests
8
+ - [x] tests
9
9
  - [x] "controller"-style classes for reply organization
10
10
  - [x] runtime subscription additions
11
11
  - [x] multiple queues
12
- - [ ] `on_error` handler so you can send a response (what's standard?)
13
- - [ ] config options for URL/host/port/etc.
12
+ - [x] config options for URL/host/port/etc.
14
13
  - [ ] config for restart behavior (default is to restart listening on any `StandardError`)
15
- - [ ] consider using child processes instead of threads
14
+ - [ ] `on_error` handler so you can send a response (what's standard?)
15
+ - [ ] support lifecycle callbacks (like `on_connect`, `on_disconnect`, etc.) provided by the `nats` gem
16
+ - [ ] ability to _request_ (not just reply)
16
17
 
17
18
  ## Installation
18
19
 
@@ -44,6 +45,8 @@ This gem also requires a NATS server to be installed and running before use. See
44
45
 
45
46
  ## Usage
46
47
 
48
+ <a id="starting-the-nats-server-section">
49
+
47
50
  ### Starting the NATS server
48
51
 
49
52
  You'll need to start a NATS server before running your Ruby application. If you installed it via Docker, you might start it like so:
@@ -56,6 +59,57 @@ docker run -p 4222:4222 -p 8222:8222 -p 6222:6222 -ti nats:latest
56
59
 
57
60
  > **NOTE:** For other methods of running a NATS server, see [the NATS documentation](https://docs.nats.io/nats-server/installation).
58
61
 
62
+ ### Configuration
63
+
64
+ Use `Natsy::Config::set` to set configuration options. These options can either be set via a `Hash`/keyword arguments passed to the `::set` method, or set by invoking the method with a block and assigning your options to the yielded `Natsy::Config::Options` instance.
65
+
66
+ This README will use the following two syntaxes interchangably; remember that they do **exactly the same thing:**
67
+
68
+ ```ruby
69
+ Natsy::Config.set(
70
+ urls: ["nats://foo.bar:4567", "nats://foo.bar:5678"],
71
+ default_queue: "foobar",
72
+ logger: Rails.logger,
73
+ )
74
+ ```
75
+
76
+ ```ruby
77
+ Natsy::Config.set do |options|
78
+ options.urls = ["nats://foo.bar:4567", "nats://foo.bar:5678"]
79
+ options.default_queue = "foobar"
80
+ options.logger = Rails.logger
81
+ end
82
+ ```
83
+
84
+ The following options are available:
85
+
86
+ - `url`: A single URL string (including protocol, domain, and port) which points to the relevant NATS server (see [here](#setting-nats-server-url-section) for more info)
87
+ - `urls`: An array of URL strings in case you need to listen to multiple NATS servers (see [here](#setting-nats-server-url-section) for more info)
88
+ - `logger`: A logger where `natsy` can write helpful information (see [here](#logging-section) for more info)
89
+ - `default_queue`: The default queue that your application should fall back to if none is given in a more specific context (see [here](#default-queue-section) for more info)
90
+
91
+ <a id="setting-nats-server-url-section"></a>
92
+
93
+ ### Setting the NATS server URL(s)
94
+
95
+ Set the URL/URLs at which your NATS server mediates messages.
96
+
97
+ ```ruby
98
+ Natsy::Config.set do |options|
99
+ options.url = "nats://foo.bar:4567"
100
+ end
101
+ ```
102
+
103
+ ```ruby
104
+ Natsy::Config.set do |options|
105
+ options.urls = ["nats://foo.bar:4567", "nats://foo.bar:5678"]
106
+ end
107
+ ```
108
+
109
+ > **NOTE:** If no `url`/`urls` option is specified, `natsy` will fall back on the default NATS server URL, which is `nats://localhost:4222`.
110
+
111
+ <a id="logging-section"></a>
112
+
59
113
  ### Logging
60
114
 
61
115
  #### Attaching a logger
@@ -66,23 +120,24 @@ Attach a logger to have `natsy` write out logs for messages received, responses
66
120
  require 'natsy'
67
121
  require 'logger'
68
122
 
69
- nats_logger = Logger.new(STDOUT)
70
- nats_logger.level = Logger::INFO
71
-
72
- Natsy::Client.logger = nats_logger
123
+ Natsy::Config.set do |options|
124
+ nats_logger = Logger.new(STDOUT)
125
+ nats_logger.level = Logger::INFO
126
+ options.logger = nats_logger
127
+ end
73
128
  ```
74
129
 
75
130
  In a Rails application, you might do this instead:
76
131
 
77
132
  ```ruby
78
- Natsy::Client.logger = Rails.logger
133
+ Natsy::Config.set(logger: Rails.logger)
79
134
  ```
80
135
 
81
136
  #### Log levels
82
137
 
83
138
  The following will be logged at the specified log levels
84
139
 
85
- - `DEBUG`: Lifecycle events (starting NATS listeners, stopping NATS, reply registration, setting the default queue, etc.), as well as everything under `INFO`, `WARN`, and `ERROR`
140
+ - `DEBUG`: Lifecycle events (starting NATS listeners, stopping NATS, reply registration, etc.), as well as everything under `INFO`, `WARN`, and `ERROR`
86
141
  - `INFO`: Message activity over NATS (received a message, replied with a message, etc.), as well as everything under `WARN` and `ERROR`
87
142
  - `WARN`: Error handled gracefully (listening restarted due to some exception, etc.), as well as everything under `ERROR`
88
143
  - `ERROR`: Some exception was raised in-thread (error in handler, error in subscription, etc.)
@@ -94,13 +149,13 @@ The following will be logged at the specified log levels
94
149
  Set a default queue for subscriptions.
95
150
 
96
151
  ```ruby
97
- Natsy::Client.default_queue = "foobar"
152
+ Natsy::Config.set(default_queue: "foobar")
98
153
  ```
99
154
 
100
- Leave the `::default_queue` blank (or assign `nil`) to use no default queue.
155
+ Leave the `default_queue` blank (or assign `nil`) to use no default queue.
101
156
 
102
157
  ```ruby
103
- Natsy::Client.default_queue = nil
158
+ Natsy::Config.set(default_queue: nil)
104
159
  ```
105
160
 
106
161
  <a id="reply-to-section"></a>
@@ -153,15 +208,29 @@ The following should be enough to start a `natsy` setup in your Ruby application
153
208
  require 'natsy'
154
209
  require 'logger'
155
210
 
156
- nats_logger = Logger.new(STDOUT)
157
- nats_logger.level = Logger::DEBUG
211
+ Natsy::Config.set do |options|
212
+ nats_logger = Logger.new(STDOUT)
213
+ nats_logger.level = Logger::DEBUG
158
214
 
159
- Natsy::Client.logger = nats_logger
160
- Natsy::Client.default_queue = "foobar"
215
+ options.logger = nats_logger
216
+ options.urls = ["nats://foo.bar:4567", "nats://foo.bar:5678"]
217
+ options.default_queue = "foobar"
218
+ end
161
219
 
162
- Natsy::Client.reply_to("some.subject") { |data| "Got it! #{data.inspect}" }
163
- Natsy::Client.reply_to("some.*.pattern") { |data, subject| "Got #{data} on #{subject}" }
164
- Natsy::Client.reply_to("subject.in.queue", queue: "barbaz") { { msg: "My turn!", turn: 5 } }
220
+ Natsy::Client.reply_to("some.subject") do |data|
221
+ "Got it! #{data.inspect}"
222
+ end
223
+
224
+ Natsy::Client.reply_to("some.*.pattern") do |data, subject|
225
+ "Got #{data} on #{subject}"
226
+ end
227
+
228
+ Natsy::Client.reply_to("subject.in.queue", queue: "barbaz") do
229
+ {
230
+ msg: "My turn!",
231
+ turn: 5,
232
+ }
233
+ end
165
234
 
166
235
  Natsy::Client.start!
167
236
  ```
@@ -172,7 +241,7 @@ Natsy::Client.start!
172
241
 
173
242
  Create controller classes which inherit from `Natsy::Controller` in order to give your message listeners some structure.
174
243
 
175
- Use the `::default_queue` macro to set a default queue string. If omitted, the controller will fall back on the global default queue assigned with `Natsy::Client::default_queue=` (as described [here](#default-queue-section)). If no default queue is set in either the controller or globally, then the default queue will be blank. Set the default queue to `nil` in a controller to override the global default queue and explicitly make the default queue blank for that controller.
244
+ Use the `::default_queue` macro to set a default queue string. If omitted, the controller will fall back on the global default queue assigned to `Natsy::Config::default_queue` (as described [here](#default-queue-section)). If no default queue is set in either the controller or globally, then the default queue will be blank. Set the default queue to `nil` in a controller to fall back to the global default queue.
176
245
 
177
246
  Use the `::subject` macro to create a block for listening to that subject segment. Nested calls to `::subject` will append each subsequent subject/pattern string to the last (joined by a periods). There is no limit to the level of nesting.
178
247
 
@@ -208,9 +277,13 @@ class HelloController < Natsy::Controller
208
277
  end
209
278
 
210
279
  subject "hows" do
211
- subject "*" do
280
+ # The queue at this point is "foobar"
281
+ subject "*", queue: "barbaz" do # Override the default queue at any point
282
+ # The queue at this point is "barbaz" (!)
212
283
  subject "doing" do
213
- response do |data, subject|
284
+ # The queue at this point is "barbaz"
285
+ response queue: "bazbam" do |data, subject|
286
+ # The queue at this point is "bazbam" (!)
214
287
  # The subject at this point is "hows.<wildcard>.doing" (i.e., the
215
288
  # subjects "hows.jack.doing" and "hows.jill.doing" will both match)
216
289
  sender_name = data["name"]
@@ -229,8 +302,7 @@ end
229
302
  > For example: in a Rails project (assuming you have your NATS controllers in a directory called `app/nats/`), you may want to put something like the following in an initializer (such as `config/initializers/nats.rb`):
230
303
  >
231
304
  > ```ruby
232
- > Natsy::Client.logger = Rails.logger
233
- > Natsy::Client.default_queue = "foobar"
305
+ > Natsy::Config.set(logger: Rails.logger, default_queue: "foobar")
234
306
  >
235
307
  > # ...
236
308
  >
@@ -267,7 +339,7 @@ bin/console
267
339
 
268
340
  ### Run the tests
269
341
 
270
- To run the RSpec test suites, run:
342
+ To run the RSpec test suites, first [start the NATS server](#starting-the-nats-server-section). Then, run the tests:
271
343
 
272
344
  ```bash
273
345
  bundle exec rake spec
@@ -285,6 +357,12 @@ rake spec
285
357
  bundle exec rubocop
286
358
  ```
287
359
 
360
+ ...or (if your Ruby setup has good defaults) just this:
361
+
362
+ ```bash
363
+ rubocop
364
+ ```
365
+
288
366
  ### Create a release
289
367
 
290
368
  Bump the `Natsy::VERSION` value in `lib/natsy/version.rb`, commit, and then run:
data/lib/natsy.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "nats/client"
4
4
  require_relative "natsy/version"
5
5
  require_relative "natsy/utils"
6
+ require_relative "natsy/config"
6
7
  require_relative "natsy/client"
7
8
  require_relative "natsy/controller"
8
9
 
@@ -14,4 +15,7 @@ module Natsy
14
15
 
15
16
  # New subscription has been added at runtime
16
17
  class NewSubscriptionsError < Natsy::Error; end
18
+
19
+ # Invalid options have been provided to +Natsy::Config+
20
+ class InvalidConfigError < Natsy::Error; end
17
21
  end
data/lib/natsy/client.rb CHANGED
@@ -10,50 +10,6 @@ module Natsy
10
10
  # most functionality if desired.
11
11
  class Client
12
12
  class << self
13
- # Optional logger for lifecycle events, messages received, etc.
14
- attr_reader :logger
15
-
16
- # Optional default queue for message subscription and replies.
17
- attr_reader :default_queue
18
-
19
- # Attach a logger to have +natsy+ write out logs for messages
20
- # received, responses sent, errors raised, lifecycle events, etc.
21
- #
22
- # @example
23
- # require 'natsy'
24
- # require 'logger'
25
- #
26
- # nats_logger = Logger.new(STDOUT)
27
- # nats_logger.level = Logger::INFO
28
- #
29
- # Natsy::Client.logger = nats_logger
30
- #
31
- # In a Rails application, you might do this instead:
32
- #
33
- # @example
34
- # Natsy::Client.logger = Rails.logger
35
- #
36
- def logger=(some_logger)
37
- @logger = some_logger
38
- log("Set the logger to #{@logger.inspect}")
39
- end
40
-
41
- # Set a default queue for subscriptions.
42
- #
43
- # @example
44
- # Natsy::Client.default_queue = "foobar"
45
- #
46
- # Leave the +::default_queue+ blank (or assign +nil+) to use no default
47
- # queue.
48
- #
49
- # @example
50
- # Natsy::Client.default_queue = nil
51
- #
52
- def default_queue=(some_queue)
53
- @default_queue = Utils.presence(some_queue.to_s)
54
- log("Setting the default queue to #{@default_queue || '(none)'}", level: :debug)
55
- end
56
-
57
13
  # Returns +true+ if +::start!+ has already been called (meaning the client
58
14
  # is listening to NATS messages). Returns +false+ if it has not yet been
59
15
  # called, or if it has been stopped.
@@ -72,8 +28,9 @@ module Natsy
72
28
  # method. Pass a subject string as the first argument (either a static
73
29
  # subject string or a pattern to match more than one subject). Specify a
74
30
  # queue (or don't) with the +queue:+ option. If you don't provide the
75
- # +queue:+ option, it will be set to the value of +default_queue+, or to
76
- # +nil+ (no queue) if a default queue hasn't been set.
31
+ # +queue:+ option, it will be set to the value of
32
+ # +Natsy::Config::default_queue+, or to +nil+ (no queue) if a default
33
+ # queue hasn't been set.
77
34
  #
78
35
  # The result of the given block will be published in reply to the message.
79
36
  # The block is passed two arguments when a message matching the subject is
@@ -101,7 +58,7 @@ module Natsy
101
58
  # end
102
59
  #
103
60
  def reply_to(subject, queue: nil, &block)
104
- queue = Utils.presence(queue) || default_queue
61
+ queue = Utils.presence(queue) || Config.default_queue
105
62
  queue_desc = " in queue '#{queue}'" if queue
106
63
  log("Registering a reply handler for subject '#{subject}'#{queue_desc}", level: :debug)
107
64
  register_reply!(subject: subject.to_s, handler: block, queue: queue.to_s)
@@ -145,44 +102,54 @@ module Natsy
145
102
 
146
103
  started!
147
104
 
148
- self.current_thread = Thread.new do
105
+ thread = Thread.new do
149
106
  Thread.handle_interrupt(StandardError => :never) do
150
107
  Thread.handle_interrupt(StandardError => :immediate) { listen }
151
108
  rescue NATS::ConnectError => e
152
109
  log("Could not connect to NATS server:", level: :error)
153
110
  log(e.full_message, level: :error, indent: 2)
154
111
  Thread.current.exit
155
- rescue NewSubscriptionsError => e
112
+ rescue NewSubscriptionsError => _e
156
113
  log("New subscriptions! Restarting...", level: :info)
157
114
  restart!
158
- raise e # TODO: there has to be a better way
115
+ Thread.current.exit
116
+ # raise e # TODO: there has to be a better way
159
117
  rescue StandardError => e
160
118
  log("Encountered an error:", level: :error)
161
119
  log(e.full_message, level: :error, indent: 2)
162
120
  restart!
163
- raise e
121
+ Thread.current.exit
122
+ # raise e
164
123
  end
165
124
  end
125
+
126
+ threads << thread
166
127
  end
167
128
 
168
- private
129
+ # **USE WITH CAUTION:** This method (+::reset!+) clears all subscriptions,
130
+ # stops listening (if started), and kills any active threads.
131
+ def reset!
132
+ replies.clear
133
+ stop!
134
+ kill!
135
+ end
169
136
 
170
- attr_accessor :current_thread
137
+ private
171
138
 
172
- def log(text, level: :info, indent: 0)
173
- return unless logger
139
+ def threads
140
+ @threads ||= []
141
+ end
174
142
 
175
- timestamp = Time.now.to_s
176
- text_lines = text.split("\n")
177
- indentation = indent.is_a?(String) ? indent : (" " * indent)
143
+ def current_thread
144
+ threads.last
145
+ end
178
146
 
179
- text_lines.each do |line|
180
- logger.send(level, "[#{timestamp}] Natsy | #{indentation}#{line}")
181
- end
147
+ def log(text, level: :info, indent: 0)
148
+ Utils.log(Config.logger, text, level: level, indent: indent)
182
149
  end
183
150
 
184
151
  def kill!
185
- current_thread.kill if current_thread && current_thread.alive?
152
+ threads.each { |thread| thread.kill if thread.alive? }
186
153
  end
187
154
 
188
155
  def stop!
@@ -200,6 +167,7 @@ module Natsy
200
167
  def restart!
201
168
  log("Restarting NATS", level: :warn)
202
169
  stop!
170
+ kill!
203
171
  start!
204
172
  end
205
173
 
@@ -228,7 +196,7 @@ module Natsy
228
196
  reply = {
229
197
  subject: subject,
230
198
  handler: handler,
231
- queue: Utils.presence(queue) || default_queue,
199
+ queue: Utils.presence(queue) || Config.default_queue,
232
200
  }
233
201
 
234
202
  replies << reply
@@ -237,30 +205,43 @@ module Natsy
237
205
  end
238
206
 
239
207
  def listen
240
- NATS.start do
208
+ NATS.start(servers: Natsy::Config.urls) do
241
209
  replies.each do |replier|
242
210
  queue_desc = " in queue '#{replier[:queue]}'" if replier[:queue]
243
211
  log("Subscribing to subject '#{replier[:subject]}'#{queue_desc}", level: :debug)
244
212
 
245
- NATS.subscribe(replier[:subject], queue: replier[:queue]) do |message, inbox, subject|
246
- parsed_message = JSON.parse(message)
247
- id, data, pattern = parsed_message.values_at("id", "data", "pattern")
213
+ NATS.subscribe(replier[:subject], queue: replier[:queue]) do |message, reply_subject, subject|
214
+ parsed_message = begin
215
+ JSON.parse(message)
216
+ rescue StandardError
217
+ message
218
+ end
219
+
220
+ id, data, pattern = if parsed_message.is_a?(Hash)
221
+ parsed_message.values_at("id", "data", "pattern")
222
+ else
223
+ [nil, parsed_message, nil]
224
+ end
225
+
226
+ message_data = id && data && pattern ? data : parsed_message
248
227
 
249
228
  log("Received a message!")
250
229
  message_desc = <<~LOG_MESSAGE
251
230
  id: #{id || '(none)'}
252
231
  pattern: #{pattern || '(none)'}
253
232
  subject: #{subject || '(none)'}
254
- data: #{data.to_json}
255
- inbox: #{inbox || '(none)'}
233
+ data: #{message_data.to_json}
234
+ inbox: #{reply_subject || '(none)'}
235
+ queue: #{replier[:queue] || '(none)'}
236
+ message: #{message}
256
237
  LOG_MESSAGE
257
238
  log(message_desc, indent: 2)
258
239
 
259
- response_data = replier[:handler].call(data)
240
+ raw_response = replier[:handler].call(message_data, subject)
260
241
 
261
- log("Responding with '#{response_data}'")
242
+ log("Responding with '#{raw_response}'")
262
243
 
263
- NATS.publish(inbox, response_data.to_json, queue: replier[:queue])
244
+ NATS.publish(reply_subject, raw_response.to_json) if Utils.present?(reply_subject)
264
245
  end
265
246
  end
266
247
  end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+ require_relative "./utils"
5
+
6
+ module Natsy
7
+ # Represents the configuration options for +Natsy+. Configuration options are
8
+ # set using the `Natsy::Config::set` method, either as arguments or by using
9
+ # the appropriate setters on the object passed to the block.
10
+ class Config
11
+ # A +Natsy::Config::Options+ object is passed as a single argument to the
12
+ # block (if provided) for the +Natsy::Config::set+ method. This class should
13
+ # probably *NOT* be instantiated directly; instead, set the relevant options
14
+ # using +Natsy::Config.set(some_option: "...", some_other_option: "...")+.
15
+ # If you find yourself instantiating this class directly, there's probably a
16
+ # better way to do what you're trying to do.
17
+ class Options
18
+ # Specify a NATS server URL (or multiple URLs)
19
+ #
20
+ # **NOTE:** The following two examples do exactly the same thing.
21
+ #
22
+ # @example
23
+ # Natsy::Config.set(url: "nats://foo.bar:4567"))
24
+ #
25
+ # @example
26
+ # Natsy::Config.set do |options|
27
+ # options.url = "nats://foo.bar:4567"
28
+ # end
29
+ #
30
+ # **NOTE:** The following two examples do exactly the same thing.
31
+ #
32
+ # @example
33
+ # Natsy::Config.set(urls: ["nats://foo.bar:4567", "nats://foo.bar:5678"])
34
+ #
35
+ # @example
36
+ # Natsy::Config.set do |options|
37
+ # options.urls = ["nats://foo.bar:4567", "nats://foo.bar:5678"]
38
+ # end
39
+ #
40
+ # If left blank/omitted, +natsy+ will fall back on the default URL, which
41
+ # is +nats://localhost:4222+.
42
+ #
43
+ attr_accessor :url, :urls
44
+
45
+ # Attach a logger to have +natsy+ write out logs for messages
46
+ # received, responses sent, errors raised, lifecycle events, etc.
47
+ #
48
+ # **NOTE:** The following two examples do exactly the same thing.
49
+ #
50
+ # @example
51
+ # require 'natsy'
52
+ # require 'logger'
53
+ #
54
+ # nats_logger = Logger.new(STDOUT)
55
+ # nats_logger.level = Logger::INFO
56
+ #
57
+ # Natsy::Config.set(logger: nats_logger)
58
+ #
59
+ # @example
60
+ # require 'natsy'
61
+ # require 'logger'
62
+ #
63
+ # Natsy::Config.set do |options|
64
+ # nats_logger = Logger.new(STDOUT)
65
+ # nats_logger.level = Logger::INFO
66
+ #
67
+ # options.logger = nats_logger
68
+ # end
69
+ #
70
+ #
71
+ # In a Rails application, you might do this instead:
72
+ #
73
+ # @example
74
+ # Natsy::Config.set(logger: Rails.logger)
75
+ #
76
+ attr_reader :logger
77
+
78
+ # {include:Natsy::Config::Options#logger}
79
+ def logger=(new_logger)
80
+ @logger = new_logger
81
+ Utils.log(@logger, "Set the logger to #{@logger.inspect}", level: :debug)
82
+ end
83
+
84
+ # Set a default queue for subscriptions.
85
+ #
86
+ # **NOTE:** The following two examples do exactly the same thing.
87
+ #
88
+ # @example
89
+ # Natsy::Config.set(default_queue: "foobar")
90
+ #
91
+ # @example
92
+ # Natsy::Config.set do |options|
93
+ # options.default_queue = "foobar"
94
+ # end
95
+ #
96
+ # Leave the +::default_queue+ blank (or assign +nil+) to use no default
97
+ # queue.
98
+ #
99
+ # **NOTE:** The following two examples do exactly the same thing.
100
+ #
101
+ # @example
102
+ # Natsy::Config.set(default_queue: nil)
103
+ #
104
+ # @example
105
+ # Natsy::Config.set do |options|
106
+ # options.default_queue = nil
107
+ # end
108
+ #
109
+ attr_reader :default_queue
110
+
111
+ # {include:Natsy::Config::Options#default_queue}
112
+ def default_queue=(new_queue)
113
+ @default_queue = Utils.presence(new_queue.to_s)
114
+ Utils.log(logger, "Setting the default queue to #{@default_queue || '(none)'}", level: :debug)
115
+ end
116
+
117
+ # Returns ONLY the config options THAT HAVE BEEN SET as a +Hash+. Will not
118
+ # have keys for properties that are unassigned, but will have keys for
119
+ # properties assigned +nil+.
120
+ def to_h
121
+ hash = {}
122
+ hash[:url] = url if defined?(@url)
123
+ hash[:urls] = urls if defined?(@urls)
124
+ hash[:logger] = logger if defined?(@logger)
125
+ hash[:default_queue] = default_queue if defined?(@default_queue)
126
+ hash
127
+ end
128
+ end
129
+
130
+ # Valid option keys that can be given to +Natsy::Config::set+, either in a
131
+ # +Hash+ passed to the method, keyword arguments passed to the method, or by
132
+ # using setters on the +Natsy::Config::Options+ object passed to the block.
133
+ VALID_OPTIONS = %i[
134
+ url
135
+ urls
136
+ logger
137
+ default_queue
138
+ ].freeze
139
+
140
+ # The default NATS server URL (used if none is configured)
141
+ DEFAULT_URL = "nats://localhost:4222"
142
+
143
+ class << self
144
+ # Specify configuration options, either by providing them as keyword
145
+ # arguments or by using a block. Should you choose to set options using
146
+ # a block, it will be passed a single argument (an instance of
147
+ # +Natsy::Config::Options+). You can set any options on the instance that
148
+ # you see fit.
149
+ #
150
+ # **NOTE:** The following two examples do exactly the same thing.
151
+ #
152
+ # @example
153
+ # Natsy::Config.set(
154
+ # urls: ["nats://foo.bar:4567", "nats://foo.bar:5678"],
155
+ # default_queue: "foobar",
156
+ # logger: Rails.logger,
157
+ # )
158
+ #
159
+ # @example
160
+ # Natsy::Config.set do |options|
161
+ # options.urls = ["nats://foo.bar:4567", "nats://foo.bar:5678"]
162
+ # options.default_queue = "foobar"
163
+ # options.logger = Rails.logger
164
+ # end
165
+ #
166
+ def set(keyword_options = {})
167
+ new_hash_options = (keyword_options || {}).transform_keys(&:to_sym)
168
+
169
+ invalid_config = lambda do |detail, keys|
170
+ raise InvalidConfigError, "Invalid options provided #{detail}: #{keys.join(', ')}"
171
+ end
172
+
173
+ invalid_keys = invalid_option_keys(new_hash_options)
174
+ invalid_config.call("as arguments", invalid_keys) if invalid_keys.any?
175
+
176
+ # Want to take advantage of the setters on +Natsy::Config::Options+...
177
+ new_hash_options_object = new_hash_options.each_with_object(Options.new) do |(key, value), options|
178
+ options.send(:"#{key}=", value)
179
+ end
180
+
181
+ given_options.merge!(new_hash_options_object.to_h)
182
+
183
+ new_block_options_object = Options.new
184
+ yield(new_block_options_object) if block_given?
185
+
186
+ invalid_keys = invalid_option_keys(new_block_options_object)
187
+ invalid_config.call("in block", invalid_keys) if invalid_keys.any?
188
+
189
+ given_options.merge!(new_block_options_object.to_h)
190
+ end
191
+
192
+ # The NATS server URLs that +natsy+ should listen on.
193
+ #
194
+ # See also: {Natsy::Config::Options#urls=}
195
+ #
196
+ def urls
197
+ given_url_list = [given_options[:url]].flatten
198
+ given_urls_list = [given_options[:urls]].flatten
199
+ all_given_urls = [*given_url_list, *given_urls_list].compact.uniq
200
+ Utils.presence(all_given_urls) || [DEFAULT_URL]
201
+ end
202
+
203
+ # The logger that +natsy+ should use to write out logs for messages
204
+ # received, responses sent, errors raised, lifecycle events, etc.
205
+ #
206
+ # See also: {Natsy::Config::Options#logger=}
207
+ #
208
+ def logger
209
+ Utils.presence(given_options[:logger])
210
+ end
211
+
212
+ # The default queue that +natsy+ should use for subscriptions.
213
+ #
214
+ # See also: {Natsy::Config::Options#default_queue=}
215
+ #
216
+ def default_queue
217
+ Utils.presence(given_options[:default_queue])
218
+ end
219
+
220
+ # Returns all config options as a +Hash+.
221
+ def to_h
222
+ {
223
+ urls: urls,
224
+ logger: logger,
225
+ default_queue: default_queue,
226
+ }
227
+ end
228
+
229
+ # Alias for {Natsy::Config::to_h}.
230
+ def as_json(*_args)
231
+ to_h
232
+ end
233
+
234
+ # Serialize the configuration into a JSON object string.
235
+ def to_json(*_args)
236
+ to_h.to_json
237
+ end
238
+
239
+ # Reset the configuration to default values.
240
+ def reset!
241
+ @given_options = nil
242
+ end
243
+
244
+ private
245
+
246
+ def given_options
247
+ @given_options ||= {}
248
+ end
249
+
250
+ def invalid_option_keys(options)
251
+ options.to_h.keys - VALID_OPTIONS
252
+ end
253
+ end
254
+ end
255
+ end
@@ -25,18 +25,17 @@ module Natsy
25
25
  # end
26
26
  #
27
27
  # If omitted, the controller will fall back on the global default queue
28
- # assigned with +Natsy::Client::default_queue=+. If no default
29
- # queue is set in either the controller or globally, then the default
30
- # queue will be blank. Set the default queue to +nil+ in a controller to
31
- # override the global default queue and explicitly make the default queue
32
- # blank for that controller.
28
+ # assigned with +Natsy::Config::set+. If no default queue is set in either
29
+ # the controller or globally, then the default queue will be blank. Set
30
+ # the default queue to +nil+ in a controller to fall back to the global
31
+ # default queue.
33
32
  #
34
33
  def default_queue(some_queue = NO_QUEUE_GIVEN)
35
34
  # +NO_QUEUE_GIVEN+ is a special symbol (rather than +nil+) so that the
36
35
  # default queue can be "unset" to +nil+ (given a non-+nil+ global
37
- # default set with +Natsy::Client::default_queue=+).
36
+ # default set with +Natsy::Client::set+).
38
37
  if some_queue == NO_QUEUE_GIVEN
39
- @default_queue || Client.default_queue
38
+ @default_queue || Config.default_queue
40
39
  else
41
40
  @default_queue = Utils.presence(some_queue.to_s)
42
41
  end
data/lib/natsy/utils.rb CHANGED
@@ -15,6 +15,20 @@ module Natsy
15
15
  def presence(value)
16
16
  present?(value) ? value : nil
17
17
  end
18
+
19
+ def log(logger, text, level: :info, indent: 0)
20
+ return unless logger
21
+
22
+ timestamp = Time.now.to_s
23
+ text_lines = text.split("\n")
24
+ indentation = indent.is_a?(String) ? indent : (" " * indent)
25
+
26
+ text_lines.each do |line|
27
+ logger.send(level, "[#{timestamp}] Natsy | #{indentation}#{line}")
28
+ end
29
+
30
+ nil
31
+ end
18
32
  end
19
33
  end
20
34
  end
data/lib/natsy/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Natsy
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.3"
5
5
  end
data/natsy.gemspec CHANGED
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.require_paths = ["lib"]
29
29
 
30
30
  spec.add_development_dependency "bundler", "~> 2.2"
31
+ spec.add_development_dependency "pry"
31
32
  spec.add_development_dependency "rake", "~> 13.0"
32
33
  spec.add_development_dependency "rspec", "~> 3.0"
33
34
  spec.add_development_dependency "rubocop", "~> 1.10"
@@ -35,7 +36,6 @@ Gem::Specification.new do |spec|
35
36
  spec.add_development_dependency "rubocop-rake", "~> 0.5"
36
37
  spec.add_development_dependency "rubocop-rspec", "~> 2.2"
37
38
  spec.add_development_dependency "solargraph"
38
- spec.add_development_dependency "pry"
39
39
 
40
40
  spec.add_runtime_dependency "nats", "~> 0.11"
41
41
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: natsy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keegan Leitz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-05-17 00:00:00.000000000 Z
11
+ date: 2021-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rake
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -122,20 +136,6 @@ dependencies:
122
136
  - - ">="
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: pry
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: nats
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -161,7 +161,6 @@ files:
161
161
  - ".gitignore"
162
162
  - ".rspec"
163
163
  - ".rubocop.yml"
164
- - CHANGELOG.md
165
164
  - Gemfile
166
165
  - LICENSE.txt
167
166
  - README.md
@@ -170,6 +169,7 @@ files:
170
169
  - bin/setup
171
170
  - lib/natsy.rb
172
171
  - lib/natsy/client.rb
172
+ - lib/natsy/config.rb
173
173
  - lib/natsy/controller.rb
174
174
  - lib/natsy/utils.rb
175
175
  - lib/natsy/version.rb
@@ -178,7 +178,7 @@ homepage: https://github.com/openbay/natsy
178
178
  licenses:
179
179
  - MIT
180
180
  metadata:
181
- documentation_uri: https://www.rubydoc.info/gems/natsy/0.3.0
181
+ documentation_uri: https://www.rubydoc.info/gems/natsy/0.4.3
182
182
  post_install_message:
183
183
  rdoc_options: []
184
184
  require_paths:
data/CHANGELOG.md DELETED
@@ -1,5 +0,0 @@
1
- ## [Unreleased]
2
-
3
- ## [0.1.0] - 2021-05-10
4
-
5
- - Initial release