ruby_nest_nats 0.1.2 → 0.1.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: 0dbcc88e2386b8f1b6408688c8fafba196037fb70e089a6f51fd4fca2f4b06a1
4
- data.tar.gz: 96b78dccc0a509f21bd1ed5573940e57b76a51df21e20b6a95fd2f145b067714
3
+ metadata.gz: 12ced8e11e87c01a5106285c4b01e7cb75f338adbb952b95d2362d928c76133c
4
+ data.tar.gz: '049dd24c5e1015e8b3e90be3f77f352bd934c7837c71881e8f4235a47498e2ec'
5
5
  SHA512:
6
- metadata.gz: 06db896c2a6e94bac268624a7e75e9e6fa203bc6c6955a6893f661b11c5249faf0b97c397ebc9b646b395b781597b8d4b5cf2d0718ee233014bb62f3ccc27d9f
7
- data.tar.gz: 0d5f123eadb77873eeed442162f72b20693696a9f1a57b9759edac7df74ab147df48a8ebe16a7d4f124a897bddc303941f84cac294457a08fcc3bf8ef3199b5f
6
+ metadata.gz: 3340f499fe967b290f3b2b21e2f34b69a55ed8b8a003c9e0af897df556890fcb884bdf99f25f67e5195a614c4fde21f311d395d7376bab07b1f51fe88bd37b56
7
+ data.tar.gz: 23eabe3e8b26ba41f9e4ca38f55e8ae48c7362a4658d16d92a9b6284843d55ce34a912c3eae8ad8d312a14457b5ea230cc85422bbde3539c66b3788f1c52ba6d
data/README.md CHANGED
@@ -1,46 +1,202 @@
1
1
  # RubyNestNats
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ruby_nest_nats`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ The `ruby_nest_nats` gem allows you to listen for (and reply to) NATS messages asynchronously in a Ruby application.
6
4
 
7
5
  ## TODO
8
6
 
9
- - [ ] docs
7
+ - [x] docs
10
8
  - [ ] tests
11
- - [ ] multiple queues
9
+ - [ ] "controller"-style classes for reply organization
10
+ - [x] multiple queues
12
11
  - [ ] `on_error` handler so you can send a response (what's standard?)
12
+ - [ ] config options for URL/host/port/etc.
13
13
  - [ ] config for restart behavior (default is to restart listening on any `StandardError`)
14
14
 
15
15
  ## Installation
16
16
 
17
- Add this line to your application's Gemfile:
17
+ ### Locally (to your application)
18
+
19
+ Add the gem to your application's `Gemfile`:
18
20
 
19
21
  ```ruby
20
22
  gem 'ruby_nest_nats'
21
23
  ```
22
24
 
23
- And then execute:
25
+ ...and then run:
24
26
 
25
- $ bundle install
27
+ ```bash
28
+ bundle install
29
+ ```
26
30
 
27
- Or install it yourself as:
31
+ ### Globally (to your system)
28
32
 
29
- $ gem install ruby_nest_nats
33
+ Alternatively, install it globally:
34
+
35
+ ```bash
36
+ gem install ruby_nest_nats
37
+ ```
30
38
 
31
39
  ## Usage
32
40
 
33
- TODO: Write usage instructions here
41
+ ### Logging
42
+
43
+ #### Attaching a logger
44
+
45
+ Attach a logger to have `ruby_nest_nats` write out logs for messages received, responses sent, errors raised, lifecycle events, etc.
46
+
47
+ ```rb
48
+ require 'logger'
49
+
50
+ nats_logger = Logger.new(STDOUT)
51
+ nats_logger.level = Logger::INFO
52
+
53
+ RubyNestNats::Client.logger = nats_logger
54
+ ```
55
+
56
+ In a Rails application, you might do this instead:
57
+
58
+ ```rb
59
+ RubyNestNats::Client.logger = Rails.logger
60
+ ```
61
+
62
+ #### Log levels
63
+
64
+ The following will be logged at the specified log levels
65
+
66
+ - `DEBUG`: Lifecycle events (starting NATS listeners, stopping NATS, reply registration, setting the default queue, etc.), as well as everything under `INFO`, `WARN`, and `ERROR`
67
+ - `INFO`: Message activity over NATS (received a message, replied with a message, etc.), as well as everything under `WARN` and `ERROR`
68
+ - `WARN`: Error handled gracefully (listening restarted due to some exception, etc.), as well as everything under `ERROR`
69
+ - `ERROR`: Some exception was raised in-thread (error in handler, error in subscription, etc.)
70
+
71
+ ### Setting a default queue
72
+
73
+ Set a default queue for subscriptions.
74
+
75
+ ```rb
76
+ RubyNestNats::Client.default_queue = "foobar"
77
+ ```
78
+
79
+ Leave the `::default_queue` blank (or assign `nil`) to use no default queue.
80
+
81
+ ```rb
82
+ RubyNestNats::Client.default_queue = nil
83
+ ```
84
+
85
+ ### Registering message handlers
86
+
87
+ Register a message handler with the `RubyNestNats::Client::reply_to` method. Pass a subject string as the first argument (either a static subject string or a pattern to match more than one subject). Specify a queue (or don't) with the `queue:` option. If you don't provide the `queue:` option, it will be set to the value of `default_queue`, or to `nil` (no queue) if a default queue hasn't been set.
88
+
89
+ The result of the given block will be published in reply to the message. The block is passed two arguments when a message matching the subject is received: `data` and `subject`. The `data` argument is the payload of the message (JSON objects/arrays will be parsed into string-keyed `Hash` objects/`Array` objects, respectively). The `subject` argument is the subject of the message received (mostly only useful if a _pattern_ was specified instead of a static subject string).
90
+
91
+ ```rb
92
+ RubyNestNats::Client.reply_to("some.subject", queue: "foobar") { |data| "Got it! #{data.inspect}" }
93
+
94
+ RubyNestNats::Client.reply_to("some.*.pattern") { |data, subject| "Got #{data} on #{subject}" }
95
+
96
+ RubyNestNats::Client.reply_to("other.subject") do |data|
97
+ if data["foo"] == "bar"
98
+ { is_bar: "Yep!" }
99
+ else
100
+ { is_bar: "No way!" }
101
+ end
102
+ end
103
+
104
+ RubyNestNats::Client.reply_to("subject.in.queue", queue: "barbaz") do
105
+ "My turn!"
106
+ end
107
+ ```
108
+
109
+ ### Starting the listeners
110
+
111
+ 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.
112
+
113
+ > **NOTE:** If an error is raised in one of the handlers, `RubyNestNats::Client` will restart automatically.
114
+
115
+ ```rb
116
+ RubyNestNats::Client.start!
117
+ ```
118
+
119
+ ### Full example
120
+
121
+ ```rb
122
+ RubyNestNats::Client.logger = Rails.logger
123
+ RubyNestNats::Client.default_queue = "foobar"
124
+
125
+ RubyNestNats::Client.reply_to("some.subject") { |data| "Got it! #{data.inspect}" }
126
+ RubyNestNats::Client.reply_to("some.*.pattern") { |data, subject| "Got #{data} on #{subject}" }
127
+ RubyNestNats::Client.reply_to("subject.in.queue", queue: "barbaz") { { msg: "My turn!", turn: 5 } }
128
+
129
+ RubyNestNats::Client.start!
130
+ ```
34
131
 
35
132
  ## Development
36
133
 
134
+ ### Install dependencies
135
+
136
+ To install the Ruby dependencies, run:
137
+
138
+ ```bash
139
+ bin/setup
140
+ ```
141
+
142
+ This gem also requires a NATS server to be running. See [the NATS documentation](https://docs.nats.io/nats-server/installation) for more details.
143
+
144
+ ### Open a console
145
+
146
+ To open a REPL with the gem's code loaded, run:
147
+
148
+ ```bash
149
+ bin/console
150
+ ```
151
+
152
+ ### Run the tests
153
+
154
+ To run the RSpec test suites, run:
155
+
156
+ ```bash
157
+ bundle exec rake spec
158
+ ```
159
+
160
+ ...or (if your Ruby setup has good defaults) just this:
161
+
162
+ ```bash
163
+ rake spec
164
+ ```
165
+
166
+ ### Run the linter
167
+
168
+ ```bash
169
+ bundle exec rubocop
170
+ ```
171
+
172
+ ### Create a release
173
+
174
+ Bump the `RubyNestNats::VERSION` value in `lib/ruby_nest_nats/version.rb`, commit, and then run:
175
+
176
+ ```bash
177
+ bundle exec rake release
178
+ ```
179
+
180
+ ...or (if your Ruby setup has good defaults) just this:
181
+
182
+ ```bash
183
+ rake release
184
+ ```
185
+
186
+ This will:
187
+
188
+ 1. create a git tag for the new version,
189
+ 1. push the commits,
190
+ 1. build the gem, and
191
+ 1. push it to [rubygems.org](https://rubygems.org/gems/ruby_nest_nats).
192
+
37
193
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
38
194
 
39
195
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
40
196
 
41
197
  ## Contributing
42
198
 
43
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ruby_nest_nats.
199
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Openbay/ruby_nest_nats.
44
200
 
45
201
  ## License
46
202
 
@@ -9,6 +9,7 @@ module RubyNestNats
9
9
  class Client
10
10
  class << self
11
11
  def logger=(some_logger)
12
+ log("Setting the logger to #{some_logger.inspect}")
12
13
  @logger = some_logger
13
14
  end
14
15
 
@@ -16,82 +17,148 @@ module RubyNestNats
16
17
  @logger
17
18
  end
18
19
 
19
- def log(text)
20
- logger.info("RubyNestNats | #{text}") if logger
20
+ def default_queue=(some_queue)
21
+ queue = presence(some_queue.to_s)
22
+ log("Setting the default queue to #{queue}", level: :debug)
23
+ @default_queue = queue
21
24
  end
22
25
 
23
- def queue=(some_queue)
24
- @queue = some_queue.to_s
25
- end
26
-
27
- def queue
28
- @queue
29
- end
30
-
31
- def replies
32
- @replies ||= []
26
+ def default_queue
27
+ @default_queue
33
28
  end
34
29
 
35
30
  def started?
36
31
  @started ||= false
37
32
  end
38
33
 
39
- def reply_to(raw_subject, &block)
40
- subject = raw_subject.to_s
41
-
42
- if started?
43
- raise StandardError, "NATS already started"
44
- elsif !block_given?
45
- raise ArgumentError, "Response block must be provided"
46
- elsif replies.any? { |reply| reply[:subject] == subject }
47
- raise ArgumentError, "Already registered a reply to #{subject}"
48
- end
34
+ def stopped?
35
+ !started?
36
+ end
49
37
 
50
- log("Registering a reply handler for subject '#{subject}'#{" in queue '#{queue}'" if queue}")
51
- replies << { subject: subject, handler: block, queue: queue }
38
+ def reply_to(subject, queue: nil, &block)
39
+ subject = subject.to_s
40
+ queue = (presence(queue) || default_queue).to_s
41
+ log("Registering a reply handler for subject '#{subject}'#{" in queue '#{queue}'" if queue}", level: :debug)
42
+ register_reply!(subject: subject, handler: block, queue: queue)
52
43
  end
53
44
 
54
45
  def listen
55
46
  NATS.start do
56
47
  replies.each do |replier|
48
+ log("Subscribing to subject '#{replier[:subject]}'#{" in queue '#{replier[:queue]}'" if replier[:queue]}", level: :debug)
49
+
57
50
  NATS.subscribe(replier[:subject], queue: replier[:queue]) do |message, inbox, subject|
58
- log("Received the message '#{message}' for subject '#{subject}' with reply inbox '#{inbox}'")
59
- response = replier[:handler].call(JSON.parse(message)["data"])
60
- log("Responding with '#{response}'")
61
- NATS.publish(inbox, response.to_json, queue: replier[:queue])
51
+ parsed_message = JSON.parse(message)
52
+ id, data, pattern = parsed_message.values_at("id", "data", "pattern")
53
+
54
+ log("Received a message!")
55
+ message_desc = <<~LOG_MESSAGE
56
+ id: #{id || '(none)'}
57
+ pattern: #{pattern || '(none)'}
58
+ subject: #{subject || '(none)'}
59
+ data: #{data.to_json}
60
+ inbox: #{inbox || '(none)'}
61
+ LOG_MESSAGE
62
+ log(message_desc, indent: 2)
63
+
64
+ response_data = replier[:handler].call(data)
65
+
66
+ log("Responding with '#{response_data}'")
67
+
68
+ NATS.publish(inbox, response_data.to_json, queue: replier[:queue])
62
69
  end
63
70
  end
64
71
  end
65
72
  end
66
73
 
67
74
  def stop!
68
- log("Stopping NATS")
69
- NATS.stop
70
- @started = false
75
+ log("Stopping NATS", level: :debug)
76
+ NATS.stop rescue nil
77
+ stopped!
71
78
  end
72
79
 
73
80
  def restart!
74
- log("Restarting NATS...")
81
+ log("Restarting NATS", level: :warn)
75
82
  stop!
76
83
  start!
77
84
  end
78
85
 
79
86
  def start!
80
- log("Starting NATS")
81
- return log("NATS is already running") if started?
87
+ log("Starting NATS", level: :debug)
82
88
 
83
- @started = true
89
+ if started?
90
+ log("NATS is already running", level: :debug)
91
+ return
92
+ end
93
+
94
+ started!
84
95
 
85
96
  Thread.new do
86
97
  Thread.handle_interrupt(StandardError => :never) do
87
98
  begin
88
99
  Thread.handle_interrupt(StandardError => :immediate) { listen }
89
- ensure
100
+ rescue => e
101
+ log("Encountered an error:", level: :error)
102
+ log(e.full_message, level: :error, indent: 2)
103
+
90
104
  restart!
105
+ raise e
91
106
  end
92
107
  end
93
108
  end
94
109
  end
110
+
111
+ private
112
+
113
+ def log(text, level: :info, indent: 0)
114
+ return unless logger
115
+
116
+ timestamp = Time.now.to_s
117
+ text_lines = text.split("\n")
118
+ indentation = indent.is_a?(String) ? indent : (" " * indent)
119
+
120
+ text_lines.each do |line|
121
+ logger.send(level, "[#{timestamp}] RubyNestNats | #{indentation}#{line}")
122
+ end
123
+ end
124
+
125
+ def started!
126
+ @started = true
127
+ end
128
+
129
+ def stopped!
130
+ @started = false
131
+ end
132
+
133
+ def replies
134
+ @replies ||= []
135
+ end
136
+
137
+ def reply_registered?(raw_subject)
138
+ subject = raw_subject.to_s
139
+ replies.any? { |reply| reply[:subject] == subject }
140
+ end
141
+
142
+ def register_reply!(subject:, handler:, queue: nil)
143
+ raise StandardError, "NATS already started" if started? # TODO: remove when runtime additions are implemented
144
+ raise ArgumentError, "Subject must be a string" unless subject.is_a?(String)
145
+ raise ArgumentError, "Must provide a message handler for #{subject}" unless handler.respond_to?(:call)
146
+ raise ArgumentError, "Already registered a reply to #{subject}" if reply_registered?(subject)
147
+
148
+ replies << { subject: subject, handler: handler, queue: presence(queue) || default_queue }
149
+ end
150
+
151
+ def blank?(value)
152
+ value.respond_to?(:empty?) ? value.empty? : !value
153
+ end
154
+
155
+ def present?(value)
156
+ !blank?(value)
157
+ end
158
+
159
+ def presence(value)
160
+ present?(value) ? value : nil
161
+ end
95
162
  end
96
163
  end
97
164
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyNestNats
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
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.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keegan Leitz