unleash 4.0.0 → 4.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/pull_request.yml +4 -2
- data/.rubocop.yml +4 -6
- data/README.md +171 -20
- data/examples/bootstrap.rb +51 -0
- data/examples/default-toggles.json +42 -0
- data/lib/unleash/bootstrap/configuration.rb +25 -0
- data/lib/unleash/bootstrap/handler.rb +22 -0
- data/lib/unleash/bootstrap/provider/base.rb +14 -0
- data/lib/unleash/bootstrap/provider/from_file.rb +14 -0
- data/lib/unleash/bootstrap/provider/from_url.rb +19 -0
- data/lib/unleash/client.rb +9 -9
- data/lib/unleash/configuration.rb +10 -3
- data/lib/unleash/constraint.rb +88 -10
- data/lib/unleash/context.rb +2 -1
- data/lib/unleash/feature_toggle.rb +9 -7
- data/lib/unleash/scheduled_executor.rb +5 -2
- data/lib/unleash/strategy/remote_address.rb +17 -1
- data/lib/unleash/toggle_fetcher.rb +29 -10
- data/lib/unleash/util/http.rb +6 -3
- data/lib/unleash/version.rb +1 -1
- metadata +10 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c185b4a8d20c33f221a482a9f2b58fbf028c019513a89b528a491f738ec1308b
|
4
|
+
data.tar.gz: 8283a887720d6c8f9a72719a8ff4ff800fd88f3941d5f7d5cea83e4ea079a414
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 614df5b819537826bc575a1b6b061ca17561d2108c12e9b32581e458f37ca24beccc83046b1b59c9be0d2f2904087b92f6410f3d31bfa57b176228611a55d96e
|
7
|
+
data.tar.gz: 35528c4f4f5d39db50c13c7266ac620dec6cdb21154b6f0736dea2946e22b81270737d9085144894d7c1b96c858364ce44f5f948ba24f731e2f033ee0677d67c
|
@@ -15,7 +15,9 @@ jobs:
|
|
15
15
|
- ubuntu
|
16
16
|
- macos
|
17
17
|
ruby-version:
|
18
|
-
- jruby
|
18
|
+
- jruby-9.2
|
19
|
+
- jruby-9.3
|
20
|
+
- 3.1
|
19
21
|
- 3.0
|
20
22
|
- 2.7
|
21
23
|
- 2.6
|
@@ -31,7 +33,7 @@ jobs:
|
|
31
33
|
- name: Install dependencies
|
32
34
|
run: bundle install
|
33
35
|
- name: Download test cases
|
34
|
-
run: git clone --depth 5 --branch v4.
|
36
|
+
run: git clone --depth 5 --branch v4.1.0 https://github.com/Unleash/client-specification.git client-specification
|
35
37
|
- name: rubocop
|
36
38
|
uses: reviewdog/action-rubocop@v2
|
37
39
|
with:
|
data/.rubocop.yml
CHANGED
@@ -7,8 +7,9 @@ Naming/PredicateName:
|
|
7
7
|
AllowedMethods:
|
8
8
|
- is_enabled?
|
9
9
|
|
10
|
+
|
10
11
|
Metrics/ClassLength:
|
11
|
-
Max:
|
12
|
+
Max: 125
|
12
13
|
Layout/LineLength:
|
13
14
|
Max: 140
|
14
15
|
Metrics/MethodLength:
|
@@ -16,13 +17,10 @@ Metrics/MethodLength:
|
|
16
17
|
Metrics/BlockLength:
|
17
18
|
Max: 110
|
18
19
|
Exclude:
|
19
|
-
- 'spec
|
20
|
-
- 'spec/unleash/client_spec.rb'
|
21
|
-
- 'spec/unleash/context_spec.rb'
|
22
|
-
- 'spec/unleash/feature_toggle_spec.rb'
|
20
|
+
- 'spec/**/*.rb'
|
23
21
|
|
24
22
|
Metrics/AbcSize:
|
25
|
-
Max:
|
23
|
+
Max: 30
|
26
24
|
Metrics/CyclomaticComplexity:
|
27
25
|
Max: 9
|
28
26
|
Metrics/PerceivedComplexity:
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Unleash::Client
|
2
2
|
|
3
|
-
|
4
|
-
[![Coverage Status](https://coveralls.io/repos/github/Unleash/unleash-client-ruby/badge.svg?branch=
|
3
|
+
![Build Status](https://github.com/Unleash/unleash-client-ruby/actions/workflows/pull_request.yml/badge.svg?branch=main)
|
4
|
+
[![Coverage Status](https://coveralls.io/repos/github/Unleash/unleash-client-ruby/badge.svg?branch=main)](https://coveralls.io/github/Unleash/unleash-client-ruby?branch=main)
|
5
5
|
[![Gem Version](https://badge.fury.io/rb/unleash.svg)](https://badge.fury.io/rb/unleash)
|
6
6
|
|
7
7
|
Unleash client so you can roll out your features with confidence.
|
@@ -10,11 +10,13 @@ Leverage the [Unleash Server](https://github.com/Unleash/unleash) for powerful f
|
|
10
10
|
|
11
11
|
## Supported Ruby Interpreters
|
12
12
|
|
13
|
+
* MRI 3.1
|
13
14
|
* MRI 3.0
|
14
15
|
* MRI 2.7
|
15
16
|
* MRI 2.6
|
16
17
|
* MRI 2.5
|
17
|
-
* jruby
|
18
|
+
* jruby 9.3
|
19
|
+
* jruby 9.2
|
18
20
|
|
19
21
|
## Installation
|
20
22
|
|
@@ -69,18 +71,23 @@ Argument | Description | Required? | Type | Default Value|
|
|
69
71
|
`environment` | Environment the program is running on. Could be for example `prod` or `dev`. Not yet in use. | N | String | `default` |
|
70
72
|
`project_name` | Name of the project to retrieve features from. If not set, all feature flags will be retrieved. | N | String | nil |
|
71
73
|
`refresh_interval` | How often the unleash client should check with the server for configuration changes. | N | Integer | 15 |
|
72
|
-
`metrics_interval` | How often the unleash client should send metrics to server. | N | Integer |
|
74
|
+
`metrics_interval` | How often the unleash client should send metrics to server. | N | Integer | 60 |
|
73
75
|
`disable_client` | Disables all communication with the Unleash server, effectively taking it *offline*. If set, `is_enabled?` will always answer with the `default_value` and configuration validation is skipped. Defeats the entire purpose of using unleash, but can be useful in when running tests. | N | Boolean | `false` |
|
74
76
|
`disable_metrics` | Disables sending metrics to Unleash server. | N | Boolean | `false` |
|
75
77
|
`custom_http_headers` | Custom headers to send to Unleash. As of Unleash v4.0.0, the `Authorization` header is required. For example: `{'Authorization': '<API token>'}` | N | Hash | {} |
|
76
78
|
`timeout` | How long to wait for the connection to be established or wait in reading state (open_timeout/read_timeout) | N | Integer | 30 |
|
77
|
-
`retry_limit` | How many consecutive failures in connecting to the Unleash server are allowed before giving up.
|
79
|
+
`retry_limit` | How many consecutive failures in connecting to the Unleash server are allowed before giving up. The default is to retry indefinitely. | N | Float::INFINITY | 5 |
|
78
80
|
`backup_file` | Filename to store the last known state from the Unleash server. Best to not change this from the default. | N | String | `Dir.tmpdir + "/unleash-#{app_name}-repo.json` |
|
79
81
|
`logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` |
|
80
82
|
`log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::WARN` |
|
83
|
+
`bootstrap_config` | Bootstrap config on how to loaded data on start-up. This is useful for loading large states on startup without (or before) hitting the network. | N | Unleash::Bootstrap::Configuration | `nil` |
|
81
84
|
|
82
|
-
For
|
85
|
+
For a more in-depth look, please see `lib/unleash/configuration.rb`.
|
83
86
|
|
87
|
+
Environment Variable | Description
|
88
|
+
---------|---------
|
89
|
+
`UNLEASH_BOOTSTRAP_FILE` | File to read bootstrap data from
|
90
|
+
`UNLEASH_BOOTSTRAP_URL` | URL to read bootstrap data from
|
84
91
|
|
85
92
|
## Usage in a plain Ruby Application
|
86
93
|
|
@@ -88,7 +95,7 @@ For in a more in depth look, please see `lib/unleash/configuration.rb`.
|
|
88
95
|
require 'unleash'
|
89
96
|
require 'unleash/context'
|
90
97
|
|
91
|
-
@unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: 'http://unleash.herokuapp.com/api', custom_http_headers: {'Authorization': '<API token>'})
|
98
|
+
@unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: 'http://unleash.herokuapp.com/api', custom_http_headers: { 'Authorization': '<API token>' })
|
92
99
|
|
93
100
|
feature_name = "AwesomeFeature"
|
94
101
|
unleash_context = Unleash::Context.new
|
@@ -117,29 +124,97 @@ Unleash.configure do |config|
|
|
117
124
|
end
|
118
125
|
|
119
126
|
UNLEASH = Unleash::Client.new
|
127
|
+
|
128
|
+
# Or if preferred:
|
129
|
+
# Rails.configuration.unleash = Unleash::Client.new
|
130
|
+
```
|
131
|
+
For `config.instance_id` use a string with a unique identification for the running instance.
|
132
|
+
For example: it could be the hostname, if you only run one App per host.
|
133
|
+
Or the docker container id, if you are running in docker.
|
134
|
+
If it is not set the client will generate an unique UUID for each execution.
|
135
|
+
|
136
|
+
To have it available in the `rails console` command as well, also add to the file above:
|
137
|
+
```ruby
|
138
|
+
Rails.application.console do
|
139
|
+
UNLEASH = Unleash::Client.new
|
140
|
+
# or
|
141
|
+
# Rails.configuration.unleash = Unleash::Client.new
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
#### Add Initializer if using [Puma in clustered mode](https://github.com/puma/puma#clustered-mode)
|
146
|
+
|
147
|
+
That is, multiple workers configured in `puma.rb`:
|
148
|
+
```ruby
|
149
|
+
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
|
120
150
|
```
|
121
|
-
For `config.instance_id` use a string with a unique identification for the running instance. For example: it could be the hostname, if you only run one App per host. Or the docker container id, if you are running in docker. If it is not set the client will generate an unique UUID for each execution.
|
122
151
|
|
152
|
+
##### with `preload_app!`
|
123
153
|
|
124
|
-
|
154
|
+
Then you may keep the client configuration still in `config/initializers/unleash.rb`:
|
155
|
+
```ruby
|
156
|
+
Unleash.configure do |config|
|
157
|
+
config.app_name = Rails.application.class.parent.to_s
|
158
|
+
config.environment = Rails.env
|
159
|
+
config.url = 'http://unleash.herokuapp.com/api'
|
160
|
+
config.custom_http_headers = {'Authorization': '<API token>'}
|
161
|
+
end
|
162
|
+
```
|
125
163
|
|
126
|
-
|
164
|
+
But you must ensure that the unleash client is instantiated only after the process is forked.
|
165
|
+
This is done by creating the client inside the `on_worker_boot` code block in `puma.rb` as below:
|
127
166
|
|
128
167
|
```ruby
|
168
|
+
#...
|
169
|
+
preload_app!
|
170
|
+
#...
|
171
|
+
|
129
172
|
on_worker_boot do
|
130
173
|
# ...
|
131
174
|
|
132
|
-
Unleash.
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
175
|
+
::UNLEASH = Unleash::Client.new
|
176
|
+
end
|
177
|
+
|
178
|
+
on_worker_shutdown do
|
179
|
+
::UNLEASH.shutdown
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
183
|
+
##### without `preload_app!`
|
184
|
+
|
185
|
+
By not using `preload_app!`:
|
186
|
+
- the `Rails` constant will NOT be available.
|
187
|
+
- but phased restarts will be possible.
|
188
|
+
|
189
|
+
You need to ensure that in `puma.rb`:
|
190
|
+
- loading unleash sdk with `require 'unleash'` explicitly, as it will not be pre-loaded.
|
191
|
+
- all parameters must be explicitly set in the `on_worker_boot` block, as `config/initializers/unleash.rb` is not read.
|
192
|
+
- there are no references to `Rails` constant, as that is not yet available.
|
193
|
+
|
194
|
+
Example for `puma.rb`:
|
195
|
+
```ruby
|
196
|
+
require 'unleash'
|
197
|
+
|
198
|
+
#...
|
199
|
+
# no preload_app!
|
200
|
+
|
201
|
+
on_worker_boot do
|
202
|
+
# ...
|
203
|
+
|
204
|
+
::UNLEASH = Unleash::Client.new(
|
205
|
+
app_name: 'my_rails_app',
|
206
|
+
environment: 'development',
|
207
|
+
url: 'http://unleash.herokuapp.com/api',
|
208
|
+
custom_http_headers: {'Authorization': '<API token>'},
|
209
|
+
)
|
210
|
+
end
|
211
|
+
|
212
|
+
on_worker_shutdown do
|
213
|
+
::UNLEASH.shutdown
|
139
214
|
end
|
140
215
|
```
|
141
216
|
|
142
|
-
|
217
|
+
Note that we also added shutdown hooks in `on_worker_shutdown`, to ensure a clean shutdown.
|
143
218
|
|
144
219
|
#### Add Initializer if using [Phusion Passenger](https://github.com/phusion/passenger)
|
145
220
|
|
@@ -164,6 +239,23 @@ PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
|
164
239
|
end
|
165
240
|
```
|
166
241
|
|
242
|
+
#### Add Initializer hooks when using within [Sidekiq](https://github.com/mperham/sidekiq)
|
243
|
+
|
244
|
+
Note that in this case we require that the code block for `Unleash.configure` is set beforehand.
|
245
|
+
For example in `config/initializers/unleash.rb`.
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
Sidekiq.configure_server do |config|
|
249
|
+
config.on(:startup) do
|
250
|
+
UNLEASH = Unleash::Client.new
|
251
|
+
end
|
252
|
+
|
253
|
+
config.on(:shutdown) do
|
254
|
+
UNLEASH.shutdown
|
255
|
+
end
|
256
|
+
end
|
257
|
+
```
|
258
|
+
|
167
259
|
#### Set Unleash::Context
|
168
260
|
|
169
261
|
Be sure to add the following method and callback in the application controller to have `@unleash_context` set for all requests:
|
@@ -203,7 +295,8 @@ if Rails.configuration.unleash.is_enabled? "AwesomeFeature", @unleash_context
|
|
203
295
|
end
|
204
296
|
```
|
205
297
|
|
206
|
-
If the feature is not found in the server, it will by default return false.
|
298
|
+
If the feature is not found in the server, it will by default return false.
|
299
|
+
However you can override that by setting the default return value to `true`:
|
207
300
|
|
208
301
|
```ruby
|
209
302
|
if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context, true
|
@@ -266,6 +359,65 @@ variant = UNLEASH.get_variant "ColorVariants", @unleash_context, fallback_varian
|
|
266
359
|
puts "variant color is: #{variant.payload.fetch('color')}"
|
267
360
|
```
|
268
361
|
|
362
|
+
## Bootstrapping
|
363
|
+
|
364
|
+
Bootstrap configuration allows the client to be initialized with a predefined set of toggle states.
|
365
|
+
Bootstrapping can be configured by providing a bootstrap configuration when initializing the client.
|
366
|
+
```ruby
|
367
|
+
@unleash = Unleash::Client.new(
|
368
|
+
url: 'http://unleash.herokuapp.com/api',
|
369
|
+
app_name: 'my_ruby_app',
|
370
|
+
custom_http_headers: { 'Authorization': '<API token>' },
|
371
|
+
bootstrap_config: Unleash::Bootstrap::Configuration.new({
|
372
|
+
url: "http://unleash.herokuapp.com/api/client/features",
|
373
|
+
url_headers: {'Authorization': '<API token>'}
|
374
|
+
})
|
375
|
+
)
|
376
|
+
```
|
377
|
+
The `Bootstrap::Configuration` initializer takes a hash with one of the following options specified:
|
378
|
+
|
379
|
+
* `file_path` - An absolute or relative path to a file containing a JSON string of the response body from the Unleash server. This can also be set though the `UNLEASH_BOOTSTRAP_FILE` environment variable.
|
380
|
+
* `url` - A url pointing to an Unleash server's features endpoint, the code sample above is illustrative. This can also be set though the `UNLEASH_BOOTSTRAP_URL` environment variable.
|
381
|
+
* `url_headers` - Headers for the GET http request to the `url` above. Only used if the `url` parameter is also set. If this option isn't set then the bootstrapper will use the same url headers as the Unleash client.
|
382
|
+
* `data` - A raw JSON string as returned by the Unleash server.
|
383
|
+
* `block` - A lambda containing custom logic if you need it, an example is provided below.
|
384
|
+
|
385
|
+
You should only specify one type of bootstrapping since only one will be invoked and the others will be ignored.
|
386
|
+
The order of preference is as follows:
|
387
|
+
|
388
|
+
- Select a data bootstrapper if it exists.
|
389
|
+
- If no data bootstrapper exists, select the block bootstrapper.
|
390
|
+
- If no block bootstrapper exists, select the file bootstrapper from either parameters or the specified environment variable.
|
391
|
+
- If no file bootstrapper exists, then check for a URL bootstrapper from either the parameters or the specified environment variable.
|
392
|
+
|
393
|
+
|
394
|
+
Example usage:
|
395
|
+
|
396
|
+
First saving the toggles locally:
|
397
|
+
```shell
|
398
|
+
curl -H 'Authorization: <API token>' -XGET 'http://unleash.herokuapp.com/api' > ./default-toggles.json
|
399
|
+
```
|
400
|
+
|
401
|
+
Now using them on start up:
|
402
|
+
|
403
|
+
```ruby
|
404
|
+
|
405
|
+
custom_boostrapper = lambda {
|
406
|
+
File.read('./default-toggles.json')
|
407
|
+
}
|
408
|
+
|
409
|
+
@unleash = Unleash::Client.new(
|
410
|
+
app_name: 'my_ruby_app',
|
411
|
+
url: 'http://unleash.herokuapp.com/api',
|
412
|
+
custom_http_headers: { 'Authorization': '<API token>' },
|
413
|
+
bootstrap_config: Unleash::Bootstrap::Configuration.new({
|
414
|
+
block: custom_boostrapper
|
415
|
+
})
|
416
|
+
)
|
417
|
+
```
|
418
|
+
|
419
|
+
This example could be easily achieved with a file bootstrapper, this is just to illustrate the usage of custom bootstrapping.
|
420
|
+
Be aware that the client initializer will block until bootstrapping is complete.
|
269
421
|
|
270
422
|
#### Client methods
|
271
423
|
|
@@ -304,7 +456,6 @@ This client comes with the all the required strategies out of the box:
|
|
304
456
|
* UnknownStrategy
|
305
457
|
* UserWithIdStrategy
|
306
458
|
|
307
|
-
|
308
459
|
## Development
|
309
460
|
|
310
461
|
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.
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'unleash'
|
4
|
+
require 'unleash/context'
|
5
|
+
require 'unleash/bootstrap/configuration'
|
6
|
+
|
7
|
+
puts ">> START bootstrap.rb"
|
8
|
+
|
9
|
+
@unleash = Unleash::Client.new(
|
10
|
+
url: 'http://unleash.herokuapp.com/api',
|
11
|
+
custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
|
12
|
+
app_name: 'bootstrap-test',
|
13
|
+
instance_id: 'local-test-cli',
|
14
|
+
refresh_interval: 2,
|
15
|
+
disable_client: true,
|
16
|
+
disable_metrics: true,
|
17
|
+
metrics_interval: 2,
|
18
|
+
retry_limit: 2,
|
19
|
+
bootstrap_config: Unleash::Bootstrap::Configuration.new(file_path: "examples/default-toggles.json")
|
20
|
+
)
|
21
|
+
|
22
|
+
feature_name = "featureX"
|
23
|
+
unleash_context = Unleash::Context.new
|
24
|
+
unleash_context.user_id = 123
|
25
|
+
|
26
|
+
sleep 1
|
27
|
+
3.times do
|
28
|
+
if @unleash.is_enabled?(feature_name, unleash_context)
|
29
|
+
puts "> #{feature_name} is enabled"
|
30
|
+
else
|
31
|
+
puts "> #{feature_name} is not enabled"
|
32
|
+
end
|
33
|
+
sleep 1
|
34
|
+
puts "---"
|
35
|
+
puts ""
|
36
|
+
puts ""
|
37
|
+
end
|
38
|
+
|
39
|
+
sleep 3
|
40
|
+
feature_name = "foobar"
|
41
|
+
if @unleash.is_enabled?(feature_name, unleash_context, true)
|
42
|
+
puts "> #{feature_name} is enabled"
|
43
|
+
else
|
44
|
+
puts "> #{feature_name} is not enabled"
|
45
|
+
end
|
46
|
+
|
47
|
+
puts "> shutting down client..."
|
48
|
+
|
49
|
+
@unleash.shutdown
|
50
|
+
|
51
|
+
puts ">> END bootstrap.rb"
|
@@ -0,0 +1,42 @@
|
|
1
|
+
{
|
2
|
+
"version": 1,
|
3
|
+
"features": [
|
4
|
+
{
|
5
|
+
"name": "featureX",
|
6
|
+
"enabled": true,
|
7
|
+
"strategies": [
|
8
|
+
{
|
9
|
+
"name": "default"
|
10
|
+
}
|
11
|
+
]
|
12
|
+
},
|
13
|
+
{
|
14
|
+
"name": "featureY",
|
15
|
+
"enabled": false,
|
16
|
+
"strategies": [
|
17
|
+
{
|
18
|
+
"name": "baz",
|
19
|
+
"parameters": {
|
20
|
+
"foo": "bar"
|
21
|
+
}
|
22
|
+
}
|
23
|
+
]
|
24
|
+
},
|
25
|
+
{
|
26
|
+
"name": "featureZ",
|
27
|
+
"enabled": true,
|
28
|
+
"strategies": [
|
29
|
+
{
|
30
|
+
"name": "default"
|
31
|
+
},
|
32
|
+
{
|
33
|
+
"name": "hola",
|
34
|
+
"parameters": {
|
35
|
+
"name": "val"
|
36
|
+
}
|
37
|
+
}
|
38
|
+
]
|
39
|
+
}
|
40
|
+
]
|
41
|
+
}
|
42
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Unleash
|
2
|
+
module Bootstrap
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :data, :file_path, :url, :url_headers, :block
|
5
|
+
|
6
|
+
def initialize(opts = {})
|
7
|
+
self.file_path = resolve_value_indifferently(opts, 'file_path') || ENV['UNLEASH_BOOTSTRAP_FILE'] || nil
|
8
|
+
self.url = resolve_value_indifferently(opts, 'url') || ENV['UNLEASH_BOOTSTRAP_URL'] || nil
|
9
|
+
self.url_headers = resolve_value_indifferently(opts, 'url_headers')
|
10
|
+
self.data = resolve_value_indifferently(opts, 'data')
|
11
|
+
self.block = resolve_value_indifferently(opts, 'block')
|
12
|
+
end
|
13
|
+
|
14
|
+
def valid?
|
15
|
+
![self.data, self.file_path, self.url, self.block].all?(&:nil?)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def resolve_value_indifferently(opts, key)
|
21
|
+
opts[key] || opts[key.to_sym]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'unleash/bootstrap/provider/from_url'
|
2
|
+
require 'unleash/bootstrap/provider/from_file'
|
3
|
+
|
4
|
+
module Unleash
|
5
|
+
module Bootstrap
|
6
|
+
class Handler
|
7
|
+
attr_accessor :configuration
|
8
|
+
|
9
|
+
def initialize(configuration)
|
10
|
+
self.configuration = configuration
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [String] JSON string representing data returned from an Unleash server
|
14
|
+
def retrieve_toggles
|
15
|
+
return configuration.data unless self.configuration.data.nil?
|
16
|
+
return configuration.block.call if self.configuration.block.is_a?(Proc)
|
17
|
+
return Provider::FromFile.read(configuration.file_path) unless self.configuration.file_path.nil?
|
18
|
+
return Provider::FromUrl.read(configuration.url, configuration.url_headers) unless self.configuration.url.nil?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'unleash/bootstrap/provider/base'
|
2
|
+
|
3
|
+
module Unleash
|
4
|
+
module Bootstrap
|
5
|
+
module Provider
|
6
|
+
class FromUrl < Base
|
7
|
+
# @param url [String]
|
8
|
+
# @param headers [Hash, nil] HTTP headers to use. If not set, the unleash client SDK ones will be used.
|
9
|
+
def self.read(url, headers = nil)
|
10
|
+
response = Unleash::Util::Http.get(URI.parse(url), nil, headers)
|
11
|
+
|
12
|
+
return nil if response.code != '200'
|
13
|
+
|
14
|
+
response.body
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/unleash/client.rb
CHANGED
@@ -18,8 +18,9 @@ module Unleash
|
|
18
18
|
Unleash.logger = Unleash.configuration.logger.clone
|
19
19
|
Unleash.logger.level = Unleash.configuration.log_level
|
20
20
|
|
21
|
+
Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
|
21
22
|
if Unleash.configuration.disable_client
|
22
|
-
Unleash.logger.warn "Unleash::Client is disabled! Will only return default results!"
|
23
|
+
Unleash.logger.warn "Unleash::Client is disabled! Will only return default (or bootstrapped if available) results!"
|
23
24
|
return
|
24
25
|
end
|
25
26
|
|
@@ -37,11 +38,6 @@ module Unleash
|
|
37
38
|
default_value_param
|
38
39
|
end
|
39
40
|
|
40
|
-
if Unleash.configuration.disable_client
|
41
|
-
Unleash.logger.warn "unleash_client is disabled! Always returning #{default_value} for feature #{feature}!"
|
42
|
-
return default_value
|
43
|
-
end
|
44
|
-
|
45
41
|
toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
|
46
42
|
|
47
43
|
if toggle_as_hash.nil?
|
@@ -51,7 +47,7 @@ module Unleash
|
|
51
47
|
|
52
48
|
toggle = Unleash::FeatureToggle.new(toggle_as_hash)
|
53
49
|
|
54
|
-
toggle.is_enabled?(context
|
50
|
+
toggle.is_enabled?(context)
|
55
51
|
end
|
56
52
|
|
57
53
|
# enabled? is a more ruby idiomatic method name than is_enabled?
|
@@ -121,11 +117,11 @@ module Unleash
|
|
121
117
|
end
|
122
118
|
|
123
119
|
def start_toggle_fetcher
|
124
|
-
Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
|
125
120
|
self.fetcher_scheduled_executor = Unleash::ScheduledExecutor.new(
|
126
121
|
'ToggleFetcher',
|
127
122
|
Unleash.configuration.refresh_interval,
|
128
|
-
Unleash.configuration.retry_limit
|
123
|
+
Unleash.configuration.retry_limit,
|
124
|
+
first_fetch_is_eager
|
129
125
|
)
|
130
126
|
self.fetcher_scheduled_executor.run do
|
131
127
|
Unleash.toggle_fetcher.fetch
|
@@ -161,5 +157,9 @@ module Unleash
|
|
161
157
|
def disabled_variant
|
162
158
|
@disabled_variant ||= Unleash::FeatureToggle.disabled_variant
|
163
159
|
end
|
160
|
+
|
161
|
+
def first_fetch_is_eager
|
162
|
+
Unleash.configuration.use_bootstrap?
|
163
|
+
end
|
164
164
|
end
|
165
165
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'securerandom'
|
2
2
|
require 'tmpdir'
|
3
|
+
require 'unleash/bootstrap/configuration'
|
3
4
|
|
4
5
|
module Unleash
|
5
6
|
class Configuration
|
@@ -18,7 +19,8 @@ module Unleash
|
|
18
19
|
:metrics_interval,
|
19
20
|
:backup_file,
|
20
21
|
:logger,
|
21
|
-
:log_level
|
22
|
+
:log_level,
|
23
|
+
:bootstrap_config
|
22
24
|
|
23
25
|
def initialize(opts = {})
|
24
26
|
ensure_valid_opts(opts)
|
@@ -70,6 +72,10 @@ module Unleash
|
|
70
72
|
self.url.delete_suffix '/'
|
71
73
|
end
|
72
74
|
|
75
|
+
def use_bootstrap?
|
76
|
+
self.bootstrap_config&.valid?
|
77
|
+
end
|
78
|
+
|
73
79
|
private
|
74
80
|
|
75
81
|
def ensure_valid_opts(opts)
|
@@ -87,11 +93,12 @@ module Unleash
|
|
87
93
|
self.disable_client = false
|
88
94
|
self.disable_metrics = false
|
89
95
|
self.refresh_interval = 10
|
90
|
-
self.metrics_interval =
|
96
|
+
self.metrics_interval = 60
|
91
97
|
self.timeout = 30
|
92
|
-
self.retry_limit =
|
98
|
+
self.retry_limit = Float::INFINITY
|
93
99
|
self.backup_file = nil
|
94
100
|
self.log_level = Logger::WARN
|
101
|
+
self.bootstrap_config = nil
|
95
102
|
|
96
103
|
self.custom_http_headers = {}
|
97
104
|
end
|
data/lib/unleash/constraint.rb
CHANGED
@@ -1,26 +1,104 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
1
3
|
module Unleash
|
2
4
|
class Constraint
|
3
|
-
attr_accessor :context_name, :operator, :
|
5
|
+
attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
|
6
|
+
|
7
|
+
OPERATORS = {
|
8
|
+
IN: ->(context_v, constraint_v){ constraint_v.include? context_v },
|
9
|
+
NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v },
|
10
|
+
STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
|
11
|
+
STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
|
12
|
+
STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
|
13
|
+
NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } },
|
14
|
+
NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } },
|
15
|
+
NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } },
|
16
|
+
NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } },
|
17
|
+
NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } },
|
18
|
+
DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } },
|
19
|
+
DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
|
20
|
+
SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
|
21
|
+
SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } },
|
22
|
+
SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } }
|
23
|
+
}.freeze
|
4
24
|
|
5
|
-
|
25
|
+
LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
|
6
26
|
|
7
|
-
def initialize(context_name, operator,
|
27
|
+
def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
|
8
28
|
raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
|
9
|
-
raise ArgumentError, "operator does not hold a valid value:" +
|
10
|
-
|
29
|
+
raise ArgumentError, "operator does not hold a valid value:" + OPERATORS.keys unless OPERATORS.include? operator.to_sym
|
30
|
+
|
31
|
+
self.validate_constraint_value_type(operator.to_sym, value)
|
11
32
|
|
12
33
|
self.context_name = context_name
|
13
|
-
self.operator = operator
|
14
|
-
self.
|
34
|
+
self.operator = operator.to_sym
|
35
|
+
self.value = value
|
36
|
+
self.inverted = !!inverted
|
37
|
+
self.case_insensitive = !!case_insensitive
|
15
38
|
end
|
16
39
|
|
17
40
|
def matches_context?(context)
|
18
|
-
Unleash.logger.debug "Unleash::Constraint matches_context?
|
41
|
+
Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" \
|
19
42
|
" #{context.get_by_name(self.context_name)} "
|
43
|
+
match = matches_constraint?(context)
|
44
|
+
self.inverted ? !match : match
|
45
|
+
rescue KeyError
|
46
|
+
Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
|
47
|
+
found on the context"
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.on_valid_date(val1, val2)
|
52
|
+
val1 = DateTime.parse(val1)
|
53
|
+
val2 = DateTime.parse(val2)
|
54
|
+
yield(val1, val2)
|
55
|
+
rescue ArgumentError
|
56
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
57
|
+
or constraint_value (#{val2}) into a date. Returning false!"
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.on_valid_float(val1, val2)
|
62
|
+
val1 = Float(val1)
|
63
|
+
val2 = Float(val2)
|
64
|
+
yield(val1, val2)
|
65
|
+
rescue ArgumentError
|
66
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
67
|
+
or constraint_value (#{val2}) into a number. Returning false!"
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.on_valid_version(val1, val2)
|
72
|
+
val1 = Gem::Version.new(val1)
|
73
|
+
val2 = Gem::Version.new(val2)
|
74
|
+
yield(val1, val2)
|
75
|
+
rescue ArgumentError
|
76
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
77
|
+
or constraint_value (#{val2}) into a version. Return false!"
|
78
|
+
false
|
79
|
+
end
|
80
|
+
|
81
|
+
# This should be a private method but for some reason this fails on Ruby 2.5
|
82
|
+
def validate_constraint_value_type(operator, value)
|
83
|
+
raise ArgumentError, "context_name is not an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
|
84
|
+
raise ArgumentError, "context_name is not a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def matches_constraint?(context)
|
90
|
+
unless OPERATORS.include?(self.operator)
|
91
|
+
Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
|
92
|
+
false
|
93
|
+
end
|
94
|
+
|
95
|
+
v = self.value.dup
|
96
|
+
context_value = context.get_by_name(self.context_name)
|
20
97
|
|
21
|
-
|
98
|
+
v.map!(&:upcase) if self.case_insensitive
|
99
|
+
context_value.upcase! if self.case_insensitive
|
22
100
|
|
23
|
-
operator
|
101
|
+
OPERATORS[self.operator].call(context_value, v)
|
24
102
|
end
|
25
103
|
end
|
26
104
|
end
|
data/lib/unleash/context.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Unleash
|
2
2
|
class Context
|
3
|
-
ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address].freeze
|
3
|
+
ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address, :current_time].freeze
|
4
4
|
|
5
5
|
attr_accessor(*[ATTRS, :properties].flatten)
|
6
6
|
|
@@ -12,6 +12,7 @@ module Unleash
|
|
12
12
|
self.user_id = value_for('userId', params)
|
13
13
|
self.session_id = value_for('sessionId', params)
|
14
14
|
self.remote_address = value_for('remoteAddress', params)
|
15
|
+
self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
|
15
16
|
|
16
17
|
properties = value_for('properties', params)
|
17
18
|
self.properties = properties.is_a?(Hash) ? properties.transform_keys(&:to_sym) : {}
|
@@ -23,8 +23,8 @@ module Unleash
|
|
23
23
|
"<FeatureToggle: name=#{name},enabled=#{enabled},strategies=#{strategies},variant_definitions=#{variant_definitions}>"
|
24
24
|
end
|
25
25
|
|
26
|
-
def is_enabled?(context
|
27
|
-
result = am_enabled?(context
|
26
|
+
def is_enabled?(context)
|
27
|
+
result = am_enabled?(context)
|
28
28
|
|
29
29
|
choice = result ? :yes : :no
|
30
30
|
Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
|
@@ -37,7 +37,7 @@ module Unleash
|
|
37
37
|
|
38
38
|
context = ensure_valid_context(context)
|
39
39
|
|
40
|
-
return Unleash::FeatureToggle.disabled_variant unless self.enabled && am_enabled?(context
|
40
|
+
return Unleash::FeatureToggle.disabled_variant unless self.enabled && am_enabled?(context)
|
41
41
|
return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
|
42
42
|
|
43
43
|
variant = variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
|
@@ -57,7 +57,7 @@ module Unleash
|
|
57
57
|
end
|
58
58
|
|
59
59
|
# only check if it is enabled, do not do metrics
|
60
|
-
def am_enabled?(context
|
60
|
+
def am_enabled?(context)
|
61
61
|
result =
|
62
62
|
if self.enabled
|
63
63
|
self.strategies.empty? ||
|
@@ -65,10 +65,10 @@ module Unleash
|
|
65
65
|
strategy_enabled?(s, context) && strategy_constraint_matches?(s, context)
|
66
66
|
end
|
67
67
|
else
|
68
|
-
|
68
|
+
false
|
69
69
|
end
|
70
70
|
|
71
|
-
Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled}
|
71
|
+
Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \
|
72
72
|
"and Strategies combined with contraints returned #{result})"
|
73
73
|
|
74
74
|
result
|
@@ -139,7 +139,9 @@ module Unleash
|
|
139
139
|
Constraint.new(
|
140
140
|
c.fetch('contextName'),
|
141
141
|
c.fetch('operator'),
|
142
|
-
c.fetch('values')
|
142
|
+
c.fetch('values', nil) || c.fetch('value', nil),
|
143
|
+
inverted: c.fetch('inverted', false),
|
144
|
+
case_insensitive: c.fetch('caseInsensitive', false)
|
143
145
|
)
|
144
146
|
end
|
145
147
|
)
|
@@ -1,19 +1,22 @@
|
|
1
1
|
module Unleash
|
2
2
|
class ScheduledExecutor
|
3
|
-
attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread
|
3
|
+
attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread, :immediate_execution
|
4
4
|
|
5
|
-
def initialize(name, interval, max_exceptions = 5)
|
5
|
+
def initialize(name, interval, max_exceptions = 5, immediate_execution = false)
|
6
6
|
self.name = name || ''
|
7
7
|
self.interval = interval
|
8
8
|
self.max_exceptions = max_exceptions
|
9
9
|
self.retry_count = 0
|
10
10
|
self.thread = nil
|
11
|
+
self.immediate_execution = immediate_execution
|
11
12
|
end
|
12
13
|
|
13
14
|
def run(&blk)
|
14
15
|
self.thread = Thread.new do
|
15
16
|
Thread.current[:name] = self.name
|
16
17
|
|
18
|
+
run_blk{ blk.call } if self.immediate_execution
|
19
|
+
|
17
20
|
Unleash.logger.debug "thread #{name} loop starting"
|
18
21
|
loop do
|
19
22
|
Unleash.logger.debug "thread #{name} sleeping for #{interval} seconds"
|
@@ -13,7 +13,23 @@ module Unleash
|
|
13
13
|
return false unless params.fetch(PARAM, nil).is_a? String
|
14
14
|
return false unless context.class.name == 'Unleash::Context'
|
15
15
|
|
16
|
-
|
16
|
+
remote_address = ipaddr_or_nil_from_str(context.remote_address)
|
17
|
+
|
18
|
+
params[PARAM]
|
19
|
+
.split(',')
|
20
|
+
.map(&:strip)
|
21
|
+
.map{ |ipblock| ipaddr_or_nil_from_str(ipblock) }
|
22
|
+
.compact
|
23
|
+
.map{ |ipb| ipb.include? remote_address }
|
24
|
+
.any?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def ipaddr_or_nil_from_str(ip)
|
30
|
+
IPAddr.new(ip)
|
31
|
+
rescue StandardError
|
32
|
+
nil
|
17
33
|
end
|
18
34
|
end
|
19
35
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'unleash/configuration'
|
2
|
+
require 'unleash/bootstrap/handler'
|
2
3
|
require 'net/http'
|
3
4
|
require 'json'
|
4
5
|
|
@@ -13,10 +14,15 @@ module Unleash
|
|
13
14
|
self.toggle_resource = ConditionVariable.new
|
14
15
|
self.retry_count = 0
|
15
16
|
|
16
|
-
# start by fetching synchronously, and failing back to reading the backup file.
|
17
17
|
begin
|
18
|
-
|
18
|
+
# if bootstrap configuration is available, initialize. An immediate API read is also triggered
|
19
|
+
if Unleash.configuration.use_bootstrap?
|
20
|
+
bootstrap
|
21
|
+
else
|
22
|
+
fetch
|
23
|
+
end
|
19
24
|
rescue StandardError => e
|
25
|
+
# fail back to reading the backup file
|
20
26
|
Unleash.logger.warn "ToggleFetcher was unable to fetch from the network, attempting to read from backup file."
|
21
27
|
Unleash.logger.debug "Exception Caught: #{e}"
|
22
28
|
read!
|
@@ -36,6 +42,8 @@ module Unleash
|
|
36
42
|
# rename to refresh_from_server! ??
|
37
43
|
def fetch
|
38
44
|
Unleash.logger.debug "fetch()"
|
45
|
+
return if Unleash.configuration.disable_client
|
46
|
+
|
39
47
|
response = Unleash::Util::Http.get(Unleash.configuration.fetch_toggles_uri, etag)
|
40
48
|
|
41
49
|
if response.code == '304'
|
@@ -46,14 +54,7 @@ module Unleash
|
|
46
54
|
end
|
47
55
|
|
48
56
|
self.etag = response['ETag']
|
49
|
-
|
50
|
-
|
51
|
-
if response_hash['version'] >= 1
|
52
|
-
features = response_hash['features']
|
53
|
-
else
|
54
|
-
raise NotImplemented, "Version of features provided by unleash server" \
|
55
|
-
" is unsupported by this client."
|
56
|
-
end
|
57
|
+
features = get_features(response.body)
|
57
58
|
|
58
59
|
# always synchronize with the local cache when fetching:
|
59
60
|
synchronize_with_local_cache!(features)
|
@@ -126,5 +127,23 @@ module Unleash
|
|
126
127
|
file&.close
|
127
128
|
end
|
128
129
|
end
|
130
|
+
|
131
|
+
def bootstrap
|
132
|
+
bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles
|
133
|
+
synchronize_with_local_cache! get_features bootstrap_payload
|
134
|
+
update_running_client!
|
135
|
+
|
136
|
+
# reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again
|
137
|
+
Unleash.configuration.bootstrap_config = nil
|
138
|
+
end
|
139
|
+
|
140
|
+
# @param response_body [String]
|
141
|
+
def get_features(response_body)
|
142
|
+
response_hash = JSON.parse(response_body)
|
143
|
+
return response_hash['features'] if response_hash['version'] >= 1
|
144
|
+
|
145
|
+
raise NotImplemented, "Version of features provided by unleash server" \
|
146
|
+
" is unsupported by this client."
|
147
|
+
end
|
129
148
|
end
|
130
149
|
end
|
data/lib/unleash/util/http.rb
CHANGED
@@ -4,10 +4,10 @@ require 'uri'
|
|
4
4
|
module Unleash
|
5
5
|
module Util
|
6
6
|
module Http
|
7
|
-
def self.get(uri, etag = nil)
|
7
|
+
def self.get(uri, etag = nil, headers_override = nil)
|
8
8
|
http = http_connection(uri)
|
9
9
|
|
10
|
-
request = Net::HTTP::Get.new(uri.request_uri, http_headers(etag))
|
10
|
+
request = Net::HTTP::Get.new(uri.request_uri, http_headers(etag, headers_override))
|
11
11
|
|
12
12
|
http.request(request)
|
13
13
|
end
|
@@ -30,10 +30,13 @@ module Unleash
|
|
30
30
|
http
|
31
31
|
end
|
32
32
|
|
33
|
-
|
33
|
+
# @param etag [String, nil]
|
34
|
+
# @param headers_override [Hash, nil]
|
35
|
+
def self.http_headers(etag = nil, headers_override = nil)
|
34
36
|
Unleash.logger.debug "ETag: #{etag}" unless etag.nil?
|
35
37
|
|
36
38
|
headers = (Unleash.configuration.http_headers || {}).dup
|
39
|
+
headers = headers_override if headers_override.is_a?(Hash)
|
37
40
|
headers['Content-Type'] = 'application/json'
|
38
41
|
headers['If-None-Match'] = etag unless etag.nil?
|
39
42
|
|
data/lib/unleash/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unleash
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Renato Arruda
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-03-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: murmurhash3
|
@@ -157,9 +157,16 @@ files:
|
|
157
157
|
- bin/console
|
158
158
|
- bin/setup
|
159
159
|
- bin/unleash-client
|
160
|
+
- examples/bootstrap.rb
|
161
|
+
- examples/default-toggles.json
|
160
162
|
- examples/simple.rb
|
161
163
|
- lib/unleash.rb
|
162
164
|
- lib/unleash/activation_strategy.rb
|
165
|
+
- lib/unleash/bootstrap/configuration.rb
|
166
|
+
- lib/unleash/bootstrap/handler.rb
|
167
|
+
- lib/unleash/bootstrap/provider/base.rb
|
168
|
+
- lib/unleash/bootstrap/provider/from_file.rb
|
169
|
+
- lib/unleash/bootstrap/provider/from_url.rb
|
163
170
|
- lib/unleash/client.rb
|
164
171
|
- lib/unleash/configuration.rb
|
165
172
|
- lib/unleash/constraint.rb
|
@@ -204,7 +211,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
204
211
|
- !ruby/object:Gem::Version
|
205
212
|
version: '0'
|
206
213
|
requirements: []
|
207
|
-
rubygems_version: 3.
|
214
|
+
rubygems_version: 3.3.6
|
208
215
|
signing_key:
|
209
216
|
specification_version: 4
|
210
217
|
summary: Unleash feature toggle client.
|