natsy 0.3.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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