unleash 4.2.0 → 4.4.2

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: 93e6312bc93458378b877b6e949274295a1e0037af2cf8206d94b9e2852727eb
4
- data.tar.gz: ecb08a7c1a4e9b0b63fad299649632443a99f7b8e4752b486970d7656b862e46
3
+ metadata.gz: e8461b30f4a58518d73731731a7dfbc32041a5a017362319b9d8ba76d2cecb98
4
+ data.tar.gz: '06098a3f106f50b0ddad46e17bef57584918ecbefe88486009d95edbdb546550'
5
5
  SHA512:
6
- metadata.gz: eef06ea5a86751e6cfe68306a1d7f7e6c49fb0641c4916ddbdda30b4c1bec51a1349487dc156851229c4d496fe67cac96f0e5c875027998d0252bf154f2e02c9
7
- data.tar.gz: 6fdf04c5e92b1de875ada2035b9fb495e80cf5138f4cd0171bd6686026a1280857c855ccdaa9bd15a915509f5bac4f9da50d08368f7de663c0a762b8654a3112
6
+ metadata.gz: ec0644f3bd93de7f75b06e44c059d39ff3ed87af00ee7ca17d1ad3a3e444626f190c4e26863167b116eda85ce0c01196542f8167f4494052fdae9b3e3e09e15c
7
+ data.tar.gz: 11764caf8ccd33c757cbd282a5e5f3d6d6b9dc7b70c29c0e9e196b512be3c1baee10abb21279358e7c7001a91b3825d5c42661910695e74e9e1585e2003c9b58
data/.github/stale.yml ADDED
@@ -0,0 +1 @@
1
+ _extends: .github
@@ -0,0 +1,14 @@
1
+ name: Add new item to project board
2
+
3
+ on:
4
+ issues:
5
+ types:
6
+ - opened
7
+ pull_request_target:
8
+ types:
9
+ - opened
10
+
11
+ jobs:
12
+ add-to-project:
13
+ uses: unleash/.github/.github/workflows/add-item-to-project.yml@main
14
+ secrets: inherit
@@ -5,8 +5,21 @@ on:
5
5
  pull_request:
6
6
 
7
7
  jobs:
8
- test:
8
+ lint:
9
+ name: RuboCop
10
+ timeout-minutes: 30
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v2
14
+ - name: Set up Ruby
15
+ uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: "2.7"
18
+ bundler-cache: true
19
+ - name: Run RuboCop
20
+ run: bundle exec rubocop
9
21
 
22
+ test:
10
23
  runs-on: ${{ matrix.os }}-latest
11
24
 
12
25
  strategy:
@@ -15,16 +28,18 @@ jobs:
15
28
  - ubuntu
16
29
  - macos
17
30
  ruby-version:
18
- - jruby-9.2
31
+ - jruby-9.4
19
32
  - jruby-9.3
33
+ - jruby-9.2
34
+ - 3.2
20
35
  - 3.1
21
- - 3.0
36
+ - '3.0'
22
37
  - 2.7
23
38
  - 2.6
24
39
  - 2.5
25
40
 
26
41
  steps:
27
- - uses: actions/checkout@v2
42
+ - uses: actions/checkout@v3
28
43
  - name: Set up Ruby ${{ matrix.ruby-version }}
29
44
  uses: ruby/setup-ruby@v1
30
45
  with:
@@ -33,14 +48,7 @@ jobs:
33
48
  - name: Install dependencies
34
49
  run: bundle install
35
50
  - name: Download test cases
36
- run: git clone --depth 5 --branch v4.1.0 https://github.com/Unleash/client-specification.git client-specification
37
- - name: rubocop
38
- uses: reviewdog/action-rubocop@v2
39
- with:
40
- github_token: ${{ secrets.GITHUB_TOKEN }}
41
- rubocop_version: gemfile
42
- rubocop_extensions: rubocop-rspec:gemfile
43
- reporter: github-pr-review # Default is github-pr-check
51
+ run: git clone --depth 5 --branch v4.2.2 https://github.com/Unleash/client-specification.git client-specification
44
52
  - name: Run tests
45
53
  run: bundle exec rake
46
54
  env:
data/.gitignore CHANGED
@@ -1,6 +1,5 @@
1
1
  /.bundle/
2
2
  /.yardoc
3
- /Gemfile.lock
4
3
  /_yardoc/
5
4
  /coverage/
6
5
  /doc/
@@ -14,6 +13,7 @@
14
13
 
15
14
  # rspec failure tracking
16
15
  .rspec_status
16
+ Gemfile.lock
17
17
 
18
18
  # Clone of the client-specification
19
19
  /client-specification/
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --format documentation
2
2
  --color
3
+ --require 'spec_helper'
data/.rubocop.yml CHANGED
@@ -6,10 +6,11 @@ AllCops:
6
6
  Naming/PredicateName:
7
7
  AllowedMethods:
8
8
  - is_enabled?
9
+ - is_disabled?
9
10
 
10
11
 
11
12
  Metrics/ClassLength:
12
- Max: 125
13
+ Max: 135
13
14
  Layout/LineLength:
14
15
  Max: 140
15
16
  Metrics/MethodLength:
@@ -22,7 +23,7 @@ Metrics/BlockLength:
22
23
  Metrics/AbcSize:
23
24
  Max: 30
24
25
  Metrics/CyclomaticComplexity:
25
- Max: 9
26
+ Max: 10
26
27
  Metrics/PerceivedComplexity:
27
28
  Max: 10
28
29
 
@@ -34,6 +35,9 @@ Style/StringLiterals:
34
35
  Style/RedundantSelf:
35
36
  Enabled: false
36
37
 
38
+ Style/OptionalBooleanParameter:
39
+ Enabled: false
40
+
37
41
  Style/SymbolArray:
38
42
  EnforcedStyle: brackets
39
43
  Style/WordArray:
data/README.md CHANGED
@@ -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.2
13
14
  * MRI 3.1
14
15
  * MRI 3.0
15
16
  * MRI 2.7
16
17
  * MRI 2.6
17
18
  * MRI 2.5
19
+ * jruby 9.4
18
20
  * jruby 9.3
19
21
  * jruby 9.2
20
22
 
@@ -23,7 +25,7 @@ Leverage the [Unleash Server](https://github.com/Unleash/unleash) for powerful f
23
25
  Add this line to your application's Gemfile:
24
26
 
25
27
  ```ruby
26
- gem 'unleash', '~> 4.0.0'
28
+ gem 'unleash', '~> 4.4.0'
27
29
  ```
28
30
 
29
31
  And then execute:
@@ -41,7 +43,7 @@ It is **required** to configure:
41
43
  - `app_name` with the name of the runninng application.
42
44
  - `custom_http_headers` with `{'Authorization': '<API token>'}` when using Unleash v4.0.0 and later.
43
45
 
44
- Please substitute the example `'http://unleash.herokuapp.com/api'` for the url of your own instance.
46
+ Please substitute the example `'https://unleash.herokuapp.com/api'` for the url of your own instance.
45
47
 
46
48
  It is **highly recommended** to configure:
47
49
  - `instance_id` parameter with a unique identifier for the running instance.
@@ -50,7 +52,7 @@ It is **highly recommended** to configure:
50
52
  ```ruby
51
53
  Unleash.configure do |config|
52
54
  config.app_name = 'my_ruby_app'
53
- config.url = 'http://unleash.herokuapp.com/api'
55
+ config.url = 'https://unleash.herokuapp.com/api'
54
56
  config.custom_http_headers = {'Authorization': '<API token>'}
55
57
  end
56
58
  ```
@@ -58,7 +60,23 @@ end
58
60
  or instantiate the client with the valid configuration:
59
61
 
60
62
  ```ruby
61
- UNLEASH = Unleash::Client.new(url: 'http://unleash.herokuapp.com/api', app_name: 'my_ruby_app', custom_http_headers: {'Authorization': '<API token>'})
63
+ UNLEASH = Unleash::Client.new(url: 'https://unleash.herokuapp.com/api', app_name: 'my_ruby_app', custom_http_headers: {'Authorization': '<API token>'})
64
+ ```
65
+
66
+ ## Dynamic custom HTTP headers
67
+ If you need custom HTTP headers that change during the lifetime of the client, the `custom_http_headers` can be given as a `Proc`.
68
+
69
+ ```ruby
70
+ Unleash.configure do |config|
71
+ config.app_name = 'my_ruby_app'
72
+ config.url = 'https://unleash.herokuapp.com/api'
73
+ config.custom_http_headers = proc do
74
+ {
75
+ 'Authorization': '<API token>',
76
+ 'X-Client-Request-Time': Time.now.iso8601
77
+ }
78
+ end
79
+ end
62
80
  ```
63
81
 
64
82
  #### List of Arguments
@@ -74,13 +92,14 @@ Argument | Description | Required? | Type | Default Value|
74
92
  `metrics_interval` | How often the unleash client should send metrics to server. | N | Integer | 60 |
75
93
  `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` |
76
94
  `disable_metrics` | Disables sending metrics to Unleash server. | N | Boolean | `false` |
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 | {} |
95
+ `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/Proc | {} |
78
96
  `timeout` | How long to wait for the connection to be established or wait in reading state (open_timeout/read_timeout) | N | Integer | 30 |
79
97
  `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 |
80
98
  `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` |
81
99
  `logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` |
82
100
  `log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::WARN` |
83
101
  `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` |
102
+ `strategies` | Strategies manager that holds all strategies and allows to add custom strategies | N | Unleash::Strategies | `Unleash::Strategies.new` |
84
103
 
85
104
  For a more in-depth look, please see `lib/unleash/configuration.rb`.
86
105
 
@@ -95,7 +114,7 @@ Environment Variable | Description
95
114
  require 'unleash'
96
115
  require 'unleash/context'
97
116
 
98
- @unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: 'http://unleash.herokuapp.com/api', custom_http_headers: { 'Authorization': '<API token>' })
117
+ @unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: 'https://unleash.herokuapp.com/api', custom_http_headers: { 'Authorization': '<API token>' })
99
118
 
100
119
  feature_name = "AwesomeFeature"
101
120
  unleash_context = Unleash::Context.new
@@ -106,6 +125,12 @@ if @unleash.is_enabled?(feature_name, unleash_context)
106
125
  else
107
126
  puts " #{feature_name} is disabled according to unleash"
108
127
  end
128
+
129
+ if @unleash.is_disabled?(feature_name, unleash_context)
130
+ puts " #{feature_name} is disabled according to unleash"
131
+ else
132
+ puts " #{feature_name} is enabled according to unleash"
133
+ end
109
134
  ```
110
135
 
111
136
  ## Usage in a Rails Application
@@ -117,7 +142,7 @@ Put in `config/initializers/unleash.rb`:
117
142
  ```ruby
118
143
  Unleash.configure do |config|
119
144
  config.app_name = Rails.application.class.parent.to_s
120
- config.url = 'http://unleash.herokuapp.com/api'
145
+ config.url = 'https://unleash.herokuapp.com/api'
121
146
  # config.instance_id = "#{Socket.gethostname}"
122
147
  config.logger = Rails.logger
123
148
  config.environment = Rails.env
@@ -156,7 +181,7 @@ Then you may keep the client configuration still in `config/initializers/unleash
156
181
  Unleash.configure do |config|
157
182
  config.app_name = Rails.application.class.parent.to_s
158
183
  config.environment = Rails.env
159
- config.url = 'http://unleash.herokuapp.com/api'
184
+ config.url = 'https://unleash.herokuapp.com/api'
160
185
  config.custom_http_headers = {'Authorization': '<API token>'}
161
186
  end
162
187
  ```
@@ -204,7 +229,7 @@ on_worker_boot do
204
229
  ::UNLEASH = Unleash::Client.new(
205
230
  app_name: 'my_rails_app',
206
231
  environment: 'development',
207
- url: 'http://unleash.herokuapp.com/api',
232
+ url: 'https://unleash.herokuapp.com/api',
208
233
  custom_http_headers: {'Authorization': '<API token>'},
209
234
  )
210
235
  end
@@ -230,7 +255,7 @@ PhusionPassenger.on_event(:starting_worker_process) do |forked|
230
255
  # config.instance_id = "#{Socket.gethostname}"
231
256
  config.logger = Rails.logger
232
257
  config.environment = Rails.env
233
- config.url = 'http://unleash.herokuapp.com/api'
258
+ config.url = 'https://unleash.herokuapp.com/api'
234
259
  config.custom_http_headers = {'Authorization': '<API token>'}
235
260
  end
236
261
 
@@ -285,6 +310,9 @@ Then wherever in your application that you need a feature toggle, you can use:
285
310
  if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context
286
311
  puts "AwesomeFeature is enabled"
287
312
  end
313
+ if UNLEASH.is_disabled? "AwesomeFeature", @unleash_context
314
+ puts "AwesomeFeature is disabled"
315
+ end
288
316
  ```
289
317
 
290
318
  or if client is set in `Rails.configuration.unleash`:
@@ -293,6 +321,9 @@ or if client is set in `Rails.configuration.unleash`:
293
321
  if Rails.configuration.unleash.is_enabled? "AwesomeFeature", @unleash_context
294
322
  puts "AwesomeFeature is enabled"
295
323
  end
324
+ if Rails.configuration.unleash.is_disabled? "AwesomeFeature", @unleash_context
325
+ puts "AwesomeFeature is enabled"
326
+ end
296
327
  ```
297
328
 
298
329
  If the feature is not found in the server, it will by default return false.
@@ -365,11 +396,11 @@ Bootstrap configuration allows the client to be initialized with a predefined se
365
396
  Bootstrapping can be configured by providing a bootstrap configuration when initializing the client.
366
397
  ```ruby
367
398
  @unleash = Unleash::Client.new(
368
- url: 'http://unleash.herokuapp.com/api',
399
+ url: 'https://unleash.herokuapp.com/api',
369
400
  app_name: 'my_ruby_app',
370
401
  custom_http_headers: { 'Authorization': '<API token>' },
371
402
  bootstrap_config: Unleash::Bootstrap::Configuration.new({
372
- url: "http://unleash.herokuapp.com/api/client/features",
403
+ url: "https://unleash.herokuapp.com/api/client/features",
373
404
  url_headers: {'Authorization': '<API token>'}
374
405
  })
375
406
  )
@@ -395,7 +426,7 @@ Example usage:
395
426
 
396
427
  First saving the toggles locally:
397
428
  ```shell
398
- curl -H 'Authorization: <API token>' -XGET 'http://unleash.herokuapp.com/api' > ./default-toggles.json
429
+ curl -H 'Authorization: <API token>' -XGET 'https://unleash.herokuapp.com/api' > ./default-toggles.json
399
430
  ```
400
431
 
401
432
  Now using them on start up:
@@ -408,7 +439,7 @@ custom_boostrapper = lambda {
408
439
 
409
440
  @unleash = Unleash::Client.new(
410
441
  app_name: 'my_ruby_app',
411
- url: 'http://unleash.herokuapp.com/api',
442
+ url: 'https://unleash.herokuapp.com/api',
412
443
  custom_http_headers: { 'Authorization': '<API token>' },
413
444
  bootstrap_config: Unleash::Bootstrap::Configuration.new({
414
445
  block: custom_boostrapper
@@ -426,6 +457,9 @@ Method Name | Description | Return Type |
426
457
  `is_enabled?` | Check if feature toggle is to be enabled or not. | Boolean |
427
458
  `enabled?` | Alias to the `is_enabled?` method. But more ruby idiomatic. | Boolean |
428
459
  `if_enabled` | Run a code block, if a feature is enabled. | `yield` |
460
+ `is_disabled?` | Check if feature toggle is to be enabled or not. | Boolean |
461
+ `disabled?` | Alias to the `is_disabled?` method. But more ruby idiomatic. | Boolean |
462
+ `if_disabled` | Run a code block, if a feature is disabled. | `yield` |
429
463
  `get_variant` | Get variant for a given feature | `Unleash::Variant` |
430
464
  `shutdown` | Save metrics to disk, flush metrics to server, and then kill ToggleFetcher and MetricsReporter threads. A safe shutdown. Not really useful in long running applications, like web applications. | nil |
431
465
  `shutdown!` | Kill ToggleFetcher and MetricsReporter threads immediately. | nil |
@@ -456,10 +490,37 @@ This client comes with the all the required strategies out of the box:
456
490
  * UnknownStrategy
457
491
  * UserWithIdStrategy
458
492
 
493
+ ## Custom Strategies
494
+
495
+ Client allows to add [custom activation strategies](https://docs.getunleash.io/advanced/custom_activation_strategy) using configuration.
496
+ In order for strategy to work correctly it should support two methods `name` and `is_enabled?`
497
+
498
+ ```ruby
499
+ class MyCustomStrategy
500
+ def name
501
+ 'muCustomStrategy'
502
+ end
503
+
504
+ def is_enabled?(params = {}, context = nil)
505
+ true
506
+ end
507
+ end
508
+
509
+ Unleash.configure do |config|
510
+ config.strategies.add(MyCustomStrategy.new)
511
+ end
512
+ ```
513
+
459
514
  ## Development
460
515
 
461
516
  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.
462
517
 
518
+ This SDK is also built against the Unleash Client Specification tests. To run the Ruby SDK against this test suite, you'll need to have a copy on your machine, you can clone the repository directly using:
519
+
520
+ `git clone --depth 5 --branch v4.2.2 https://github.com/Unleash/client-specification.git client-specification`
521
+
522
+ After doing this, `rake spec` will also run the client specification tests.
523
+
463
524
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
464
525
 
465
526
 
@@ -7,7 +7,7 @@ require 'unleash/bootstrap/configuration'
7
7
  puts ">> START bootstrap.rb"
8
8
 
9
9
  @unleash = Unleash::Client.new(
10
- url: 'http://unleash.herokuapp.com/api',
10
+ url: 'https://unleash.herokuapp.com/api',
11
11
  custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
12
12
  app_name: 'bootstrap-test',
13
13
  instance_id: 'local-test-cli',
data/examples/simple.rb CHANGED
@@ -6,7 +6,7 @@ require 'unleash/context'
6
6
  puts ">> START simple.rb"
7
7
 
8
8
  # Unleash.configure do |config|
9
- # config.url = 'http://unleash.herokuapp.com/api'
9
+ # config.url = 'https://unleash.herokuapp.com/api'
10
10
  # config.custom_http_headers = { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' }
11
11
  # config.app_name = 'simple-test'
12
12
  # config.refresh_interval = 2
@@ -18,7 +18,7 @@ puts ">> START simple.rb"
18
18
  # or:
19
19
 
20
20
  @unleash = Unleash::Client.new(
21
- url: 'http://unleash.herokuapp.com/api',
21
+ url: 'https://unleash.herokuapp.com/api',
22
22
  custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
23
23
  app_name: 'simple-test',
24
24
  instance_id: 'local-test-cli',
@@ -1,9 +1,10 @@
1
1
  module Unleash
2
2
  class ActivationStrategy
3
- attr_accessor :name, :params, :constraints
3
+ attr_accessor :name, :params, :constraints, :disabled
4
4
 
5
5
  def initialize(name, params, constraints = [])
6
6
  self.name = name
7
+ self.disabled = false
7
8
 
8
9
  if params.is_a?(Hash)
9
10
  self.params = params
@@ -18,6 +19,7 @@ module Unleash
18
19
  self.constraints = constraints
19
20
  else
20
21
  Unleash.logger.warn "Invalid constraints provided for ActivationStrategy (contraints: #{constraints})"
22
+ self.disabled = true
21
23
  self.constraints = []
22
24
  end
23
25
  end
@@ -12,7 +12,7 @@ module Unleash
12
12
  attr_accessor :fetcher_scheduled_executor, :metrics_scheduled_executor
13
13
 
14
14
  def initialize(*opts)
15
- Unleash.configuration ||= Unleash::Configuration.new(*opts)
15
+ Unleash.configuration = Unleash::Configuration.new(*opts) unless opts.empty?
16
16
  Unleash.configuration.validate!
17
17
 
18
18
  Unleash.logger = Unleash.configuration.logger.clone
@@ -45,19 +45,30 @@ module Unleash
45
45
  return default_value
46
46
  end
47
47
 
48
- toggle = Unleash::FeatureToggle.new(toggle_as_hash)
48
+ toggle = Unleash::FeatureToggle.new(toggle_as_hash, Unleash&.segment_cache)
49
+
50
+ toggle.is_enabled?(context)
51
+ end
49
52
 
50
- toggle.is_enabled?(context, default_value)
53
+ def is_disabled?(feature, context = nil, default_value_param = true, &fallback_blk)
54
+ !is_enabled?(feature, context, !default_value_param, &fallback_blk)
51
55
  end
52
56
 
53
57
  # enabled? is a more ruby idiomatic method name than is_enabled?
54
58
  alias enabled? is_enabled?
59
+ # disabled? is a more ruby idiomatic method name than is_disabled?
60
+ alias disabled? is_disabled?
55
61
 
56
62
  # execute a code block (passed as a parameter), if is_enabled? is true.
57
63
  def if_enabled(feature, context = nil, default_value = false, &blk)
58
64
  yield(blk) if is_enabled?(feature, context, default_value)
59
65
  end
60
66
 
67
+ # execute a code block (passed as a parameter), if is_disabled? is true.
68
+ def if_disabled(feature, context = nil, default_value = true, &blk)
69
+ yield(blk) if is_disabled?(feature, context, default_value)
70
+ end
71
+
61
72
  def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant)
62
73
  Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"
63
74
 
@@ -110,7 +121,7 @@ module Unleash
110
121
  'appName': Unleash.configuration.app_name,
111
122
  'instanceId': Unleash.configuration.instance_id,
112
123
  'sdkVersion': "unleash-client-ruby:" + Unleash::VERSION,
113
- 'strategies': Unleash::STRATEGIES.keys,
124
+ 'strategies': Unleash.strategies.keys,
114
125
  'started': Time.now.iso8601(Unleash::TIME_RESOLUTION),
115
126
  'interval': Unleash.configuration.metrics_interval_in_millis
116
127
  }
@@ -20,10 +20,11 @@ module Unleash
20
20
  :backup_file,
21
21
  :logger,
22
22
  :log_level,
23
- :bootstrap_config
23
+ :bootstrap_config,
24
+ :strategies
24
25
 
25
26
  def initialize(opts = {})
26
- ensure_valid_opts(opts)
27
+ validate_custom_http_headers!(opts[:custom_http_headers]) if opts.has_key?(:custom_http_headers)
27
28
  set_defaults
28
29
 
29
30
  initialize_default_logger if opts[:logger].nil?
@@ -40,7 +41,8 @@ module Unleash
40
41
  return if self.disable_client
41
42
 
42
43
  raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? || self.url.nil?
43
- raise ArgumentError, "custom_http_headers must be a hash." unless self.custom_http_headers.is_a?(Hash)
44
+
45
+ validate_custom_http_headers!(self.custom_http_headers)
44
46
  end
45
47
 
46
48
  def refresh_backup_file!
@@ -50,8 +52,9 @@ module Unleash
50
52
  def http_headers
51
53
  {
52
54
  'UNLEASH-INSTANCEID' => self.instance_id,
53
- 'UNLEASH-APPNAME' => self.app_name
54
- }.merge(custom_http_headers.dup)
55
+ 'UNLEASH-APPNAME' => self.app_name,
56
+ 'Unleash-Client-Spec' => '4.2.2'
57
+ }.merge!(generate_custom_http_headers)
55
58
  end
56
59
 
57
60
  def fetch_toggles_uri
@@ -78,12 +81,6 @@ module Unleash
78
81
 
79
82
  private
80
83
 
81
- def ensure_valid_opts(opts)
82
- unless opts[:custom_http_headers].is_a?(Hash) || opts[:custom_http_headers].nil?
83
- raise ArgumentError, "custom_http_headers must be a hash."
84
- end
85
- end
86
-
87
84
  def set_defaults
88
85
  self.app_name = nil
89
86
  self.environment = 'default'
@@ -99,12 +96,13 @@ module Unleash
99
96
  self.backup_file = nil
100
97
  self.log_level = Logger::WARN
101
98
  self.bootstrap_config = nil
99
+ self.strategies = Unleash::Strategies.new
102
100
 
103
101
  self.custom_http_headers = {}
104
102
  end
105
103
 
106
104
  def initialize_default_logger
107
- self.logger = Logger.new(STDOUT)
105
+ self.logger = Logger.new($stdout)
108
106
 
109
107
  # on default logger, use custom formatter that includes thread_name:
110
108
  self.logger.formatter = proc do |severity, datetime, _progname, msg|
@@ -118,6 +116,18 @@ module Unleash
118
116
  self
119
117
  end
120
118
 
119
+ def validate_custom_http_headers!(custom_http_headers)
120
+ return if custom_http_headers.is_a?(Hash) || custom_http_headers.respond_to?(:call)
121
+
122
+ raise ArgumentError, "custom_http_headers must be a Hash or a Proc."
123
+ end
124
+
125
+ def generate_custom_http_headers
126
+ return self.custom_http_headers.call if self.custom_http_headers.respond_to?(:call)
127
+
128
+ self.custom_http_headers
129
+ end
130
+
121
131
  def set_option(opt, val)
122
132
  __send__("#{opt}=", val)
123
133
  rescue NoMethodError
@@ -1,12 +1,11 @@
1
1
  require 'date'
2
-
3
2
  module Unleash
4
3
  class Constraint
5
4
  attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
6
5
 
7
6
  OPERATORS = {
8
- IN: ->(context_v, constraint_v){ constraint_v.include? context_v },
9
- NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v },
7
+ IN: ->(context_v, constraint_v){ constraint_v.include? context_v.to_s },
8
+ NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v.to_s },
10
9
  STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
11
10
  STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
12
11
  STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
@@ -19,16 +18,21 @@ module Unleash
19
18
  DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
20
19
  SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
21
20
  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) } }
21
+ SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } },
22
+ FALLBACK_VALIDATOR: ->(_context_v, _constraint_v){ false }
23
23
  }.freeze
24
24
 
25
25
  LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
26
26
 
27
27
  def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
28
28
  raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
29
- raise ArgumentError, "operator does not hold a valid value:" + OPERATORS.keys unless OPERATORS.include? operator.to_sym
30
29
 
31
- self.validate_constraint_value_type(operator.to_sym, value)
30
+ unless OPERATORS.include? operator.to_sym
31
+ Unleash.logger.warn "Operator #{operator} is not a supported operator, " \
32
+ "falling back to FALLBACK_VALIDATOR which skips this constraint."
33
+ operator = "FALLBACK_VALIDATOR"
34
+ end
35
+ self.log_inconsistent_constraint_configuration(operator.to_sym, value)
32
36
 
33
37
  self.context_name = context_name
34
38
  self.operator = operator.to_sym
@@ -38,8 +42,9 @@ module Unleash
38
42
  end
39
43
 
40
44
  def matches_context?(context)
41
- Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" \
42
- " #{context.get_by_name(self.context_name)} "
45
+ Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})"
46
+ return false if context.nil?
47
+
43
48
  match = matches_constraint?(context)
44
49
  self.inverted ? !match : match
45
50
  rescue KeyError
@@ -79,19 +84,25 @@ module Unleash
79
84
  end
80
85
 
81
86
  # 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)
87
+ def log_inconsistent_constraint_configuration(operator, value)
88
+ Unleash.logger.warn "value is a String, operator is expecting an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
89
+ Unleash.logger.warn "value is an Array, operator is expecting a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
85
90
  end
86
91
 
87
92
  private
88
93
 
89
94
  def matches_constraint?(context)
95
+ Unleash.logger.debug "Unleash::Constraint matches_constraint? value: #{self.value} operator: #{self.operator} " \
96
+ " context.get_by_name(#{self.context_name})"
97
+
90
98
  unless OPERATORS.include?(self.operator)
91
99
  Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
92
100
  false
93
101
  end
94
102
 
103
+ # when the operator is NOT_IN and there is no data, return true. In all other cases the operator doesn't match.
104
+ return self.operator == :NOT_IN unless context.include?(self.context_name)
105
+
95
106
  v = self.value.dup
96
107
  context_value = context.get_by_name(self.context_name)
97
108
 
@@ -9,7 +9,7 @@ module Unleash
9
9
 
10
10
  self.app_name = value_for('appName', params, Unleash&.configuration&.app_name)
11
11
  self.environment = value_for('environment', params, Unleash&.configuration&.environment || 'default')
12
- self.user_id = value_for('userId', params)
12
+ self.user_id = value_for('userId', params)&.to_s
13
13
  self.session_id = value_for('sessionId', params)
14
14
  self.remote_address = value_for('remoteAddress', params)
15
15
  self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
@@ -33,6 +33,13 @@ module Unleash
33
33
  end
34
34
  end
35
35
 
36
+ def include?(name)
37
+ normalized_name = underscore(name)
38
+ return self.instance_variable_defined? "@#{normalized_name}" if ATTRS.include? normalized_name.to_sym
39
+
40
+ self.properties.include?(normalized_name.to_sym) || self.properties.include?(name.to_sym)
41
+ end
42
+
36
43
  private
37
44
 
38
45
  # Method to fetch values from hash for two types of keys: string in camelCase and symbol in snake_case
@@ -9,13 +9,13 @@ module Unleash
9
9
  class FeatureToggle
10
10
  attr_accessor :name, :enabled, :strategies, :variant_definitions
11
11
 
12
- def initialize(params = {})
12
+ def initialize(params = {}, segment_map = {})
13
13
  params = {} if params.nil?
14
14
 
15
15
  self.name = params.fetch('name', nil)
16
16
  self.enabled = params.fetch('enabled', false)
17
17
 
18
- self.strategies = initialize_strategies(params)
18
+ self.strategies = initialize_strategies(params, segment_map)
19
19
  self.variant_definitions = initialize_variant_definitions(params)
20
20
  end
21
21
 
@@ -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, default_result)
27
- result = am_enabled?(context, default_result)
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,12 +37,11 @@ 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, true)
41
- return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
42
-
43
- variant = variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
40
+ toggle_enabled = am_enabled?(context)
41
+ variant = resolve_variant(context, toggle_enabled)
44
42
 
45
- Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics
43
+ choice = toggle_enabled ? :yes : :no
44
+ Unleash.toggle_metrics.increment_variant(self.name, choice, variant.name) unless Unleash.configuration.disable_metrics
46
45
  variant
47
46
  end
48
47
 
@@ -52,12 +51,19 @@ module Unleash
52
51
 
53
52
  private
54
53
 
54
+ def resolve_variant(context, toggle_enabled)
55
+ return Unleash::FeatureToggle.disabled_variant unless toggle_enabled
56
+ return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
57
+
58
+ variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
59
+ end
60
+
55
61
  def resolve_stickiness
56
62
  self.variant_definitions&.map(&:stickiness)&.compact&.first || "default"
57
63
  end
58
64
 
59
65
  # only check if it is enabled, do not do metrics
60
- def am_enabled?(context, default_result)
66
+ def am_enabled?(context)
61
67
  result =
62
68
  if self.enabled
63
69
  self.strategies.empty? ||
@@ -65,22 +71,24 @@ module Unleash
65
71
  strategy_enabled?(s, context) && strategy_constraint_matches?(s, context)
66
72
  end
67
73
  else
68
- default_result
74
+ false
69
75
  end
70
76
 
71
- Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} " \
77
+ Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \
72
78
  "and Strategies combined with contraints returned #{result})"
73
79
 
74
80
  result
75
81
  end
76
82
 
77
83
  def strategy_enabled?(strategy, context)
78
- r = Unleash::STRATEGIES.fetch(strategy.name.to_sym, :unknown).is_enabled?(strategy.params, context)
84
+ r = Unleash.strategies.fetch(strategy.name).is_enabled?(strategy.params, context)
79
85
  Unleash.logger.debug "Unleash::FeatureToggle.strategy_enabled? Strategy #{strategy.name} returned #{r} with context: #{context}"
80
86
  r
81
87
  end
82
88
 
83
89
  def strategy_constraint_matches?(strategy, context)
90
+ return false if strategy.disabled
91
+
84
92
  strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
85
93
  end
86
94
 
@@ -128,26 +136,35 @@ module Unleash
128
136
  context
129
137
  end
130
138
 
131
- def initialize_strategies(params)
139
+ def initialize_strategies(params, segment_map)
132
140
  params.fetch('strategies', [])
133
- .select{ |s| s.has_key?('name') && Unleash::STRATEGIES.has_key?(s['name'].to_sym) }
141
+ .select{ |s| s.has_key?('name') && Unleash.strategies.includes?(s['name']) }
134
142
  .map do |s|
135
143
  ActivationStrategy.new(
136
144
  s['name'],
137
145
  s['parameters'],
138
- (s['constraints'] || []).map do |c|
139
- Constraint.new(
140
- c.fetch('contextName'),
141
- c.fetch('operator'),
142
- c.fetch('values', nil) || c.fetch('value', nil),
143
- inverted: c.fetch('inverted', false),
144
- case_insensitive: c.fetch('caseInsensitive', false)
145
- )
146
- end
146
+ resolve_constraints(s, segment_map)
147
147
  )
148
148
  end || []
149
149
  end
150
150
 
151
+ def resolve_constraints(strategy, segment_map)
152
+ segment_constraints = (strategy["segments"] || []).map do |segment_id|
153
+ segment_map[segment_id]&.fetch("constraints")
154
+ end
155
+ (strategy.fetch("constraints", []) + segment_constraints).flatten.map do |constraint|
156
+ return nil if constraint.nil?
157
+
158
+ Constraint.new(
159
+ constraint.fetch('contextName'),
160
+ constraint.fetch('operator'),
161
+ constraint.fetch('value', nil) || constraint.fetch('values', nil),
162
+ inverted: constraint.fetch('inverted', false),
163
+ case_insensitive: constraint.fetch('caseInsensitive', false)
164
+ )
165
+ end
166
+ end
167
+
151
168
  def initialize_variant_definitions(params)
152
169
  (params.fetch('variants', []) || [])
153
170
  .select{ |v| v.is_a?(Hash) && v.has_key?('name') }
@@ -1,33 +1,41 @@
1
1
  module Unleash
2
2
  class Metrics
3
- attr_accessor :features
4
-
5
- # NOTE: no mutexes for features
3
+ attr_accessor :features, :features_lock
6
4
 
7
5
  def initialize
8
6
  self.features = {}
7
+ self.features_lock = Mutex.new
9
8
  end
10
9
 
11
10
  def to_s
12
- self.features.to_json
11
+ self.features_lock.synchronize do
12
+ return self.features.to_json
13
+ end
13
14
  end
14
15
 
15
16
  def increment(feature, choice)
16
17
  raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice
17
18
 
18
- self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
19
- self.features[feature][choice] += 1
19
+ self.features_lock.synchronize do
20
+ self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
21
+ self.features[feature][choice] += 1
22
+ end
20
23
  end
21
24
 
22
- def increment_variant(feature, variant)
23
- self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
24
- self.features[feature]['variant'] = {} unless self.features[feature].include? 'variant'
25
- self.features[feature]['variant'][variant] = 0 unless self.features[feature]['variant'].include? variant
26
- self.features[feature]['variant'][variant] += 1
25
+ def increment_variant(feature, choice, variant)
26
+ self.features_lock.synchronize do
27
+ self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
28
+ self.features[feature][choice] += 1
29
+ self.features[feature]['variant'] = {} unless self.features[feature].include? 'variant'
30
+ self.features[feature]['variant'][variant] = 0 unless self.features[feature]['variant'].include? variant
31
+ self.features[feature]['variant'][variant] += 1
32
+ end
27
33
  end
28
34
 
29
35
  def reset
30
- self.features = {}
36
+ self.features_lock.synchronize do
37
+ self.features = {}
38
+ end
31
39
  end
32
40
  end
33
41
  end
@@ -0,0 +1,80 @@
1
+ require 'unleash/strategy/base'
2
+ Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path }
3
+
4
+ module Unleash
5
+ class Strategies
6
+ def initialize
7
+ @strategies = {}
8
+ register_strategies
9
+ end
10
+
11
+ def keys
12
+ @strategies.keys
13
+ end
14
+
15
+ def includes?(name)
16
+ @strategies.has_key?(name.to_s)
17
+ end
18
+
19
+ def fetch(name)
20
+ raise Unleash::Strategy::NotImplemented, "Strategy is not implemented" unless (strategy = @strategies[name.to_s])
21
+
22
+ strategy
23
+ end
24
+
25
+ def add(strategy)
26
+ @strategies[strategy.name] = strategy
27
+ end
28
+
29
+ def []=(key, strategy)
30
+ warn_deprecated_registration(strategy, 'modifying Unleash::STRATEGIES')
31
+ @strategies[key.to_s] = strategy
32
+ end
33
+
34
+ def [](key)
35
+ @strategies[key.to_s]
36
+ end
37
+
38
+ def register_strategies
39
+ register_base_strategies
40
+ register_custom_strategies
41
+ end
42
+
43
+ protected
44
+
45
+ # Deprecated: Use Unleash.configuration to add custom strategies
46
+ def register_custom_strategies
47
+ Unleash::Strategy.constants
48
+ .select{ |c| Unleash::Strategy.const_get(c).is_a? Class }
49
+ .reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) } # Reject abstract classes
50
+ .map{ |c| Object.const_get("Unleash::Strategy::#{c}") }
51
+ .reject{ |c| DEFAULT_STRATEGIES.include?(c) } # Reject base classes
52
+ .each do |c|
53
+ strategy = c.new
54
+ warn_deprecated_registration(strategy, 'adding custom class into Unleash::Strategy namespace')
55
+ self.add(strategy)
56
+ end
57
+ end
58
+
59
+ def register_base_strategies
60
+ DEFAULT_STRATEGIES.each{ |c| self.add(c.new) }
61
+ end
62
+
63
+ DEFAULT_STRATEGIES = [
64
+ Unleash::Strategy::ApplicationHostname,
65
+ Unleash::Strategy::Default,
66
+ Unleash::Strategy::FlexibleRollout,
67
+ Unleash::Strategy::GradualRolloutRandom,
68
+ Unleash::Strategy::GradualRolloutSessionId,
69
+ Unleash::Strategy::GradualRolloutUserId,
70
+ Unleash::Strategy::RemoteAddress,
71
+ Unleash::Strategy::UserWithId
72
+ ].freeze
73
+
74
+ def warn_deprecated_registration(strategy, method)
75
+ warn "[DEPRECATED] Registering custom Unleash strategy by #{method} is deprecated.
76
+ Please use Unleash configuration to register custom strategy: " \
77
+ "`Unleash.configure {|c| c.strategies.add(#{strategy.class.name}.new) }`"
78
+ end
79
+ end
80
+ end
@@ -10,7 +10,7 @@ module Unleash
10
10
  # need: params['percentage']
11
11
  def is_enabled?(params = {}, context = nil)
12
12
  return false unless params.is_a?(Hash)
13
- return false unless context.class.name == 'Unleash::Context'
13
+ return false unless context.instance_of?(Unleash::Context)
14
14
 
15
15
  stickiness = params.fetch('stickiness', 'default')
16
16
  stickiness_id = resolve_stickiness(stickiness, context)
@@ -10,7 +10,7 @@ module Unleash
10
10
  # need: params['percentage'], params['groupId'], context.user_id,
11
11
  def is_enabled?(params = {}, context = nil)
12
12
  return false unless params.is_a?(Hash) && params.has_key?('percentage')
13
- return false unless context.class.name == 'Unleash::Context'
13
+ return false unless context.instance_of?(Unleash::Context)
14
14
  return false if context.session_id.nil? || context.session_id.empty?
15
15
 
16
16
  percentage = Integer(params['percentage'] || 0)
@@ -10,7 +10,7 @@ module Unleash
10
10
  # need: params['percentage'], params['groupId'], context.user_id,
11
11
  def is_enabled?(params = {}, context = nil, _constraints = [])
12
12
  return false unless params.is_a?(Hash) && params.has_key?('percentage')
13
- return false unless context.class.name == 'Unleash::Context'
13
+ return false unless context.instance_of?(Unleash::Context)
14
14
  return false if context.user_id.nil? || context.user_id.empty?
15
15
 
16
16
  percentage = Integer(params['percentage'] || 0)
@@ -11,7 +11,7 @@ module Unleash
11
11
  def is_enabled?(params = {}, context = nil)
12
12
  return false unless params.is_a?(Hash) && params.has_key?(PARAM)
13
13
  return false unless params.fetch(PARAM, nil).is_a? String
14
- return false unless context.class.name == 'Unleash::Context'
14
+ return false unless context.instance_of?(Unleash::Context)
15
15
 
16
16
  remote_address = ipaddr_or_nil_from_str(context.remote_address)
17
17
 
@@ -11,7 +11,7 @@ module Unleash
11
11
  def is_enabled?(params = {}, context = nil)
12
12
  return false unless params.is_a?(Hash) && params.has_key?(PARAM)
13
13
  return false unless params.fetch(PARAM, nil).is_a? String
14
- return false unless context.class.name == 'Unleash::Context'
14
+ return false unless context.instance_of?(Unleash::Context)
15
15
 
16
16
  params[PARAM].split(",").map(&:strip).include?(context.user_id)
17
17
  end
@@ -5,11 +5,12 @@ require 'json'
5
5
 
6
6
  module Unleash
7
7
  class ToggleFetcher
8
- attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count
8
+ attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache
9
9
 
10
10
  def initialize
11
11
  self.etag = nil
12
12
  self.toggle_cache = nil
13
+ self.segment_cache = nil
13
14
  self.toggle_lock = Mutex.new
14
15
  self.toggle_resource = ConditionVariable.new
15
16
  self.retry_count = 0
@@ -65,24 +66,20 @@ module Unleash
65
66
 
66
67
  def save!
67
68
  Unleash.logger.debug "Will save toggles to disk now"
68
- begin
69
- backup_file = Unleash.configuration.backup_file
70
- backup_file_tmp = "#{backup_file}.tmp"
71
69
 
72
- self.toggle_lock.synchronize do
73
- file = File.open(backup_file_tmp, "w")
70
+ backup_file = Unleash.configuration.backup_file
71
+ backup_file_tmp = "#{backup_file}.tmp"
72
+
73
+ self.toggle_lock.synchronize do
74
+ File.open(backup_file_tmp, "w") do |file|
74
75
  file.write(self.toggle_cache.to_json)
75
- file.close
76
- File.rename(backup_file_tmp, backup_file)
77
76
  end
78
- rescue StandardError => e
79
- # This is not really the end of the world. Swallowing the exception.
80
- Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
81
- Unleash.logger.error "stacktrace: #{e.backtrace}"
82
- ensure
83
- file&.close if defined?(file)
84
- self.toggle_lock.unlock if self.toggle_lock.locked?
77
+ File.rename(backup_file_tmp, backup_file)
85
78
  end
79
+ rescue StandardError => e
80
+ # This is not really the end of the world. Swallowing the exception.
81
+ Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
82
+ Unleash.logger.error "stacktrace: #{e.backtrace}"
86
83
  end
87
84
 
88
85
  private
@@ -99,9 +96,10 @@ module Unleash
99
96
  end
100
97
 
101
98
  def update_running_client!
102
- if Unleash.toggles != self.toggles
99
+ if Unleash.toggles != self.toggles["features"] || Unleash.segment_cache != self.toggles["segments"]
103
100
  Unleash.logger.info "Updating toggles to main client, there has been a change in the server."
104
- Unleash.toggles = self.toggles
101
+ Unleash.toggles = self.toggles["features"]
102
+ Unleash.segment_cache = self.toggles["segments"]
105
103
  end
106
104
  end
107
105
 
@@ -110,22 +108,15 @@ module Unleash
110
108
  backup_file = Unleash.configuration.backup_file
111
109
  return nil unless File.exist?(backup_file)
112
110
 
113
- begin
114
- file = File.new(backup_file, "r")
115
- file_content = file.read
116
-
117
- backup_as_hash = JSON.parse(file_content)
118
- synchronize_with_local_cache!(backup_as_hash)
119
- update_running_client!
120
- rescue IOError => e
121
- Unleash.logger.error "Unable to read the backup_file: #{e}"
122
- rescue JSON::ParserError => e
123
- Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
124
- rescue StandardError => e
125
- Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
126
- ensure
127
- file&.close
128
- end
111
+ backup_as_hash = JSON.parse(File.read(backup_file))
112
+ synchronize_with_local_cache!(backup_as_hash)
113
+ update_running_client!
114
+ rescue IOError => e
115
+ Unleash.logger.error "Unable to read the backup_file: #{e}"
116
+ rescue JSON::ParserError => e
117
+ Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
118
+ rescue StandardError => e
119
+ Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
129
120
  end
130
121
 
131
122
  def bootstrap
@@ -137,10 +128,19 @@ module Unleash
137
128
  Unleash.configuration.bootstrap_config = nil
138
129
  end
139
130
 
131
+ def build_segment_map(segments_array)
132
+ return {} if segments_array.nil?
133
+
134
+ segments_array.map{ |segment| [segment["id"], segment] }.to_h
135
+ end
136
+
140
137
  # @param response_body [String]
141
138
  def get_features(response_body)
142
139
  response_hash = JSON.parse(response_body)
143
- return response_hash['features'] if response_hash['version'] >= 1
140
+
141
+ if response_hash['version'] >= 1
142
+ return { "features" => response_hash["features"], "segments" => build_segment_map(response_hash["segments"]) }
143
+ end
144
144
 
145
145
  raise NotImplemented, "Version of features provided by unleash server" \
146
146
  " is unsupported by this client."
@@ -4,7 +4,7 @@ module Unleash
4
4
  class VariantDefinition
5
5
  attr_accessor :name, :weight, :payload, :overrides, :stickiness
6
6
 
7
- def initialize(name, weight = 0, payload = nil, stickiness = nil, overrides = [])
7
+ def initialize(name, weight = 0, payload = nil, stickiness = nil, overrides = []) # rubocop:disable Metrics/ParameterLists
8
8
  self.name = name
9
9
  self.weight = weight
10
10
  self.payload = payload
@@ -14,7 +14,7 @@ module Unleash
14
14
  end
15
15
 
16
16
  def matches_context?(context)
17
- raise ArgumentError, 'context must be of class Unleash::Context' unless context.class.name == 'Unleash::Context'
17
+ raise ArgumentError, 'context must be of class Unleash::Context' unless context.instance_of?(Unleash::Context)
18
18
 
19
19
  context_value =
20
20
  case self.context_name
@@ -1,3 +1,3 @@
1
1
  module Unleash
2
- VERSION = "4.2.0".freeze
2
+ VERSION = "4.4.2".freeze
3
3
  end
data/lib/unleash.rb CHANGED
@@ -1,35 +1,31 @@
1
1
  require 'unleash/version'
2
2
  require 'unleash/configuration'
3
- require 'unleash/strategy/base'
3
+ require 'unleash/strategies'
4
4
  require 'unleash/context'
5
5
  require 'unleash/client'
6
6
  require 'logger'
7
7
 
8
- Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path unless path.end_with? '_spec.rb' }
9
-
10
8
  module Unleash
11
9
  TIME_RESOLUTION = 3
12
10
 
13
- STRATEGIES = Unleash::Strategy.constants
14
- .select{ |c| Unleash::Strategy.const_get(c).is_a? Class }
15
- .reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) }
16
- .map do |c|
17
- lowered_c = c.to_s
18
- lowered_c[0] = lowered_c[0].downcase
19
- [lowered_c.to_sym, Object.const_get("Unleash::Strategy::#{c}").new]
20
- end
21
- .to_h
22
-
23
11
  class << self
24
- attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :logger
12
+ attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :segment_cache, :logger
25
13
  end
26
14
 
15
+ self.configuration = Unleash::Configuration.new
16
+
17
+ # Deprecated: Use Unleash.configure to add custom strategies
18
+ STRATEGIES = self.configuration.strategies
19
+
27
20
  # Support for configuration via yield:
28
21
  def self.configure
29
- self.configuration ||= Unleash::Configuration.new
30
22
  yield(configuration)
31
23
 
32
24
  self.configuration.validate!
33
25
  self.configuration.refresh_backup_file!
34
26
  end
27
+
28
+ def self.strategies
29
+ self.configuration.strategies
30
+ end
35
31
  end
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "rspec-json_expectations", "~> 2.2"
32
32
  spec.add_development_dependency "webmock", "~> 3.8"
33
33
 
34
- spec.add_development_dependency "rubocop", "~> 0.80"
34
+ spec.add_development_dependency "rubocop", "~> 1.28.2"
35
35
  spec.add_development_dependency "simplecov", "~> 0.21.2"
36
36
  spec.add_development_dependency "simplecov-lcov", "~> 0.8.0"
37
37
  end
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.2.0
4
+ version: 4.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renato Arruda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-18 00:00:00.000000000 Z
11
+ date: 2023-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: murmurhash3
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0.80'
103
+ version: 1.28.2
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0.80'
110
+ version: 1.28.2
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: simplecov
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -146,6 +146,8 @@ executables:
146
146
  extensions: []
147
147
  extra_rdoc_files: []
148
148
  files:
149
+ - ".github/stale.yml"
150
+ - ".github/workflows/add-to-project.yml"
149
151
  - ".github/workflows/pull_request.yml"
150
152
  - ".gitignore"
151
153
  - ".rspec"
@@ -175,6 +177,7 @@ files:
175
177
  - lib/unleash/metrics.rb
176
178
  - lib/unleash/metrics_reporter.rb
177
179
  - lib/unleash/scheduled_executor.rb
180
+ - lib/unleash/strategies.rb
178
181
  - lib/unleash/strategy/application_hostname.rb
179
182
  - lib/unleash/strategy/base.rb
180
183
  - lib/unleash/strategy/default.rb
@@ -211,7 +214,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
211
214
  - !ruby/object:Gem::Version
212
215
  version: '0'
213
216
  requirements: []
214
- rubygems_version: 3.3.6
217
+ rubygems_version: 3.3.5
215
218
  signing_key:
216
219
  specification_version: 4
217
220
  summary: Unleash feature toggle client.