ruby_nest_nats 0.2.0 → 0.2.1

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: c27d22ced425f310876883bb15afd2aaa52b24235bb095551b7981bedca4b210
4
- data.tar.gz: 2537c259848226de1481ce0579b2036200b7d0e45a2a6b098fddd988bae16b4d
3
+ metadata.gz: 8ff7ec8086e242612ea12d75ef5a8a52330b8f84d3cd12bc097be093b00ef5c7
4
+ data.tar.gz: aae8c3e437f6c94abf4f3c4a551efd009308b0702a064c848e8935ee8087a01f
5
5
  SHA512:
6
- metadata.gz: b8b7397a66a1f2984d54ae60bc3f299b384695f450a066cb00a64386fcfaf86be7dcad7e1c9e6f9fac4ede7994f41a2f1fa1cbd267563d5551dc2b53ac3512b0
7
- data.tar.gz: a998c0f7015d4e6a1700ac23db6516cdd29572b92a1aaa18fe784b144862f5a229123820467a4d1e2e9baa5d5e30a618fbbe3bd526ac5e67b6640e08ca6b7e73
6
+ metadata.gz: 49f30461903b281191bbd2aa1cfd2d579416b355b0d1f5c5abef3b42057b99a66989a33d8995318bf1c57d66ec4528d8e97f411759dba2cf5d74187d28060120
7
+ data.tar.gz: 73a64ce207bce9fcb050d535eb21a10ff561da69d328c4a3ebd4cbee6d9a5705f675d0bdbf086f9cffd31eb9a57691de001004a75be3b0cab7149b0744c630e4
data/.rubocop.yml CHANGED
@@ -26,6 +26,7 @@ Layout/FirstArrayElementIndentation:
26
26
  # Metrics
27
27
 
28
28
  Metrics/AbcSize:
29
+ Max: 30
29
30
  CountRepeatedAttributes: false
30
31
  Exclude:
31
32
  - 'spec/**/*_spec.rb'
data/README.md CHANGED
@@ -38,12 +38,24 @@ Alternatively, install it globally:
38
38
  gem install ruby_nest_nats
39
39
  ```
40
40
 
41
- ### NATS server
41
+ ### NATS server (important!)
42
42
 
43
- **IMPORTANT:** This gem also requires a NATS server to be installed and running before use. See [the NATS documentation](https://docs.nats.io/nats-server/installation) for more details.
43
+ This gem also requires a NATS server to be installed and running before use. See [the NATS documentation](https://docs.nats.io/nats-server/installation) for more details.
44
44
 
45
45
  ## Usage
46
46
 
47
+ ### Starting the NATS server
48
+
49
+ 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:
50
+
51
+ ```bash
52
+ docker run -p 4222:4222 -p 8222:8222 -p 6222:6222 -ti nats:latest
53
+ ```
54
+
55
+ > **NOTE:** You may need to run that command with `sudo` on some systems, depending on the permissions of your Docker installation.
56
+
57
+ > **NOTE:** For other methods of running a NATS server, see [the NATS documentation](https://docs.nats.io/nats-server/installation).
58
+
47
59
  ### Logging
48
60
 
49
61
  #### Attaching a logger
@@ -121,12 +133,16 @@ end
121
133
 
122
134
  Start listening for messages with the `RubyNestNats::Client::start!` method. This will spin up a non-blocking thread that subscribes to subjects (as specified by invocation(s) of `::reply_to`) and waits for messages to come in. When a message is received, the appropriate `::reply_to` block will be used to compute a response, and that response will be published.
123
135
 
124
- > **NOTE:** If an error is raised in one of the handlers, `RubyNestNats::Client` will restart automatically.
125
-
126
136
  ```rb
127
137
  RubyNestNats::Client.start!
128
138
  ```
129
139
 
140
+ > **NOTE:** If an error is raised in one of the handlers, `RubyNestNats::Client` will restart automatically.
141
+
142
+ > **NOTE:** You _can_ invoke `::reply_to` to create additional message subscriptions after `RubyNestNats::Client.start!`, but be aware that this forces the client to restart. You may see (benign, already-handled) errors in the logs generated when this restart happens. It will force the client to restart and re-subscribe after _each additional `::reply_to` invoked after `::start!`._ So, if you have a lot of additional `::reply_to` invocations, you may want to consider refactoring so that your call to `RubyNestNats::Client.start!` occurs _after_ those additions.
143
+
144
+ > **NOTE:** The `::start!` method can be safely called multiple times; only the first will be honored, and any subsequent calls to `::start!` after the client is already started will do nothing (except write a _"NATS is already running"_ log to the logger at the `DEBUG` level).
145
+
130
146
  ### Basic full working example (in vanilla Ruby)
131
147
 
132
148
  The following should be enough to start a `ruby_nest_nats` setup in your Ruby application, using what we've learned so far.
@@ -150,21 +166,17 @@ RubyNestNats::Client.reply_to("subject.in.queue", queue: "barbaz") { { msg: "My
150
166
  RubyNestNats::Client.start!
151
167
  ```
152
168
 
153
- > **NOTE:** You _can_ invoke `::reply_to` to create additional message subscriptions after `RubyNestNats::Client.start!`, but be aware that this forces the client to restart. You may see (benign, already-handled) errors in the logs generated when this restart happens. It will force the client to restart and re-subscribe after _each additional `::reply_to` invoked after `::start!`._ So, if you have a lot of additional `::reply_to` invocations, you may want to consider refactoring so that your call to `RubyNestNats::Client.start!` occurs _after_ those additions.
154
-
155
- > **NOTE:** The `::start!` method can be safely called multiple times; only the first will be honored, and any subsequent calls to `::start!` after the client is already started will do nothing (except write a _"NATS is already running"_ log to the logger at the `DEBUG` level).
156
-
157
169
  <a id="controller-section"></a>
158
170
 
159
171
  ### Creating "controller"-style classes for listener organization
160
172
 
161
173
  Create controller classes which inherit from `RubyNestNats::Controller` in order to give your message listeners some structure.
162
174
 
163
- Use the `::default_queue` macro to set a default queue string. If omitted, the controller will fall back on the default queue assigned with `RubyNestNats::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.
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 `RubyNestNats::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.
164
176
 
165
177
  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.
166
178
 
167
- You can register a response for the built-up subject/pattern string using the `::response` macro. Pass a block to `::response` which optionally takes two arguments ([the same arguments supplied to `RubyNestNats::Client::reply_to`](#reply-to-section)). The result of that block will be sent as a response to the message received.
179
+ You can register a response for the built-up subject/pattern string using the `::response` macro. Pass a block to `::response` which optionally takes two arguments ([the same arguments supplied to the block of `RubyNestNats::Client::reply_to`](#reply-to-section)). The result of that block will be sent as a response to the message received.
168
180
 
169
181
  ```rb
170
182
  class HelloController < RubyNestNats::Controller
@@ -6,7 +6,10 @@ require_relative "ruby_nest_nats/utils"
6
6
  require_relative "ruby_nest_nats/client"
7
7
  require_relative "ruby_nest_nats/controller"
8
8
 
9
+ # The `RubyNestNats` module provides the top-level namespace for the NATS client
10
+ # and controller machinery.
9
11
  module RubyNestNats
10
- class Error < StandardError; end
11
- class NewSubscriptionsError < StandardError; end
12
+ class Error < StandardError; end # :nodoc:
13
+
14
+ class NewSubscriptionsError < RubyNestNats::Error; end # :nodoc:
12
15
  end
@@ -1,43 +1,143 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require "nats/client"
3
5
  require_relative "./utils"
4
6
 
5
7
  module RubyNestNats
8
+ # The `RubyNestNats::Client` class provides a basic interface for subscribing
9
+ # to messages by subject & queue, and replying to those messages. It also logs
10
+ # most functionality if desired.
6
11
  class Client
7
12
  class << self
13
+ attr_reader :logger, :default_queue # :nodoc:
14
+
15
+ # Attach a logger to have `ruby_nest_nats` write out logs for messages
16
+ # received, responses sent, errors raised, lifecycle events, etc.
17
+ #
18
+ # ```rb
19
+ # require 'ruby_nest_nats'
20
+ # require 'logger'
21
+ #
22
+ # nats_logger = Logger.new(STDOUT)
23
+ # nats_logger.level = Logger::INFO
24
+ #
25
+ # RubyNestNats::Client.logger = nats_logger
26
+ # ```
27
+ #
28
+ # In a Rails application, you might do this instead:
29
+ #
30
+ # ```rb
31
+ # RubyNestNats::Client.logger = Rails.logger
32
+ # ```
33
+ #
8
34
  def logger=(some_logger)
9
35
  log("Setting the logger to #{some_logger.inspect}")
10
36
  @logger = some_logger
11
37
  end
12
38
 
13
- def logger
14
- @logger
15
- end
16
-
39
+ # Set a default queue for subscriptions.
40
+ #
41
+ # ```rb
42
+ # RubyNestNats::Client.default_queue = "foobar"
43
+ # ```
44
+ #
45
+ # Leave the `::default_queue` blank (or assign `nil`) to use no default
46
+ # queue.
47
+ #
48
+ # ```rb
49
+ # RubyNestNats::Client.default_queue = nil
50
+ # ```
51
+ #
17
52
  def default_queue=(some_queue)
18
53
  queue = Utils.presence(some_queue.to_s)
19
54
  log("Setting the default queue to #{queue || '(none)'}", level: :debug)
20
55
  @default_queue = queue
21
56
  end
22
57
 
23
- def default_queue
24
- @default_queue
25
- end
26
-
58
+ # Returns `true` if `::start!` has already been called (meaning the client
59
+ # is listening to NATS messages). Returns `false` if it has not yet been
60
+ # called, or if it has been stopped.
27
61
  def started?
28
62
  @started ||= false
29
63
  end
30
64
 
65
+ # Opposite of `::started?`: returns `false` if `::start!` has already been
66
+ # called (meaning the client is listening to NATS messages). Returns
67
+ # `true` if it has not yet been called, or if it has been stopped.
31
68
  def stopped?
32
69
  !started?
33
70
  end
34
71
 
72
+ # Register a message handler with the `RubyNestNats::Client::reply_to`
73
+ # method. Pass a subject string as the first argument (either a static
74
+ # subject string or a pattern to match more than one subject). Specify a
75
+ # queue (or don't) with the `queue:` option. If you don't provide the
76
+ # `queue:` option, it will be set to the value of `default_queue`, or to
77
+ # `nil` (no queue) if a default queue hasn't been set.
78
+ #
79
+ # The result of the given block will be published in reply to the message.
80
+ # The block is passed two arguments when a message matching the subject is
81
+ # received: `data` and `subject`. The `data` argument is the payload of
82
+ # the message (JSON objects/arrays will be parsed into string-keyed `Hash`
83
+ # objects/`Array` objects, respectively). The `subject` argument is the
84
+ # subject of the message received (mostly only useful if a _pattern_ was
85
+ # specified instead of a static subject string).
86
+ #
87
+ # ```rb
88
+ # RubyNestNats::Client.reply_to("some.subject", queue: "foobar") { |data| "Got it! #{data.inspect}" }
89
+ #
90
+ # RubyNestNats::Client.reply_to("some.*.pattern") { |data, subject| "Got #{data} on #{subject}" }
91
+ #
92
+ # RubyNestNats::Client.reply_to("other.subject") do |data|
93
+ # if data["foo"] == "bar"
94
+ # { is_bar: "Yep!" }
95
+ # else
96
+ # { is_bar: "No way!" }
97
+ # end
98
+ # end
99
+ #
100
+ # RubyNestNats::Client.reply_to("subject.in.queue", queue: "barbaz") do
101
+ # "My turn!"
102
+ # end
103
+ # ```
104
+ #
35
105
  def reply_to(subject, queue: nil, &block)
36
106
  queue = Utils.presence(queue) || default_queue
37
- log("Registering a reply handler for subject '#{subject}'#{" in queue '#{queue}'" if queue}", level: :debug)
107
+ queue_desc = " in queue '#{queue}'" if queue
108
+ log("Registering a reply handler for subject '#{subject}'#{queue_desc}", level: :debug)
38
109
  register_reply!(subject: subject.to_s, handler: block, queue: queue.to_s)
39
110
  end
40
111
 
112
+ # Start listening for messages with the `RubyNestNats::Client::start!`
113
+ # method. This will spin up a non-blocking thread that subscribes to
114
+ # subjects (as specified by invocation(s) of `::reply_to`) and waits for
115
+ # messages to come in. When a message is received, the appropriate
116
+ # `::reply_to` block will be used to compute a response, and that response
117
+ # will be published.
118
+ #
119
+ # ```rb
120
+ # RubyNestNats::Client.start!
121
+ # ```
122
+ #
123
+ # **NOTE:** If an error is raised in one of the handlers,
124
+ # `RubyNestNats::Client` will restart automatically.
125
+ #
126
+ # **NOTE:** You _can_ invoke `::reply_to` to create additional message
127
+ # subscriptions after `RubyNestNats::Client.start!`, but be aware that
128
+ # this forces the client to restart. You may see (benign, already-handled)
129
+ # errors in the logs generated when this restart happens. It will force
130
+ # the client to restart and re-subscribe after _each additional
131
+ # `::reply_to` invoked after `::start!`._ So, if you have a lot of
132
+ # additional `::reply_to` invocations, you may want to consider
133
+ # refactoring so that your call to `RubyNestNats::Client.start!` occurs
134
+ # _after_ those additions.
135
+ #
136
+ # **NOTE:** The `::start!` method can be safely called multiple times;
137
+ # only the first will be honored, and any subsequent calls to `::start!`
138
+ # after the client is already started will do nothing (except write a
139
+ # _"NATS is already running"_ log to the logger at the `DEBUG` level).
140
+ #
41
141
  def start!
42
142
  log("Starting NATS", level: :debug)
43
143
 
@@ -50,22 +150,20 @@ module RubyNestNats
50
150
 
51
151
  self.current_thread = Thread.new do
52
152
  Thread.handle_interrupt(StandardError => :never) do
53
- begin
54
- Thread.handle_interrupt(StandardError => :immediate) { listen }
55
- rescue NATS::ConnectError => e
56
- log("Could not connect to NATS server:", level: :error)
57
- log(e.full_message, level: :error, indent: 2)
58
- Thread.current.exit
59
- rescue NewSubscriptionsError => e
60
- log("New subscriptions! Restarting...", level: :info)
61
- restart!
62
- raise e # TODO: there has to be a better way
63
- rescue StandardError => e
64
- log("Encountered an error:", level: :error)
65
- log(e.full_message, level: :error, indent: 2)
66
- restart!
67
- raise e
68
- end
153
+ Thread.handle_interrupt(StandardError => :immediate) { listen }
154
+ rescue NATS::ConnectError => e
155
+ log("Could not connect to NATS server:", level: :error)
156
+ log(e.full_message, level: :error, indent: 2)
157
+ Thread.current.exit
158
+ rescue NewSubscriptionsError => e
159
+ log("New subscriptions! Restarting...", level: :info)
160
+ restart!
161
+ raise e # TODO: there has to be a better way
162
+ rescue StandardError => e
163
+ log("Encountered an error:", level: :error)
164
+ log(e.full_message, level: :error, indent: 2)
165
+ restart!
166
+ raise e
69
167
  end
70
168
  end
71
169
  end
@@ -86,7 +184,13 @@ module RubyNestNats
86
184
 
87
185
  def stop!
88
186
  log("Stopping NATS", level: :debug)
89
- NATS.stop rescue nil
187
+
188
+ begin
189
+ NATS.stop
190
+ rescue StandardError
191
+ nil
192
+ end
193
+
90
194
  stopped!
91
195
  end
92
196
 
@@ -108,13 +212,7 @@ module RubyNestNats
108
212
  @replies ||= []
109
213
  end
110
214
 
111
- def current_thread
112
- @current_thread
113
- end
114
-
115
- def current_thread=(some_thread)
116
- @current_thread = some_thread
117
- end
215
+ attr_accessor :current_thread
118
216
 
119
217
  def reply_registered?(raw_subject)
120
218
  subject = raw_subject.to_s
@@ -129,18 +227,19 @@ module RubyNestNats
129
227
  reply = {
130
228
  subject: subject,
131
229
  handler: handler,
132
- queue: Utils.presence(queue) || default_queue
230
+ queue: Utils.presence(queue) || default_queue,
133
231
  }
134
232
 
135
233
  replies << reply
136
234
 
137
- self.current_thread.raise(NewSubscriptionsError, "New reply registered") if started?
235
+ current_thread.raise(NewSubscriptionsError, "New reply registered") if started?
138
236
  end
139
237
 
140
238
  def listen
141
239
  NATS.start do
142
240
  replies.each do |replier|
143
- log("Subscribing to subject '#{replier[:subject]}'#{" in queue '#{replier[:queue]}'" if replier[:queue]}", level: :debug)
241
+ queue_desc = " in queue '#{replier[:queue]}'" if replier[:queue]
242
+ log("Subscribing to subject '#{replier[:subject]}'#{queue_desc}", level: :debug)
144
243
 
145
244
  NATS.subscribe(replier[:subject], queue: replier[:queue]) do |message, inbox, subject|
146
245
  parsed_message = JSON.parse(message)
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "./utils"
2
4
 
3
5
  module RubyNestNats
6
+ # Create controller classes which inherit from `RubyNestNats::Controller` in
7
+ # order to give your message listeners some structure.
4
8
  class Controller
5
- NO_QUEUE_GIVEN = :ruby_nest_nats_super_special_no_op_queue_symbol_qwertyuiop_1234567890
9
+ NO_QUEUE_GIVEN = :ruby_nest_nats_super_special_no_op_queue_symbol_qwertyuiop1234567890
6
10
 
7
11
  class << self
8
12
  # Default queue for the controller. Falls back to the client's default
@@ -20,6 +24,13 @@ module RubyNestNats
20
24
  # # ...
21
25
  # end
22
26
  #
27
+ # If omitted, the controller will fall back on the global default queue
28
+ # assigned with `RubyNestNats::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.
33
+ #
23
34
  def default_queue(some_queue = NO_QUEUE_GIVEN)
24
35
  # `NO_QUEUE_GIVEN` is a special symbol (rather than `nil`) so that the
25
36
  # default queue can be "unset" to `nil` (given a non-`nil` global
@@ -31,6 +42,55 @@ module RubyNestNats
31
42
  end
32
43
  end
33
44
 
45
+ # Use the `::subject` macro to create a block for listening to that
46
+ # subject segment. Nested calls to `::subject` will append each subsequent
47
+ # subject/pattern string to the last (joined by a periods). There is no
48
+ # limit to the level of nesting.
49
+ #
50
+ # **NOTE:** The following two examples do exactly the same thing.
51
+ #
52
+ # Example:
53
+ #
54
+ # class FoobarNatsController < RubyNatsController
55
+ # # ...
56
+ #
57
+ # subject "hello.wassup" do
58
+ # response do |data, subject|
59
+ # # The subject at this point is "hello.wassup"
60
+ # # ...
61
+ # end
62
+ # end
63
+ #
64
+ # subject "hello.howdy" do
65
+ # response do |data, subject|
66
+ # # The subject at this point is "hello.howdy"
67
+ # # ...
68
+ # end
69
+ # end
70
+ # end
71
+ #
72
+ # Example:
73
+ #
74
+ # class FoobarNatsController < RubyNatsController
75
+ # # ...
76
+ #
77
+ # subject "hello" do
78
+ # subject "wassup" do
79
+ # response do |data, subject|
80
+ # # The subject at this point is "hello.wassup"
81
+ # # ...
82
+ # end
83
+ # end
84
+ #
85
+ # subject "howdy" do
86
+ # response do |data, subject|
87
+ # # The subject at this point is "hello.howdy"
88
+ # # ...
89
+ # end
90
+ # end
91
+ # end
92
+ # end
93
+ #
34
94
  def subject(subject_segment, queue: nil)
35
95
  subject_chain.push(subject_segment)
36
96
  old_queue = current_queue
@@ -40,6 +100,40 @@ module RubyNestNats
40
100
  subject_chain.pop
41
101
  end
42
102
 
103
+ # You can register a response for the built-up subject/pattern string
104
+ # using the `::response` macro. Pass a block to `::response` which
105
+ # optionally takes two arguments (the same arguments supplied to the block
106
+ # of `RubyNestNats::Client::reply_to`). The result of that block will be
107
+ # sent as a response to the message received.
108
+ #
109
+ # Example:
110
+ #
111
+ # class FoobarNatsController < RubyNatsController
112
+ # # ...
113
+ #
114
+ # subject "hello" do
115
+ # subject "wassup" do
116
+ # response do |data, subject|
117
+ # # The subject at this point is "hello.wassup".
118
+ # # Assume the message sent a JSON payload of {"name":"Bob"}
119
+ # # in this example.
120
+ # # We'll reply with a string response:
121
+ # "I'm all right, #{data['name']}"
122
+ # end
123
+ # end
124
+ #
125
+ # subject "howdy" do
126
+ # response do |data, subject|
127
+ # # The subject at this point is "hello.howdy".
128
+ # # Assume the message sent a JSON payload of {"name":"Bob"}
129
+ # # in this example.
130
+ # # We'll reply with a JSON response (a Ruby `Hash`):
131
+ # { message: "I'm okay, #{data['name']}. Thanks for asking!" }
132
+ # end
133
+ # end
134
+ # end
135
+ # end
136
+ #
43
137
  def response(queue: nil, &block)
44
138
  response_queue = Utils.presence(queue.to_s) || current_queue || default_queue
45
139
  Client.reply_to(current_subject, queue: response_queue, &block)
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RubyNestNats
2
- class Utils
4
+ class Utils # :nodoc:
3
5
  class << self
4
6
  def blank?(value)
5
7
  value.respond_to?(:empty?) ? value.empty? : !value
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyNestNats
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_nest_nats
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keegan Leitz