unleash 4.2.0 → 4.4.2

Sign up to get free protection for your applications and to get access to all the features.
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.