natsy 0.3.0
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 +7 -0
- data/.github/workflows/main.yml +44 -0
- data/.gitignore +70 -0
- data/.rubocop.yml +103 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +319 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/natsy.rb +17 -0
- data/lib/natsy/client.rb +270 -0
- data/lib/natsy/controller.rb +158 -0
- data/lib/natsy/utils.rb +20 -0
- data/lib/natsy/version.rb +5 -0
- data/natsy.gemspec +41 -0
- metadata +201 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a037edf2d9a4b510d45ac57b9a891ccaf5c8b14a82ad359b5ef24974f119342f
|
4
|
+
data.tar.gz: 7351f54131880e05d0ddf5955a8bc48af0635abf53de8fcd13a06f8ef9587e72
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 071a63d868fbd6441c0b33f459c643d0d23f471c63317274dc5ae45ddf896fd69cee84701da5c1cd8ccb86ff1b8cc90e8c9cc9f98ced72508b48879a93e8b572
|
7
|
+
data.tar.gz: 13345e7ab11e6f770310d27d8dbd2cffa64b0e27dd04b63096bce5827562361b261b81f7e762c328f7a01d6d971399cb8f0c39d61efd370d69fc7aee9a3cd031
|
@@ -0,0 +1,44 @@
|
|
1
|
+
name: Test and lint
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- main
|
7
|
+
|
8
|
+
pull_request:
|
9
|
+
branches:
|
10
|
+
- main
|
11
|
+
|
12
|
+
# Allows you to run this workflow manually from the Actions tab
|
13
|
+
workflow_dispatch:
|
14
|
+
|
15
|
+
jobs:
|
16
|
+
build:
|
17
|
+
name: Run RSpec tests and RuboCop lints
|
18
|
+
|
19
|
+
runs-on: ubuntu-latest
|
20
|
+
|
21
|
+
strategy:
|
22
|
+
matrix:
|
23
|
+
ruby-version:
|
24
|
+
- 2.7
|
25
|
+
- 2.6
|
26
|
+
|
27
|
+
steps:
|
28
|
+
- name: Checkout the repo
|
29
|
+
uses: actions/checkout@v2
|
30
|
+
|
31
|
+
- name: Set up Ruby v${{ matrix.ruby-version }}
|
32
|
+
uses: ruby/setup-ruby@v1
|
33
|
+
with:
|
34
|
+
ruby-version: ${{ matrix.ruby-version }}
|
35
|
+
bundler-cache: true
|
36
|
+
|
37
|
+
- name: Install dependencies
|
38
|
+
run: bundle install
|
39
|
+
|
40
|
+
- name: Run RSpec tests
|
41
|
+
run: bundle exec rake spec
|
42
|
+
|
43
|
+
- name: Run RuboCop lints
|
44
|
+
run: bundle exec rubocop
|
data/.gitignore
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# generic stuff
|
2
|
+
.env
|
3
|
+
*.gem
|
4
|
+
*.rbc
|
5
|
+
log/*.log
|
6
|
+
/.config
|
7
|
+
/InstalledFiles
|
8
|
+
/pkg/
|
9
|
+
/tmp/
|
10
|
+
|
11
|
+
# testy stuff
|
12
|
+
.rspec
|
13
|
+
.rspec_status
|
14
|
+
*.orig
|
15
|
+
/coverage/
|
16
|
+
/coverage/
|
17
|
+
/db/*.sqlite3
|
18
|
+
/db/*.sqlite3-[0-9]*
|
19
|
+
/db/*.sqlite3-journal
|
20
|
+
/public/system
|
21
|
+
/spec/examples.txt
|
22
|
+
/spec/reports/
|
23
|
+
/spec/tmp
|
24
|
+
/test/tmp/
|
25
|
+
/test/version_tmp/
|
26
|
+
capybara-*.html
|
27
|
+
pickle-email-*.html
|
28
|
+
rerun.txt
|
29
|
+
test/dummy/db/*.sqlite3
|
30
|
+
test/dummy/db/*.sqlite3-journal
|
31
|
+
test/dummy/log/*.log
|
32
|
+
test/dummy/node_modules/
|
33
|
+
test/dummy/storage/
|
34
|
+
test/dummy/tmp/
|
35
|
+
test/dummy/yarn-error.log
|
36
|
+
|
37
|
+
# debuggy stuff
|
38
|
+
.byebug_history
|
39
|
+
|
40
|
+
## doccy stuff
|
41
|
+
/.yardoc/
|
42
|
+
/_yardoc/
|
43
|
+
/doc/
|
44
|
+
/rdoc/
|
45
|
+
|
46
|
+
## bundly stuff
|
47
|
+
/.bundle/
|
48
|
+
/vendor/bundle
|
49
|
+
/lib/bundler/man/
|
50
|
+
|
51
|
+
# "for a library or gem, you might want to ignore these files since the code is
|
52
|
+
# intended to run in multiple environments"
|
53
|
+
Gemfile.lock
|
54
|
+
.ruby-version
|
55
|
+
.ruby-gemset
|
56
|
+
|
57
|
+
# rvmmy stuff
|
58
|
+
.rvmrc
|
59
|
+
|
60
|
+
# editory stuff
|
61
|
+
.idea
|
62
|
+
.vscode
|
63
|
+
*.rdb
|
64
|
+
|
65
|
+
# systemy stuff
|
66
|
+
*.swm
|
67
|
+
*.swn
|
68
|
+
*.swo
|
69
|
+
*.swp
|
70
|
+
*.DS_Store
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-performance
|
3
|
+
- rubocop-rspec
|
4
|
+
- rubocop-rake
|
5
|
+
|
6
|
+
# Globals
|
7
|
+
|
8
|
+
AllCops:
|
9
|
+
NewCops: enable
|
10
|
+
TargetRubyVersion: 2.6
|
11
|
+
|
12
|
+
# Layout
|
13
|
+
|
14
|
+
Layout/LineLength:
|
15
|
+
Max: 120
|
16
|
+
Exclude:
|
17
|
+
- 'spec/**/*_spec.rb'
|
18
|
+
- '*.gemspec'
|
19
|
+
|
20
|
+
Layout/EndAlignment:
|
21
|
+
EnforcedStyleAlignWith: variable
|
22
|
+
|
23
|
+
Layout/FirstArrayElementIndentation:
|
24
|
+
EnforcedStyle: consistent
|
25
|
+
|
26
|
+
# Metrics
|
27
|
+
|
28
|
+
Metrics/AbcSize:
|
29
|
+
Max: 30
|
30
|
+
CountRepeatedAttributes: false
|
31
|
+
Exclude:
|
32
|
+
- 'spec/**/*_spec.rb'
|
33
|
+
- '*.gemspec'
|
34
|
+
|
35
|
+
Metrics/BlockLength:
|
36
|
+
Exclude:
|
37
|
+
- 'spec/**/*_spec.rb'
|
38
|
+
- '*.gemspec'
|
39
|
+
|
40
|
+
Metrics/ClassLength:
|
41
|
+
Max: 150
|
42
|
+
CountComments: false
|
43
|
+
CountAsOne:
|
44
|
+
- array
|
45
|
+
- hash
|
46
|
+
- heredoc
|
47
|
+
Exclude:
|
48
|
+
- 'spec/**/*_spec.rb'
|
49
|
+
- '*.gemspec'
|
50
|
+
|
51
|
+
Metrics/MethodLength:
|
52
|
+
Max: 20
|
53
|
+
CountComments: false
|
54
|
+
CountAsOne:
|
55
|
+
- array
|
56
|
+
- hash
|
57
|
+
- heredoc
|
58
|
+
|
59
|
+
Metrics/ModuleLength:
|
60
|
+
Max: 150
|
61
|
+
CountComments: false
|
62
|
+
CountAsOne:
|
63
|
+
- array
|
64
|
+
- hash
|
65
|
+
- heredoc
|
66
|
+
Exclude:
|
67
|
+
- 'spec/**/*_spec.rb'
|
68
|
+
- '*.gemspec'
|
69
|
+
|
70
|
+
# Rspec
|
71
|
+
|
72
|
+
RSpec/ExampleLength:
|
73
|
+
Max: 25
|
74
|
+
|
75
|
+
RSpec/MessageSpies:
|
76
|
+
Enabled: false
|
77
|
+
|
78
|
+
RSpec/MultipleExpectations:
|
79
|
+
Enabled: false
|
80
|
+
|
81
|
+
RSpec/NestedGroups:
|
82
|
+
Max: 10
|
83
|
+
|
84
|
+
# Style
|
85
|
+
|
86
|
+
Style/DoubleNegation:
|
87
|
+
Enabled: false
|
88
|
+
|
89
|
+
Style/ExpandPathArguments:
|
90
|
+
Exclude:
|
91
|
+
- 'adornable.gemspec'
|
92
|
+
|
93
|
+
Style/StringLiterals:
|
94
|
+
Enabled: false
|
95
|
+
|
96
|
+
Style/TrailingCommaInArguments:
|
97
|
+
EnforcedStyleForMultiline: consistent_comma
|
98
|
+
|
99
|
+
Style/TrailingCommaInArrayLiteral:
|
100
|
+
EnforcedStyleForMultiline: consistent_comma
|
101
|
+
|
102
|
+
Style/TrailingCommaInHashLiteral:
|
103
|
+
EnforcedStyleForMultiline: consistent_comma
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Keegan Leitz
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,319 @@
|
|
1
|
+
# Natsy
|
2
|
+
|
3
|
+
The `natsy` gem allows you to listen for (and reply to) NATS messages asynchronously in a Ruby application.
|
4
|
+
|
5
|
+
## TODO
|
6
|
+
|
7
|
+
- [x] docs
|
8
|
+
- [ ] tests
|
9
|
+
- [x] "controller"-style classes for reply organization
|
10
|
+
- [x] runtime subscription additions
|
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.
|
14
|
+
- [ ] config for restart behavior (default is to restart listening on any `StandardError`)
|
15
|
+
- [ ] consider using child processes instead of threads
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
### Locally (to your application)
|
20
|
+
|
21
|
+
Add the gem to your application's `Gemfile`:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'natsy'
|
25
|
+
```
|
26
|
+
|
27
|
+
...and then run:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
bundle install
|
31
|
+
```
|
32
|
+
|
33
|
+
### Globally (to your system)
|
34
|
+
|
35
|
+
Alternatively, install it globally:
|
36
|
+
|
37
|
+
```bash
|
38
|
+
gem install natsy
|
39
|
+
```
|
40
|
+
|
41
|
+
### NATS server (important!)
|
42
|
+
|
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
|
+
|
45
|
+
## Usage
|
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
|
+
|
59
|
+
### Logging
|
60
|
+
|
61
|
+
#### Attaching a logger
|
62
|
+
|
63
|
+
Attach a logger to have `natsy` write out logs for messages received, responses sent, errors raised, lifecycle events, etc.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
require 'natsy'
|
67
|
+
require 'logger'
|
68
|
+
|
69
|
+
nats_logger = Logger.new(STDOUT)
|
70
|
+
nats_logger.level = Logger::INFO
|
71
|
+
|
72
|
+
Natsy::Client.logger = nats_logger
|
73
|
+
```
|
74
|
+
|
75
|
+
In a Rails application, you might do this instead:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
Natsy::Client.logger = Rails.logger
|
79
|
+
```
|
80
|
+
|
81
|
+
#### Log levels
|
82
|
+
|
83
|
+
The following will be logged at the specified log levels
|
84
|
+
|
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`
|
86
|
+
- `INFO`: Message activity over NATS (received a message, replied with a message, etc.), as well as everything under `WARN` and `ERROR`
|
87
|
+
- `WARN`: Error handled gracefully (listening restarted due to some exception, etc.), as well as everything under `ERROR`
|
88
|
+
- `ERROR`: Some exception was raised in-thread (error in handler, error in subscription, etc.)
|
89
|
+
|
90
|
+
<a id="default-queue-section"></a>
|
91
|
+
|
92
|
+
### Setting a default queue
|
93
|
+
|
94
|
+
Set a default queue for subscriptions.
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
Natsy::Client.default_queue = "foobar"
|
98
|
+
```
|
99
|
+
|
100
|
+
Leave the `::default_queue` blank (or assign `nil`) to use no default queue.
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
Natsy::Client.default_queue = nil
|
104
|
+
```
|
105
|
+
|
106
|
+
<a id="reply-to-section"></a>
|
107
|
+
|
108
|
+
### Registering message handlers
|
109
|
+
|
110
|
+
Register a message handler with the `Natsy::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.
|
111
|
+
|
112
|
+
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).
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
Natsy::Client.reply_to("some.subject", queue: "foobar") { |data| "Got it! #{data.inspect}" }
|
116
|
+
|
117
|
+
Natsy::Client.reply_to("some.*.pattern") { |data, subject| "Got #{data} on #{subject}" }
|
118
|
+
|
119
|
+
Natsy::Client.reply_to("other.subject") do |data|
|
120
|
+
if data["foo"] == "bar"
|
121
|
+
{ is_bar: "Yep!" }
|
122
|
+
else
|
123
|
+
{ is_bar: "No way!" }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
Natsy::Client.reply_to("subject.in.queue", queue: "barbaz") do
|
128
|
+
"My turn!"
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
### Starting the listeners
|
133
|
+
|
134
|
+
Start listening for messages with the `Natsy::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.
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
Natsy::Client.start!
|
138
|
+
```
|
139
|
+
|
140
|
+
> **NOTE:** If an error is raised in one of the handlers, `Natsy::Client` will restart automatically.
|
141
|
+
|
142
|
+
> **NOTE:** You _can_ invoke `::reply_to` to create additional message subscriptions after `Natsy::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 `Natsy::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
|
+
|
146
|
+
### Basic full working example (in vanilla Ruby)
|
147
|
+
|
148
|
+
The following should be enough to start a `natsy` setup in your Ruby application, using what we've learned so far.
|
149
|
+
|
150
|
+
> **NOTE:** For a more organized structure and implementation in a larger app (like a Rails project), see the ["controller" section below](#controller-section).
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
require 'natsy'
|
154
|
+
require 'logger'
|
155
|
+
|
156
|
+
nats_logger = Logger.new(STDOUT)
|
157
|
+
nats_logger.level = Logger::DEBUG
|
158
|
+
|
159
|
+
Natsy::Client.logger = nats_logger
|
160
|
+
Natsy::Client.default_queue = "foobar"
|
161
|
+
|
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 } }
|
165
|
+
|
166
|
+
Natsy::Client.start!
|
167
|
+
```
|
168
|
+
|
169
|
+
<a id="controller-section"></a>
|
170
|
+
|
171
|
+
### Creating "controller"-style classes for listener organization
|
172
|
+
|
173
|
+
Create controller classes which inherit from `Natsy::Controller` in order to give your message listeners some structure.
|
174
|
+
|
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.
|
176
|
+
|
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.
|
178
|
+
|
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 `Natsy::Client::reply_to`](#reply-to-section)). The result of that block will be sent as a response to the message received.
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
class HelloController < Natsy::Controller
|
183
|
+
default_queue "foobar"
|
184
|
+
|
185
|
+
subject "hello" do
|
186
|
+
subject "jerk" do
|
187
|
+
response do |data|
|
188
|
+
# The subject at this point is "hello.jerk"
|
189
|
+
"Hey #{data['name']}... that's not cool, man."
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
subject "and" do
|
194
|
+
subject "wassup" do
|
195
|
+
response do |data|
|
196
|
+
# The subject at this point is "hello.and.wassup"
|
197
|
+
"Hey, how ya doin', #{data['name']}?"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
subject "goodbye" do
|
202
|
+
response do |data|
|
203
|
+
# The subject at this point is "hello.and.goodbye"
|
204
|
+
"Hi #{data['name']}! But also GOODBYE."
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
subject "hows" do
|
211
|
+
subject "*" do
|
212
|
+
subject "doing" do
|
213
|
+
response do |data, subject|
|
214
|
+
# The subject at this point is "hows.<wildcard>.doing" (i.e., the
|
215
|
+
# subjects "hows.jack.doing" and "hows.jill.doing" will both match)
|
216
|
+
sender_name = data["name"]
|
217
|
+
other_person_name = subject.split(".")[1]
|
218
|
+
desc = rand < 0.5 ? "terribly" : "great"
|
219
|
+
"Well, #{sender_name}, #{other_person_name} is actually doing #{desc}."
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
```
|
226
|
+
|
227
|
+
> **NOTE:** If you implement controllers like this and you are using code-autoloading machinery (like Zeitwerk in Rails), you will need to make sure these paths are eager-loaded when your app starts. **If you don't, `natsy` will not register the listeners,** and will not respond to messages for the specified subjects.
|
228
|
+
>
|
229
|
+
> 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
|
+
>
|
231
|
+
> ```ruby
|
232
|
+
> Natsy::Client.logger = Rails.logger
|
233
|
+
> Natsy::Client.default_queue = "foobar"
|
234
|
+
>
|
235
|
+
> # ...
|
236
|
+
>
|
237
|
+
> Rails.application.config.after_initialize do
|
238
|
+
> nats_controller_paths = Dir[Rails.root.join("app", "nats", "**", "*_controller.rb")]
|
239
|
+
> nats_controller_paths.each { |file_path| require_dependency(file_path) }
|
240
|
+
>
|
241
|
+
> Natsy::Client.start!
|
242
|
+
> end
|
243
|
+
> ```
|
244
|
+
|
245
|
+
## Development
|
246
|
+
|
247
|
+
### Install dependencies
|
248
|
+
|
249
|
+
To install the Ruby dependencies, run:
|
250
|
+
|
251
|
+
```bash
|
252
|
+
bin/setup
|
253
|
+
```
|
254
|
+
|
255
|
+
This gem also requires a NATS server to be installed and running. See [the NATS documentation](https://docs.nats.io/nats-server/installation) for more details.
|
256
|
+
<!-- sudo docker run -p 4222:4222 -p 8222:8222 -p 6222:6222 -ti nats:latest -->
|
257
|
+
<!-- nats-tail -s nats://localhost:4222 ">" -->
|
258
|
+
<!-- curl --data '{"name":"Keegan"}' --header 'Content-Type: application/json' http://localhost:3000/hello -->
|
259
|
+
|
260
|
+
### Open a console
|
261
|
+
|
262
|
+
To open a REPL with the gem's code loaded, run:
|
263
|
+
|
264
|
+
```bash
|
265
|
+
bin/console
|
266
|
+
```
|
267
|
+
|
268
|
+
### Run the tests
|
269
|
+
|
270
|
+
To run the RSpec test suites, run:
|
271
|
+
|
272
|
+
```bash
|
273
|
+
bundle exec rake spec
|
274
|
+
```
|
275
|
+
|
276
|
+
...or (if your Ruby setup has good defaults) just this:
|
277
|
+
|
278
|
+
```bash
|
279
|
+
rake spec
|
280
|
+
```
|
281
|
+
|
282
|
+
### Run the linter
|
283
|
+
|
284
|
+
```bash
|
285
|
+
bundle exec rubocop
|
286
|
+
```
|
287
|
+
|
288
|
+
### Create a release
|
289
|
+
|
290
|
+
Bump the `Natsy::VERSION` value in `lib/natsy/version.rb`, commit, and then run:
|
291
|
+
|
292
|
+
```bash
|
293
|
+
bundle exec rake release
|
294
|
+
```
|
295
|
+
|
296
|
+
...or (if your Ruby setup has good defaults) just this:
|
297
|
+
|
298
|
+
```bash
|
299
|
+
rake release
|
300
|
+
```
|
301
|
+
|
302
|
+
This will:
|
303
|
+
|
304
|
+
1. create a git tag for the new version,
|
305
|
+
1. push the commits,
|
306
|
+
1. build the gem, and
|
307
|
+
1. push it to [rubygems.org](https://rubygems.org/gems/natsy).
|
308
|
+
|
309
|
+
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.
|
310
|
+
|
311
|
+
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).
|
312
|
+
|
313
|
+
## Contributing
|
314
|
+
|
315
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Openbay/natsy.
|
316
|
+
|
317
|
+
## License
|
318
|
+
|
319
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "natsy"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/natsy.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nats/client"
|
4
|
+
require_relative "natsy/version"
|
5
|
+
require_relative "natsy/utils"
|
6
|
+
require_relative "natsy/client"
|
7
|
+
require_relative "natsy/controller"
|
8
|
+
|
9
|
+
# The +Natsy+ module provides the top-level namespace for the NATS client
|
10
|
+
# and controller machinery.
|
11
|
+
module Natsy
|
12
|
+
# Basic error
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
# New subscription has been added at runtime
|
16
|
+
class NewSubscriptionsError < Natsy::Error; end
|
17
|
+
end
|
data/lib/natsy/client.rb
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "nats/client"
|
5
|
+
require_relative "./utils"
|
6
|
+
|
7
|
+
module Natsy
|
8
|
+
# The +Natsy::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.
|
11
|
+
class Client
|
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
|
+
# Returns +true+ if +::start!+ has already been called (meaning the client
|
58
|
+
# is listening to NATS messages). Returns +false+ if it has not yet been
|
59
|
+
# called, or if it has been stopped.
|
60
|
+
def started?
|
61
|
+
@started ||= false
|
62
|
+
end
|
63
|
+
|
64
|
+
# Opposite of +::started?+: returns +false+ if +::start!+ has already been
|
65
|
+
# called (meaning the client is listening to NATS messages). Returns
|
66
|
+
# +true+ if it has not yet been called, or if it has been stopped.
|
67
|
+
def stopped?
|
68
|
+
!started?
|
69
|
+
end
|
70
|
+
|
71
|
+
# Register a message handler with the +Natsy::Client::reply_to+
|
72
|
+
# method. Pass a subject string as the first argument (either a static
|
73
|
+
# subject string or a pattern to match more than one subject). Specify a
|
74
|
+
# 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.
|
77
|
+
#
|
78
|
+
# The result of the given block will be published in reply to the message.
|
79
|
+
# The block is passed two arguments when a message matching the subject is
|
80
|
+
# received: +data+ and +subject+. The +data+ argument is the payload of
|
81
|
+
# the message (JSON objects/arrays will be parsed into string-keyed +Hash+
|
82
|
+
# objects/+Array+ objects, respectively). The +subject+ argument is the
|
83
|
+
# subject of the message received (mostly only useful if a _pattern_ was
|
84
|
+
# specified instead of a static subject string).
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# Natsy::Client.reply_to("some.subject", queue: "foobar") { |data| "Got it! #{data.inspect}" }
|
88
|
+
#
|
89
|
+
# Natsy::Client.reply_to("some.*.pattern") { |data, subject| "Got #{data} on #{subject}" }
|
90
|
+
#
|
91
|
+
# Natsy::Client.reply_to("other.subject") do |data|
|
92
|
+
# if data["foo"] == "bar"
|
93
|
+
# { is_bar: "Yep!" }
|
94
|
+
# else
|
95
|
+
# { is_bar: "No way!" }
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# Natsy::Client.reply_to("subject.in.queue", queue: "barbaz") do
|
100
|
+
# "My turn!"
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
def reply_to(subject, queue: nil, &block)
|
104
|
+
queue = Utils.presence(queue) || default_queue
|
105
|
+
queue_desc = " in queue '#{queue}'" if queue
|
106
|
+
log("Registering a reply handler for subject '#{subject}'#{queue_desc}", level: :debug)
|
107
|
+
register_reply!(subject: subject.to_s, handler: block, queue: queue.to_s)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Start listening for messages with the +Natsy::Client::start!+
|
111
|
+
# method. This will spin up a non-blocking thread that subscribes to
|
112
|
+
# subjects (as specified by invocation(s) of +::reply_to+) and waits for
|
113
|
+
# messages to come in. When a message is received, the appropriate
|
114
|
+
# +::reply_to+ block will be used to compute a response, and that response
|
115
|
+
# will be published.
|
116
|
+
#
|
117
|
+
# @example
|
118
|
+
# Natsy::Client.start!
|
119
|
+
#
|
120
|
+
# **NOTE:** If an error is raised in one of the handlers,
|
121
|
+
# +Natsy::Client+ will restart automatically.
|
122
|
+
#
|
123
|
+
# **NOTE:** You _can_ invoke +::reply_to+ to create additional message
|
124
|
+
# subscriptions after +Natsy::Client.start!+, but be aware that
|
125
|
+
# this forces the client to restart. You may see (benign, already-handled)
|
126
|
+
# errors in the logs generated when this restart happens. It will force
|
127
|
+
# the client to restart and re-subscribe after _each additional
|
128
|
+
# +::reply_to+ invoked after +::start!+._ So, if you have a lot of
|
129
|
+
# additional +::reply_to+ invocations, you may want to consider
|
130
|
+
# refactoring so that your call to +Natsy::Client.start!+ occurs
|
131
|
+
# _after_ those additions.
|
132
|
+
#
|
133
|
+
# **NOTE:** The +::start!+ method can be safely called multiple times;
|
134
|
+
# only the first will be honored, and any subsequent calls to +::start!+
|
135
|
+
# after the client is already started will do nothing (except write a
|
136
|
+
# _"NATS is already running"_ log to the logger at the +DEBUG+ level).
|
137
|
+
#
|
138
|
+
def start!
|
139
|
+
log("Starting NATS", level: :debug)
|
140
|
+
|
141
|
+
if started?
|
142
|
+
log("NATS is already running", level: :debug)
|
143
|
+
return
|
144
|
+
end
|
145
|
+
|
146
|
+
started!
|
147
|
+
|
148
|
+
self.current_thread = Thread.new do
|
149
|
+
Thread.handle_interrupt(StandardError => :never) do
|
150
|
+
Thread.handle_interrupt(StandardError => :immediate) { listen }
|
151
|
+
rescue NATS::ConnectError => e
|
152
|
+
log("Could not connect to NATS server:", level: :error)
|
153
|
+
log(e.full_message, level: :error, indent: 2)
|
154
|
+
Thread.current.exit
|
155
|
+
rescue NewSubscriptionsError => e
|
156
|
+
log("New subscriptions! Restarting...", level: :info)
|
157
|
+
restart!
|
158
|
+
raise e # TODO: there has to be a better way
|
159
|
+
rescue StandardError => e
|
160
|
+
log("Encountered an error:", level: :error)
|
161
|
+
log(e.full_message, level: :error, indent: 2)
|
162
|
+
restart!
|
163
|
+
raise e
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
attr_accessor :current_thread
|
171
|
+
|
172
|
+
def log(text, level: :info, indent: 0)
|
173
|
+
return unless logger
|
174
|
+
|
175
|
+
timestamp = Time.now.to_s
|
176
|
+
text_lines = text.split("\n")
|
177
|
+
indentation = indent.is_a?(String) ? indent : (" " * indent)
|
178
|
+
|
179
|
+
text_lines.each do |line|
|
180
|
+
logger.send(level, "[#{timestamp}] Natsy | #{indentation}#{line}")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def kill!
|
185
|
+
current_thread.kill if current_thread && current_thread.alive?
|
186
|
+
end
|
187
|
+
|
188
|
+
def stop!
|
189
|
+
log("Stopping NATS", level: :debug)
|
190
|
+
|
191
|
+
begin
|
192
|
+
NATS.stop
|
193
|
+
rescue StandardError
|
194
|
+
nil
|
195
|
+
end
|
196
|
+
|
197
|
+
stopped!
|
198
|
+
end
|
199
|
+
|
200
|
+
def restart!
|
201
|
+
log("Restarting NATS", level: :warn)
|
202
|
+
stop!
|
203
|
+
start!
|
204
|
+
end
|
205
|
+
|
206
|
+
def started!
|
207
|
+
@started = true
|
208
|
+
end
|
209
|
+
|
210
|
+
def stopped!
|
211
|
+
@started = false
|
212
|
+
end
|
213
|
+
|
214
|
+
def replies
|
215
|
+
@replies ||= []
|
216
|
+
end
|
217
|
+
|
218
|
+
def reply_registered?(raw_subject)
|
219
|
+
subject = raw_subject.to_s
|
220
|
+
replies.any? { |reply| reply[:subject] == subject }
|
221
|
+
end
|
222
|
+
|
223
|
+
def register_reply!(subject:, handler:, queue: nil)
|
224
|
+
raise ArgumentError, "Subject must be a string" unless subject.is_a?(String)
|
225
|
+
raise ArgumentError, "Must provide a message handler for #{subject}" unless handler.respond_to?(:call)
|
226
|
+
raise ArgumentError, "Already registered a reply to #{subject}" if reply_registered?(subject)
|
227
|
+
|
228
|
+
reply = {
|
229
|
+
subject: subject,
|
230
|
+
handler: handler,
|
231
|
+
queue: Utils.presence(queue) || default_queue,
|
232
|
+
}
|
233
|
+
|
234
|
+
replies << reply
|
235
|
+
|
236
|
+
current_thread.raise(NewSubscriptionsError, "New reply registered") if started?
|
237
|
+
end
|
238
|
+
|
239
|
+
def listen
|
240
|
+
NATS.start do
|
241
|
+
replies.each do |replier|
|
242
|
+
queue_desc = " in queue '#{replier[:queue]}'" if replier[:queue]
|
243
|
+
log("Subscribing to subject '#{replier[:subject]}'#{queue_desc}", level: :debug)
|
244
|
+
|
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")
|
248
|
+
|
249
|
+
log("Received a message!")
|
250
|
+
message_desc = <<~LOG_MESSAGE
|
251
|
+
id: #{id || '(none)'}
|
252
|
+
pattern: #{pattern || '(none)'}
|
253
|
+
subject: #{subject || '(none)'}
|
254
|
+
data: #{data.to_json}
|
255
|
+
inbox: #{inbox || '(none)'}
|
256
|
+
LOG_MESSAGE
|
257
|
+
log(message_desc, indent: 2)
|
258
|
+
|
259
|
+
response_data = replier[:handler].call(data)
|
260
|
+
|
261
|
+
log("Responding with '#{response_data}'")
|
262
|
+
|
263
|
+
NATS.publish(inbox, response_data.to_json, queue: replier[:queue])
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./utils"
|
4
|
+
|
5
|
+
module Natsy
|
6
|
+
# Create controller classes which inherit from +Natsy::Controller+ in
|
7
|
+
# order to give your message listeners some structure.
|
8
|
+
class Controller
|
9
|
+
NO_QUEUE_GIVEN = :natsy_super_special_no_op_queue_symbol_qwertyuiop1234567890
|
10
|
+
private_constant :NO_QUEUE_GIVEN
|
11
|
+
|
12
|
+
class << self
|
13
|
+
# Default queue for the controller. Falls back to the client's default
|
14
|
+
# queue if the controller's default queue is +nil+.
|
15
|
+
#
|
16
|
+
# - Call with no argument (+::default_queue+) to get the default queue.
|
17
|
+
# - Call as a macro with an argument (+default_queue "something"+) to set
|
18
|
+
# the default queue.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# class FoobarNatsController < RubyNatsController
|
22
|
+
# default_queue "foobar"
|
23
|
+
#
|
24
|
+
# # ...
|
25
|
+
# end
|
26
|
+
#
|
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.
|
33
|
+
#
|
34
|
+
def default_queue(some_queue = NO_QUEUE_GIVEN)
|
35
|
+
# +NO_QUEUE_GIVEN+ is a special symbol (rather than +nil+) so that the
|
36
|
+
# default queue can be "unset" to +nil+ (given a non-+nil+ global
|
37
|
+
# default set with +Natsy::Client::default_queue=+).
|
38
|
+
if some_queue == NO_QUEUE_GIVEN
|
39
|
+
@default_queue || Client.default_queue
|
40
|
+
else
|
41
|
+
@default_queue = Utils.presence(some_queue.to_s)
|
42
|
+
end
|
43
|
+
end
|
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
|
+
# class FoobarNatsController < RubyNatsController
|
54
|
+
# # ...
|
55
|
+
#
|
56
|
+
# subject "hello.wassup" do
|
57
|
+
# response do |data, subject|
|
58
|
+
# # The subject at this point is "hello.wassup"
|
59
|
+
# # ...
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# subject "hello.howdy" do
|
64
|
+
# response do |data, subject|
|
65
|
+
# # The subject at this point is "hello.howdy"
|
66
|
+
# # ...
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# class FoobarNatsController < RubyNatsController
|
73
|
+
# # ...
|
74
|
+
#
|
75
|
+
# subject "hello" do
|
76
|
+
# subject "wassup" do
|
77
|
+
# response do |data, subject|
|
78
|
+
# # The subject at this point is "hello.wassup"
|
79
|
+
# # ...
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# subject "howdy" do
|
84
|
+
# response do |data, subject|
|
85
|
+
# # The subject at this point is "hello.howdy"
|
86
|
+
# # ...
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
def subject(subject_segment, queue: nil)
|
93
|
+
subject_chain.push(subject_segment)
|
94
|
+
old_queue = current_queue
|
95
|
+
self.current_queue = queue if Utils.present?(queue)
|
96
|
+
yield
|
97
|
+
self.current_queue = old_queue
|
98
|
+
subject_chain.pop
|
99
|
+
end
|
100
|
+
|
101
|
+
# You can register a response for the built-up subject/pattern string
|
102
|
+
# using the +::response+ macro. Pass a block to +::response+ which
|
103
|
+
# optionally takes two arguments (the same arguments supplied to the block
|
104
|
+
# of +Natsy::Client::reply_to+). The result of that block will be
|
105
|
+
# sent as a response to the message received.
|
106
|
+
#
|
107
|
+
# @example
|
108
|
+
# class FoobarNatsController < RubyNatsController
|
109
|
+
# # ...
|
110
|
+
#
|
111
|
+
# subject "hello" do
|
112
|
+
# subject "wassup" do
|
113
|
+
# response do |data, subject|
|
114
|
+
# # The subject at this point is "hello.wassup".
|
115
|
+
# # Assume the message sent a JSON payload of {"name":"Bob"}
|
116
|
+
# # in this example.
|
117
|
+
# # We'll reply with a string response:
|
118
|
+
# "I'm all right, #{data['name']}"
|
119
|
+
# end
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# subject "howdy" do
|
123
|
+
# response do |data, subject|
|
124
|
+
# # The subject at this point is "hello.howdy".
|
125
|
+
# # Assume the message sent a JSON payload of {"name":"Bob"}
|
126
|
+
# # in this example.
|
127
|
+
# # We'll reply with a JSON response (a Ruby +Hash+):
|
128
|
+
# { message: "I'm okay, #{data['name']}. Thanks for asking!" }
|
129
|
+
# end
|
130
|
+
# end
|
131
|
+
# end
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
def response(queue: nil, &block)
|
135
|
+
response_queue = Utils.presence(queue.to_s) || current_queue || default_queue
|
136
|
+
Client.reply_to(current_subject, queue: response_queue, &block)
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def subject_chain
|
142
|
+
@subject_chain ||= []
|
143
|
+
end
|
144
|
+
|
145
|
+
def current_subject
|
146
|
+
subject_chain.join(".")
|
147
|
+
end
|
148
|
+
|
149
|
+
def current_queue
|
150
|
+
@current_queue ||= nil
|
151
|
+
end
|
152
|
+
|
153
|
+
def current_queue=(some_queue)
|
154
|
+
@current_queue = Utils.presence(some_queue)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
data/lib/natsy/utils.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Natsy
|
4
|
+
# Some internal utility methods
|
5
|
+
class Utils
|
6
|
+
class << self
|
7
|
+
def blank?(value)
|
8
|
+
value.respond_to?(:empty?) ? value.empty? : !value
|
9
|
+
end
|
10
|
+
|
11
|
+
def present?(value)
|
12
|
+
!blank?(value)
|
13
|
+
end
|
14
|
+
|
15
|
+
def presence(value)
|
16
|
+
present?(value) ? value : nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/natsy.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/natsy/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "natsy"
|
7
|
+
spec.version = Natsy::VERSION
|
8
|
+
spec.authors = ["Keegan Leitz"]
|
9
|
+
spec.email = ["keegan@openbay.com"]
|
10
|
+
|
11
|
+
spec.summary = "Listen for (and reply to) NATS messages asynchronously in a Ruby application"
|
12
|
+
spec.description = "Listen for (and reply to) NATS messages asynchronously in a Ruby application"
|
13
|
+
spec.homepage = "https://github.com/openbay/natsy"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.6")
|
16
|
+
|
17
|
+
rubydoc_url = "https://www.rubydoc.info/gems/natsy/#{Natsy::VERSION}"
|
18
|
+
spec.metadata["documentation_uri"] = rubydoc_url
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
24
|
+
end
|
25
|
+
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 2.2"
|
31
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
33
|
+
spec.add_development_dependency "rubocop", "~> 1.10"
|
34
|
+
spec.add_development_dependency "rubocop-performance", "~> 1.9"
|
35
|
+
spec.add_development_dependency "rubocop-rake", "~> 0.5"
|
36
|
+
spec.add_development_dependency "rubocop-rspec", "~> 2.2"
|
37
|
+
spec.add_development_dependency "solargraph"
|
38
|
+
spec.add_development_dependency "pry"
|
39
|
+
|
40
|
+
spec.add_runtime_dependency "nats", "~> 0.11"
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: natsy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Keegan Leitz
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-05-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.2'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '13.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '13.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.10'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.10'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-performance
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.9'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.9'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.5'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.5'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop-rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.2'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.2'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: solargraph
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
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
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: nats
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.11'
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.11'
|
153
|
+
description: Listen for (and reply to) NATS messages asynchronously in a Ruby application
|
154
|
+
email:
|
155
|
+
- keegan@openbay.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- ".github/workflows/main.yml"
|
161
|
+
- ".gitignore"
|
162
|
+
- ".rspec"
|
163
|
+
- ".rubocop.yml"
|
164
|
+
- CHANGELOG.md
|
165
|
+
- Gemfile
|
166
|
+
- LICENSE.txt
|
167
|
+
- README.md
|
168
|
+
- Rakefile
|
169
|
+
- bin/console
|
170
|
+
- bin/setup
|
171
|
+
- lib/natsy.rb
|
172
|
+
- lib/natsy/client.rb
|
173
|
+
- lib/natsy/controller.rb
|
174
|
+
- lib/natsy/utils.rb
|
175
|
+
- lib/natsy/version.rb
|
176
|
+
- natsy.gemspec
|
177
|
+
homepage: https://github.com/openbay/natsy
|
178
|
+
licenses:
|
179
|
+
- MIT
|
180
|
+
metadata:
|
181
|
+
documentation_uri: https://www.rubydoc.info/gems/natsy/0.3.0
|
182
|
+
post_install_message:
|
183
|
+
rdoc_options: []
|
184
|
+
require_paths:
|
185
|
+
- lib
|
186
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
187
|
+
requirements:
|
188
|
+
- - ">="
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '2.6'
|
191
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
192
|
+
requirements:
|
193
|
+
- - ">="
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
version: '0'
|
196
|
+
requirements: []
|
197
|
+
rubygems_version: 3.0.9
|
198
|
+
signing_key:
|
199
|
+
specification_version: 4
|
200
|
+
summary: Listen for (and reply to) NATS messages asynchronously in a Ruby application
|
201
|
+
test_files: []
|