ruby_nest_nats 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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