blackevin 0.1.1 → 0.2.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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +86 -41
- data/blackevin.gemspec +20 -19
- data/lib/blackevin/configuration.rb +3 -14
- data/lib/blackevin/endpoints.rb +2 -2
- data/lib/blackevin/errors.rb +7 -7
- data/lib/blackevin/railtie.rb +2 -0
- data/lib/blackevin/rest/auth.rb +11 -8
- data/lib/blackevin/rest/channel.rb +56 -0
- data/lib/blackevin/rest/channels.rb +5 -81
- data/lib/blackevin/rest/clients.rb +5 -4
- data/lib/blackevin/rest/presence.rb +38 -0
- data/lib/blackevin/rest/queues.rb +20 -19
- data/lib/blackevin/rest.rb +54 -17
- data/lib/blackevin/token_details.rb +1 -1
- data/lib/blackevin/token_endpoint.rb +5 -4
- data/lib/blackevin/token_request.rb +1 -1
- data/lib/blackevin/version.rb +1 -1
- data/lib/blackevin.rb +12 -18
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b88ba03b32ee0b815636e6cebc77a95669c1df92d3734ac6c6ec66023f781012
|
|
4
|
+
data.tar.gz: 7f01c4066dc941d1b86493eac3cd60dc2be0838475de5a3227da6a7d8dff4419
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4fe362bb04448fd8cfc8177da89d07dbd225ebe4e7de4ee661021c8be7a338571c9b036dd654ff22439475e6a0d87fcfceec5131580aeb958837e52ee7211250
|
|
7
|
+
data.tar.gz: 210ea311e3c7acf55d63032fab8ebc05127c71a43874c0e015cae522063a42247ce3293251b56833c0c9ff84ca515e36169cefed024ca6cce1e6bacc867d16f2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
**Breaking.** The client is an instance, always.
|
|
6
|
+
|
|
7
|
+
- Removed `Blackevin.rest`. There is no process-wide client and no class-level call that
|
|
8
|
+
touches the network. Build one: `bk = Blackevin::Rest.new`.
|
|
9
|
+
- `Blackevin::Rest.new` now falls back, option by option, to `Blackevin.configure` and then to
|
|
10
|
+
`BLACKEVIN_KEY` — so `new` with no arguments works wherever `Blackevin.rest` used to.
|
|
11
|
+
- Every part stands alone: `Blackevin::Rest::Auth.new`, `Channels.new`, `Channel.new(name)`,
|
|
12
|
+
`Presence.new(name)`, `Clients.new` and `Queues.new` take an optional `client:` and build a
|
|
13
|
+
`Blackevin::Rest` without one. `bk.auth`, `bk.channels` and the rest are those same classes.
|
|
14
|
+
- One class per file under `lib/blackevin/rest/`.
|
|
15
|
+
- `Blackevin.configure` only stores defaults. Reconfiguring no longer affects a client already built.
|
|
16
|
+
- `Blackevin::TokenEndpoint` without `rest:` builds a `Blackevin::Rest` per request.
|
|
17
|
+
- The Railtie is unchanged: it fills the configuration from `config.blackevin.*` and credentials.
|
|
18
|
+
- Style is RuboCop running Standard's rules with single quotes (`bundle exec rubocop`).
|
|
19
|
+
- `bundle exec rake coverage` runs the suite under SimpleCov, Rails subprocesses merged in, and
|
|
20
|
+
fails under 100% lines or 95% branches.
|
|
21
|
+
|
|
22
|
+
Migrating: replace `Blackevin.rest` with `Blackevin::Rest.new`.
|
|
23
|
+
|
|
3
24
|
## 0.1.1
|
|
4
25
|
|
|
5
26
|
- Remove lint from CI
|
data/README.md
CHANGED
|
@@ -10,30 +10,70 @@ library — nothing that can break under you in an upgrade. Ruby 3.0 and up, in
|
|
|
10
10
|
Rails, Sinatra, Hanami or a plain script.
|
|
11
11
|
|
|
12
12
|
```ruby
|
|
13
|
-
gem
|
|
13
|
+
gem 'blackevin'
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## The client
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
You work with an instance. There is no global client and no class-level call
|
|
19
|
+
that touches the network.
|
|
19
20
|
|
|
20
21
|
```ruby
|
|
21
|
-
Blackevin.
|
|
22
|
+
bk = Blackevin::Rest.new
|
|
23
|
+
|
|
24
|
+
ch = bk.channels.get('room:42')
|
|
25
|
+
ch.publish('greeting', {text: 'hi'})
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
With `BLACKEVIN_KEY` in the environment, `new` needs no arguments. Creating one is
|
|
29
|
+
cheap — it opens no connection and holds no state — so build it where you use
|
|
30
|
+
it, or keep one in a constant; both are fine, and an instance is thread-safe.
|
|
31
|
+
|
|
32
|
+
### Each part stands alone
|
|
33
|
+
|
|
34
|
+
Nothing forces a chain through `Blackevin::Rest`. Every part is a class you can
|
|
35
|
+
build by itself; without a `client:` it makes its own from the configuration.
|
|
25
36
|
|
|
26
37
|
```ruby
|
|
38
|
+
Blackevin::Rest::Auth.new.create_token_request(client_id: current_user.id)
|
|
39
|
+
Blackevin::Rest::Channel.new('room:42').publish('greeting', {text: 'hi'})
|
|
40
|
+
Blackevin::Rest::Presence.new('room:42').get
|
|
41
|
+
Blackevin::Rest::Queues.new.list
|
|
42
|
+
Blackevin::Rest::Clients.new.disconnect(user.id)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Pass `client:` to share one, or to use another key:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
bk = Blackevin::Rest.new(key: other_key)
|
|
49
|
+
|
|
50
|
+
Blackevin::Rest::Queues.new(client: bk).list
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`bk.auth`, `bk.channels`, `bk.clients` and `bk.queues` are these same classes,
|
|
54
|
+
already holding `bk`.
|
|
55
|
+
|
|
56
|
+
Every option can be passed directly:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
bk = Blackevin::Rest.new(key: 'ck_live_….kAbC', read_timeout: 3)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Defaults, set once
|
|
63
|
+
|
|
64
|
+
`Blackevin.configure` holds the defaults a new `Blackevin::Rest` starts from. It
|
|
65
|
+
builds nothing and calls nothing. An argument to `new` wins over it, and it wins
|
|
66
|
+
over the environment.
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# config/initializers/blackevin.rb
|
|
27
70
|
Blackevin.configure do |config|
|
|
28
|
-
config.key =
|
|
29
|
-
config.open_timeout = 5
|
|
71
|
+
config.key = Rails.application.credentials.dig(:blackevin, :key) # secret.keyId
|
|
72
|
+
config.open_timeout = 5 # seconds
|
|
30
73
|
config.read_timeout = 10
|
|
31
74
|
end
|
|
32
75
|
```
|
|
33
76
|
|
|
34
|
-
`Blackevin.rest` is one shared, thread-safe client. For more than one key, build
|
|
35
|
-
your own: `Blackevin::Rest.new(key: …)`.
|
|
36
|
-
|
|
37
77
|
Against a local or self-hosted node, which serves REST and the socket from one
|
|
38
78
|
origin, set `BLACKEVIN_ENDPOINT=ws://localhost:3000` (or `config.endpoint`) and the
|
|
39
79
|
REST base is derived from it.
|
|
@@ -55,7 +95,7 @@ class BlackevinTokensController < ApplicationController
|
|
|
55
95
|
before_action :authenticate_user!
|
|
56
96
|
|
|
57
97
|
def show
|
|
58
|
-
render json: Blackevin.
|
|
98
|
+
render json: Blackevin::Rest::Auth.new.create_token_request(
|
|
59
99
|
client_id: current_user.id,
|
|
60
100
|
ttl: 1.hour.in_milliseconds,
|
|
61
101
|
capability: {
|
|
@@ -67,7 +107,7 @@ class BlackevinTokensController < ApplicationController
|
|
|
67
107
|
end
|
|
68
108
|
|
|
69
109
|
# config/routes.rb
|
|
70
|
-
resource :blackevin_token, only: :show, path:
|
|
110
|
+
resource :blackevin_token, only: :show, path: 'blackevin/token'
|
|
71
111
|
```
|
|
72
112
|
|
|
73
113
|
The key can live in credentials instead of the environment — the Railtie reads
|
|
@@ -75,20 +115,20 @@ The key can live in credentials instead of the environment — the Railtie reads
|
|
|
75
115
|
|
|
76
116
|
```ruby
|
|
77
117
|
# config/environments/development.rb
|
|
78
|
-
config.blackevin.endpoint =
|
|
118
|
+
config.blackevin.endpoint = 'ws://localhost:3000'
|
|
79
119
|
```
|
|
80
120
|
|
|
81
121
|
### Sinatra
|
|
82
122
|
|
|
83
123
|
```ruby
|
|
84
|
-
require
|
|
85
|
-
require
|
|
124
|
+
require 'sinatra'
|
|
125
|
+
require 'blackevin'
|
|
86
126
|
|
|
87
|
-
get
|
|
127
|
+
get '/blackevin/token' do
|
|
88
128
|
halt 401 unless current_user
|
|
89
129
|
|
|
90
130
|
content_type :json
|
|
91
|
-
Blackevin.
|
|
131
|
+
Blackevin::Rest::Auth.new.create_token_request(client_id: current_user.id).to_json
|
|
92
132
|
end
|
|
93
133
|
```
|
|
94
134
|
|
|
@@ -99,24 +139,24 @@ and answers who is asking; `nil` is a 401.
|
|
|
99
139
|
|
|
100
140
|
```ruby
|
|
101
141
|
TOKENS = Blackevin::TokenEndpoint.new do |env|
|
|
102
|
-
user = env[
|
|
142
|
+
user = env['warden']&.user
|
|
103
143
|
|
|
104
144
|
next unless user
|
|
105
145
|
|
|
106
146
|
{client_id: user.id, capability: {"team:#{user.team_id}" => %w[subscribe publish]}}
|
|
107
147
|
end
|
|
108
148
|
|
|
109
|
-
# Hanami: mount TOKENS, at:
|
|
110
|
-
# Rails: mount TOKENS, at:
|
|
111
|
-
# Rack: map(
|
|
149
|
+
# Hanami: mount TOKENS, at: '/blackevin/token'
|
|
150
|
+
# Rails: mount TOKENS, at: '/blackevin/token'
|
|
151
|
+
# Rack: map('/blackevin/token') { run TOKENS }
|
|
112
152
|
```
|
|
113
153
|
|
|
114
154
|
## Publish
|
|
115
155
|
|
|
116
156
|
```ruby
|
|
117
|
-
channel = Blackevin.
|
|
157
|
+
channel = Blackevin::Rest::Channel.new("orders:#{order.id}")
|
|
118
158
|
|
|
119
|
-
channel.publish(
|
|
159
|
+
channel.publish('status', {state: 'shipped'})
|
|
120
160
|
```
|
|
121
161
|
|
|
122
162
|
It goes through the same fanout a socket publish does: subscribers, account
|
|
@@ -128,7 +168,7 @@ class BlackevinPublishJob < ApplicationJob
|
|
|
128
168
|
retry_on Blackevin::ConnectionError, wait: :polynomially_longer
|
|
129
169
|
|
|
130
170
|
def perform(channel, event, data)
|
|
131
|
-
Blackevin.
|
|
171
|
+
Blackevin::Rest::Channel.new(channel).publish(event, data)
|
|
132
172
|
end
|
|
133
173
|
end
|
|
134
174
|
```
|
|
@@ -141,15 +181,15 @@ channel.presence.get # => [Blackevin::PresenceMember]
|
|
|
141
181
|
channel.presence.history # => [Blackevin::PresenceEvent]
|
|
142
182
|
|
|
143
183
|
message = channel.history.first
|
|
144
|
-
message.name #
|
|
145
|
-
message.data # {
|
|
184
|
+
message.name # 'status'
|
|
185
|
+
message.data # {'state' => 'shipped'}
|
|
146
186
|
message.time # a Time; message.timestamp is the wire value, in ms
|
|
147
187
|
```
|
|
148
188
|
|
|
149
189
|
## Sign a user out everywhere
|
|
150
190
|
|
|
151
191
|
```ruby
|
|
152
|
-
Blackevin.
|
|
192
|
+
Blackevin::Rest::Clients.new.disconnect(user.id) # => how many connections were closed
|
|
153
193
|
```
|
|
154
194
|
|
|
155
195
|
## Queues
|
|
@@ -157,10 +197,10 @@ Blackevin.rest.clients.disconnect(user.id) # => how many connections were clos
|
|
|
157
197
|
The key needs the `amqp-subscribe` capability. A queue is addressed by its `id`.
|
|
158
198
|
|
|
159
199
|
```ruby
|
|
160
|
-
queues = Blackevin.
|
|
200
|
+
queues = Blackevin::Rest::Queues.new
|
|
161
201
|
|
|
162
|
-
queue = queues.upsert(name:
|
|
163
|
-
queues.add_rule(queue.id, source_pattern:
|
|
202
|
+
queue = queues.upsert(name: 'inbox', max_length: 10_000)
|
|
203
|
+
queues.add_rule(queue.id, source_pattern: 'orders:*')
|
|
164
204
|
|
|
165
205
|
queues.update(queue.id, enabled: false) # pause
|
|
166
206
|
queues.list(all: true) # paused ones included
|
|
@@ -173,10 +213,10 @@ One rescue covers everything the SDK raises:
|
|
|
173
213
|
|
|
174
214
|
```ruby
|
|
175
215
|
begin
|
|
176
|
-
channel.publish(
|
|
216
|
+
channel.publish('status', payload)
|
|
177
217
|
rescue Blackevin::Error => error
|
|
178
218
|
error.status_code # 403, 429, … or nil when no response arrived
|
|
179
|
-
error.reason #
|
|
219
|
+
error.reason # 'queue_limit', 'connection_limit', or nil
|
|
180
220
|
error.quota? # a plan ceiling — show an upgrade prompt, retry later
|
|
181
221
|
error.message # the server's own sentence; do not branch on it
|
|
182
222
|
end
|
|
@@ -188,7 +228,7 @@ rescue can branch by shape:
|
|
|
188
228
|
```ruby
|
|
189
229
|
rescue Blackevin::Error => error
|
|
190
230
|
case error
|
|
191
|
-
in {reason:
|
|
231
|
+
in {reason: 'queue_limit'} then redirect_to upgrade_path
|
|
192
232
|
in {status_code: 401 | 403} then raise
|
|
193
233
|
in {status_code: nil} then retry_job wait: 30.seconds
|
|
194
234
|
end
|
|
@@ -202,21 +242,25 @@ rescue Blackevin::Error => error
|
|
|
202
242
|
A token instead of a key restricts the client to that token's capability:
|
|
203
243
|
|
|
204
244
|
```ruby
|
|
205
|
-
|
|
206
|
-
|
|
245
|
+
auth = Blackevin::Rest::Auth.new
|
|
246
|
+
|
|
247
|
+
details = auth.request_token(
|
|
248
|
+
auth.create_token_request(client_id: 'worker-1', capability: {'jobs:*' => %w[publish]})
|
|
207
249
|
)
|
|
208
250
|
|
|
209
|
-
Blackevin::Rest.new(token: details.token)
|
|
251
|
+
worker = Blackevin::Rest.new(token: details.token)
|
|
252
|
+
|
|
253
|
+
Blackevin::Rest::Channel.new('jobs:1', client: worker).publish('done')
|
|
210
254
|
```
|
|
211
255
|
|
|
212
256
|
## Testing your app
|
|
213
257
|
|
|
214
|
-
Swap the transport and
|
|
258
|
+
Swap the default transport and no `Blackevin::Rest` leaves the process:
|
|
215
259
|
|
|
216
260
|
```ruby
|
|
217
261
|
Blackevin.configure do |config|
|
|
218
|
-
config.key =
|
|
219
|
-
config.transport = ->(request) { Blackevin::Response.new(status: 200, body:
|
|
262
|
+
config.key = 'secret.test'
|
|
263
|
+
config.transport = ->(request) { Blackevin::Response.new(status: 200, body: '{}') }
|
|
220
264
|
end
|
|
221
265
|
```
|
|
222
266
|
|
|
@@ -224,7 +268,8 @@ end
|
|
|
224
268
|
|
|
225
269
|
```sh
|
|
226
270
|
bundle install
|
|
227
|
-
bundle exec rake # rspec +
|
|
271
|
+
bundle exec rake # rspec + rubocop (Standard rules, single quotes)
|
|
272
|
+
bundle exec rake coverage # SimpleCov: report in coverage/index.html, fails under the floor
|
|
228
273
|
bundle exec rake contract:sync # refresh spec/contract from ../blackevin/spec
|
|
229
274
|
```
|
|
230
275
|
|
|
@@ -236,7 +281,7 @@ Bump `lib/blackevin/version.rb`, add the entry to `CHANGELOG.md`, commit, then:
|
|
|
236
281
|
bundle exec rake tag # tags vX.Y.Z from version.rb and pushes it
|
|
237
282
|
```
|
|
238
283
|
|
|
239
|
-
The tag runs `.github/workflows/release.yml`:
|
|
284
|
+
The tag runs `.github/workflows/release.yml`: tests, publish to RubyGems by
|
|
240
285
|
Trusted Publishing (no API key), and a GitHub release with the `.gem` attached.
|
|
241
286
|
|
|
242
287
|
`spec/contract` is a copy of the language-neutral contract — an OpenAPI file and
|
data/blackevin.gemspec
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'lib/blackevin/version'
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name =
|
|
6
|
+
spec.name = 'blackevin'
|
|
7
7
|
spec.version = Blackevin::VERSION
|
|
8
|
-
spec.authors = [
|
|
8
|
+
spec.authors = ['Blackevin']
|
|
9
9
|
|
|
10
|
-
spec.summary =
|
|
11
|
-
spec.description =
|
|
12
|
-
|
|
13
|
-
spec.homepage =
|
|
14
|
-
spec.license =
|
|
15
|
-
spec.required_ruby_version =
|
|
10
|
+
spec.summary = 'Blackevin Ruby SDK'
|
|
11
|
+
spec.description = 'Sign token requests, publish, read history and presence, and manage queues on Blackevin. ' \
|
|
12
|
+
'Standard library only: no runtime dependencies. Ruby 3.0 and up. Works in Rails, Sinatra, Hanami or plain Ruby.'
|
|
13
|
+
spec.homepage = 'https://blackevin.com'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.0'
|
|
16
16
|
|
|
17
17
|
spec.metadata = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
'rubygems_mfa_required' => 'true',
|
|
19
|
+
'documentation_uri' => 'https://docs.blackevin.com',
|
|
20
|
+
'source_code_uri' => 'https://github.com/thadeu/blackevin-ruby',
|
|
21
|
+
'changelog_uri' => 'https://github.com/thadeu/blackevin-ruby/blob/main/CHANGELOG.md'
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
spec.files = Dir.chdir(__dir__) do
|
|
25
|
-
Dir[
|
|
25
|
+
Dir['README.md', 'CHANGELOG.md', 'LICENSE', 'blackevin.gemspec', 'lib/**/*.rb']
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
spec.require_paths = [
|
|
28
|
+
spec.require_paths = ['lib']
|
|
29
29
|
|
|
30
|
-
spec.add_development_dependency
|
|
31
|
-
spec.add_development_dependency
|
|
32
|
-
spec.add_development_dependency
|
|
33
|
-
spec.add_development_dependency
|
|
30
|
+
spec.add_development_dependency 'rake', '>= 13.0'
|
|
31
|
+
spec.add_development_dependency 'rspec', '>= 3.13', '< 4.0'
|
|
32
|
+
spec.add_development_dependency 'simplecov', '>= 0.22'
|
|
33
|
+
spec.add_development_dependency 'standard', '>= 1.0'
|
|
34
|
+
spec.add_development_dependency 'railties', '>= 7.0'
|
|
34
35
|
end
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Blackevin
|
|
4
|
-
#
|
|
4
|
+
# The defaults a new {Blackevin::Rest} starts from. An argument given to
|
|
5
|
+
# +Rest.new+ wins over these, and these win over the environment.
|
|
5
6
|
#
|
|
6
7
|
# Blackevin.configure do |config|
|
|
7
8
|
# config.key = ENV.fetch("BLACKEVIN_KEY")
|
|
8
9
|
# end
|
|
9
10
|
#
|
|
10
11
|
# Nothing here is required: with +BLACKEVIN_KEY+ in the environment,
|
|
11
|
-
#
|
|
12
|
+
# +Blackevin::Rest.new+ works unconfigured.
|
|
12
13
|
class Configuration
|
|
13
14
|
ENV_KEY = 'BLACKEVIN_KEY'
|
|
14
15
|
|
|
@@ -33,17 +34,5 @@ module Blackevin
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def key = @key || ENV[ENV_KEY]
|
|
36
|
-
|
|
37
|
-
# @return [Hash] the options {Blackevin::Rest.new} takes
|
|
38
|
-
def to_rest_options
|
|
39
|
-
{
|
|
40
|
-
key: key,
|
|
41
|
-
rest_endpoint: rest_endpoint,
|
|
42
|
-
endpoint: endpoint,
|
|
43
|
-
open_timeout: open_timeout,
|
|
44
|
-
read_timeout: read_timeout,
|
|
45
|
-
transport: transport
|
|
46
|
-
}
|
|
47
|
-
end
|
|
48
37
|
end
|
|
49
38
|
end
|
data/lib/blackevin/endpoints.rb
CHANGED
|
@@ -26,8 +26,8 @@ module Blackevin
|
|
|
26
26
|
explicit_ws = present(endpoint) || present(env[ENV_WS])
|
|
27
27
|
|
|
28
28
|
rest = present(rest_endpoint) ||
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
present(env[ENV_REST]) ||
|
|
30
|
+
(explicit_ws ? derive_rest(explicit_ws) : DEFAULT_REST)
|
|
31
31
|
|
|
32
32
|
Resolved.new(endpoint: explicit_ws || DEFAULT_WS, rest_endpoint: rest)
|
|
33
33
|
end
|
data/lib/blackevin/errors.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Blackevin
|
|
|
33
33
|
# in {status_code: 401 | 403} then rotate_key
|
|
34
34
|
# in {status_code: nil} then retry_later
|
|
35
35
|
# end
|
|
36
|
-
def deconstruct_keys(_keys) = {
|
|
36
|
+
def deconstruct_keys(_keys) = {status_code: status_code, reason: reason, message: message}
|
|
37
37
|
|
|
38
38
|
# Builds the error for a non-2xx response, keeping the server's sentence.
|
|
39
39
|
#
|
|
@@ -48,14 +48,14 @@ module Blackevin
|
|
|
48
48
|
parsed = parse(body)
|
|
49
49
|
|
|
50
50
|
detail = case parsed
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
in {error: String => sentence} unless sentence.empty? then sentence
|
|
52
|
+
else status
|
|
53
|
+
end
|
|
54
54
|
|
|
55
55
|
reason = case parsed
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
in {reason: String => slug} then slug
|
|
57
|
+
else nil
|
|
58
|
+
end
|
|
59
59
|
|
|
60
60
|
new("#{what} failed: #{detail}", status, reason)
|
|
61
61
|
end
|
data/lib/blackevin/railtie.rb
CHANGED
|
@@ -12,6 +12,8 @@ module Blackevin
|
|
|
12
12
|
# config.blackevin.rest_endpoint = "http://localhost:3000"
|
|
13
13
|
#
|
|
14
14
|
# An explicit +Blackevin.configure+ or +BLACKEVIN_KEY+ still wins over credentials.
|
|
15
|
+
# It only fills {Blackevin.configuration}; the application still builds its own
|
|
16
|
+
# +Blackevin::Rest.new+.
|
|
15
17
|
class Railtie < Rails::Railtie
|
|
16
18
|
SETTINGS = %i[key rest_endpoint endpoint open_timeout read_timeout transport].freeze
|
|
17
19
|
|
data/lib/blackevin/rest/auth.rb
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'securerandom'
|
|
5
5
|
|
|
6
6
|
module Blackevin
|
|
7
7
|
class Rest
|
|
8
8
|
# Token requests: signing one locally, and exchanging one for a token.
|
|
9
9
|
class Auth
|
|
10
10
|
DEFAULT_TTL_MS = 3_600_000
|
|
11
|
-
DEFAULT_CAPABILITY = {
|
|
11
|
+
DEFAULT_CAPABILITY = {'*' => %w[subscribe publish presence history]}.freeze
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# @param client [Blackevin::Rest, nil] default: a new one, from the configuration
|
|
14
|
+
# @param clock [#call, nil] returns milliseconds since the epoch; injected for tests
|
|
15
|
+
# @param nonce [#call, nil] returns a fresh nonce; injected for tests
|
|
16
|
+
def initialize(client: nil, clock: nil, nonce: nil)
|
|
17
|
+
@client = Rest.resolve(client)
|
|
15
18
|
@clock = clock || -> { Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) }
|
|
16
19
|
@nonce = nonce || -> { SecureRandom.hex(16) }
|
|
17
20
|
end
|
|
@@ -31,7 +34,7 @@ module Blackevin
|
|
|
31
34
|
# @return [Blackevin::TokenRequest] signed
|
|
32
35
|
# @raise [Blackevin::ConfigurationError] without a usable API key
|
|
33
36
|
def create_token_request(client_id: nil, ttl: nil, capability: nil, timestamp: nil, nonce: nil)
|
|
34
|
-
api_key = @
|
|
37
|
+
api_key = @client.api_key
|
|
35
38
|
|
|
36
39
|
request = TokenRequest.new(
|
|
37
40
|
key_name: api_key.key_name,
|
|
@@ -60,9 +63,9 @@ module Blackevin
|
|
|
60
63
|
in Hash => hash then TokenRequest.from_h(hash).to_h
|
|
61
64
|
else raise ConfigurationError, "request_token takes a TokenRequest or a Hash, got #{token_request.class}"
|
|
62
65
|
end
|
|
63
|
-
path = "/keys/#{Rest.escape(wire.fetch(
|
|
66
|
+
path = "/keys/#{Rest.escape(wire.fetch('keyName'))}/requestToken"
|
|
64
67
|
|
|
65
|
-
TokenDetails.from_h(@
|
|
68
|
+
TokenDetails.from_h(@client.request('requestToken', 'POST', path, body: wire, authorize: false))
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
private
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Blackevin
|
|
4
|
+
class Rest
|
|
5
|
+
# One channel: publish, history, presence.
|
|
6
|
+
class Channel
|
|
7
|
+
# @return [String]
|
|
8
|
+
attr_reader :name
|
|
9
|
+
|
|
10
|
+
# @return [Blackevin::Rest::Presence]
|
|
11
|
+
attr_reader :presence
|
|
12
|
+
|
|
13
|
+
# @param name [String]
|
|
14
|
+
# @param client [Blackevin::Rest, nil] default: a new one, from the configuration
|
|
15
|
+
def initialize(name, client: nil)
|
|
16
|
+
@name = name.to_s
|
|
17
|
+
|
|
18
|
+
raise ConfigurationError, 'a channel needs a name' if @name.empty?
|
|
19
|
+
|
|
20
|
+
@client = Rest.resolve(client)
|
|
21
|
+
@path = "/api/channels/#{Rest.escape(@name)}"
|
|
22
|
+
@presence = Presence.new(@name, client: @client)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Publishes without a socket — from a job, a cron, or a webhook arriving
|
|
26
|
+
# at your backend. Subscribers, account queues and integrations see it
|
|
27
|
+
# exactly as they see a socket publish.
|
|
28
|
+
#
|
|
29
|
+
# @param event [String, Symbol] the event name
|
|
30
|
+
# @param data [Object, nil] anything JSON can carry
|
|
31
|
+
# @return [true]
|
|
32
|
+
# @raise [Blackevin::Error]
|
|
33
|
+
def publish(event, data = nil)
|
|
34
|
+
body = {'name' => event.to_s}
|
|
35
|
+
body['data'] = data unless data.nil?
|
|
36
|
+
|
|
37
|
+
@client.request('publish', 'POST', "#{@path}/publish", body: body)
|
|
38
|
+
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Stored messages, newest first.
|
|
43
|
+
#
|
|
44
|
+
# @param limit [Integer] clamped by the node to 1..1000
|
|
45
|
+
# @return [Array<Blackevin::Message>]
|
|
46
|
+
# @raise [Blackevin::Error]
|
|
47
|
+
def history(limit: DEFAULT_HISTORY_LIMIT)
|
|
48
|
+
body = @client.request('history', 'GET', "#{@path}/history", query: {'limit' => limit})
|
|
49
|
+
|
|
50
|
+
Array(body['messages']).map { Message.from_h(_1) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def inspect = "#<Blackevin::Rest::Channel name=#{name.inspect}>"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -4,8 +4,9 @@ module Blackevin
|
|
|
4
4
|
class Rest
|
|
5
5
|
# The channels of one client. +get+ returns the same object for the same name.
|
|
6
6
|
class Channels
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# @param client [Blackevin::Rest, nil] default: a new one, from the configuration
|
|
8
|
+
def initialize(client: nil)
|
|
9
|
+
@client = Rest.resolve(client)
|
|
9
10
|
@channels = {}
|
|
10
11
|
@mutex = Mutex.new
|
|
11
12
|
end
|
|
@@ -15,89 +16,12 @@ module Blackevin
|
|
|
15
16
|
def get(name)
|
|
16
17
|
name = name.to_s
|
|
17
18
|
|
|
18
|
-
raise ConfigurationError,
|
|
19
|
+
raise ConfigurationError, 'a channel needs a name' if name.empty?
|
|
19
20
|
|
|
20
|
-
@mutex.synchronize { @channels[name] ||= Channel.new(
|
|
21
|
+
@mutex.synchronize { @channels[name] ||= Channel.new(name, client: @client) }
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
alias_method :[], :get
|
|
24
25
|
end
|
|
25
|
-
|
|
26
|
-
# One channel: publish, history, presence.
|
|
27
|
-
class Channel
|
|
28
|
-
DEFAULT_HISTORY_LIMIT = 100
|
|
29
|
-
|
|
30
|
-
# @return [String]
|
|
31
|
-
attr_reader :name
|
|
32
|
-
|
|
33
|
-
# @return [Blackevin::Rest::Presence]
|
|
34
|
-
attr_reader :presence
|
|
35
|
-
|
|
36
|
-
def initialize(rest, name)
|
|
37
|
-
@rest = rest
|
|
38
|
-
@name = name
|
|
39
|
-
@path = "/api/channels/#{Rest.escape(name)}"
|
|
40
|
-
@presence = Presence.new(rest, @path)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Publishes without a socket — from a job, a cron, or a webhook arriving
|
|
44
|
-
# at your backend. Subscribers, account queues and integrations see it
|
|
45
|
-
# exactly as they see a socket publish.
|
|
46
|
-
#
|
|
47
|
-
# @param event [String, Symbol] the event name
|
|
48
|
-
# @param data [Object, nil] anything JSON can carry
|
|
49
|
-
# @return [true]
|
|
50
|
-
# @raise [Blackevin::Error]
|
|
51
|
-
def publish(event, data = nil)
|
|
52
|
-
body = {"name" => event.to_s}
|
|
53
|
-
body["data"] = data unless data.nil?
|
|
54
|
-
|
|
55
|
-
@rest.request("publish", "POST", "#{@path}/publish", body: body)
|
|
56
|
-
|
|
57
|
-
true
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Stored messages, newest first.
|
|
61
|
-
#
|
|
62
|
-
# @param limit [Integer] clamped by the node to 1..1000
|
|
63
|
-
# @return [Array<Blackevin::Message>]
|
|
64
|
-
# @raise [Blackevin::Error]
|
|
65
|
-
def history(limit: DEFAULT_HISTORY_LIMIT)
|
|
66
|
-
body = @rest.request("history", "GET", "#{@path}/history", query: {"limit" => limit})
|
|
67
|
-
|
|
68
|
-
Array(body["messages"]).map { Message.from_h(_1) }
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def inspect = "#<Blackevin::Rest::Channel name=#{name.inspect}>"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Presence on one channel.
|
|
75
|
-
class Presence
|
|
76
|
-
def initialize(rest, channel_path)
|
|
77
|
-
@rest = rest
|
|
78
|
-
@path = "#{channel_path}/presence"
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Who is present now.
|
|
82
|
-
#
|
|
83
|
-
# @return [Array<Blackevin::PresenceMember>]
|
|
84
|
-
# @raise [Blackevin::Error]
|
|
85
|
-
def get
|
|
86
|
-
body = @rest.request("presence get", "GET", @path)
|
|
87
|
-
|
|
88
|
-
Array(body["members"]).map { PresenceMember.from_h(_1) }
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Past enter, leave and update events, newest first.
|
|
92
|
-
#
|
|
93
|
-
# @param limit [Integer] clamped by the node to 1..1000
|
|
94
|
-
# @return [Array<Blackevin::PresenceEvent>]
|
|
95
|
-
# @raise [Blackevin::Error]
|
|
96
|
-
def history(limit: Channel::DEFAULT_HISTORY_LIMIT)
|
|
97
|
-
body = @rest.request("presence history", "GET", "#{@path}/history", query: {"limit" => limit})
|
|
98
|
-
|
|
99
|
-
Array(body["events"]).map { PresenceEvent.from_h(_1) }
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
26
|
end
|
|
103
27
|
end
|
|
@@ -4,8 +4,9 @@ module Blackevin
|
|
|
4
4
|
class Rest
|
|
5
5
|
# Acting on connected clients.
|
|
6
6
|
class Clients
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# @param client [Blackevin::Rest, nil] default: a new one, from the configuration
|
|
8
|
+
def initialize(client: nil)
|
|
9
|
+
@client = Rest.resolve(client)
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
# Closes every connection a client holds — for signing a user out
|
|
@@ -17,11 +18,11 @@ module Blackevin
|
|
|
17
18
|
def disconnect(client_id)
|
|
18
19
|
client_id = client_id.to_s
|
|
19
20
|
|
|
20
|
-
raise ConfigurationError,
|
|
21
|
+
raise ConfigurationError, 'disconnect needs a client_id' if client_id.empty?
|
|
21
22
|
|
|
22
23
|
path = "/api/clients/#{Rest.escape(client_id)}/connections/close"
|
|
23
24
|
|
|
24
|
-
@
|
|
25
|
+
@client.request('disconnect', 'POST', path)['closed'].to_i
|
|
25
26
|
end
|
|
26
27
|
end
|
|
27
28
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Blackevin
|
|
4
|
+
class Rest
|
|
5
|
+
# Presence on one channel.
|
|
6
|
+
class Presence
|
|
7
|
+
# @param channel [String] the channel's name
|
|
8
|
+
# @param client [Blackevin::Rest, nil] default: a new one, from the configuration
|
|
9
|
+
def initialize(channel, client: nil)
|
|
10
|
+
raise ConfigurationError, 'presence needs a channel name' if channel.to_s.empty?
|
|
11
|
+
|
|
12
|
+
@client = Rest.resolve(client)
|
|
13
|
+
@path = "/api/channels/#{Rest.escape(channel)}/presence"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Who is present now.
|
|
17
|
+
#
|
|
18
|
+
# @return [Array<Blackevin::PresenceMember>]
|
|
19
|
+
# @raise [Blackevin::Error]
|
|
20
|
+
def get
|
|
21
|
+
body = @client.request('presence get', 'GET', @path)
|
|
22
|
+
|
|
23
|
+
Array(body['members']).map { PresenceMember.from_h(_1) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Past enter, leave and update events, newest first.
|
|
27
|
+
#
|
|
28
|
+
# @param limit [Integer] clamped by the node to 1..1000
|
|
29
|
+
# @return [Array<Blackevin::PresenceEvent>]
|
|
30
|
+
# @raise [Blackevin::Error]
|
|
31
|
+
def history(limit: DEFAULT_HISTORY_LIMIT)
|
|
32
|
+
body = @client.request('presence history', 'GET', "#{@path}/history", query: {'limit' => limit})
|
|
33
|
+
|
|
34
|
+
Array(body['events']).map { PresenceEvent.from_h(_1) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -7,17 +7,18 @@ module Blackevin
|
|
|
7
7
|
# A queue is addressed by its id, never its name: the name is the physical
|
|
8
8
|
# queue on the broker, so a 404 must mean "no such queue".
|
|
9
9
|
class Queues
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
# @param client [Blackevin::Rest, nil] default: a new one, from the configuration
|
|
11
|
+
def initialize(client: nil)
|
|
12
|
+
@client = Rest.resolve(client)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
# @param all [Boolean] include paused queues
|
|
15
16
|
# @return [Array<Blackevin::Queue>]
|
|
16
17
|
# @raise [Blackevin::Error]
|
|
17
18
|
def list(all: false)
|
|
18
|
-
body = @
|
|
19
|
+
body = @client.request('list queues', 'GET', '/api/queues', query: all ? {'all' => '1'} : nil)
|
|
19
20
|
|
|
20
|
-
Array(body[
|
|
21
|
+
Array(body['queues']).map { Queue.from_h(_1) }
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
# Creates a queue, or edits the one already holding this name. Only a new
|
|
@@ -29,10 +30,10 @@ module Blackevin
|
|
|
29
30
|
# @return [Blackevin::Queue]
|
|
30
31
|
# @raise [Blackevin::Error] with reason "queue_limit" at the plan's ceiling
|
|
31
32
|
def upsert(name:, max_length: nil, enabled: true)
|
|
32
|
-
body = {
|
|
33
|
-
body[
|
|
33
|
+
body = {'name' => name.to_s, 'enabled' => enabled}
|
|
34
|
+
body['maxLength'] = max_length unless max_length.nil?
|
|
34
35
|
|
|
35
|
-
Queue.from_h(@
|
|
36
|
+
Queue.from_h(@client.request('upsert queue', 'POST', '/api/queues', body: body).fetch('queue'))
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
alias_method :create, :upsert
|
|
@@ -48,13 +49,13 @@ module Blackevin
|
|
|
48
49
|
def update(id, **changes)
|
|
49
50
|
unknown = changes.except(:enabled, :max_length).keys
|
|
50
51
|
|
|
51
|
-
raise ArgumentError, "unknown keywords: #{unknown.join(
|
|
52
|
+
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
52
53
|
|
|
53
54
|
body = {}
|
|
54
|
-
body[
|
|
55
|
-
body[
|
|
55
|
+
body['enabled'] = changes[:enabled] if changes.key?(:enabled)
|
|
56
|
+
body['maxLength'] = changes[:max_length] if changes.key?(:max_length)
|
|
56
57
|
|
|
57
|
-
Queue.from_h(@
|
|
58
|
+
Queue.from_h(@client.request('update queue', 'PATCH', queue_path(id), body: body).fetch('queue'))
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
# Deletes the queue and whatever is waiting in it.
|
|
@@ -63,16 +64,16 @@ module Blackevin
|
|
|
63
64
|
# @return [String] the name of the deleted queue
|
|
64
65
|
# @raise [Blackevin::Error]
|
|
65
66
|
def delete(id)
|
|
66
|
-
@
|
|
67
|
+
@client.request('delete queue', 'DELETE', queue_path(id))['deleted']
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
# @param id [String] the queue's id
|
|
70
71
|
# @return [Array<Blackevin::QueueRule>]
|
|
71
72
|
# @raise [Blackevin::Error]
|
|
72
73
|
def rules(id)
|
|
73
|
-
body = @
|
|
74
|
+
body = @client.request('list queue rules', 'GET', "#{queue_path(id)}/rules")
|
|
74
75
|
|
|
75
|
-
Array(body[
|
|
76
|
+
Array(body['rules']).map { QueueRule.from_h(_1) }
|
|
76
77
|
end
|
|
77
78
|
|
|
78
79
|
# Copies messages from channels matching the pattern into the queue.
|
|
@@ -83,10 +84,10 @@ module Blackevin
|
|
|
83
84
|
# @return [Blackevin::QueueRule]
|
|
84
85
|
# @raise [Blackevin::Error]
|
|
85
86
|
def add_rule(id, source_pattern:, filter: nil)
|
|
86
|
-
body = {
|
|
87
|
-
body[
|
|
87
|
+
body = {'sourcePattern' => source_pattern.to_s}
|
|
88
|
+
body['filter'] = filter unless filter.nil?
|
|
88
89
|
|
|
89
|
-
QueueRule.from_h(@
|
|
90
|
+
QueueRule.from_h(@client.request('add queue rule', 'POST', "#{queue_path(id)}/rules", body: body).fetch('rule'))
|
|
90
91
|
end
|
|
91
92
|
|
|
92
93
|
# Stops the copies at the source. Messages already enqueued stay.
|
|
@@ -96,7 +97,7 @@ module Blackevin
|
|
|
96
97
|
# @return [true]
|
|
97
98
|
# @raise [Blackevin::Error]
|
|
98
99
|
def delete_rule(id, rule_id)
|
|
99
|
-
@
|
|
100
|
+
@client.request('delete queue rule', 'DELETE', "#{queue_path(id)}/rules/#{Rest.escape(rule_id)}")
|
|
100
101
|
|
|
101
102
|
true
|
|
102
103
|
end
|
|
@@ -104,7 +105,7 @@ module Blackevin
|
|
|
104
105
|
private
|
|
105
106
|
|
|
106
107
|
def queue_path(id)
|
|
107
|
-
raise ConfigurationError,
|
|
108
|
+
raise ConfigurationError, 'a queue is addressed by its id' if id.to_s.empty?
|
|
108
109
|
|
|
109
110
|
"/api/queues/#{Rest.escape(id)}"
|
|
110
111
|
end
|
data/lib/blackevin/rest.rb
CHANGED
|
@@ -5,14 +5,28 @@ require 'json'
|
|
|
5
5
|
module Blackevin
|
|
6
6
|
# The REST client: no socket, no reconnection, no state beyond its options.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
8
|
+
# bk = Blackevin::Rest.new
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# bk.auth.create_token_request(client_id: 'bob')
|
|
11
|
+
# bk.channels.get('room:42').publish('greeting', {text: 'hi'})
|
|
12
|
+
#
|
|
13
|
+
# Each part also stands alone, so nothing forces a chain through this class:
|
|
14
|
+
#
|
|
15
|
+
# Blackevin::Rest::Auth.new.create_token_request(client_id: 'bob')
|
|
16
|
+
# Blackevin::Rest::Channel.new('room:42').publish('greeting', {text: 'hi'})
|
|
17
|
+
# Blackevin::Rest::Queues.new.list
|
|
18
|
+
#
|
|
19
|
+
# Each takes an optional +client:+ and builds a +Blackevin::Rest.new+ without one.
|
|
20
|
+
#
|
|
21
|
+
# Every option falls back to {Blackevin.configuration}, and the key falls back
|
|
22
|
+
# from there to +BLACKEVIN_KEY+, so +new+ with no arguments is the usual call.
|
|
23
|
+
# Creating one is cheap — it opens nothing — so there is no need to share it.
|
|
12
24
|
#
|
|
13
25
|
# Safe to share across threads: every call opens its own connection and the
|
|
14
26
|
# instance holds nothing mutable but a channel cache behind a mutex.
|
|
15
27
|
class Rest
|
|
28
|
+
DEFAULT_HISTORY_LIMIT = 100
|
|
29
|
+
|
|
16
30
|
USER_AGENT = "blackevin-ruby/#{VERSION} ruby/#{RUBY_VERSION}"
|
|
17
31
|
|
|
18
32
|
# @return [Blackevin::Rest::Auth]
|
|
@@ -30,33 +44,45 @@ module Blackevin
|
|
|
30
44
|
# @return [String] the resolved REST base URL
|
|
31
45
|
attr_reader :rest_endpoint
|
|
32
46
|
|
|
33
|
-
# @param key [String, nil] full API key +secret.keyId+; required to sign token requests
|
|
47
|
+
# @param key [String, nil] full API key +secret.keyId+; required to sign token requests.
|
|
48
|
+
# Default: the configured key, then +BLACKEVIN_KEY+
|
|
34
49
|
# @param token [String, nil] a token, when acting as one client rather than as the account
|
|
35
50
|
# @param rest_endpoint [String, nil] overrides the resolved REST host
|
|
36
51
|
# @param endpoint [String, nil] socket endpoint; read only to derive the REST base
|
|
37
|
-
# @param open_timeout [Numeric] seconds
|
|
38
|
-
# @param read_timeout [Numeric] seconds
|
|
52
|
+
# @param open_timeout [Numeric, nil] seconds; default from the configuration (5)
|
|
53
|
+
# @param read_timeout [Numeric, nil] seconds; default from the configuration (10)
|
|
39
54
|
# @param transport [#call, nil] replaces Net::HTTP; receives a {Request}, returns a {Response}
|
|
40
55
|
# @param clock [#call] returns milliseconds since the epoch; injected for tests
|
|
41
56
|
# @param nonce [#call] returns a fresh nonce; injected for tests
|
|
42
57
|
# @param env [#[]] where the endpoint variables are read from
|
|
43
|
-
def initialize(key: nil, token: nil, rest_endpoint: nil, endpoint: nil, open_timeout:
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
def initialize(key: nil, token: nil, rest_endpoint: nil, endpoint: nil, open_timeout: nil, read_timeout: nil,
|
|
59
|
+
transport: nil, clock: nil, nonce: nil, env: ENV)
|
|
60
|
+
defaults = Blackevin.configuration
|
|
61
|
+
|
|
62
|
+
@key = key || defaults.key
|
|
46
63
|
@token = token
|
|
47
|
-
@rest_endpoint = Endpoints.resolve(endpoint: endpoint, rest_endpoint: rest_endpoint, env: env).rest_endpoint
|
|
48
|
-
@transport = transport || Transport.new(open_timeout: open_timeout, read_timeout: read_timeout)
|
|
49
64
|
|
|
50
|
-
@
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
@rest_endpoint = Endpoints.resolve(
|
|
66
|
+
endpoint: endpoint || defaults.endpoint,
|
|
67
|
+
rest_endpoint: rest_endpoint || defaults.rest_endpoint,
|
|
68
|
+
env: env
|
|
69
|
+
).rest_endpoint
|
|
70
|
+
|
|
71
|
+
@transport = transport || defaults.transport || Transport.new(
|
|
72
|
+
open_timeout: open_timeout || defaults.open_timeout,
|
|
73
|
+
read_timeout: read_timeout || defaults.read_timeout
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@auth = Auth.new(client: self, clock: clock, nonce: nonce)
|
|
77
|
+
@channels = Channels.new(client: self)
|
|
78
|
+
@clients = Clients.new(client: self)
|
|
79
|
+
@queues = Queues.new(client: self)
|
|
54
80
|
end
|
|
55
81
|
|
|
56
82
|
# @return [Blackevin::ApiKey]
|
|
57
83
|
# @raise [Blackevin::ConfigurationError] when no key was given, or it is malformed
|
|
58
84
|
def api_key
|
|
59
|
-
raise ConfigurationError, 'this call needs the API key: pass key
|
|
85
|
+
raise ConfigurationError, 'this call needs the API key: pass key:, set it in Blackevin.configure, or set BLACKEVIN_KEY' if @key.to_s.strip.empty?
|
|
60
86
|
|
|
61
87
|
ApiKey.parse(@key)
|
|
62
88
|
end
|
|
@@ -72,9 +98,20 @@ module Blackevin
|
|
|
72
98
|
"Basic #{[@key].pack('m0')}"
|
|
73
99
|
end
|
|
74
100
|
|
|
101
|
+
# What every resource class does with its +client:+ argument: use the one it
|
|
102
|
+
# was given, or build one from the configuration.
|
|
103
|
+
#
|
|
104
|
+
# @api private
|
|
105
|
+
def self.resolve(client)
|
|
106
|
+
return new if client.nil?
|
|
107
|
+
return client if client.respond_to?(:request) && client.respond_to?(:api_key)
|
|
108
|
+
|
|
109
|
+
raise ConfigurationError, "client: must be a Blackevin::Rest, got #{client.class}"
|
|
110
|
+
end
|
|
111
|
+
|
|
75
112
|
# @api private
|
|
76
113
|
def request(what, method, path, query: nil, body: nil, authorize: true)
|
|
77
|
-
headers = {
|
|
114
|
+
headers = {'accept' => 'application/json', 'user-agent' => USER_AGENT}
|
|
78
115
|
authorization = authorize ? authorization_header : nil
|
|
79
116
|
|
|
80
117
|
headers['authorization'] = authorization if authorization
|
|
@@ -54,7 +54,7 @@ module Blackevin
|
|
|
54
54
|
# Pattern matching, with the Ruby names. The token itself is left out on
|
|
55
55
|
# purpose: a pattern's bindings end up in logs more easily than a reader.
|
|
56
56
|
def deconstruct_keys(_keys)
|
|
57
|
-
{
|
|
57
|
+
{key_name: key_name, issued: issued, expires: expires, capability: capability, client_id: client_id}
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# The token is a credential; keep it out of logs.
|
|
@@ -29,7 +29,8 @@ module Blackevin
|
|
|
29
29
|
'cache-control' => 'no-store'
|
|
30
30
|
}.freeze
|
|
31
31
|
|
|
32
|
-
# @param rest [Blackevin::Rest, nil] defaults to {Blackevin
|
|
32
|
+
# @param rest [Blackevin::Rest, nil] defaults to a new {Blackevin::Rest} per request, which
|
|
33
|
+
# picks up whatever {Blackevin.configure} holds at that moment
|
|
33
34
|
# @yieldparam env [Hash] the Rack env
|
|
34
35
|
# @yieldreturn [Hash, nil] keyword arguments for +create_token_request+, or nil to answer 401
|
|
35
36
|
def initialize(rest: nil, &identify)
|
|
@@ -43,14 +44,14 @@ module Blackevin
|
|
|
43
44
|
# @return [Array(Integer, Hash, Array<String>)]
|
|
44
45
|
def call(env)
|
|
45
46
|
unless ALLOWED_METHODS.include?(env['REQUEST_METHOD'])
|
|
46
|
-
return respond(405, {
|
|
47
|
+
return respond(405, {'error' => 'method not allowed'}, 'allow' => ALLOWED_METHODS.join(', '))
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
case @identify.call(env)
|
|
50
51
|
in nil | false
|
|
51
|
-
respond(401, {
|
|
52
|
+
respond(401, {'error' => 'unauthorized'})
|
|
52
53
|
in Hash => params
|
|
53
|
-
respond(200, (@rest ||
|
|
54
|
+
respond(200, (@rest || Rest.new).auth.create_token_request(**params.transform_keys(&:to_sym)).to_h)
|
|
54
55
|
in other
|
|
55
56
|
raise ConfigurationError, "the TokenEndpoint block must return a Hash or nil, got #{other.class}"
|
|
56
57
|
end
|
|
@@ -9,7 +9,7 @@ module Blackevin
|
|
|
9
9
|
#
|
|
10
10
|
# Serialises to the wire shape, so a controller can render it as is:
|
|
11
11
|
#
|
|
12
|
-
# render json: Blackevin.
|
|
12
|
+
# render json: Blackevin::Rest.new.auth.create_token_request(client_id: current_user.id)
|
|
13
13
|
class TokenRequest
|
|
14
14
|
# The field ORDER and the newline after each are the wire contract. The node
|
|
15
15
|
# recomputes exactly this text to verify, and a reordering breaks every
|
data/lib/blackevin/version.rb
CHANGED
data/lib/blackevin.rb
CHANGED
|
@@ -11,6 +11,8 @@ require_relative 'blackevin/transport'
|
|
|
11
11
|
require_relative 'blackevin/configuration'
|
|
12
12
|
require_relative 'blackevin/rest'
|
|
13
13
|
require_relative 'blackevin/rest/auth'
|
|
14
|
+
require_relative 'blackevin/rest/presence'
|
|
15
|
+
require_relative 'blackevin/rest/channel'
|
|
14
16
|
require_relative 'blackevin/rest/channels'
|
|
15
17
|
require_relative 'blackevin/rest/clients'
|
|
16
18
|
require_relative 'blackevin/rest/queues'
|
|
@@ -19,9 +21,12 @@ require_relative 'blackevin/token_endpoint'
|
|
|
19
21
|
# Server-side SDK for Blackevin: sign token requests, publish, read history and
|
|
20
22
|
# presence, manage queues. Standard library only.
|
|
21
23
|
#
|
|
22
|
-
#
|
|
24
|
+
# bk = Blackevin::Rest.new
|
|
23
25
|
#
|
|
24
|
-
#
|
|
26
|
+
# bk.channels.get('room:42').publish('greeting', {text: 'hi'})
|
|
27
|
+
#
|
|
28
|
+
# There is no process-wide client. {Blackevin.configure} only holds the defaults
|
|
29
|
+
# a new {Blackevin::Rest} starts from.
|
|
25
30
|
module Blackevin
|
|
26
31
|
@mutex = Mutex.new
|
|
27
32
|
|
|
@@ -31,31 +36,20 @@ module Blackevin
|
|
|
31
36
|
@mutex.synchronize { @configuration ||= Configuration.new }
|
|
32
37
|
end
|
|
33
38
|
|
|
39
|
+
# Sets the defaults every later {Blackevin::Rest.new} starts from. It builds
|
|
40
|
+
# nothing and calls nothing: a client already created keeps what it was given.
|
|
41
|
+
#
|
|
34
42
|
# @yieldparam config [Blackevin::Configuration]
|
|
35
43
|
# @return [Blackevin::Configuration]
|
|
36
44
|
def configure
|
|
37
45
|
yield configuration
|
|
38
46
|
|
|
39
|
-
@mutex.synchronize { @rest = nil }
|
|
40
|
-
|
|
41
47
|
configuration
|
|
42
48
|
end
|
|
43
49
|
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# @return [Blackevin::Rest]
|
|
47
|
-
def rest
|
|
48
|
-
options = configuration.to_rest_options
|
|
49
|
-
|
|
50
|
-
@mutex.synchronize { @rest ||= Rest.new(**options) }
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Forgets the configuration and the client. For test suites.
|
|
50
|
+
# Forgets the configuration. For test suites.
|
|
54
51
|
def reset!
|
|
55
|
-
@mutex.synchronize
|
|
56
|
-
@configuration = nil
|
|
57
|
-
@rest = nil
|
|
58
|
-
end
|
|
52
|
+
@mutex.synchronize { @configuration = nil }
|
|
59
53
|
end
|
|
60
54
|
end
|
|
61
55
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: blackevin
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Blackevin
|
|
@@ -43,6 +43,20 @@ dependencies:
|
|
|
43
43
|
- - "<"
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
45
|
version: '4.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: simplecov
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0.22'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0.22'
|
|
46
60
|
- !ruby/object:Gem::Dependency
|
|
47
61
|
name: standard
|
|
48
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -90,8 +104,10 @@ files:
|
|
|
90
104
|
- lib/blackevin/railtie.rb
|
|
91
105
|
- lib/blackevin/rest.rb
|
|
92
106
|
- lib/blackevin/rest/auth.rb
|
|
107
|
+
- lib/blackevin/rest/channel.rb
|
|
93
108
|
- lib/blackevin/rest/channels.rb
|
|
94
109
|
- lib/blackevin/rest/clients.rb
|
|
110
|
+
- lib/blackevin/rest/presence.rb
|
|
95
111
|
- lib/blackevin/rest/queues.rb
|
|
96
112
|
- lib/blackevin/token_details.rb
|
|
97
113
|
- lib/blackevin/token_endpoint.rb
|