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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c81bfd943d2a666cb4708cbf7fb68dda7e9bfa73a4563ad3ec444475031bc62
4
- data.tar.gz: ef8433cd9a72fe8429521b93e2724044ec5fac06a94af1053962db3e67468379
3
+ metadata.gz: b88ba03b32ee0b815636e6cebc77a95669c1df92d3734ac6c6ec66023f781012
4
+ data.tar.gz: 7f01c4066dc941d1b86493eac3cd60dc2be0838475de5a3227da6a7d8dff4419
5
5
  SHA512:
6
- metadata.gz: f11f01f55bbc204af64100bffb53e88540c3c8374c80df2d42a68b551279f9f3b80c0fc3611458fcd5643645a6efbad48b8f4a073af44c76c2b96e4811959193
7
- data.tar.gz: d4fe938a5b52edfe02b9cf9e86b300b6ebe98fcceb22807dc0c7d7eb937b39f8f010a4a9de9c0f804f61359338934e523f536f9bd15ecac2484729a768d846e6
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 "blackevin"
13
+ gem 'blackevin'
14
14
  ```
15
15
 
16
- ## Configure
16
+ ## The client
17
17
 
18
- With `BLACKEVIN_KEY` in the environment there is nothing to configure:
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.rest.channels.get("room:42").publish("greeting", {text: "hi"})
22
+ bk = Blackevin::Rest.new
23
+
24
+ ch = bk.channels.get('room:42')
25
+ ch.publish('greeting', {text: 'hi'})
22
26
  ```
23
27
 
24
- Or say it explicitly, once, at boot:
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 = ENV.fetch("BLACKEVIN_KEY") # secret.keyId, from the console
29
- config.open_timeout = 5 # seconds
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.rest.auth.create_token_request(
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: "blackevin/token"
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 = "ws://localhost:3000"
118
+ config.blackevin.endpoint = 'ws://localhost:3000'
79
119
  ```
80
120
 
81
121
  ### Sinatra
82
122
 
83
123
  ```ruby
84
- require "sinatra"
85
- require "blackevin"
124
+ require 'sinatra'
125
+ require 'blackevin'
86
126
 
87
- get "/blackevin/token" do
127
+ get '/blackevin/token' do
88
128
  halt 401 unless current_user
89
129
 
90
130
  content_type :json
91
- Blackevin.rest.auth.create_token_request(client_id: current_user.id).to_json
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["warden"]&.user
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: "/blackevin/token"
110
- # Rails: mount TOKENS, at: "/blackevin/token"
111
- # Rack: map("/blackevin/token") { run TOKENS }
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.rest.channels.get("orders:#{order.id}")
157
+ channel = Blackevin::Rest::Channel.new("orders:#{order.id}")
118
158
 
119
- channel.publish("status", {state: "shipped"})
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.rest.channels.get(channel).publish(event, data)
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 # "status"
145
- message.data # {"state" => "shipped"}
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.rest.clients.disconnect(user.id) # => how many connections were closed
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.rest.queues
200
+ queues = Blackevin::Rest::Queues.new
161
201
 
162
- queue = queues.upsert(name: "inbox", max_length: 10_000)
163
- queues.add_rule(queue.id, source_pattern: "orders:*")
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("status", payload)
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 # "queue_limit", "connection_limit", or nil
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: "queue_limit"} then redirect_to upgrade_path
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
- details = Blackevin.rest.auth.request_token(
206
- Blackevin.rest.auth.create_token_request(client_id: "worker-1", capability: {"jobs:*" => %w[publish]})
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).channels.get("jobs:1").publish("done")
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 nothing leaves the process:
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 = "secret.test"
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 + standard
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`: lint, tests, publish to RubyGems by
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 "lib/blackevin/version"
3
+ require_relative 'lib/blackevin/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = "blackevin"
6
+ spec.name = 'blackevin'
7
7
  spec.version = Blackevin::VERSION
8
- spec.authors = ["Blackevin"]
8
+ spec.authors = ['Blackevin']
9
9
 
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"
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
- "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"
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["README.md", "CHANGELOG.md", "LICENSE", "blackevin.gemspec", "lib/**/*.rb"]
25
+ Dir['README.md', 'CHANGELOG.md', 'LICENSE', 'blackevin.gemspec', 'lib/**/*.rb']
26
26
  end
27
27
 
28
- spec.require_paths = ["lib"]
28
+ spec.require_paths = ['lib']
29
29
 
30
- spec.add_development_dependency "rake", ">= 13.0"
31
- spec.add_development_dependency "rspec", ">= 3.13", "< 4.0"
32
- spec.add_development_dependency "standard", ">= 1.0"
33
- spec.add_development_dependency "railties", ">= 7.0"
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
- # Process-wide defaults for {Blackevin.rest}.
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
- # {Blackevin.rest} works unconfigured.
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
@@ -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
- present(env[ENV_REST]) ||
30
- (explicit_ws ? derive_rest(explicit_ws) : DEFAULT_REST)
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
@@ -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) = { status_code: status_code, reason: reason, message: message }
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
- in { error: String => sentence } unless sentence.empty? then sentence
52
- else status
53
- end
51
+ in {error: String => sentence} unless sentence.empty? then sentence
52
+ else status
53
+ end
54
54
 
55
55
  reason = case parsed
56
- in { reason: String => slug } then slug
57
- else nil
58
- end
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
@@ -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
 
@@ -1,17 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "securerandom"
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 = {"*" => %w[subscribe publish presence history]}.freeze
11
+ DEFAULT_CAPABILITY = {'*' => %w[subscribe publish presence history]}.freeze
12
12
 
13
- def initialize(rest, clock: nil, nonce: nil)
14
- @rest = rest
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 = @rest.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("keyName"))}/requestToken"
66
+ path = "/keys/#{Rest.escape(wire.fetch('keyName'))}/requestToken"
64
67
 
65
- TokenDetails.from_h(@rest.request("requestToken", "POST", path, body: wire, authorize: false))
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
- def initialize(rest)
8
- @rest = rest
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, "a channel needs a name" if name.empty?
19
+ raise ConfigurationError, 'a channel needs a name' if name.empty?
19
20
 
20
- @mutex.synchronize { @channels[name] ||= Channel.new(@rest, name) }
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
- def initialize(rest)
8
- @rest = rest
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, "disconnect needs a client_id" if client_id.empty?
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
- @rest.request("disconnect", "POST", path)["closed"].to_i
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
- def initialize(rest)
11
- @rest = rest
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 = @rest.request("list queues", "GET", "/api/queues", query: all ? {"all" => "1"} : nil)
19
+ body = @client.request('list queues', 'GET', '/api/queues', query: all ? {'all' => '1'} : nil)
19
20
 
20
- Array(body["queues"]).map { Queue.from_h(_1) }
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 = {"name" => name.to_s, "enabled" => enabled}
33
- body["maxLength"] = max_length unless max_length.nil?
33
+ body = {'name' => name.to_s, 'enabled' => enabled}
34
+ body['maxLength'] = max_length unless max_length.nil?
34
35
 
35
- Queue.from_h(@rest.request("upsert queue", "POST", "/api/queues", body: body).fetch("queue"))
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(", ")}" unless unknown.empty?
52
+ raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
52
53
 
53
54
  body = {}
54
- body["enabled"] = changes[:enabled] if changes.key?(:enabled)
55
- body["maxLength"] = changes[:max_length] if changes.key?(:max_length)
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(@rest.request("update queue", "PATCH", queue_path(id), body: body).fetch("queue"))
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
- @rest.request("delete queue", "DELETE", queue_path(id))["deleted"]
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 = @rest.request("list queue rules", "GET", "#{queue_path(id)}/rules")
74
+ body = @client.request('list queue rules', 'GET', "#{queue_path(id)}/rules")
74
75
 
75
- Array(body["rules"]).map { QueueRule.from_h(_1) }
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 = {"sourcePattern" => source_pattern.to_s}
87
- body["filter"] = filter unless filter.nil?
87
+ body = {'sourcePattern' => source_pattern.to_s}
88
+ body['filter'] = filter unless filter.nil?
88
89
 
89
- QueueRule.from_h(@rest.request("add queue rule", "POST", "#{queue_path(id)}/rules", body: body).fetch("rule"))
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
- @rest.request("delete queue rule", "DELETE", "#{queue_path(id)}/rules/#{Rest.escape(rule_id)}")
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, "a queue is addressed by its id" if id.to_s.empty?
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
@@ -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
- # rest = Blackevin::Rest.new(key: ENV.fetch("BLACKEVIN_KEY"))
8
+ # bk = Blackevin::Rest.new
9
9
  #
10
- # rest.auth.create_token_request(client_id: "bob")
11
- # rest.channels.get("room:42").publish("greeting", {text: "hi"})
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: 5, read_timeout: 10,
44
- transport: nil, clock: nil, nonce: nil, env: ENV)
45
- @key = key
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
- @auth = Auth.new(self, clock: clock, nonce: nonce)
51
- @channels = Channels.new(self)
52
- @clients = Clients.new(self)
53
- @queues = Queues.new(self)
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: or set BLACKEVIN_KEY' if @key.to_s.strip.empty?
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 = { 'accept' => 'application/json', 'user-agent' => USER_AGENT }
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
- { key_name: key_name, issued: issued, expires: expires, capability: capability, client_id: client_id }
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.rest}, resolved per request
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, { 'error' => 'method not allowed' }, 'allow' => ALLOWED_METHODS.join(', '))
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, { 'error' => 'unauthorized' })
52
+ respond(401, {'error' => 'unauthorized'})
52
53
  in Hash => params
53
- respond(200, (@rest || Blackevin.rest).auth.create_token_request(**params.transform_keys(&:to_sym)).to_h)
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.rest.auth.create_token_request(client_id: current_user.id)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Blackevin
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
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
- # Blackevin.configure { |config| config.key = ENV.fetch("BLACKEVIN_KEY") }
24
+ # bk = Blackevin::Rest.new
23
25
  #
24
- # Blackevin.rest.channels.get("room:42").publish("greeting", {text: "hi"})
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
- # The process-wide client, built from {configuration} on first use.
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 do
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.1.1
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