unleash 4.1.0 → 4.2.0

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: 14f988b84b07e45401ad61a5814c1ca44185fc9358cef33f050b1d3bae45dd4e
4
- data.tar.gz: afda9cf356088ef976047fe58d38d3b457fb1d8797cb7f519de6bdb953f43267
3
+ metadata.gz: 93e6312bc93458378b877b6e949274295a1e0037af2cf8206d94b9e2852727eb
4
+ data.tar.gz: ecb08a7c1a4e9b0b63fad299649632443a99f7b8e4752b486970d7656b862e46
5
5
  SHA512:
6
- metadata.gz: c1ac85ea8b774a230212583b33b9fdab3110fadab2fa1652810ea0913312c1fd20abe4471761b5ccd399d66a52995c0ddb6b803ee42aa4c4432add8a5524e51d
7
- data.tar.gz: b03da5caf2bf25683b2d61599a37f15582d26664553ef72096dfd3073aceb96224eaa46fd19c948f9f4fc143e3226ee176a4e66d762016aedf736fb903edae85
6
+ metadata.gz: eef06ea5a86751e6cfe68306a1d7f7e6c49fb0641c4916ddbdda30b4c1bec51a1349487dc156851229c4d496fe67cac96f0e5c875027998d0252bf154f2e02c9
7
+ data.tar.gz: 6fdf04c5e92b1de875ada2035b9fb495e80cf5138f4cd0171bd6686026a1280857c855ccdaa9bd15a915509f5bac4f9da50d08368f7de663c0a762b8654a3112
@@ -33,7 +33,7 @@ jobs:
33
33
  - name: Install dependencies
34
34
  run: bundle install
35
35
  - name: Download test cases
36
- run: git clone --depth 5 --branch v4.0.0 https://github.com/Unleash/client-specification.git client-specification
36
+ run: git clone --depth 5 --branch v4.1.0 https://github.com/Unleash/client-specification.git client-specification
37
37
  - name: rubocop
38
38
  uses: reviewdog/action-rubocop@v2
39
39
  with:
data/.rubocop.yml CHANGED
@@ -7,8 +7,9 @@ Naming/PredicateName:
7
7
  AllowedMethods:
8
8
  - is_enabled?
9
9
 
10
+
10
11
  Metrics/ClassLength:
11
- Max: 120
12
+ Max: 125
12
13
  Layout/LineLength:
13
14
  Max: 140
14
15
  Metrics/MethodLength:
@@ -16,13 +17,10 @@ Metrics/MethodLength:
16
17
  Metrics/BlockLength:
17
18
  Max: 110
18
19
  Exclude:
19
- - 'spec/unleash/configuration_spec.rb'
20
- - 'spec/unleash/client_spec.rb'
21
- - 'spec/unleash/context_spec.rb'
22
- - 'spec/unleash/feature_toggle_spec.rb'
20
+ - 'spec/**/*.rb'
23
21
 
24
22
  Metrics/AbcSize:
25
- Max: 28
23
+ Max: 30
26
24
  Metrics/CyclomaticComplexity:
27
25
  Max: 9
28
26
  Metrics/PerceivedComplexity:
data/README.md CHANGED
@@ -76,7 +76,7 @@ Argument | Description | Required? | Type | Default Value|
76
76
  `disable_metrics` | Disables sending metrics to Unleash server. | N | Boolean | `false` |
77
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 | {} |
78
78
  `timeout` | How long to wait for the connection to be established or wait in reading state (open_timeout/read_timeout) | N | Integer | 30 |
79
- `retry_limit` | How many consecutive failures in connecting to the Unleash server are allowed before giving up. Use `Float::INFINITY` if you would like it to never give up. | N | Numeric | 5 |
79
+ `retry_limit` | How many consecutive failures in connecting to the Unleash server are allowed before giving up. The default is to retry indefinitely. | N | Float::INFINITY | 5 |
80
80
  `backup_file` | Filename to store the last known state from the Unleash server. Best to not change this from the default. | N | String | `Dir.tmpdir + "/unleash-#{app_name}-repo.json` |
81
81
  `logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` |
82
82
  `log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::WARN` |
@@ -124,29 +124,97 @@ Unleash.configure do |config|
124
124
  end
125
125
 
126
126
  UNLEASH = Unleash::Client.new
127
+
128
+ # Or if preferred:
129
+ # Rails.configuration.unleash = Unleash::Client.new
130
+ ```
131
+ For `config.instance_id` use a string with a unique identification for the running instance.
132
+ For example: it could be the hostname, if you only run one App per host.
133
+ Or the docker container id, if you are running in docker.
134
+ If it is not set the client will generate an unique UUID for each execution.
135
+
136
+ To have it available in the `rails console` command as well, also add to the file above:
137
+ ```ruby
138
+ Rails.application.console do
139
+ UNLEASH = Unleash::Client.new
140
+ # or
141
+ # Rails.configuration.unleash = Unleash::Client.new
142
+ end
143
+ ```
144
+
145
+ #### Add Initializer if using [Puma in clustered mode](https://github.com/puma/puma#clustered-mode)
146
+
147
+ That is, multiple workers configured in `puma.rb`:
148
+ ```ruby
149
+ workers ENV.fetch("WEB_CONCURRENCY") { 2 }
127
150
  ```
128
- For `config.instance_id` use a string with a unique identification for the running instance. For example: it could be the hostname, if you only run one App per host. Or the docker container id, if you are running in docker. If it is not set the client will generate an unique UUID for each execution.
129
151
 
152
+ ##### with `preload_app!`
130
153
 
131
- #### Add Initializer if using [Puma](https://github.com/puma/puma)
154
+ Then you may keep the client configuration still in `config/initializers/unleash.rb`:
155
+ ```ruby
156
+ Unleash.configure do |config|
157
+ config.app_name = Rails.application.class.parent.to_s
158
+ config.environment = Rails.env
159
+ config.url = 'http://unleash.herokuapp.com/api'
160
+ config.custom_http_headers = {'Authorization': '<API token>'}
161
+ end
162
+ ```
132
163
 
133
- In `puma.rb` ensure that the unleash client is configured and instantiated as below, inside the `on_worker_boot` code block:
164
+ But you must ensure that the unleash client is instantiated only after the process is forked.
165
+ This is done by creating the client inside the `on_worker_boot` code block in `puma.rb` as below:
134
166
 
135
167
  ```ruby
168
+ #...
169
+ preload_app!
170
+ #...
171
+
136
172
  on_worker_boot do
137
173
  # ...
138
174
 
139
- Unleash.configure do |config|
140
- config.app_name = Rails.application.class.parent.to_s
141
- config.environment = Rails.env
142
- config.url = 'http://unleash.herokuapp.com/api'
143
- config.custom_http_headers = {'Authorization': '<API token>'}
144
- end
145
- Rails.configuration.unleash = Unleash::Client.new
175
+ ::UNLEASH = Unleash::Client.new
176
+ end
177
+
178
+ on_worker_shutdown do
179
+ ::UNLEASH.shutdown
146
180
  end
147
181
  ```
148
182
 
149
- Instead of the configuration in `config/initializers/unleash.rb`.
183
+ ##### without `preload_app!`
184
+
185
+ By not using `preload_app!`:
186
+ - the `Rails` constant will NOT be available.
187
+ - but phased restarts will be possible.
188
+
189
+ You need to ensure that in `puma.rb`:
190
+ - loading unleash sdk with `require 'unleash'` explicitly, as it will not be pre-loaded.
191
+ - all parameters must be explicitly set in the `on_worker_boot` block, as `config/initializers/unleash.rb` is not read.
192
+ - there are no references to `Rails` constant, as that is not yet available.
193
+
194
+ Example for `puma.rb`:
195
+ ```ruby
196
+ require 'unleash'
197
+
198
+ #...
199
+ # no preload_app!
200
+
201
+ on_worker_boot do
202
+ # ...
203
+
204
+ ::UNLEASH = Unleash::Client.new(
205
+ app_name: 'my_rails_app',
206
+ environment: 'development',
207
+ url: 'http://unleash.herokuapp.com/api',
208
+ custom_http_headers: {'Authorization': '<API token>'},
209
+ )
210
+ end
211
+
212
+ on_worker_shutdown do
213
+ ::UNLEASH.shutdown
214
+ end
215
+ ```
216
+
217
+ Note that we also added shutdown hooks in `on_worker_shutdown`, to ensure a clean shutdown.
150
218
 
151
219
  #### Add Initializer if using [Phusion Passenger](https://github.com/phusion/passenger)
152
220
 
@@ -171,6 +239,23 @@ PhusionPassenger.on_event(:starting_worker_process) do |forked|
171
239
  end
172
240
  ```
173
241
 
242
+ #### Add Initializer hooks when using within [Sidekiq](https://github.com/mperham/sidekiq)
243
+
244
+ Note that in this case we require that the code block for `Unleash.configure` is set beforehand.
245
+ For example in `config/initializers/unleash.rb`.
246
+
247
+ ```ruby
248
+ Sidekiq.configure_server do |config|
249
+ config.on(:startup) do
250
+ UNLEASH = Unleash::Client.new
251
+ end
252
+
253
+ config.on(:shutdown) do
254
+ UNLEASH.shutdown
255
+ end
256
+ end
257
+ ```
258
+
174
259
  #### Set Unleash::Context
175
260
 
176
261
  Be sure to add the following method and callback in the application controller to have `@unleash_context` set for all requests:
@@ -210,7 +295,8 @@ if Rails.configuration.unleash.is_enabled? "AwesomeFeature", @unleash_context
210
295
  end
211
296
  ```
212
297
 
213
- If the feature is not found in the server, it will by default return false. However you can override that by setting the default return value to `true`:
298
+ If the feature is not found in the server, it will by default return false.
299
+ However you can override that by setting the default return value to `true`:
214
300
 
215
301
  ```ruby
216
302
  if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context, true
@@ -275,7 +361,8 @@ puts "variant color is: #{variant.payload.fetch('color')}"
275
361
 
276
362
  ## Bootstrapping
277
363
 
278
- Bootstrap configuration allows the client to be initialized with a predefined set of toggle states. Bootstrapping can be configured by providing a bootstrap configuration when initializing the client.
364
+ Bootstrap configuration allows the client to be initialized with a predefined set of toggle states.
365
+ Bootstrapping can be configured by providing a bootstrap configuration when initializing the client.
279
366
  ```ruby
280
367
  @unleash = Unleash::Client.new(
281
368
  url: 'http://unleash.herokuapp.com/api',
@@ -295,7 +382,8 @@ The `Bootstrap::Configuration` initializer takes a hash with one of the followin
295
382
  * `data` - A raw JSON string as returned by the Unleash server.
296
383
  * `block` - A lambda containing custom logic if you need it, an example is provided below.
297
384
 
298
- You should only specify one type of bootstrapping since only one will be invoked and the others will be ignored. The order of preference is as follows:
385
+ You should only specify one type of bootstrapping since only one will be invoked and the others will be ignored.
386
+ The order of preference is as follows:
299
387
 
300
388
  - Select a data bootstrapper if it exists.
301
389
  - If no data bootstrapper exists, select the block bootstrapper.
@@ -324,11 +412,12 @@ custom_boostrapper = lambda {
324
412
  custom_http_headers: { 'Authorization': '<API token>' },
325
413
  bootstrap_config: Unleash::Bootstrap::Configuration.new({
326
414
  block: custom_boostrapper
327
- }
415
+ })
328
416
  )
329
417
  ```
330
418
 
331
- This example could be easily achieved with a file bootstrapper, this is just to illustrate the usage of custom bootstrapping. Be aware that the client initializer will block until bootstrapping is complete.
419
+ This example could be easily achieved with a file bootstrapper, this is just to illustrate the usage of custom bootstrapping.
420
+ Be aware that the client initializer will block until bootstrapping is complete.
332
421
 
333
422
  #### Client methods
334
423
 
@@ -95,7 +95,7 @@ module Unleash
95
95
  self.refresh_interval = 10
96
96
  self.metrics_interval = 60
97
97
  self.timeout = 30
98
- self.retry_limit = 5
98
+ self.retry_limit = Float::INFINITY
99
99
  self.backup_file = nil
100
100
  self.log_level = Logger::WARN
101
101
  self.bootstrap_config = nil
@@ -1,26 +1,104 @@
1
+ require 'date'
2
+
1
3
  module Unleash
2
4
  class Constraint
3
- attr_accessor :context_name, :operator, :values
5
+ attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
6
+
7
+ OPERATORS = {
8
+ IN: ->(context_v, constraint_v){ constraint_v.include? context_v },
9
+ NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v },
10
+ STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
11
+ STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
12
+ STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
13
+ NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } },
14
+ NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } },
15
+ NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } },
16
+ NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } },
17
+ NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } },
18
+ DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } },
19
+ DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
20
+ SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
21
+ SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } },
22
+ SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } }
23
+ }.freeze
4
24
 
5
- VALID_OPERATORS = ['IN', 'NOT_IN'].freeze
25
+ LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
6
26
 
7
- def initialize(context_name, operator, values = [])
27
+ def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
8
28
  raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
9
- raise ArgumentError, "operator does not hold a valid value:" + VALID_OPERATORS unless VALID_OPERATORS.include? operator
10
- raise ArgumentError, "values does not hold an Array" unless values.is_a?(Array)
29
+ raise ArgumentError, "operator does not hold a valid value:" + OPERATORS.keys unless OPERATORS.include? operator.to_sym
30
+
31
+ self.validate_constraint_value_type(operator.to_sym, value)
11
32
 
12
33
  self.context_name = context_name
13
- self.operator = operator
14
- self.values = values
34
+ self.operator = operator.to_sym
35
+ self.value = value
36
+ self.inverted = !!inverted
37
+ self.case_insensitive = !!case_insensitive
15
38
  end
16
39
 
17
40
  def matches_context?(context)
18
- Unleash.logger.debug "Unleash::Constraint matches_context? values: #{self.values} context.get_by_name(#{self.context_name})" \
41
+ Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" \
19
42
  " #{context.get_by_name(self.context_name)} "
43
+ match = matches_constraint?(context)
44
+ self.inverted ? !match : match
45
+ rescue KeyError
46
+ Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
47
+ found on the context"
48
+ false
49
+ end
50
+
51
+ def self.on_valid_date(val1, val2)
52
+ val1 = DateTime.parse(val1)
53
+ val2 = DateTime.parse(val2)
54
+ yield(val1, val2)
55
+ rescue ArgumentError
56
+ Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
57
+ or constraint_value (#{val2}) into a date. Returning false!"
58
+ false
59
+ end
60
+
61
+ def self.on_valid_float(val1, val2)
62
+ val1 = Float(val1)
63
+ val2 = Float(val2)
64
+ yield(val1, val2)
65
+ rescue ArgumentError
66
+ Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
67
+ or constraint_value (#{val2}) into a number. Returning false!"
68
+ false
69
+ end
70
+
71
+ def self.on_valid_version(val1, val2)
72
+ val1 = Gem::Version.new(val1)
73
+ val2 = Gem::Version.new(val2)
74
+ yield(val1, val2)
75
+ rescue ArgumentError
76
+ Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
77
+ or constraint_value (#{val2}) into a version. Return false!"
78
+ false
79
+ end
80
+
81
+ # This should be a private method but for some reason this fails on Ruby 2.5
82
+ def validate_constraint_value_type(operator, value)
83
+ raise ArgumentError, "context_name is not an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
84
+ raise ArgumentError, "context_name is not a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
85
+ end
86
+
87
+ private
88
+
89
+ def matches_constraint?(context)
90
+ unless OPERATORS.include?(self.operator)
91
+ Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
92
+ false
93
+ end
94
+
95
+ v = self.value.dup
96
+ context_value = context.get_by_name(self.context_name)
20
97
 
21
- is_included = self.values.include? context.get_by_name(self.context_name)
98
+ v.map!(&:upcase) if self.case_insensitive
99
+ context_value.upcase! if self.case_insensitive
22
100
 
23
- operator == 'IN' ? is_included : !is_included
101
+ OPERATORS[self.operator].call(context_value, v)
24
102
  end
25
103
  end
26
104
  end
@@ -1,6 +1,6 @@
1
1
  module Unleash
2
2
  class Context
3
- ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address].freeze
3
+ ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address, :current_time].freeze
4
4
 
5
5
  attr_accessor(*[ATTRS, :properties].flatten)
6
6
 
@@ -12,6 +12,7 @@ module Unleash
12
12
  self.user_id = value_for('userId', params)
13
13
  self.session_id = value_for('sessionId', params)
14
14
  self.remote_address = value_for('remoteAddress', params)
15
+ self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
15
16
 
16
17
  properties = value_for('properties', params)
17
18
  self.properties = properties.is_a?(Hash) ? properties.transform_keys(&:to_sym) : {}
@@ -139,7 +139,9 @@ module Unleash
139
139
  Constraint.new(
140
140
  c.fetch('contextName'),
141
141
  c.fetch('operator'),
142
- c.fetch('values')
142
+ c.fetch('values', nil) || c.fetch('value', nil),
143
+ inverted: c.fetch('inverted', false),
144
+ case_insensitive: c.fetch('caseInsensitive', false)
143
145
  )
144
146
  end
145
147
  )
@@ -1,3 +1,3 @@
1
1
  module Unleash
2
- VERSION = "4.1.0".freeze
2
+ VERSION = "4.2.0".freeze
3
3
  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.1.0
4
+ version: 4.2.0
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-02-11 00:00:00.000000000 Z
11
+ date: 2022-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: murmurhash3