ruby_nest_nats 0.2.0 → 0.2.1

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: 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