unleash 3.2.5 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/bin/unleash-client CHANGED
@@ -12,11 +12,13 @@ options = {
12
12
  url: 'http://localhost:4242',
13
13
  demo: false,
14
14
  disable_metrics: true,
15
+ custom_http_headers: {},
15
16
  sleep: 0.1
16
17
  }
17
18
 
18
19
  OptionParser.new do |opts|
19
- opts.banner = "Usage: #{__FILE__} [options] feature [key1=val1] [key2=val2]"
20
+ opts.banner = "Usage: #{__FILE__} [options] feature [contextKey1=val1] [contextKey2=val2] \n\n" \
21
+ "Where contextKey1 could be user_id, session_id, remote_address or any field in the Context class (or any property within it).\n"
20
22
 
21
23
  opts.on("-V", "--variant", "Fetch variant for feature") do |v|
22
24
  options[:variant] = v
@@ -46,6 +48,13 @@ OptionParser.new do |opts|
46
48
  options[:sleep] = s
47
49
  end
48
50
 
51
+ opts.on("-H", "--http-headers='Authorization: *:developement.secretstring'",
52
+ "Adds http headers to all requests on the unleash server. Use multiple times for multiple headers.") do |h|
53
+ http_header_as_hash = [h].to_h{ |l| l.split(": ") }.transform_keys(&:to_sym)
54
+
55
+ options[:custom_http_headers].merge!(http_header_as_hash)
56
+ end
57
+
49
58
  opts.on("-h", "--help", "Prints this help") do
50
59
  puts opts
51
60
  exit
@@ -70,10 +79,11 @@ log_level = \
70
79
  url: options[:url],
71
80
  app_name: 'unleash-client-ruby-cli',
72
81
  disable_metrics: options[:metrics],
82
+ custom_http_headers: options[:custom_http_headers],
73
83
  log_level: log_level
74
84
  )
75
85
 
76
- context_params = ARGV.map{ |e| e.split("=") }.map{ |k, v| [k.to_sym, v] }.to_h
86
+ context_params = ARGV.to_h{ |l| l.split("=") }.transform_keys(&:to_sym)
77
87
  context_properties = context_params.reject{ |k, _v| [:user_id, :session_id, :remote_address].include? k }
78
88
  context_params.select!{ |k, _v| [:user_id, :session_id, :remote_address].include? k }
79
89
  context_params.merge!(properties: context_properties) unless context_properties.nil?
@@ -97,12 +107,12 @@ if options[:demo]
97
107
  end
98
108
  elsif options[:variant]
99
109
  variant = @unleash.get_variant(feature_name, unleash_context)
100
- puts " For feature \'#{feature_name}\' got variant \'#{variant}\'"
110
+ puts " For feature '#{feature_name}' got variant '#{variant}'"
101
111
  else
102
112
  if @unleash.is_enabled?(feature_name, unleash_context)
103
- puts " \'#{feature_name}\' is enabled according to unleash"
113
+ puts " '#{feature_name}' is enabled according to unleash"
104
114
  else
105
- puts " \'#{feature_name}\' is disabled according to unleash"
115
+ puts " '#{feature_name}' is disabled according to unleash"
106
116
  end
107
117
  end
108
118
 
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'unleash'
4
+ require 'unleash/context'
5
+ require 'unleash/bootstrap/configuration'
6
+
7
+ puts ">> START bootstrap.rb"
8
+
9
+ @unleash = Unleash::Client.new(
10
+ url: 'http://unleash.herokuapp.com/api',
11
+ custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
12
+ app_name: 'bootstrap-test',
13
+ instance_id: 'local-test-cli',
14
+ refresh_interval: 2,
15
+ disable_client: true,
16
+ disable_metrics: true,
17
+ metrics_interval: 2,
18
+ retry_limit: 2,
19
+ bootstrap_config: Unleash::Bootstrap::Configuration.new(file_path: "examples/default-toggles.json")
20
+ )
21
+
22
+ feature_name = "featureX"
23
+ unleash_context = Unleash::Context.new
24
+ unleash_context.user_id = 123
25
+
26
+ sleep 1
27
+ 3.times do
28
+ if @unleash.is_enabled?(feature_name, unleash_context)
29
+ puts "> #{feature_name} is enabled"
30
+ else
31
+ puts "> #{feature_name} is not enabled"
32
+ end
33
+ sleep 1
34
+ puts "---"
35
+ puts ""
36
+ puts ""
37
+ end
38
+
39
+ sleep 3
40
+ feature_name = "foobar"
41
+ if @unleash.is_enabled?(feature_name, unleash_context, true)
42
+ puts "> #{feature_name} is enabled"
43
+ else
44
+ puts "> #{feature_name} is not enabled"
45
+ end
46
+
47
+ puts "> shutting down client..."
48
+
49
+ @unleash.shutdown
50
+
51
+ puts ">> END bootstrap.rb"
@@ -0,0 +1,42 @@
1
+ {
2
+ "version": 1,
3
+ "features": [
4
+ {
5
+ "name": "featureX",
6
+ "enabled": true,
7
+ "strategies": [
8
+ {
9
+ "name": "default"
10
+ }
11
+ ]
12
+ },
13
+ {
14
+ "name": "featureY",
15
+ "enabled": false,
16
+ "strategies": [
17
+ {
18
+ "name": "baz",
19
+ "parameters": {
20
+ "foo": "bar"
21
+ }
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "name": "featureZ",
27
+ "enabled": true,
28
+ "strategies": [
29
+ {
30
+ "name": "default"
31
+ },
32
+ {
33
+ "name": "hola",
34
+ "parameters": {
35
+ "name": "val"
36
+ }
37
+ }
38
+ ]
39
+ }
40
+ ]
41
+ }
42
+
data/examples/simple.rb CHANGED
@@ -7,6 +7,7 @@ puts ">> START simple.rb"
7
7
 
8
8
  # Unleash.configure do |config|
9
9
  # config.url = 'http://unleash.herokuapp.com/api'
10
+ # config.custom_http_headers = { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' }
10
11
  # config.app_name = 'simple-test'
11
12
  # config.refresh_interval = 2
12
13
  # config.metrics_interval = 2
@@ -17,13 +18,13 @@ puts ">> START simple.rb"
17
18
  # or:
18
19
 
19
20
  @unleash = Unleash::Client.new(
20
- url: 'https://app.unleash-hosted.com/demo/api',
21
+ url: 'http://unleash.herokuapp.com/api',
22
+ custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
21
23
  app_name: 'simple-test',
22
24
  instance_id: 'local-test-cli',
23
25
  refresh_interval: 2,
24
26
  metrics_interval: 2,
25
- retry_limit: 2,
26
- custom_http_headers: {'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0'},
27
+ retry_limit: 2
27
28
  )
28
29
 
29
30
  # feature_name = "AwesomeFeature"
@@ -0,0 +1,25 @@
1
+ module Unleash
2
+ module Bootstrap
3
+ class Configuration
4
+ attr_accessor :data, :file_path, :url, :url_headers, :block
5
+
6
+ def initialize(opts = {})
7
+ self.file_path = resolve_value_indifferently(opts, 'file_path') || ENV['UNLEASH_BOOTSTRAP_FILE'] || nil
8
+ self.url = resolve_value_indifferently(opts, 'url') || ENV['UNLEASH_BOOTSTRAP_URL'] || nil
9
+ self.url_headers = resolve_value_indifferently(opts, 'url_headers')
10
+ self.data = resolve_value_indifferently(opts, 'data')
11
+ self.block = resolve_value_indifferently(opts, 'block')
12
+ end
13
+
14
+ def valid?
15
+ ![self.data, self.file_path, self.url, self.block].all?(&:nil?)
16
+ end
17
+
18
+ private
19
+
20
+ def resolve_value_indifferently(opts, key)
21
+ opts[key] || opts[key.to_sym]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ require 'unleash/bootstrap/provider/from_url'
2
+ require 'unleash/bootstrap/provider/from_file'
3
+
4
+ module Unleash
5
+ module Bootstrap
6
+ class Handler
7
+ attr_accessor :configuration
8
+
9
+ def initialize(configuration)
10
+ self.configuration = configuration
11
+ end
12
+
13
+ # @return [String] JSON string representing data returned from an Unleash server
14
+ def retrieve_toggles
15
+ return configuration.data unless self.configuration.data.nil?
16
+ return configuration.block.call if self.configuration.block.is_a?(Proc)
17
+ return Provider::FromFile.read(configuration.file_path) unless self.configuration.file_path.nil?
18
+ return Provider::FromUrl.read(configuration.url, configuration.url_headers) unless self.configuration.url.nil?
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module Unleash
2
+ module Bootstrap
3
+ module Provider
4
+ class NotImplemented < RuntimeError
5
+ end
6
+
7
+ class Base
8
+ def read
9
+ raise NotImplemented, "Bootstrap is not implemented"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require 'unleash/bootstrap/provider/base'
2
+
3
+ module Unleash
4
+ module Bootstrap
5
+ module Provider
6
+ class FromFile < Base
7
+ # @param file_path [String]
8
+ def self.read(file_path)
9
+ File.read(file_path)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ require 'unleash/bootstrap/provider/base'
2
+
3
+ module Unleash
4
+ module Bootstrap
5
+ module Provider
6
+ class FromUrl < Base
7
+ # @param url [String]
8
+ # @param headers [Hash, nil] HTTP headers to use. If not set, the unleash client SDK ones will be used.
9
+ def self.read(url, headers = nil)
10
+ response = Unleash::Util::Http.get(URI.parse(url), nil, headers)
11
+
12
+ return nil if response.code != '200'
13
+
14
+ response.body
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -18,8 +18,9 @@ module Unleash
18
18
  Unleash.logger = Unleash.configuration.logger.clone
19
19
  Unleash.logger.level = Unleash.configuration.log_level
20
20
 
21
+ Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
21
22
  if Unleash.configuration.disable_client
22
- Unleash.logger.warn "Unleash::Client is disabled! Will only return default results!"
23
+ Unleash.logger.warn "Unleash::Client is disabled! Will only return default (or bootstrapped if available) results!"
23
24
  return
24
25
  end
25
26
 
@@ -28,13 +29,14 @@ module Unleash
28
29
  start_metrics unless Unleash.configuration.disable_metrics
29
30
  end
30
31
 
31
- def is_enabled?(feature, context = nil, default_value = false)
32
+ def is_enabled?(feature, context = nil, default_value_param = false, &fallback_blk)
32
33
  Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}"
33
34
 
34
- if Unleash.configuration.disable_client
35
- Unleash.logger.warn "unleash_client is disabled! Always returning #{default_value} for feature #{feature}!"
36
- return default_value
37
- end
35
+ default_value = if block_given?
36
+ default_value_param || !!fallback_blk.call(feature, context)
37
+ else
38
+ default_value_param
39
+ end
38
40
 
39
41
  toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
40
42
 
@@ -56,19 +58,19 @@ module Unleash
56
58
  yield(blk) if is_enabled?(feature, context, default_value)
57
59
  end
58
60
 
59
- def get_variant(feature, context = nil, fallback_variant = nil)
61
+ def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant)
60
62
  Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"
61
63
 
62
64
  if Unleash.configuration.disable_client
63
65
  Unleash.logger.debug "unleash_client is disabled! Always returning #{fallback_variant} for feature #{feature}!"
64
- return fallback_variant || Unleash::FeatureToggle.disabled_variant
66
+ return fallback_variant
65
67
  end
66
68
 
67
69
  toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first
68
70
 
69
71
  if toggle_as_hash.nil?
70
72
  Unleash.logger.debug "Unleash::Client.get_variant feature: #{feature} not found"
71
- return fallback_variant || Unleash::FeatureToggle.disabled_variant
73
+ return fallback_variant
72
74
  end
73
75
 
74
76
  toggle = Unleash::FeatureToggle.new(toggle_as_hash)
@@ -76,7 +78,7 @@ module Unleash
76
78
 
77
79
  if variant.nil?
78
80
  Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found"
79
- return fallback_variant || Unleash::FeatureToggle.disabled_variant
81
+ return fallback_variant
80
82
  end
81
83
 
82
84
  # TODO: Add to README: name, payload, enabled (bool)
@@ -88,7 +90,7 @@ module Unleash
88
90
  def shutdown
89
91
  unless Unleash.configuration.disable_client
90
92
  Unleash.toggle_fetcher.save!
91
- Unleash.reporter.send unless Unleash.configuration.disable_metrics
93
+ Unleash.reporter.post unless Unleash.configuration.disable_metrics
92
94
  shutdown!
93
95
  end
94
96
  end
@@ -115,11 +117,11 @@ module Unleash
115
117
  end
116
118
 
117
119
  def start_toggle_fetcher
118
- Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
119
120
  self.fetcher_scheduled_executor = Unleash::ScheduledExecutor.new(
120
121
  'ToggleFetcher',
121
122
  Unleash.configuration.refresh_interval,
122
- Unleash.configuration.retry_limit
123
+ Unleash.configuration.retry_limit,
124
+ first_fetch_is_eager
123
125
  )
124
126
  self.fetcher_scheduled_executor.run do
125
127
  Unleash.toggle_fetcher.fetch
@@ -135,7 +137,7 @@ module Unleash
135
137
  Unleash.configuration.retry_limit
136
138
  )
137
139
  self.metrics_scheduled_executor.run do
138
- Unleash.reporter.send
140
+ Unleash.reporter.post
139
141
  end
140
142
  end
141
143
 
@@ -144,12 +146,20 @@ module Unleash
144
146
 
145
147
  # Send the request, if possible
146
148
  begin
147
- response = Unleash::Util::Http.post(Unleash.configuration.client_register_url, info.to_json)
149
+ response = Unleash::Util::Http.post(Unleash.configuration.client_register_uri, info.to_json)
148
150
  rescue StandardError => e
149
151
  Unleash.logger.error "unable to register client with unleash server due to exception #{e.class}:'#{e}'."
150
152
  Unleash.logger.error "stacktrace: #{e.backtrace}"
151
153
  end
152
154
  Unleash.logger.debug "client registered: #{response}"
153
155
  end
156
+
157
+ def disabled_variant
158
+ @disabled_variant ||= Unleash::FeatureToggle.disabled_variant
159
+ end
160
+
161
+ def first_fetch_is_eager
162
+ Unleash.configuration.use_bootstrap?
163
+ end
154
164
  end
155
165
  end
@@ -1,5 +1,6 @@
1
1
  require 'securerandom'
2
2
  require 'tmpdir'
3
+ require 'unleash/bootstrap/configuration'
3
4
 
4
5
  module Unleash
5
6
  class Configuration
@@ -8,6 +9,7 @@ module Unleash
8
9
  :app_name,
9
10
  :environment,
10
11
  :instance_id,
12
+ :project_name,
11
13
  :custom_http_headers,
12
14
  :disable_client,
13
15
  :disable_metrics,
@@ -17,7 +19,8 @@ module Unleash
17
19
  :metrics_interval,
18
20
  :backup_file,
19
21
  :logger,
20
- :log_level
22
+ :log_level,
23
+ :bootstrap_config
21
24
 
22
25
  def initialize(opts = {})
23
26
  ensure_valid_opts(opts)
@@ -41,7 +44,7 @@ module Unleash
41
44
  end
42
45
 
43
46
  def refresh_backup_file!
44
- self.backup_file = Dir.tmpdir + "/unleash-#{app_name}-repo.json" if self.backup_file.nil?
47
+ self.backup_file = File.join(Dir.tmpdir, "unleash-#{app_name}-repo.json")
45
48
  end
46
49
 
47
50
  def http_headers
@@ -51,16 +54,26 @@ module Unleash
51
54
  }.merge(custom_http_headers.dup)
52
55
  end
53
56
 
54
- def fetch_toggles_url
55
- self.url + '/client/features'
57
+ def fetch_toggles_uri
58
+ uri = URI("#{self.url_stripped_of_slash}/client/features")
59
+ uri.query = "project=#{self.project_name}" unless self.project_name.nil?
60
+ uri
56
61
  end
57
62
 
58
- def client_metrics_url
59
- self.url + '/client/metrics'
63
+ def client_metrics_uri
64
+ URI("#{self.url_stripped_of_slash}/client/metrics")
60
65
  end
61
66
 
62
- def client_register_url
63
- self.url + '/client/register'
67
+ def client_register_uri
68
+ URI("#{self.url_stripped_of_slash}/client/register")
69
+ end
70
+
71
+ def url_stripped_of_slash
72
+ self.url.delete_suffix '/'
73
+ end
74
+
75
+ def use_bootstrap?
76
+ self.bootstrap_config&.valid?
64
77
  end
65
78
 
66
79
  private
@@ -76,14 +89,16 @@ module Unleash
76
89
  self.environment = 'default'
77
90
  self.url = nil
78
91
  self.instance_id = SecureRandom.uuid
92
+ self.project_name = nil
79
93
  self.disable_client = false
80
94
  self.disable_metrics = false
81
95
  self.refresh_interval = 10
82
- self.metrics_interval = 30
96
+ self.metrics_interval = 60
83
97
  self.timeout = 30
84
- self.retry_limit = 1
98
+ self.retry_limit = Float::INFINITY
85
99
  self.backup_file = nil
86
100
  self.log_level = Logger::WARN
101
+ self.bootstrap_config = nil
87
102
 
88
103
  self.custom_http_headers = {}
89
104
  end
@@ -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) : {}
@@ -28,7 +29,7 @@ module Unleash
28
29
  if ATTRS.include? normalized_name
29
30
  self.send(normalized_name)
30
31
  else
31
- self.properties.fetch(normalized_name)
32
+ self.properties.fetch(normalized_name, nil) || self.properties.fetch(name.to_sym)
32
33
  end
33
34
  end
34
35