From fb3bdf880a613e1522632142f95f882a50c0d16b Mon Sep 17 00:00:00 2001 From: sighphyre Date: Mon, 18 Sep 2023 15:34:07 +0200 Subject: [PATCH 01/35] feat: splat apply patch from local work --- Gemfile | 2 + diff | 3231 +++++++++++++++++ lib/unleash.rb | 5 +- lib/unleash/activation_strategy.rb | 31 - lib/unleash/client.rb | 36 +- lib/unleash/configuration.rb | 6 +- lib/unleash/constraint.rb | 115 - lib/unleash/context.rb | 16 + lib/unleash/feature_toggle.rb | 177 - lib/unleash/metrics.rb | 41 - lib/unleash/metrics_reporter.rb | 29 +- lib/unleash/strategies.rb | 80 - lib/unleash/strategy/application_hostname.rb | 26 - lib/unleash/strategy/base.rb | 16 - lib/unleash/strategy/default.rb | 13 - lib/unleash/strategy/flexible_rollout.rb | 55 - .../strategy/gradual_rollout_random.rb | 24 - .../strategy/gradual_rollout_sessionid.rb | 21 - .../strategy/gradual_rollout_userid.rb | 21 - lib/unleash/strategy/remote_address.rb | 36 - lib/unleash/strategy/user_with_id.rb | 20 - lib/unleash/strategy/util.rb | 16 - lib/unleash/toggle_fetcher.rb | 39 +- spec/unleash/activation_strategy_spec.rb | 42 - spec/unleash/client_specification_spec.rb | 42 +- spec/unleash/constraint_spec.rb | 458 --- spec/unleash/feature_toggle_spec.rb | 663 ---- spec/unleash/metrics_reporter_spec.rb | 40 +- spec/unleash/metrics_spec.rb | 72 - spec/unleash/strategies_spec.rb | 156 - .../strategy/application_hostname_spec.rb | 29 - spec/unleash/strategy/base_spec.rb | 11 - spec/unleash/strategy/default_spec.rb | 11 - .../unleash/strategy/flexible_rollout_spec.rb | 64 - .../strategy/gradual_rollout_random_spec.rb | 32 - .../gradual_rollout_sessionid_spec.rb | 22 - .../strategy/gradual_rollout_userid_spec.rb | 21 - spec/unleash/strategy/remote_address_spec.rb | 79 - spec/unleash/strategy/user_with_id_spec.rb | 61 - spec/unleash/strategy/util_spec.rb | 10 - spec/unleash/toggle_fetcher_spec.rb | 84 +- 41 files changed, 3377 insertions(+), 2576 deletions(-) create mode 100644 diff delete mode 100644 lib/unleash/activation_strategy.rb delete mode 100644 lib/unleash/constraint.rb delete mode 100644 lib/unleash/metrics.rb delete mode 100644 lib/unleash/strategies.rb delete mode 100644 lib/unleash/strategy/application_hostname.rb delete mode 100644 lib/unleash/strategy/base.rb delete mode 100644 lib/unleash/strategy/default.rb delete mode 100644 lib/unleash/strategy/flexible_rollout.rb delete mode 100644 lib/unleash/strategy/gradual_rollout_random.rb delete mode 100644 lib/unleash/strategy/gradual_rollout_sessionid.rb delete mode 100644 lib/unleash/strategy/gradual_rollout_userid.rb delete mode 100644 lib/unleash/strategy/remote_address.rb delete mode 100644 lib/unleash/strategy/user_with_id.rb delete mode 100644 lib/unleash/strategy/util.rb delete mode 100644 spec/unleash/activation_strategy_spec.rb delete mode 100644 spec/unleash/constraint_spec.rb delete mode 100644 spec/unleash/feature_toggle_spec.rb delete mode 100644 spec/unleash/metrics_spec.rb delete mode 100644 spec/unleash/strategies_spec.rb delete mode 100644 spec/unleash/strategy/application_hostname_spec.rb delete mode 100644 spec/unleash/strategy/base_spec.rb delete mode 100644 spec/unleash/strategy/default_spec.rb delete mode 100644 spec/unleash/strategy/flexible_rollout_spec.rb delete mode 100644 spec/unleash/strategy/gradual_rollout_random_spec.rb delete mode 100644 spec/unleash/strategy/gradual_rollout_sessionid_spec.rb delete mode 100644 spec/unleash/strategy/gradual_rollout_userid_spec.rb delete mode 100644 spec/unleash/strategy/remote_address_spec.rb delete mode 100644 spec/unleash/strategy/user_with_id_spec.rb delete mode 100644 spec/unleash/strategy/util_spec.rb diff --git a/Gemfile b/Gemfile index 17fb50c6..f0bae376 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,6 @@ source 'https://rubygems.org' +gem 'unleash-engine', path: '../yggdrasil/ruby-engine' + # Specify your gem's dependencies in unleash-client.gemspec gemspec diff --git a/diff b/diff new file mode 100644 index 00000000..83e3777b --- /dev/null +++ b/diff @@ -0,0 +1,3231 @@ +diff --git a/Gemfile b/Gemfile +index 17fb50c..f0bae37 100644 +--- a/Gemfile ++++ b/Gemfile +@@ -1,4 +1,6 @@ + source 'https://rubygems.org' + ++gem 'unleash-engine', path: '../yggdrasil/ruby-engine' ++ + # Specify your gem's dependencies in unleash-client.gemspec + gemspec +diff --git a/lib/unleash.rb b/lib/unleash.rb +index 9a6f1c7..260cf40 100644 +--- a/lib/unleash.rb ++++ b/lib/unleash.rb +@@ -1,6 +1,5 @@ + require 'unleash/version' + require 'unleash/configuration' +-require 'unleash/strategies' + require 'unleash/context' + require 'unleash/client' + require 'logger' +@@ -9,7 +8,7 @@ module Unleash + TIME_RESOLUTION = 3 + + class << self +- attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :segment_cache, :logger ++ attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :segment_cache, :logger, :engine + end + + self.configuration = Unleash::Configuration.new +@@ -26,6 +25,6 @@ module Unleash + end + + def self.strategies +- self.configuration.strategies ++ nil + end + end +diff --git a/lib/unleash/activation_strategy.rb b/lib/unleash/activation_strategy.rb +deleted file mode 100644 +index 29feb0c..0000000 +--- a/lib/unleash/activation_strategy.rb ++++ /dev/null +@@ -1,31 +0,0 @@ +-module Unleash +- class ActivationStrategy +- attr_accessor :name, :params, :constraints, :disabled +- +- def initialize(name, params, constraints = []) +- self.name = name +- self.disabled = false +- +- if params.is_a?(Hash) +- self.params = params +- elsif params.nil? +- self.params = {} +- else +- Unleash.logger.warn "Invalid params provided for ActivationStrategy (params:#{params})" +- self.params = {} +- end +- +- if constraints.is_a?(Array) && constraints.each{ |c| c.is_a?(Constraint) } +- self.constraints = constraints +- else +- Unleash.logger.warn "Invalid constraints provided for ActivationStrategy (contraints: #{constraints})" +- self.disabled = true +- self.constraints = [] +- end +- end +- +- def matches_context?(context) +- self.constraints.any?{ |c| c.matches_context? context } +- end +- end +-end +diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb +index 2191caf..4b53300 100644 +--- a/lib/unleash/client.rb ++++ b/lib/unleash/client.rb +@@ -17,6 +17,7 @@ module Unleash + + Unleash.logger = Unleash.configuration.logger.clone + Unleash.logger.level = Unleash.configuration.log_level ++ Unleash.engine = UnleashEngine.new + + Unleash.toggle_fetcher = Unleash::ToggleFetcher.new + if Unleash.configuration.disable_client +@@ -40,16 +41,18 @@ module Unleash + default_value_param + end + +- toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first ++ Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}" + +- if toggle_as_hash.nil? ++ toggle_enabled = Unleash&.engine&.enabled?(feature, context) ++ if toggle_enabled.nil? + Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found" ++ Unleash&.engine&.count_toggle(feature, false) + return default_value + end + +- toggle = Unleash::FeatureToggle.new(toggle_as_hash, Unleash&.segment_cache) ++ Unleash&.engine&.count_toggle(feature, toggle_enabled) + +- toggle.is_enabled?(context) ++ toggle_enabled + end + + def is_disabled?(feature, context = nil, default_value_param = true, &fallback_blk) +@@ -74,22 +77,20 @@ module Unleash + def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant) + Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}" + +- toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first +- +- if toggle_as_hash.nil? +- Unleash.logger.debug "Unleash::Client.get_variant feature: #{feature} not found" +- return fallback_variant ++ toggle_enabled = Unleash&.engine&.enabled?(feature, context) ++ if toggle_enabled.nil? ++ Unleash&.engine&.count_toggle(feature, false) ++ else ++ Unleash&.engine&.count_toggle(feature, toggle_enabled) + end + +- toggle = Unleash::FeatureToggle.new(toggle_as_hash) +- variant = toggle.get_variant(context, fallback_variant) +- +- if variant.nil? +- Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found" ++ variant_response = Unleash&.engine.get_variant(feature, context) ++ if variant_response.code < 0 ++ Unleash&.engine&.count_variant(feature, fallback_variant.name) + return fallback_variant + end +- +- # TODO: Add to README: name, payload, enabled (bool) ++ variant = variant_response.variant ++ Unleash&.engine&.count_variant(feature, variant.name) + + variant + end +@@ -118,7 +119,7 @@ module Unleash + 'appName': Unleash.configuration.app_name, + 'instanceId': Unleash.configuration.instance_id, + 'sdkVersion': "unleash-client-ruby:" + Unleash::VERSION, +- 'strategies': Unleash.strategies.keys, ++ 'strategies': nil, + 'started': Time.now.iso8601(Unleash::TIME_RESOLUTION), + 'interval': Unleash.configuration.metrics_interval_in_millis + } +@@ -137,7 +138,6 @@ module Unleash + end + + def start_metrics +- Unleash.toggle_metrics = Unleash::Metrics.new + Unleash.reporter = Unleash::MetricsReporter.new + self.metrics_scheduled_executor = Unleash::ScheduledExecutor.new( + 'MetricsReporter', +diff --git a/lib/unleash/configuration.rb b/lib/unleash/configuration.rb +index 4f43200..dddd90f 100644 +--- a/lib/unleash/configuration.rb ++++ b/lib/unleash/configuration.rb +@@ -40,9 +40,9 @@ module Unleash + def validate! + return if self.disable_client + +- raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? || self.url.nil? ++ raise ArgumentError, "app_name is a required parameter." if self.app_name.nil? + +- validate_custom_http_headers!(self.custom_http_headers) ++ validate_custom_http_headers!(self.custom_http_headers) unless self.url.nil? + end + + def refresh_backup_file! +@@ -96,7 +96,7 @@ module Unleash + self.backup_file = nil + self.log_level = Logger::WARN + self.bootstrap_config = nil +- self.strategies = Unleash::Strategies.new ++ self.strategies = nil + + self.custom_http_headers = {} + end +diff --git a/lib/unleash/constraint.rb b/lib/unleash/constraint.rb +deleted file mode 100644 +index 51607c1..0000000 +--- a/lib/unleash/constraint.rb ++++ /dev/null +@@ -1,115 +0,0 @@ +-require 'date' +-module Unleash +- class Constraint +- attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive +- +- OPERATORS = { +- IN: ->(context_v, constraint_v){ constraint_v.include? context_v.to_s }, +- NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v.to_s }, +- STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } }, +- STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } }, +- STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } }, +- NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } }, +- NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } }, +- NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } }, +- NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } }, +- NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } }, +- DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } }, +- DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } }, +- SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } }, +- SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } }, +- SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } }, +- FALLBACK_VALIDATOR: ->(_context_v, _constraint_v){ false } +- }.freeze +- +- LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze +- +- def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false) +- raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String) +- +- unless OPERATORS.include? operator.to_sym +- Unleash.logger.warn "Operator #{operator} is not a supported operator, " \ +- "falling back to FALLBACK_VALIDATOR which skips this constraint." +- operator = "FALLBACK_VALIDATOR" +- end +- self.log_inconsistent_constraint_configuration(operator.to_sym, value) +- +- self.context_name = context_name +- self.operator = operator.to_sym +- self.value = value +- self.inverted = !!inverted +- self.case_insensitive = !!case_insensitive +- end +- +- def matches_context?(context) +- Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" +- return false if context.nil? +- +- match = matches_constraint?(context) +- self.inverted ? !match : match +- rescue KeyError +- Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \ +- found on the context" +- false +- end +- +- def self.on_valid_date(val1, val2) +- val1 = DateTime.parse(val1) +- val2 = DateTime.parse(val2) +- yield(val1, val2) +- rescue ArgumentError +- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \ +- or constraint_value (#{val2}) into a date. Returning false!" +- false +- end +- +- def self.on_valid_float(val1, val2) +- val1 = Float(val1) +- val2 = Float(val2) +- yield(val1, val2) +- rescue ArgumentError +- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \ +- or constraint_value (#{val2}) into a number. Returning false!" +- false +- end +- +- def self.on_valid_version(val1, val2) +- val1 = Gem::Version.new(val1) +- val2 = Gem::Version.new(val2) +- yield(val1, val2) +- rescue ArgumentError +- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \ +- or constraint_value (#{val2}) into a version. Return false!" +- false +- end +- +- # This should be a private method but for some reason this fails on Ruby 2.5 +- def log_inconsistent_constraint_configuration(operator, value) +- Unleash.logger.warn "value is a String, operator is expecting an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String) +- Unleash.logger.warn "value is an Array, operator is expecting a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array) +- end +- +- private +- +- def matches_constraint?(context) +- Unleash.logger.debug "Unleash::Constraint matches_constraint? value: #{self.value} operator: #{self.operator} " \ +- " context.get_by_name(#{self.context_name})" +- +- unless OPERATORS.include?(self.operator) +- Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false." +- false +- end +- +- # when the operator is NOT_IN and there is no data, return true. In all other cases the operator doesn't match. +- return self.operator == :NOT_IN unless context.include?(self.context_name) +- +- v = self.value.dup +- context_value = context.get_by_name(self.context_name) +- +- v.map!(&:upcase) if self.case_insensitive +- context_value.upcase! if self.case_insensitive +- +- OPERATORS[self.operator].call(context_value, v) +- end +- end +-end +diff --git a/lib/unleash/context.rb b/lib/unleash/context.rb +index 98ba467..9e235ce 100644 +--- a/lib/unleash/context.rb ++++ b/lib/unleash/context.rb +@@ -23,6 +23,22 @@ module Unleash + ",app_name=#{@app_name},environment=#{@environment}>" + end + ++ def as_json ++ { ++ appName: self.app_name, ++ environment: self.environment, ++ userId: self.user_id, ++ sessionId: self.session_id, ++ remoteAddress: self.remote_address, ++ currentTime: self.current_time, ++ properties: self.properties ++ } ++ end ++ ++ def to_json(*options) ++ as_json(*options).to_json(*options) ++ end ++ + def to_h + ATTRS.map{ |attr| [attr, self.send(attr)] }.to_h.merge(properties: @properties) + end +diff --git a/lib/unleash/feature_toggle.rb b/lib/unleash/feature_toggle.rb +index ec064b3..02020e5 100644 +--- a/lib/unleash/feature_toggle.rb ++++ b/lib/unleash/feature_toggle.rb +@@ -1,187 +1,10 @@ +-require 'unleash/activation_strategy' +-require 'unleash/constraint' + require 'unleash/variant_definition' + require 'unleash/variant' +-require 'unleash/strategy/util' +-require 'securerandom' + + module Unleash + class FeatureToggle +- attr_accessor :name, :enabled, :strategies, :variant_definitions +- +- def initialize(params = {}, segment_map = {}) +- params = {} if params.nil? +- +- self.name = params.fetch('name', nil) +- self.enabled = params.fetch('enabled', false) +- +- self.strategies = initialize_strategies(params, segment_map) +- self.variant_definitions = initialize_variant_definitions(params) +- end +- +- def to_s +- "" +- end +- +- def is_enabled?(context) +- result = am_enabled?(context) +- +- choice = result ? :yes : :no +- Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics +- +- result +- end +- +- def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant) +- raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant' +- +- context = ensure_valid_context(context) +- +- toggle_enabled = am_enabled?(context) +- variant = resolve_variant(context, toggle_enabled) +- +- choice = toggle_enabled ? :yes : :no +- Unleash.toggle_metrics.increment_variant(self.name, choice, variant.name) unless Unleash.configuration.disable_metrics +- variant +- end +- + def self.disabled_variant + Unleash::Variant.new(name: 'disabled', enabled: false) + end +- +- private +- +- def resolve_variant(context, toggle_enabled) +- return Unleash::FeatureToggle.disabled_variant unless toggle_enabled +- return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0 +- +- variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness) +- end +- +- def resolve_stickiness +- self.variant_definitions&.map(&:stickiness)&.compact&.first || "default" +- end +- +- # only check if it is enabled, do not do metrics +- def am_enabled?(context) +- result = +- if self.enabled +- self.strategies.empty? || +- self.strategies.any? do |s| +- strategy_enabled?(s, context) && strategy_constraint_matches?(s, context) +- end +- else +- false +- end +- +- Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \ +- "and Strategies combined with contraints returned #{result})" +- +- result +- end +- +- def strategy_enabled?(strategy, context) +- r = Unleash.strategies.fetch(strategy.name).is_enabled?(strategy.params, context) +- Unleash.logger.debug "Unleash::FeatureToggle.strategy_enabled? Strategy #{strategy.name} returned #{r} with context: #{context}" +- r +- end +- +- def strategy_constraint_matches?(strategy, context) +- return false if strategy.disabled +- +- strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) } +- end +- +- def sum_variant_defs_weights +- self.variant_definitions.map(&:weight).reduce(0, :+) +- end +- +- def variant_salt(context, stickiness = "default") +- begin +- return context.get_by_name(stickiness) if !context.nil? && stickiness != "default" +- rescue KeyError +- Unleash.logger.warn "Custom stickiness key (#{stickiness}) not found in the provided context #{context}. " \ +- "Falling back to default behavior." +- end +- return context.user_id unless context&.user_id.to_s.empty? +- return context.session_id unless context&.session_id.to_s.empty? +- return context.remote_address unless context&.remote_address.to_s.empty? +- +- SecureRandom.random_number +- end +- +- def variant_from_override_match(context) +- variant = self.variant_definitions.find{ |vd| vd.override_matches_context?(context) } +- return nil if variant.nil? +- +- Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload) +- end +- +- def variant_from_weights(context, stickiness) +- variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context, stickiness), self.name, sum_variant_defs_weights) +- prev_weights = 0 +- +- variant_definition = self.variant_definitions +- .find do |v| +- res = (prev_weights + v.weight >= variant_weight) +- prev_weights += v.weight +- res +- end +- return self.disabled_variant if variant_definition.nil? +- +- Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload) +- end +- +- def ensure_valid_context(context) +- unless ['NilClass', 'Unleash::Context'].include? context.class.name +- Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, " \ +- "please use Unleash::Context. Context set to nil." +- context = nil +- end +- context +- end +- +- def initialize_strategies(params, segment_map) +- params.fetch('strategies', []) +- .select{ |s| s.has_key?('name') && Unleash.strategies.includes?(s['name']) } +- .map do |s| +- ActivationStrategy.new( +- s['name'], +- s['parameters'], +- resolve_constraints(s, segment_map) +- ) +- end || [] +- end +- +- def resolve_constraints(strategy, segment_map) +- segment_constraints = (strategy["segments"] || []).map do |segment_id| +- segment_map[segment_id]&.fetch("constraints") +- end +- (strategy.fetch("constraints", []) + segment_constraints).flatten.map do |constraint| +- return nil if constraint.nil? +- +- Constraint.new( +- constraint.fetch('contextName'), +- constraint.fetch('operator'), +- constraint.fetch('value', nil) || constraint.fetch('values', nil), +- inverted: constraint.fetch('inverted', false), +- case_insensitive: constraint.fetch('caseInsensitive', false) +- ) +- end +- end +- +- def initialize_variant_definitions(params) +- (params.fetch('variants', []) || []) +- .select{ |v| v.is_a?(Hash) && v.has_key?('name') } +- .map do |v| +- VariantDefinition.new( +- v.fetch('name', ''), +- v.fetch('weight', 0), +- v.fetch('payload', nil), +- v.fetch('stickiness', nil), +- v.fetch('overrides', []) +- ) +- end || [] +- end + end + end +diff --git a/lib/unleash/metrics.rb b/lib/unleash/metrics.rb +deleted file mode 100644 +index 6342ade..0000000 +--- a/lib/unleash/metrics.rb ++++ /dev/null +@@ -1,41 +0,0 @@ +-module Unleash +- class Metrics +- attr_accessor :features, :features_lock +- +- def initialize +- self.features = {} +- self.features_lock = Mutex.new +- end +- +- def to_s +- self.features_lock.synchronize do +- return self.features.to_json +- end +- end +- +- def increment(feature, choice) +- raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice +- +- self.features_lock.synchronize do +- self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature +- self.features[feature][choice] += 1 +- end +- end +- +- def increment_variant(feature, choice, variant) +- self.features_lock.synchronize do +- self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature +- self.features[feature][choice] += 1 +- self.features[feature]['variants'] = {} unless self.features[feature].include? 'variants' +- self.features[feature]['variants'][variant] = 0 unless self.features[feature]['variants'].include? variant +- self.features[feature]['variants'][variant] += 1 +- end +- end +- +- def reset +- self.features_lock.synchronize do +- self.features = {} +- end +- end +- end +-end +diff --git a/lib/unleash/metrics_reporter.rb b/lib/unleash/metrics_reporter.rb +index fc1e7ca..4cd4340 100755 +--- a/lib/unleash/metrics_reporter.rb ++++ b/lib/unleash/metrics_reporter.rb +@@ -1,5 +1,4 @@ + require 'unleash/configuration' +-require 'unleash/metrics' + require 'net/http' + require 'json' + require 'time' +@@ -15,22 +14,17 @@ module Unleash + end + + def generate_report +- now = Time.now +- +- start = self.last_time +- stop = now +- self.last_time = now +- ++ puts "Making report" ++ metrics = Unleash&.engine&.get_metrics() ++ if metrics.nil? || metrics.empty? ++ puts "nothing here" ++ return nil ++ end + report = { + 'appName': Unleash.configuration.app_name, + 'instanceId': Unleash.configuration.instance_id, +- 'bucket': { +- 'start': start.iso8601(Unleash::TIME_RESOLUTION), +- 'stop': stop.iso8601(Unleash::TIME_RESOLUTION), +- 'toggles': Unleash.toggle_metrics.features +- } ++ 'bucket': metrics + } +- Unleash.toggle_metrics.reset + + report + end +@@ -38,13 +32,14 @@ module Unleash + def post + Unleash.logger.debug "post() Report" + +- if bucket_empty? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes... ++ bucket = self.generate_report ++ if bucket.nil? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes... + Unleash.logger.debug "Report not posted to server, as it would have been empty. (and has been empty for up to 10 min)" + + return + end + +- response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, self.generate_report.to_json) ++ response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, bucket.to_json) + + if ['200', '202'].include? response.code + Unleash.logger.debug "Report sent to unleash server successfully. Server responded with http code #{response.code}" +@@ -54,9 +49,5 @@ module Unleash + end + + private +- +- def bucket_empty? +- Unleash.toggle_metrics.features.empty? +- end + end + end +diff --git a/lib/unleash/strategies.rb b/lib/unleash/strategies.rb +deleted file mode 100644 +index 842af4f..0000000 +--- a/lib/unleash/strategies.rb ++++ /dev/null +@@ -1,80 +0,0 @@ +-require 'unleash/strategy/base' +-Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path } +- +-module Unleash +- class Strategies +- def initialize +- @strategies = {} +- register_strategies +- end +- +- def keys +- @strategies.keys +- end +- +- def includes?(name) +- @strategies.has_key?(name.to_s) +- end +- +- def fetch(name) +- raise Unleash::Strategy::NotImplemented, "Strategy is not implemented" unless (strategy = @strategies[name.to_s]) +- +- strategy +- end +- +- def add(strategy) +- @strategies[strategy.name] = strategy +- end +- +- def []=(key, strategy) +- warn_deprecated_registration(strategy, 'modifying Unleash::STRATEGIES') +- @strategies[key.to_s] = strategy +- end +- +- def [](key) +- @strategies[key.to_s] +- end +- +- def register_strategies +- register_base_strategies +- register_custom_strategies +- end +- +- protected +- +- # Deprecated: Use Unleash.configuration to add custom strategies +- def register_custom_strategies +- Unleash::Strategy.constants +- .select{ |c| Unleash::Strategy.const_get(c).is_a? Class } +- .reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) } # Reject abstract classes +- .map{ |c| Object.const_get("Unleash::Strategy::#{c}") } +- .reject{ |c| DEFAULT_STRATEGIES.include?(c) } # Reject base classes +- .each do |c| +- strategy = c.new +- warn_deprecated_registration(strategy, 'adding custom class into Unleash::Strategy namespace') +- self.add(strategy) +- end +- end +- +- def register_base_strategies +- DEFAULT_STRATEGIES.each{ |c| self.add(c.new) } +- end +- +- DEFAULT_STRATEGIES = [ +- Unleash::Strategy::ApplicationHostname, +- Unleash::Strategy::Default, +- Unleash::Strategy::FlexibleRollout, +- Unleash::Strategy::GradualRolloutRandom, +- Unleash::Strategy::GradualRolloutSessionId, +- Unleash::Strategy::GradualRolloutUserId, +- Unleash::Strategy::RemoteAddress, +- Unleash::Strategy::UserWithId +- ].freeze +- +- def warn_deprecated_registration(strategy, method) +- warn "[DEPRECATED] Registering custom Unleash strategy by #{method} is deprecated. +- Please use Unleash configuration to register custom strategy: " \ +- "`Unleash.configure {|c| c.strategies.add(#{strategy.class.name}.new) }`" +- end +- end +-end +diff --git a/lib/unleash/strategy/application_hostname.rb b/lib/unleash/strategy/application_hostname.rb +deleted file mode 100644 +index f5fd578..0000000 +--- a/lib/unleash/strategy/application_hostname.rb ++++ /dev/null +@@ -1,26 +0,0 @@ +-require 'socket' +- +-module Unleash +- module Strategy +- class ApplicationHostname < Base +- attr_accessor :hostname +- +- PARAM = 'hostnames'.freeze +- +- def initialize +- self.hostname = Socket.gethostname || 'undefined' +- end +- +- def name +- 'applicationHostname' +- end +- +- # need: :params['hostnames'] +- def is_enabled?(params = {}, _context = nil) +- return false unless params.is_a?(Hash) && params.has_key?(PARAM) +- +- params[PARAM].split(",").map(&:strip).map(&:downcase).include?(self.hostname) +- end +- end +- end +-end +diff --git a/lib/unleash/strategy/base.rb b/lib/unleash/strategy/base.rb +deleted file mode 100644 +index 3e3a0f0..0000000 +--- a/lib/unleash/strategy/base.rb ++++ /dev/null +@@ -1,16 +0,0 @@ +-module Unleash +- module Strategy +- class NotImplemented < RuntimeError +- end +- +- class Base +- def name +- raise NotImplemented, "Strategy is not implemented" +- end +- +- def is_enabled?(_params = {}, _context = nil) +- raise NotImplemented, "Strategy is not implemented" +- end +- end +- end +-end +diff --git a/lib/unleash/strategy/default.rb b/lib/unleash/strategy/default.rb +deleted file mode 100644 +index d22cdbe..0000000 +--- a/lib/unleash/strategy/default.rb ++++ /dev/null +@@ -1,13 +0,0 @@ +-module Unleash +- module Strategy +- class Default < Base +- def name +- 'default' +- end +- +- def is_enabled?(_params = {}, _context = nil) +- true +- end +- end +- end +-end +diff --git a/lib/unleash/strategy/flexible_rollout.rb b/lib/unleash/strategy/flexible_rollout.rb +deleted file mode 100644 +index edb8256..0000000 +--- a/lib/unleash/strategy/flexible_rollout.rb ++++ /dev/null +@@ -1,55 +0,0 @@ +-require 'unleash/strategy/util' +- +-module Unleash +- module Strategy +- class FlexibleRollout < Base +- def name +- 'flexibleRollout' +- end +- +- # need: params['percentage'] +- def is_enabled?(params = {}, context = nil) +- return false unless params.is_a?(Hash) +- return false unless context.instance_of?(Unleash::Context) +- +- stickiness = params.fetch('stickiness', 'default') +- stickiness_id = resolve_stickiness(stickiness, context) +- +- begin +- percentage = Integer(params.fetch('rollout', 0)) +- percentage = 0 if percentage > 100 || percentage.negative? +- rescue ArgumentError +- return false +- end +- +- group_id = params.fetch('groupId', '') +- normalized_number = Util.get_normalized_number(stickiness_id, group_id) +- +- return false if stickiness_id.nil? +- +- (percentage.positive? && normalized_number <= percentage) +- end +- +- private +- +- def random +- Random.rand(0..100) +- end +- +- def resolve_stickiness(stickiness, context) +- case stickiness +- when 'random' +- random +- when 'default' +- context.user_id || context.session_id || random +- else +- begin +- context.get_by_name(stickiness) +- rescue KeyError +- nil +- end +- end +- end +- end +- end +-end +diff --git a/lib/unleash/strategy/gradual_rollout_random.rb b/lib/unleash/strategy/gradual_rollout_random.rb +deleted file mode 100644 +index 61d0784..0000000 +--- a/lib/unleash/strategy/gradual_rollout_random.rb ++++ /dev/null +@@ -1,24 +0,0 @@ +-require 'unleash/strategy/util' +- +-module Unleash +- module Strategy +- class GradualRolloutRandom < Base +- def name +- 'gradualRolloutRandom' +- end +- +- # need: params['percentage'] +- def is_enabled?(params = {}, _context = nil) +- return false unless params.is_a?(Hash) && params.has_key?('percentage') +- +- begin +- percentage = Integer(params['percentage'] || 0) +- rescue ArgumentError +- return false +- end +- +- (percentage >= Random.rand(1..100)) +- end +- end +- end +-end +diff --git a/lib/unleash/strategy/gradual_rollout_sessionid.rb b/lib/unleash/strategy/gradual_rollout_sessionid.rb +deleted file mode 100644 +index 0f2a553..0000000 +--- a/lib/unleash/strategy/gradual_rollout_sessionid.rb ++++ /dev/null +@@ -1,21 +0,0 @@ +-require 'unleash/strategy/util' +- +-module Unleash +- module Strategy +- class GradualRolloutSessionId < Base +- def name +- 'gradualRolloutSessionId' +- end +- +- # need: params['percentage'], params['groupId'], context.user_id, +- def is_enabled?(params = {}, context = nil) +- return false unless params.is_a?(Hash) && params.has_key?('percentage') +- return false unless context.instance_of?(Unleash::Context) +- return false if context.session_id.nil? || context.session_id.empty? +- +- percentage = Integer(params['percentage'] || 0) +- (percentage.positive? && Util.get_normalized_number(context.session_id, params['groupId'] || "") <= percentage) +- end +- end +- end +-end +diff --git a/lib/unleash/strategy/gradual_rollout_userid.rb b/lib/unleash/strategy/gradual_rollout_userid.rb +deleted file mode 100644 +index 1aa3c05..0000000 +--- a/lib/unleash/strategy/gradual_rollout_userid.rb ++++ /dev/null +@@ -1,21 +0,0 @@ +-require 'unleash/strategy/util' +- +-module Unleash +- module Strategy +- class GradualRolloutUserId < Base +- def name +- 'gradualRolloutUserId' +- end +- +- # need: params['percentage'], params['groupId'], context.user_id, +- def is_enabled?(params = {}, context = nil, _constraints = []) +- return false unless params.is_a?(Hash) && params.has_key?('percentage') +- return false unless context.instance_of?(Unleash::Context) +- return false if context.user_id.nil? || context.user_id.empty? +- +- percentage = Integer(params['percentage'] || 0) +- (percentage.positive? && Util.get_normalized_number(context.user_id, params['groupId'] || "") <= percentage) +- end +- end +- end +-end +diff --git a/lib/unleash/strategy/remote_address.rb b/lib/unleash/strategy/remote_address.rb +deleted file mode 100644 +index d222311..0000000 +--- a/lib/unleash/strategy/remote_address.rb ++++ /dev/null +@@ -1,36 +0,0 @@ +-module Unleash +- module Strategy +- class RemoteAddress < Base +- PARAM = 'IPs'.freeze +- +- def name +- 'remoteAddress' +- end +- +- # need: params['IPs'], context.remote_address +- def is_enabled?(params = {}, context = nil) +- return false unless params.is_a?(Hash) && params.has_key?(PARAM) +- return false unless params.fetch(PARAM, nil).is_a? String +- return false unless context.instance_of?(Unleash::Context) +- +- remote_address = ipaddr_or_nil_from_str(context.remote_address) +- +- params[PARAM] +- .split(',') +- .map(&:strip) +- .map{ |ipblock| ipaddr_or_nil_from_str(ipblock) } +- .compact +- .map{ |ipb| ipb.include? remote_address } +- .any? +- end +- +- private +- +- def ipaddr_or_nil_from_str(ip) +- IPAddr.new(ip) +- rescue StandardError +- nil +- end +- end +- end +-end +diff --git a/lib/unleash/strategy/user_with_id.rb b/lib/unleash/strategy/user_with_id.rb +deleted file mode 100644 +index c20a75b..0000000 +--- a/lib/unleash/strategy/user_with_id.rb ++++ /dev/null +@@ -1,20 +0,0 @@ +-module Unleash +- module Strategy +- class UserWithId < Base +- PARAM = 'userIds'.freeze +- +- def name +- 'userWithId' +- end +- +- # requires: params['userIds'], context.user_id, +- def is_enabled?(params = {}, context = nil) +- return false unless params.is_a?(Hash) && params.has_key?(PARAM) +- return false unless params.fetch(PARAM, nil).is_a? String +- return false unless context.instance_of?(Unleash::Context) +- +- params[PARAM].split(",").map(&:strip).include?(context.user_id) +- end +- end +- end +-end +diff --git a/lib/unleash/strategy/util.rb b/lib/unleash/strategy/util.rb +deleted file mode 100644 +index a00ade8..0000000 +--- a/lib/unleash/strategy/util.rb ++++ /dev/null +@@ -1,16 +0,0 @@ +-require 'murmurhash3' +- +-module Unleash +- module Strategy +- module Util +- module_function +- +- NORMALIZER = 100 +- +- # convert the two strings () into a number between 1 and base (100 by default) +- def get_normalized_number(identifier, group_id, base = NORMALIZER) +- MurmurHash3::V32.str_hash("#{group_id}:#{identifier}") % base + 1 +- end +- end +- end +-end +diff --git a/lib/unleash/toggle_fetcher.rb b/lib/unleash/toggle_fetcher.rb +index 2b33f4d..41361ef 100755 +--- a/lib/unleash/toggle_fetcher.rb ++++ b/lib/unleash/toggle_fetcher.rb +@@ -2,14 +2,14 @@ require 'unleash/configuration' + require 'unleash/bootstrap/handler' + require 'net/http' + require 'json' ++require 'unleash_engine' + + module Unleash + class ToggleFetcher +- attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache ++ attr_accessor :toggle_engine, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache + + def initialize + self.etag = nil +- self.toggle_cache = nil + self.segment_cache = nil + self.toggle_lock = Mutex.new + self.toggle_resource = ConditionVariable.new +@@ -35,8 +35,8 @@ module Unleash + def toggles + self.toggle_lock.synchronize do + # wait for resource, only if it is null +- self.toggle_resource.wait(self.toggle_lock) if self.toggle_cache.nil? +- return self.toggle_cache ++ self.toggle_resource.wait(self.toggle_lock) if self.toggle_engine.nil? ++ return self.toggle_engine + end + end + +@@ -55,16 +55,16 @@ module Unleash + end + + self.etag = response['ETag'] +- features = get_features(response.body) ++ engine = get_engine(response.body) + + # always synchronize with the local cache when fetching: +- synchronize_with_local_cache!(features) ++ synchronize_with_local_cache!(engine) + + update_running_client! +- save! ++ save! response.body + end + +- def save! ++ def save!(toggle_data) + Unleash.logger.debug "Will save toggles to disk now" + + backup_file = Unleash.configuration.backup_file +@@ -72,7 +72,7 @@ module Unleash + + self.toggle_lock.synchronize do + File.open(backup_file_tmp, "w") do |file| +- file.write(self.toggle_cache.to_json) ++ file.write(toggle_data) + end + File.rename(backup_file_tmp, backup_file) + end +@@ -84,10 +84,10 @@ module Unleash + + private + +- def synchronize_with_local_cache!(features) +- if self.toggle_cache != features ++ def synchronize_with_local_cache!(engine) ++ if self.toggle_engine != engine + self.toggle_lock.synchronize do +- self.toggle_cache = features ++ self.toggle_engine = engine + end + + # notify all threads waiting for this resource to no longer wait +@@ -96,10 +96,8 @@ module Unleash + end + + def update_running_client! +- if Unleash.toggles != self.toggles["features"] || Unleash.segment_cache != self.toggles["segments"] +- Unleash.logger.info "Updating toggles to main client, there has been a change in the server." +- Unleash.toggles = self.toggles["features"] +- Unleash.segment_cache = self.toggles["segments"] ++ if Unleash.engine != self.toggle_engine ++ Unleash.engine = self.toggle_engine + end + end + +@@ -121,7 +119,7 @@ module Unleash + + def bootstrap + bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles +- synchronize_with_local_cache! get_features bootstrap_payload ++ synchronize_with_local_cache! get_engine bootstrap_payload + update_running_client! + + # reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again +@@ -134,10 +132,15 @@ module Unleash + segments_array.map{ |segment| [segment["id"], segment] }.to_h + end + ++ def get_engine(response_body) ++ engine = UnleashEngine.new ++ engine.take_state(response_body) ++ engine ++ end ++ + # @param response_body [String] + def get_features(response_body) + response_hash = JSON.parse(response_body) +- + if response_hash['version'] >= 1 + return { "features" => response_hash["features"], "segments" => build_segment_map(response_hash["segments"]) } + end +diff --git a/spec/unleash/activation_strategy_spec.rb b/spec/unleash/activation_strategy_spec.rb +deleted file mode 100644 +index 812dbe0..0000000 +--- a/spec/unleash/activation_strategy_spec.rb ++++ /dev/null +@@ -1,42 +0,0 @@ +-require 'unleash/constraint' +- +-RSpec.describe Unleash::ActivationStrategy do +- before do +- Unleash.configuration = Unleash::Configuration.new +- Unleash.logger = Unleash.configuration.logger +- end +- +- let(:name) { 'test name' } +- +- describe '#initialize' do +- context 'with correct payload' do +- let(:params) { Hash.new(test: true) } +- let(:constraints) { [Unleash::Constraint.new("constraint_name", "IN", ["value"])] } +- +- it 'initializes with correct attributes' do +- expect(Unleash.logger).to_not receive(:warn) +- +- strategy = Unleash::ActivationStrategy.new(name, params, constraints) +- +- expect(strategy.name).to eq name +- expect(strategy.params).to eq params +- expect(strategy.constraints).to eq constraints +- end +- end +- +- context 'with incorrect payload' do +- let(:params) { 'bad_params' } +- let(:constraints) { [] } +- +- it 'initializes with correct attributes and logs warning' do +- expect(Unleash.logger).to receive(:warn) +- +- strategy = Unleash::ActivationStrategy.new(name, params, constraints) +- +- expect(strategy.name).to eq name +- expect(strategy.params).to eq({}) +- expect(strategy.constraints).to eq(constraints) +- end +- end +- end +-end +diff --git a/spec/unleash/client_specification_spec.rb b/spec/unleash/client_specification_spec.rb +index 621ed4f..a45ffbe 100644 +--- a/spec/unleash/client_specification_spec.rb ++++ b/spec/unleash/client_specification_spec.rb +@@ -10,37 +10,30 @@ RSpec.describe Unleash::Client do + DEFAULT_VARIANT = Unleash::Variant.new(name: 'unknown', enabled: false).freeze + + before do +- Unleash.configuration = Unleash::Configuration.new + Unleash.logger = Unleash.configuration.logger + Unleash.logger.level = Unleash.configuration.log_level +- Unleash.toggles = [] +- Unleash.toggle_metrics = {} +- +- # Do not test metrics: +- Unleash.configuration.disable_metrics = true + end + + if File.exist?(SPECIFICATION_PATH + '/index.json') + JSON.parse(File.read(SPECIFICATION_PATH + '/index.json')).each do |test_file| + describe "for #{test_file}" do + current_test_set = JSON.parse(File.read(SPECIFICATION_PATH + '/' + test_file)) ++ + context "with #{current_test_set.fetch('name')} " do +- # name = current_test_set.fetch('name', '') + tests = current_test_set.fetch('tests', []) + state = current_test_set.fetch('state', {}) +- state_features = state.fetch('features', []) +- state_segments = state.fetch('segments', []).map{ |segment| [segment["id"], segment] }.to_h +- +- let(:unleash_toggles) { state_features } +- + tests.each do |test| + it "test that #{test['description']}" do +- test_toggle = unleash_toggles.select{ |t| t.fetch('name', '') == test.fetch('toggleName') }.first +- +- toggle = Unleash::FeatureToggle.new(test_toggle, state_segments) + context = Unleash::Context.new(test['context']) + +- toggle_result = toggle.is_enabled?(context) ++ unleash = Unleash::Client.new( ++ app_name: 'bootstrap-test', ++ instance_id: 'local-test-cli', ++ disable_client: true, ++ disable_metrics: true, ++ bootstrap_config: Unleash::Bootstrap::Configuration.new(data: current_test_set.fetch('state', {}).to_json) ++ ) ++ toggle_result = unleash.is_enabled?(test.fetch('toggleName'), context) + + expect(toggle_result).to eq(test['expectedResult']) + end +@@ -49,14 +42,21 @@ RSpec.describe Unleash::Client do + variant_tests = current_test_set.fetch('variantTests', []) + variant_tests.each do |test| + it "test that #{test['description']}" do +- test_toggle = unleash_toggles.select{ |t| t.fetch('name', '') == test.fetch('toggleName') }.first +- +- toggle = Unleash::FeatureToggle.new(test_toggle, state_segments) + context = Unleash::Context.new(test['context']) + +- variant = toggle.get_variant(context, DEFAULT_VARIANT) ++ unleash = Unleash::Client.new( ++ app_name: 'bootstrap-test', ++ instance_id: 'local-test-cli', ++ disable_client: true, ++ disable_metrics: true, ++ bootstrap_config: Unleash::Bootstrap::Configuration.new(data: current_test_set.fetch('state', {}).to_json) ++ ) ++ variant = unleash.get_variant(test.fetch('toggleName'), context) ++ expectedResult = test['expectedResult'] + +- expect(variant).to eq(Unleash::Variant.new(test['expectedResult'])) ++ expect(variant.name).to eq(expectedResult['name']) ++ expect(variant.enabled).to eq(expectedResult['enabled']) ++ expect(variant.payload).to eq(expectedResult['payload']) + end + end + end +diff --git a/spec/unleash/constraint_spec.rb b/spec/unleash/constraint_spec.rb +deleted file mode 100644 +index 38c1909..0000000 +--- a/spec/unleash/constraint_spec.rb ++++ /dev/null +@@ -1,458 +0,0 @@ +-RSpec.describe Unleash::Constraint do +- before do +- Unleash.configuration = Unleash::Configuration.new +- Unleash.logger = Unleash.configuration.logger +- end +- +- describe '#is_enabled?' do +- it 'matches based on property IN value' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- env: 'dev' +- } +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('env', 'IN', ['dev']) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'IN', ['dev', 'pre']) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['dev', 'pre']) +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['pre', 'prod']) +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on property NOT_IN value' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.2', +- properties: { +- env: 'dev' +- } +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['dev']) +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['dev', 'pre']) +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['pre', 'prod']) +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on a value NOT_IN in a not existing context field' do +- context_params = { +- properties: {} +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['anything']) +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on user_id IN/NOT_IN user_id' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.3', +- properties: { +- fancy: 'polarbear' +- } +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('user_id', 'IN', ['123', '456']) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('user_id', 'IN', ['456', '789']) +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['123', '456']) +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['456', '789']) +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on user_id IN/NOT_IN user_id with user_id as int' do +- context_params = { +- user_id: 123 +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('user_id', 'IN', ['123', '456']) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('user_id', 'IN', ['456', '789']) +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['123', '456']) +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['456', '789']) +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on property STR_STARTS_WITH value' do +- context_params = { +- properties: { +- env: 'development' +- } +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['dev']) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['development']) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['ment']) +- expect(constraint.matches_context?(context)).to be false +- end +- +- it 'matches based on property STR_ENDS_WITH value' do +- context_params = { +- properties: { +- env: 'development' +- } +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['ment']) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['development']) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['dev']) +- expect(constraint.matches_context?(context)).to be false +- end +- +- it 'matches based on property STR_CONTAINS value' do +- context_params = { +- properties: { +- env: 'development' +- } +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['ment']) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['dev']) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['development']) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['DEVELOPMENT']) +- expect(constraint.matches_context?(context)).to be false +- end +- +- it 'matches based on property NUM_EQ value' do +- context_params = { +- properties: { +- distance: '0.3' +- } +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('distance', 'NUM_EQ', '0.3') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('distance', 'NUM_EQ', '0.2') +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('distance', 'NUM_EQ', (0.1 + 0.2).to_s) +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on property NUM_LT value' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- distance: '3.141' +- } +- } +- context = Unleash::Context.new(context_params) +- +- constraint = Unleash::Constraint.new('distance', 'NUM_LT', '2.718') +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('distance', 'NUM_LT', '3.141') +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('distance', 'NUM_LT', '6.282') +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on property NUM_LTE value' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- distance: '3.141' +- } +- } +- context = Unleash::Context.new(context_params) +- +- constraint = Unleash::Constraint.new('distance', 'NUM_LTE', '2.718') +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('distance', 'NUM_LTE', '3.141') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('distance', 'NUM_LTE', '6.282') +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on property NUM_GT value' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- distance: '3.141' +- } +- } +- context = Unleash::Context.new(context_params) +- +- constraint = Unleash::Constraint.new('distance', 'NUM_GT', '2.718') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('distance', 'NUM_GT', '3.141') +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('distance', 'NUM_GT', '6.282') +- expect(constraint.matches_context?(context)).to be false +- end +- +- it 'matches based on property NUM_GTE value' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- distance: '3.141' +- } +- } +- context = Unleash::Context.new(context_params) +- +- constraint = Unleash::Constraint.new('distance', 'NUM_GTE', '2.718') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('distance', 'NUM_GTE', '3.141') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('distance', 'NUM_GTE', '6.282') +- expect(constraint.matches_context?(context)).to be false +- end +- +- it 'matches based on property SEMVER_EQ value' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- env: '3.1.41-beta' +- } +- } +- context = Unleash::Context.new(context_params) +- +- constraint = Unleash::Constraint.new('env', 'SEMVER_EQ', '3.1.41-beta') +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on property SEMVER_GT value' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- env: '3.1.41-gamma' +- } +- } +- context = Unleash::Context.new(context_params) +- +- constraint = Unleash::Constraint.new('env', 'SEMVER_GT', '3.1.41-beta') +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on property SEMVER_LT value' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- env: '3.1.41-alpha' +- } +- } +- context = Unleash::Context.new(context_params) +- +- constraint = Unleash::Constraint.new('env', 'SEMVER_LT', '3.1.41-beta') +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on property DATE_AFTER value' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- currentTime: '2022-01-30T13:00:00.000Z' +- } +- context = Unleash::Context.new(context_params) +- +- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-29T13:00:00.000Z') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-29T13:00:00Z') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-29T13:00Z') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59:59.999999Z') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59:59.999Z') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59:59') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59') +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T13:00:00.000Z') +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-31T13:00:00.000Z') +- expect(constraint.matches_context?(context)).to be false +- end +- +- it 'matches based on property DATE_BEFORE value' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- currentTime: '2022-01-30T13:00:00.000Z' +- } +- context = Unleash::Context.new(context_params) +- +- constraint = Unleash::Constraint.new('currentTime', 'DATE_BEFORE', '2022-01-29T13:00:00.000Z') +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('currentTime', 'DATE_BEFORE', '2022-01-31T13:00:00.000Z') +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on case insensitive property when operator is uppercased' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- env: 'development' +- } +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['DEV'], case_insensitive: true) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['MENT'], case_insensitive: true) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['LOP'], case_insensitive: true) +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on case insensitive property when context is uppercased' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- env: 'DEVELOPMENT' +- } +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['dev'], case_insensitive: true) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['ment'], case_insensitive: true) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['lop'], case_insensitive: true) +- expect(constraint.matches_context?(context)).to be true +- end +- +- it 'matches based on inverted property' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- env: 'development' +- } +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['dev'], inverted: true) +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['ment'], inverted: true) +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['lop'], inverted: true) +- expect(constraint.matches_context?(context)).to be false +- end +- +- it 'gracefully handles invalid constraint operators' do +- context_params = { +- user_id: '123', +- session_id: 'verylongsesssionid', +- remote_address: '127.0.0.1', +- properties: { +- env: 'development' +- } +- } +- context = Unleash::Context.new(context_params) +- constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', 'dev', inverted: true) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', ['dev'], inverted: true) +- expect(constraint.matches_context?(context)).to be true +- +- constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', 'dev') +- expect(constraint.matches_context?(context)).to be false +- +- constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', ['dev']) +- expect(constraint.matches_context?(context)).to be false +- end +- +- it 'warns about constraint construction for invalid value types for operator' do +- array_constraints = ['STR_CONTAINS', 'STR_ENDS_WITH', 'STR_STARTS_WITH', 'IN', 'NOT_IN'] +- +- array_constraints.each do |operator_name| +- expect(Unleash.logger).to receive(:warn).with("value is a String, operator is expecting an Array") +- Unleash::Constraint.new('env', operator_name, '') +- end +- +- string_constraints = ['NUM_EQ', 'NUM_GT', 'NUM_GTE', 'NUM_LT', 'NUM_LTE', +- 'DATE_AFTER', 'DATE_BEFORE', 'SEMVER_EQ', 'SEMVER_GT', 'SEMVER_LT'] +- string_constraints.each do |operator_name| +- expect(Unleash.logger).to receive(:warn).with("value is an Array, operator is expecting a String") +- Unleash::Constraint.new('env', operator_name, []) +- end +- end +- end +- +- it 'does resolves to false rather than crashing when passed a nil context' do +- constraint = Unleash::Constraint.new('anything', 'NUM_GTE', '6.282') +- expect(constraint.matches_context?(nil)).to be false +- end +-end +diff --git a/spec/unleash/feature_toggle_spec.rb b/spec/unleash/feature_toggle_spec.rb +deleted file mode 100644 +index a95f243..0000000 +--- a/spec/unleash/feature_toggle_spec.rb ++++ /dev/null +@@ -1,663 +0,0 @@ +-require 'logger' +-require 'unleash' +-require 'unleash/configuration' +-require 'unleash/context' +-require 'unleash/feature_toggle' +-require 'unleash/variant' +- +-RSpec.describe Unleash::FeatureToggle do +- before do +- Unleash.configuration = Unleash::Configuration.new +- Unleash.logger = Unleash.configuration.logger +- Unleash.logger.level = Unleash.configuration.log_level +- Unleash.logger.level = Logger::ERROR +- Unleash.toggles = [] +- Unleash.toggle_metrics = {} +- +- # Do not test metrics: +- Unleash.configuration.disable_metrics = true +- end +- +- describe 'FeatureToggle with empty strategies' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "test", +- "enabled" => true, +- "strategies" => [], +- "variants" => nil +- ) +- end +- +- it 'should return true if enabled' do +- context = Unleash::Context.new(user_id: 1) +- expect(feature_toggle.is_enabled?(context)).to be_truthy +- end +- end +- +- describe 'FeatureToggle with empty strategies and disabled toggle' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "Test.userid", +- "description" => nil, +- "enabled" => false, +- "strategies" => [], +- "variants" => nil, +- "createdAt" => "2019-01-24T10:41:45.236Z" +- ) +- end +- +- it 'should return false if disabled' do +- context = Unleash::Context.new(user_id: 1) +- expect(feature_toggle.is_enabled?(context)).to be_falsey +- end +- end +- +- describe 'FeatureToggle with userId strategy and enabled toggle' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "Test.userid", +- "description" => nil, +- "enabled" => true, +- "strategies" => [ +- { +- "name" => "userWithId", +- "parameters" => { +- "userIds" => "12345" +- } +- } +- ], +- "variants" => nil, +- "createdAt" => "2019-01-24T10:41:45.236Z" +- ) +- end +- +- it 'should return true if enabled and user_id is matched' do +- context = Unleash::Context.new(user_id: "12345") +- expect(feature_toggle.is_enabled?(context)).to be_truthy +- end +- +- it 'should return false if enabled and user_id is unmatched' do +- context = Unleash::Context.new(user_id: "54321") +- expect(feature_toggle.is_enabled?(context)).to be_falsey +- end +- end +- +- describe 'FeatureToggle with userId strategy and disabled toggle' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "Test.userid", +- "description" => nil, +- "enabled" => false, +- "strategies" => [ +- { +- "name" => "userWithId", +- "parameters" => { +- "userIds" => "12345" +- } +- } +- ], +- "variants" => nil, +- "createdAt" => "2019-01-24T10:41:45.236Z" +- ) +- end +- +- it 'should return false if disabled and user_id matched' do +- context = Unleash::Context.new(user_id: "12345") +- expect(feature_toggle.is_enabled?(context)).to be_falsey +- end +- +- it 'should return false if disabled and user_id unmatched' do +- context = Unleash::Context.new(user_id: "54321") +- expect(feature_toggle.is_enabled?(context)).to be_falsey +- end +- end +- +- describe 'FeatureToggle with variants' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "Test.variants", +- "description" => nil, +- "enabled" => true, +- "strategies" => [ +- { +- "name" => "default" +- } +- ], +- "variants" => [ +- { +- "name" => "variant1", +- "weight" => 50, +- "stickiness" => "default" +- }, +- { +- "name" => "variant2", +- "weight" => 50, +- "stickiness" => "default" +- } +- ], +- "createdAt" => "2019-01-24T10:41:45.236Z" +- ) +- end +- +- let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } +- +- it 'should return variant1 for user_id:1' do +- context = Unleash::Context.new(user_id: 10) +- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( +- name: "variant1", +- enabled: true, +- payload: nil +- ) +- end +- +- it 'should return variant2 for user_id:2' do +- context = Unleash::Context.new(user_id: 2) +- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( +- name: "variant2", +- enabled: true, +- payload: nil +- ) +- end +- +- xit 'should return false if default is false.' do +- context = Unleash::Context.new(user_id: 2) +- expect(feature_toggle.get_variant(context, default_variant)).to be_falsey +- end +- end +- +- describe 'FeatureToggle including weightless variants' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "Test.variants", +- "description" => nil, +- "enabled" => true, +- "strategies" => [ +- { +- "name" => "default" +- } +- ], +- "variants" => [ +- { +- "name" => "variantA", +- "weight" => 0, +- "stickiness" => "default" +- }, +- { +- "name" => "variantB", +- "weight" => 10, +- "stickiness" => "default" +- }, +- { +- "name" => "variantC", +- "weight" => 20, +- "stickiness" => "default" +- } +- ], +- "createdAt" => "2019-01-24T10:41:45.236Z" +- ) +- end +- +- let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } +- +- it 'should return variantC for user_id:1' do +- context = Unleash::Context.new(user_id: 10) +- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( +- name: "variantC", +- enabled: true, +- payload: nil +- ) +- end +- +- it 'should return variantB for user_id:2' do +- context = Unleash::Context.new(user_id: 2) +- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( +- name: "variantB", +- enabled: true, +- payload: nil +- ) +- end +- end +- +- describe 'FeatureToggle with variants which have all zero weight' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "Test.variants", +- "description" => nil, +- "enabled" => true, +- "strategies" => [ +- { +- "name" => "default" +- } +- ], +- "variants" => [ +- { +- "name" => "variantA", +- "weight" => 0 +- }, +- { +- "name" => "variantB", +- "weight" => 0 +- } +- ], +- "createdAt" => "2019-01-24T10:41:45.236Z" +- ) +- end +- let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } +- +- it 'should return disabled for user_id:1' do +- context = Unleash::Context.new(user_id: 10) +- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( +- name: "disabled", +- enabled: false, +- payload: nil +- ) +- end +- +- it 'should return disabled for user_id:2' do +- context = Unleash::Context.new(user_id: 2) +- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( +- name: "disabled", +- enabled: false, +- payload: nil +- ) +- end +- end +- +- describe 'FeatureToggle with variants that have a variant override' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "Test.variants", +- "description" => nil, +- "enabled" => true, +- "strategies" => [ +- { +- "name" => "default" +- } +- ], +- "variants" => [ +- { +- "name" => "variant1", +- "weight" => 50, +- "stickiness" => "default", +- "payload" => { +- "type" => "string", +- "value" => "val1" +- }, +- "overrides" => [{ +- "contextName" => "userId", +- "values" => ["132", "61"] +- }] +- }, +- { +- "name" => "variant2", +- "weight" => 50, +- "stickiness" => "default", +- "payload" => { +- "type" => "string", +- "value" => "val2" +- } +- } +- ], +- "createdAt" => "2019-01-24T10:41:45.236Z" +- ) +- end +- +- it 'should return variant1 for user_id:61 from override' do +- context = Unleash::Context.new(user_id: 61) +- expect(feature_toggle.get_variant(context)).to have_attributes( +- name: "variant1", +- enabled: true, +- payload: { "type" => "string", "value" => "val1" } +- ) +- end +- +- it 'should return variant1 for user_id:132 from override' do +- context = Unleash::Context.new("userId" => 132) +- expect(feature_toggle.get_variant(context)).to have_attributes( +- name: "variant1", +- enabled: true, +- payload: { "type" => "string", "value" => "val1" } +- ) +- end +- +- it 'should return variant2 for user_id:60' do +- context = Unleash::Context.new(user_id: 60) +- expect(feature_toggle.get_variant(context)).to have_attributes( +- name: "variant2", +- enabled: true, +- payload: { "type" => "string", "value" => "val2" } +- ) +- end +- +- it 'get_variant_with_matching_override should for user_id:61' do +- # NOTE: Use send method, as we are testing a private method +- context = Unleash::Context.new(user_id: 61) +- expect(feature_toggle.send(:variant_from_override_match, context)).to have_attributes( +- name: "variant1", +- payload: { "type" => "string", "value" => "val1" } +- ) +- end +- end +- +- describe 'FeatureToggle with no variants' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "Test.variants", +- "description" => nil, +- "enabled" => true, +- "strategies" => [ +- { +- "name" => "default" +- } +- ], +- "variants" => [], +- "createdAt" => "2019-01-24T10:41:45.236Z" +- ) +- end +- let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } +- +- it 'should return disabled for user_id:1' do +- context = Unleash::Context.new(user_id: 10) +- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( +- name: "disabled", +- enabled: false, +- payload: nil +- ) +- end +- +- it 'should return disabled for user_id:2' do +- context = Unleash::Context.new(user_id: 2) +- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( +- name: "disabled", +- enabled: false, +- payload: nil +- ) +- end +- +- it 'should return an enabled fallback when the fallback is specified' do +- context = Unleash::Context.new(user_id: 2) +- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( +- name: "disabled", +- enabled: false, +- payload: nil +- ) +- end +- end +- +- describe 'FeatureToggle with invalid default_variant' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "Test.variants", +- "description" => nil, +- "enabled" => true, +- "strategies" => [ +- { +- "name" => "default" +- } +- ], +- "variants" => [], +- "createdAt" => "2019-01-24T10:41:45.236Z" +- ) +- end +- let(:valid_default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } +- let(:invalid_default_variant) { Hash.new(name: 'unknown', default: true) } +- +- it 'should raise an error for an invalid fallback variant' do +- expect{ feature_toggle.get_variant(nil, invalid_default_variant) }.to raise_error(ArgumentError) +- end +- +- it 'should not raise an error for a valid fallback variant' do +- expect{ feature_toggle.get_variant(nil, valid_default_variant) }.to_not raise_error +- end +- end +- +- describe 'FeatureToggle default Strategy with two constraints' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "Test.userid", +- "description" => "Play with strategy constraints", +- "enabled" => true, +- "strategies" => [ +- { +- "constraints" => [ +- { +- "contextName" => "environment", +- "operator" => "IN", +- "values" => [ +- "dev" +- ] +- }, +- { +- "contextName" => "userId", +- "operator" => "IN", +- "values" => ["123"] +- } +- ], +- "name" => "default", +- "parameters" => {} +- } +- ] +- ) +- end +- +- it 'should return true if it matches all constraints' do +- context = Unleash::Context.new(user_id: "123", environment: "dev") +- expect(feature_toggle.is_enabled?(context)).to be_truthy +- end +- +- it 'should return false if it does not match all constraints (env)' do +- context = Unleash::Context.new(user_id: "123", environment: "prod") +- expect(feature_toggle.is_enabled?(context)).to be_falsey +- end +- +- it 'should return false if it does not match all constraints (user_id)' do +- context = Unleash::Context.new(user_id: "11", environment: "dev") +- expect(feature_toggle.is_enabled?(context)).to be_falsey +- end +- +- it 'should return false if it does not match any constraint (env and user_id)' do +- context = Unleash::Context.new(user_id: "11", environment: "prod") +- expect(feature_toggle.is_enabled?(context)).to be_falsey +- end +- end +- +- describe 'FeatureToggle default Strategy with one constraint' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "Demo", +- "description" => "Play with strategy constraints", +- "enabled" => true, +- "strategies" => [ +- { +- "constraints" => [ +- { +- "contextName" => "environment", +- "operator" => "IN", +- "values" => [ +- "dev" +- ] +- } +- ], +- "name" => "default", +- "parameters" => {} +- } +- ] +- ) +- end +- +- it 'should return true if it matches the constraint' do +- context = Unleash::Context.new(user_id: "123", environment: "dev") +- expect(feature_toggle.is_enabled?(context)).to be_truthy +- end +- +- it 'should return false if it does not match the constraint' do +- context = Unleash::Context.new(user_id: "123", environment: "prod") +- expect(feature_toggle.is_enabled?(context)).to be_falsey +- end +- end +- +- describe 'disabled_variant' do +- it 'returns disabled variant' do +- ret = described_class.disabled_variant +- expect(ret.enabled).to be false +- expect(ret.name).to eq 'disabled' +- end +- end +- +- describe 'FeatureToggle variant with custom stickiness' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "toggleName", +- "description" => nil, +- "enabled" => true, +- "variants" => [ +- { +- "name" => "variant1", +- "weight" => 25, +- "stickiness" => "organization" +- }, +- { +- "name" => "variant2", +- "weight" => 25, +- "stickiness" => "organization" +- }, +- { +- "name" => "variant3", +- "weight" => 25, +- "stickiness" => "organization" +- }, +- { +- "name" => "variant4", +- "weight" => 25, +- "stickiness" => "organization" +- } +- +- ], +- "createdAt" => "2019-01-24T10:41:45.236Z" +- ) +- end +- +- it 'should return variant1 organization 726' do +- context = Unleash::Context.new( +- properties: { +- organization: '726' +- } +- ) +- +- expect(feature_toggle.get_variant(context)).to have_attributes( +- name: "variant1", +- enabled: true +- ) +- end +- +- it 'should return variant2 organization 48' do +- context = Unleash::Context.new( +- properties: { +- organization: '48' +- } +- ) +- +- expect(feature_toggle.get_variant(context)).to have_attributes( +- name: "variant2", +- enabled: true +- ) +- end +- +- it 'should return variant3 organization 381' do +- context = Unleash::Context.new( +- properties: { +- organization: '381' +- } +- ) +- +- expect(feature_toggle.get_variant(context)).to have_attributes( +- name: "variant3", +- enabled: true +- ) +- end +- +- it 'should return variant4 organization 222' do +- context = Unleash::Context.new( +- properties: { +- organization: '222' +- } +- ) +- +- expect(feature_toggle.get_variant(context)).to have_attributes( +- name: "variant4", +- enabled: true +- ) +- end +- +- it 'should work with a nil context' do +- variant = feature_toggle.get_variant(nil) +- +- expect(variant.name).to match(/variant\d/) +- expect(variant.enabled).to be true +- expect(variant).to be_a_kind_of(Unleash::Variant) +- end +- end +- +- describe 'FeatureToggle Variant with payload and custom stickiness' do +- let(:feature_toggle) do +- Unleash::FeatureToggle.new( +- "name" => "featureVariantX", +- "description" => nil, +- "enabled" => true, +- "strategies" => [ +- { "name" => "default" } +- ], +- "variants" => [ +- { +- "name" => "default-value", +- "payload" => { +- "type" => "string", +- "value" => "payloadData" +- }, +- "stickiness" => "custom_context_attribute", +- "weight" => 100, +- "weightType" => "variable" +- } +- ] +- ) +- end +- +- let(:expected_variant) do +- { +- name: "default-value", +- enabled: true, +- payload: { +- "type" => "string", +- "value" => "payloadData" +- } +- } +- end +- +- it 'should return the one variant, when the context correctly contains the custom stickiness parameter' do +- context = Unleash::Context.new( +- properties: { +- default: 'foo', +- custom_context_attribute: 'uniqueContextValue' +- } +- ) +- expect(feature_toggle.get_variant(context)).to have_attributes(expected_variant) +- end +- +- it 'should return the one variant, with context that is nil' do +- expect(feature_toggle.get_variant(nil)).to have_attributes(expected_variant) +- end +- +- it 'should return the one variant, even when the contexts do not contain the stickiness parameter' do +- [ +- nil, +- Unleash::Context.new, +- Unleash::Context.new(user_id: '123'), +- Unleash::Context.new(session_id: '123'), +- Unleash::Context.new(remote_address: '127.0.0.1'), +- Unleash::Context.new(properties: { not_custom_context_attribute: 'foo' }) +- ].each do |context| +- expect(feature_toggle.get_variant(context)).to have_attributes(expected_variant) +- end +- end +- end +-end +diff --git a/spec/unleash/metrics_reporter_spec.rb b/spec/unleash/metrics_reporter_spec.rb +index cd5bd4f..858a0fb 100644 +--- a/spec/unleash/metrics_reporter_spec.rb ++++ b/spec/unleash/metrics_reporter_spec.rb +@@ -27,24 +27,26 @@ RSpec.describe Unleash::MetricsReporter do + config.instance_id = 'rspec/test' + config.disable_client = true + end +- Unleash.toggle_metrics = Unleash::Metrics.new ++ Unleash.engine = UnleashEngine.new + +- Unleash.toggle_metrics.increment('featureA', :yes) +- Unleash.toggle_metrics.increment('featureA', :yes) +- Unleash.toggle_metrics.increment('featureA', :yes) +- Unleash.toggle_metrics.increment('featureA', :no) +- Unleash.toggle_metrics.increment('featureA', :no) +- Unleash.toggle_metrics.increment('featureB', :yes) ++ Unleash.engine.count_toggle('featureA', true) ++ Unleash.engine.count_toggle('featureA', true) ++ Unleash.engine.count_toggle('featureA', true) ++ Unleash.engine.count_toggle('featureA', false) ++ Unleash.engine.count_toggle('featureA', false) ++ Unleash.engine.count_toggle('featureB', true) + + report = metrics_reporter.generate_report + expect(report[:bucket][:toggles]).to include( +- "featureA" => { ++ :featureA => { + no: 2, +- yes: 3 ++ yes: 3, ++ variants: {} + }, +- "featureB" => { ++ :featureB => { + no: 0, +- yes: 1 ++ yes: 1, ++ variants: {} + } + ) + +@@ -74,14 +76,14 @@ RSpec.describe Unleash::MetricsReporter do + ) + .to_return(status: 200, body: "", headers: {}) + +- Unleash.toggle_metrics = Unleash::Metrics.new ++ Unleash.engine = UnleashEngine.new + +- Unleash.toggle_metrics.increment('featureA', :yes) +- Unleash.toggle_metrics.increment('featureA', :yes) +- Unleash.toggle_metrics.increment('featureA', :yes) +- Unleash.toggle_metrics.increment('featureA', :no) +- Unleash.toggle_metrics.increment('featureA', :no) +- Unleash.toggle_metrics.increment('featureB', :yes) ++ Unleash.engine.count_toggle('featureA', true) ++ Unleash.engine.count_toggle('featureA', true) ++ Unleash.engine.count_toggle('featureA', true) ++ Unleash.engine.count_toggle('featureA', false) ++ Unleash.engine.count_toggle('featureA', false) ++ Unleash.engine.count_toggle('featureB', true) + + metrics_reporter.post + +@@ -105,7 +107,7 @@ RSpec.describe Unleash::MetricsReporter do + end + + it "does not send a report, if there were no metrics registered/evaluated" do +- Unleash.toggle_metrics = Unleash::Metrics.new ++ Unleash.engine = UnleashEngine.new + + metrics_reporter.post + +diff --git a/spec/unleash/metrics_spec.rb b/spec/unleash/metrics_spec.rb +deleted file mode 100644 +index 2830f9b..0000000 +--- a/spec/unleash/metrics_spec.rb ++++ /dev/null +@@ -1,72 +0,0 @@ +-require "rspec/json_expectations" +- +-RSpec.describe Unleash::Metrics do +- let(:metrics) { Unleash::Metrics.new } +- +- it "counts up correctly" do +- metrics.increment('featureA', :yes) +- metrics.increment('featureA', :yes) +- metrics.increment('featureA', :yes) +- metrics.increment('featureA', :no) +- metrics.increment('featureA', :no) +- +- metrics.increment('featureB', :yes) +- metrics.increment('featureB', :no) +- metrics.increment('featureC', :no) +- +- expect(metrics.features['featureA'][:yes]).to eq(3) +- expect(metrics.features['featureA'][:no]).to eq(2) +- expect(metrics.features['featureB'][:yes]).to eq(1) +- expect(metrics.features['featureB'][:no]).to eq(1) +- expect(metrics.features['featureC'][:yes]).to eq(0) +- expect(metrics.features['featureC'][:no]).to eq(1) +- end +- +- it "resets correctly" do +- metrics = Unleash::Metrics.new +- +- metrics.increment('featureA', :yes) +- metrics.reset +- metrics.increment('featureB', :no) +- +- expect(metrics.features['featureA']).to be_nil +- expect(metrics.features['featureB'][:yes]).to eq(0) +- expect(metrics.features['featureB'][:no]).to eq(1) +- end +- +- it "spits out correct JSON" do +- metrics.reset +- metrics.increment('featureA', :yes) +- metrics.increment('featureB', :no) +- +- expect(metrics.to_s).to include_json( +- featureA: { +- yes: 1, +- no: 0 +- }, +- featureB: { +- no: 1 +- } +- ) +- end +- +- describe "when dealing with variants" do +- it "counts up correctly" do +- metrics.increment_variant('featureA', :yes, 'variantA') +- metrics.increment_variant('featureA', :yes, 'variantA') +- metrics.increment_variant('featureA', :yes, 'variantB') +- +- expect(metrics.features['featureA'][:yes]).to eq(3) +- expect(metrics.features['featureA'][:no]).to eq(0) +- expect(metrics.features['featureA']['variants']['variantA']).to eq(2) +- expect(metrics.features['featureA']['variants']['variantB']).to eq(1) +- end +- end +- +- it "increments feature toggle counter when variant is resolved" do +- metrics.increment_variant('featureA', :yes, 'variantA') +- +- expect(metrics.features['featureA'][:yes]).to eq(1) +- expect(metrics.features['featureA'][:no]).to eq(0) +- end +-end +diff --git a/spec/unleash/strategies_spec.rb b/spec/unleash/strategies_spec.rb +deleted file mode 100644 +index 87b70ff..0000000 +--- a/spec/unleash/strategies_spec.rb ++++ /dev/null +@@ -1,156 +0,0 @@ +-require "spec_helper" +- +-RSpec.describe Unleash::Strategies do +- let(:strategies) { described_class.new } +- +- # Silence warnings we are triggering in this test +- around do |example| +- old_verbose = $VERBOSE +- $VERBOSE = nil +- example.run +- ensure +- $VERBOSE = old_verbose +- end +- +- describe 'strategies registration' do +- let(:default_strategies) do +- ['applicationHostname', 'default', 'flexibleRollout', 'gradualRolloutRandom', +- 'gradualRolloutSessionId', 'gradualRolloutUserId', 'remoteAddress', +- 'userWithId'] +- end +- +- context 'when no custom strategies are defined' do +- it 'has default list' do +- expect(strategies.keys.sort).to eq(default_strategies) +- end +- end +- +- # This block testing previous way of loading strategies, when we dynamically picked up all classes +- # defined under `Unleash::Strategy` module +- context 'when custom strategy is defined' do +- let(:custom_strategy) do +- Class.new do +- def name +- 'myCustomStrategy' +- end +- end +- end +- +- before do +- # Define custom class +- Unleash::Strategy.const_set("MyCustomStrategy", custom_strategy) +- end +- +- after do +- # Remove custom class so it does not interfere with other tests +- Unleash::Strategy.send(:remove_const, :MyCustomStrategy) +- end +- +- it 'includes custom strategy in default list' do +- expect(strategies.keys.sort).to eq(default_strategies.concat(['myCustomStrategy']).sort) +- end +- +- it 'warns about deprecated functionality' do +- allow(strategies).to receive(:warn) +- strategies.send(:register_strategies) +- message = '[DEPRECATED] Registering custom Unleash strategy by adding custom class into Unleash::Strategy' +- expect(strategies).to have_received(:warn).with(start_with(message)) +- end +- end +- end +- +- describe '#includes?' do +- it 'returns true for available strategy' do +- expect(strategies.includes?('gradualRolloutRandom')).to be_truthy +- expect(strategies.includes?(:userWithId)).to be_truthy +- end +- +- it 'returns false for missing strategy' do +- expect(strategies.includes?(:missing)).to be_falsey +- end +- end +- +- describe '#fetch' do +- it 'returns available strategy' do +- expect(strategies.fetch(:flexibleRollout)).to be_instance_of(Unleash::Strategy::FlexibleRollout) +- expect(strategies.fetch('applicationHostname')).to be_instance_of(Unleash::Strategy::ApplicationHostname) +- end +- +- it 'raising error when missing' do +- message = 'Strategy is not implemented' +- expect { strategies.fetch(:missing) }.to raise_error(Unleash::Strategy::NotImplemented, message) +- end +- end +- +- describe '#[]' do +- it 'returns available strategy' do +- expect(strategies[:flexibleRollout]).to be_instance_of(Unleash::Strategy::FlexibleRollout) +- expect(strategies['applicationHostname']).to be_instance_of(Unleash::Strategy::ApplicationHostname) +- end +- +- it 'returns nil when missing strategy' do +- expect(strategies[:missing]).to be_nil +- end +- end +- +- describe '#add' do +- before do +- strategies.add(custom_strategy) +- end +- +- context 'when existing strategy is available' do +- let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'applicationHostname') } +- +- it 'overrides previous strategy strategy' do +- expect(strategies.includes?('applicationHostname')).to be_truthy +- expect(strategies.fetch(:applicationHostname)).to eq(custom_strategy) +- expect(strategies.fetch('applicationHostname')).to eq(custom_strategy) +- end +- end +- +- context 'when strategy is new' do +- let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'test') } +- +- it 'adds new strategy strategy' do +- expect(strategies.includes?('test')).to be_truthy +- expect(strategies.fetch(:test)).to eq(custom_strategy) +- expect(strategies.fetch('test')).to eq(custom_strategy) +- end +- end +- end +- +- describe '#[]=' do +- let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'strange name') } +- +- context 'when existing strategy is available' do +- let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'applicationHostname') } +- +- it 'overrides previous strategy strategy' do +- strategies[:applicationHostname] = custom_strategy +- +- expect(strategies.includes?('applicationHostname')).to be_truthy +- expect(strategies.fetch(:applicationHostname)).to eq(custom_strategy) +- expect(strategies.fetch('applicationHostname')).to eq(custom_strategy) +- end +- +- it 'warns when using this method' do +- allow(strategies).to receive(:warn) +- strategies[:applicationHostname] = custom_strategy +- message = '[DEPRECATED] Registering custom Unleash strategy by modifying Unleash::STRATEGIES' +- expect(strategies).to have_received(:warn).with(start_with(message)) +- end +- end +- +- context 'when strategy is new' do +- before do +- strategies['test'] = custom_strategy +- end +- +- it 'adds new strategy strategy' do +- expect(strategies.includes?('test')).to be_truthy +- expect(strategies.fetch(:test)).to eq(custom_strategy) +- expect(strategies.fetch('test')).to eq(custom_strategy) +- end +- end +- end +-end +diff --git a/spec/unleash/strategy/application_hostname_spec.rb b/spec/unleash/strategy/application_hostname_spec.rb +deleted file mode 100644 +index 0614533..0000000 +--- a/spec/unleash/strategy/application_hostname_spec.rb ++++ /dev/null +@@ -1,29 +0,0 @@ +-require "unleash/strategy/application_hostname" +- +-RSpec.describe Unleash::Strategy::ApplicationHostname do +- describe '#is_enabled?' do +- let(:strategy) { Unleash::Strategy::ApplicationHostname.new } +- +- before do +- expect(Socket).to receive(:gethostname).and_return("rspechost") +- end +- +- it 'correctly initialize' do +- expect(strategy.hostname).to eq("rspechost") +- end +- +- it 'should be enabled with correct params' do +- expect(strategy.is_enabled?({ 'hostnames' => 'foo,rspechost,bar' })).to be_truthy +- end +- +- it 'should be disabled with false params' do +- expect(strategy.is_enabled?({ 'hostnames' => 'abc,localhost' })).to be_falsey +- end +- +- it 'should be disabled on invalid params' do +- expect(strategy.is_enabled?(nil)).to be_falsey +- expect(strategy.is_enabled?('string')).to be_falsey +- expect(strategy.is_enabled?({})).to be_falsey +- end +- end +-end +diff --git a/spec/unleash/strategy/base_spec.rb b/spec/unleash/strategy/base_spec.rb +deleted file mode 100644 +index 3e04cde..0000000 +--- a/spec/unleash/strategy/base_spec.rb ++++ /dev/null +@@ -1,11 +0,0 @@ +-require "unleash/strategy/base" +- +-RSpec.describe Unleash::Strategy::Base do +- describe '#is_enabled?' do +- let(:strategy) { Unleash::Strategy::Base.new } +- +- it 'raise exception' do +- expect{ strategy.is_enabled? }.to raise_exception Unleash::Strategy::NotImplemented +- end +- end +-end +diff --git a/spec/unleash/strategy/default_spec.rb b/spec/unleash/strategy/default_spec.rb +deleted file mode 100644 +index 60a908b..0000000 +--- a/spec/unleash/strategy/default_spec.rb ++++ /dev/null +@@ -1,11 +0,0 @@ +-require "unleash/strategy/default" +- +-RSpec.describe Unleash::Strategy::Default do +- describe '#is_enabled?' do +- let(:strategy) { Unleash::Strategy::Default.new } +- +- it 'always returns true' do +- expect(strategy.is_enabled?).to be_truthy +- end +- end +-end +diff --git a/spec/unleash/strategy/flexible_rollout_spec.rb b/spec/unleash/strategy/flexible_rollout_spec.rb +deleted file mode 100644 +index d4783b8..0000000 +--- a/spec/unleash/strategy/flexible_rollout_spec.rb ++++ /dev/null +@@ -1,64 +0,0 @@ +-require 'unleash/strategy/flexible_rollout' +- +-RSpec.describe Unleash::Strategy::FlexibleRollout do +- describe '#is_enabled?' do +- let(:strategy) { Unleash::Strategy::FlexibleRollout.new } +- let(:unleash_context) { Unleash::Context.new } +- +- it 'should always be enabled when rollout is set to 100, disabled when set to 0' do +- params = { +- 'groupId' => 'Demo', +- 'rollout' => 100, +- 'stickiness' => 'default' +- } +- +- expect(strategy.is_enabled?(params, unleash_context)).to be_truthy +- expect(strategy.is_enabled?(params.merge({ 'rollout' => 0 }), unleash_context)).to be_falsey +- end +- +- it 'should behave predictably when based on the normalized_number' do +- allow(Unleash::Strategy::Util).to receive(:get_normalized_number).and_return(15) +- +- params = { +- 'groupId' => 'Demo', +- 'stickiness' => 'default' +- } +- +- expect(strategy.is_enabled?(params.merge({ 'rollout' => 14 }), unleash_context)).to be_falsey +- expect(strategy.is_enabled?(params.merge({ 'rollout' => 15 }), unleash_context)).to be_truthy +- expect(strategy.is_enabled?(params.merge({ 'rollout' => 16 }), unleash_context)).to be_truthy +- end +- +- it 'should be enabled when stickiness=customerId and customerId=61 and rollout=10' do +- params = { +- 'groupId' => 'Demo', +- 'rollout' => 10, +- 'stickiness' => 'customerId' +- } +- +- custom_context = Unleash::Context.new( +- properties: { +- customer_id: '61' +- } +- ) +- +- expect(strategy.is_enabled?(params, custom_context)).to be_truthy +- end +- +- it 'should be disabled when stickiness=customerId and customerId=63 and rollout=10' do +- params = { +- 'groupId' => 'Demo', +- 'rollout' => 10, +- 'stickiness' => 'customerId' +- } +- +- custom_context = Unleash::Context.new( +- properties: { +- customer_id: '63' +- } +- ) +- +- expect(strategy.is_enabled?(params, custom_context)).to be_falsey +- end +- end +-end +diff --git a/spec/unleash/strategy/gradual_rollout_random_spec.rb b/spec/unleash/strategy/gradual_rollout_random_spec.rb +deleted file mode 100644 +index c7ab26b..0000000 +--- a/spec/unleash/strategy/gradual_rollout_random_spec.rb ++++ /dev/null +@@ -1,32 +0,0 @@ +-require "unleash/strategy/gradual_rollout_random" +- +-RSpec.describe Unleash::Strategy::GradualRolloutRandom do +- describe '#is_enabled?' do +- let(:strategy) { Unleash::Strategy::GradualRolloutRandom.new } +- +- before do +- # Random.rand always returns 15, so it is not really random in our tests. +- allow(Random).to receive(:rand).and_return(15) +- end +- +- it 'return true when percentage set (20) is over the returned random value (15)' do +- expect(strategy.is_enabled?({ 'percentage' => '20' })).to be_truthy +- expect(strategy.is_enabled?({ 'percentage' => 20 })).to be_truthy +- expect(strategy.is_enabled?({ 'percentage' => 20.0 })).to be_truthy +- end +- +- it 'return false when percentage set (10) is under the returned random value (15)' do +- expect(strategy.is_enabled?({ 'percentage' => '10' })).to be_falsey +- expect(strategy.is_enabled?({ 'percentage' => 10 })).to be_falsey +- expect(strategy.is_enabled?({ 'percentage' => 10.0 })).to be_falsey +- end +- +- it 'return false when percentage is invalid' do +- expect(strategy.is_enabled?({ 'percentage' => -1 })).to be_falsey +- expect(strategy.is_enabled?({ 'percentage' => nil })).to be_falsey +- expect(strategy.is_enabled?({ 'percentage' => 'abc' })).to be_falsey +- expect(strategy.is_enabled?('text')).to be_falsey +- expect(strategy.is_enabled?(nil)).to be_falsey +- end +- end +-end +diff --git a/spec/unleash/strategy/gradual_rollout_sessionid_spec.rb b/spec/unleash/strategy/gradual_rollout_sessionid_spec.rb +deleted file mode 100644 +index b3d76f3..0000000 +--- a/spec/unleash/strategy/gradual_rollout_sessionid_spec.rb ++++ /dev/null +@@ -1,22 +0,0 @@ +-require "unleash/strategy/gradual_rollout_sessionid" +-require "unleash/strategy/util" +- +-RSpec.describe Unleash::Strategy::GradualRolloutSessionId do +- describe '#is_enabled?' do +- let(:strategy) { Unleash::Strategy::GradualRolloutSessionId.new } +- let(:unleash_context) { Unleash::Context.new(session_id: 'secretsessionidhashgoeshere') } +- let(:percentage) { Unleash::Strategy::Util.get_normalized_number(unleash_context.session_id, "") } +- +- it 'return true when percentage set is gt the number returned by the hash function' do +- expect(strategy.is_enabled?({ 'percentage' => (percentage + 1).to_s }, unleash_context)).to be_truthy +- expect(strategy.is_enabled?({ 'percentage' => percentage + 1 }, unleash_context)).to be_truthy +- expect(strategy.is_enabled?({ 'percentage' => percentage + 0.1 }, unleash_context)).to be_truthy +- end +- +- it 'return false when percentage set is lt the number returned by the hash function' do +- expect(strategy.is_enabled?({ 'percentage' => (percentage - 1).to_s }, unleash_context)).to be_falsey +- expect(strategy.is_enabled?({ 'percentage' => percentage - 1 }, unleash_context)).to be_falsey +- expect(strategy.is_enabled?({ 'percentage' => percentage - 0.1 }, unleash_context)).to be_falsey +- end +- end +-end +diff --git a/spec/unleash/strategy/gradual_rollout_userid_spec.rb b/spec/unleash/strategy/gradual_rollout_userid_spec.rb +deleted file mode 100644 +index 2ed9613..0000000 +--- a/spec/unleash/strategy/gradual_rollout_userid_spec.rb ++++ /dev/null +@@ -1,21 +0,0 @@ +-require "unleash/strategy/gradual_rollout_userid" +- +-RSpec.describe Unleash::Strategy::GradualRolloutUserId do +- describe '#is_enabled?' do +- let(:strategy) { Unleash::Strategy::GradualRolloutUserId.new } +- let(:unleash_context) { Unleash::Context.new({ 'userId' => 'alice' }) } +- let(:percentage) { Unleash::Strategy::Util.get_normalized_number(unleash_context.user_id, "") } +- +- it 'return true when percentage set is gt the number returned by the hash function' do +- expect(strategy.is_enabled?({ 'percentage' => (percentage + 1).to_s }, unleash_context)).to be_truthy +- expect(strategy.is_enabled?({ 'percentage' => percentage + 1 }, unleash_context)).to be_truthy +- expect(strategy.is_enabled?({ 'percentage' => percentage + 0.1 }, unleash_context)).to be_truthy +- end +- +- it 'return false when percentage set is lt the number returned by the hash function' do +- expect(strategy.is_enabled?({ 'percentage' => (percentage - 1).to_s }, unleash_context)).to be_falsey +- expect(strategy.is_enabled?({ 'percentage' => percentage - 1 }, unleash_context)).to be_falsey +- expect(strategy.is_enabled?({ 'percentage' => percentage - 0.1 }, unleash_context)).to be_falsey +- end +- end +-end +diff --git a/spec/unleash/strategy/remote_address_spec.rb b/spec/unleash/strategy/remote_address_spec.rb +deleted file mode 100644 +index ac3da10..0000000 +--- a/spec/unleash/strategy/remote_address_spec.rb ++++ /dev/null +@@ -1,79 +0,0 @@ +-require "unleash/strategy/remote_address" +- +-RSpec.describe Unleash::Strategy::RemoteAddress do +- describe '#is_enabled?' do +- let(:strategy) { Unleash::Strategy::RemoteAddress.new } +- let(:unleash_context) { Unleash::Context.new({ 'remoteAddress' => '127.0.0.1' }) } +- +- def context_for_addr(remote_address) +- Unleash::Context.new(remote_address: remote_address) +- end +- +- it 'should be enabled with correct params' do +- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, unleash_context)).to be_truthy +- +- unleash_context2 = Unleash::Context.new +- unleash_context2.remote_address = '172.12.0.1' +- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, unleash_context2)).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1, 172.12.0.1 , 127.0.0.1' }, unleash_context2)).to be_truthy +- end +- +- it 'should work with ipv6' do +- ips_and_cidrs = '2001:0db8:85a3:0000:0000:8a2e:0370:7300/120,2001:0db8:85a3:0000:0000:8a2e:0370:7520/123' +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:72ff'))).to be_falsey +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7330'))).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7334'))).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:73ff'))).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7400'))).to be_falsey +- +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7519'))).to be_falsey +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7520'))).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:753f'))).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7540'))).to be_falsey +- end +- +- it 'should be enabled with correct CIDR params' do +- ips_and_cidrs = '192.168.0.0/24,127.0.0.1/32,172.12.0.1' +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, unleash_context)).to be_truthy +- +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '172.12.0.1'))).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '127.0.0.1'))).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '127.0.0.1/32'))).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.0'))).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.1'))).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.255'))).to be_truthy +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.192/30'))).to be_truthy +- +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '127.0.0.2'))).to be_falsey +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.1.0'))).to be_falsey +- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.1.255'))).to be_falsey +- end +- +- it 'should be disabled with false params' do +- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,172.12.0.1' }, unleash_context)).to be_falsey +- end +- +- it 'should be disabled on invalid params' do +- expect(strategy.is_enabled?({ 'ips' => '192.168.0.1,172.12.0.1' }, unleash_context)).to be_falsey +- expect(strategy.is_enabled?({ 'IPs' => nil }, unleash_context)).to be_falsey +- expect(strategy.is_enabled?({}, unleash_context)).to be_falsey +- expect(strategy.is_enabled?('IPs_list', unleash_context)).to be_falsey +- end +- +- it 'should be disabled on invalid contexts' do +- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, Unleash::Context.new)).to be_falsey +- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, nil)).to be_falsey +- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' })).to be_falsey +- +- expect(strategy.is_enabled?({ 'IPs' => '192.168.x.y,127.0.0.1' }, Unleash::Context.new(remote_address: '192.168.x.y'))).to be_falsey +- expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' }, Unleash::Context.new(remote_address: 'foobar'))).to be_falsey +- expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' }, Unleash::Context.new(remote_address: '192.168.1.0'))).to be_falsey +- expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' }, nil)).to be_falsey +- expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' })).to be_falsey +- end +- +- it 'should be enabled for valid params even if other params are invalid' do +- expect(strategy.is_enabled?({ 'IPs' => '192.168.x.y,127.0.0.1' }, Unleash::Context.new(remote_address: '127.0.0.1'))).to be_truthy +- end +- end +-end +diff --git a/spec/unleash/strategy/user_with_id_spec.rb b/spec/unleash/strategy/user_with_id_spec.rb +deleted file mode 100644 +index 18e326a..0000000 +--- a/spec/unleash/strategy/user_with_id_spec.rb ++++ /dev/null +@@ -1,61 +0,0 @@ +-require "unleash/strategy/user_with_id" +-require "unleash/context" +- +-RSpec.describe Unleash::Strategy::UserWithId do +- describe '#is_enabled?' do +- let(:strategy) { Unleash::Strategy::UserWithId.new } +- +- context 'with string params' do +- let(:unleash_context) { Unleash::Context.new({ 'userId' => 'bob' }) } +- +- it 'should be enabled with correct params' do +- expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, unleash_context)).to be_truthy +- +- unleash_context2 = Unleash::Context.new +- unleash_context2.user_id = 'alice' +- expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, unleash_context2)).to be_truthy +- end +- +- it 'should be enabled with correct can include spaces' do +- expect(strategy.is_enabled?({ 'userIds' => ' alice ,bob,carol,dave' }, unleash_context)).to be_truthy +- end +- +- it 'should be disabled with false params' do +- expect(strategy.is_enabled?({ 'userIds' => 'alice,dave' }, unleash_context)).to be_falsey +- end +- +- it 'should be disabled on invalid params' do +- expect(strategy.is_enabled?({ 'userIds' => nil }, unleash_context)).to be_falsey +- expect(strategy.is_enabled?({}, unleash_context)).to be_falsey +- expect(strategy.is_enabled?('string', unleash_context)).to be_falsey +- expect(strategy.is_enabled?(nil, unleash_context)).to be_falsey +- end +- +- it 'should be disabled on invalid contexts' do +- expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, Unleash::Context.new)).to be_falsey +- expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, nil)).to be_falsey +- expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' })).to be_falsey +- end +- end +- +- context 'with int params' do +- let(:user_id) { 123 } +- let(:unleash_context) { Unleash::Context.new({ 'userId' => user_id }) } +- +- it 'should be enabled with correct params' do +- expect(strategy.is_enabled?({ 'userIds' => '1,2,123' }, unleash_context)).to be_truthy +- +- unleash_context2 = Unleash::Context.new(user_id: 1) +- expect(strategy.is_enabled?({ 'userIds' => '1,2,123' }, unleash_context2)).to be_truthy +- end +- +- it 'should be enabled with correct can include spaces' do +- expect(strategy.is_enabled?({ 'userIds' => ' 1 ,2, 123 ,200 ' }, unleash_context)).to be_truthy +- end +- +- it 'should be disabled with false params' do +- expect(strategy.is_enabled?({ 'userIds' => '1,2' }, unleash_context)).to be_falsey +- end +- end +- end +-end +diff --git a/spec/unleash/strategy/util_spec.rb b/spec/unleash/strategy/util_spec.rb +deleted file mode 100644 +index 234b160..0000000 +--- a/spec/unleash/strategy/util_spec.rb ++++ /dev/null +@@ -1,10 +0,0 @@ +-require "unleash/strategy/util" +- +-RSpec.describe Unleash::Strategy::Util do +- describe '.get_normalized_number' do +- it "returns correct values" do +- expect(Unleash::Strategy::Util.get_normalized_number('123', 'gr1')).to eq(73) +- expect(Unleash::Strategy::Util.get_normalized_number('999', 'groupX')).to eq(25) +- end +- end +-end +diff --git a/spec/unleash/toggle_fetcher_spec.rb b/spec/unleash/toggle_fetcher_spec.rb +index 3821051..97ae8cd 100644 +--- a/spec/unleash/toggle_fetcher_spec.rb ++++ b/spec/unleash/toggle_fetcher_spec.rb +@@ -55,25 +55,24 @@ RSpec.describe Unleash::ToggleFetcher do + end + + describe '#save!' do +- context 'when toggle_cache generation fails' do +- before do +- allow(toggle_fetcher).to receive(:toggle_cache).and_raise(StandardError) +- end +- +- it 'swallows the error' do +- expect { toggle_fetcher.save! }.not_to raise_error +- end +- end +- + context 'when toggle_cache with content is saved' do +- before do +- toggle_fetcher.toggle_cache = { features: [] } +- end +- + it 'creates a file with toggle_cache in JSON' do +- toggle_fetcher.save! ++ toggles = { ++ version: 2, ++ features: [ ++ { ++ name: "Feature.A", ++ description: "Enabled toggle", ++ enabled: true, ++ strategies: [{ ++ "name": "default" ++ }] ++ }, ++ ] ++ } ++ toggle_fetcher.save! toggles.to_json + expect(File.exist?(Unleash.configuration.backup_file)).to eq(true) +- expect(File.read(Unleash.configuration.backup_file)).to eq('{"features":[]}') ++ expect(File.read(Unleash.configuration.backup_file)).to eq('{"version":2,"features":[{"name":"Feature.A","description":"Enabled toggle","enabled":true,"strategies":[{"name":"default"}]}]}') + end + end + end +@@ -83,47 +82,28 @@ RSpec.describe Unleash::ToggleFetcher do + before do + # manually create a stub cache on disk, so we can test that we read it correctly later. + cache_creator = described_class.new +- cache_creator.toggle_cache = { features: [] } +- cache_creator.save! +- +- WebMock.stub_request(:get, "http://toggle-fetcher-test-url/client/features").to_return(status: 500) +- end ++ toggles = { ++ version: 2, ++ features: [ ++ { ++ name: "Feature.A", ++ description: "Enabled toggle", ++ enabled: true, ++ strategies: [{ ++ "name": "default" ++ }] ++ }, ++ ] ++ } + +- it 'reads the backup file for values' do +- expect(toggle_fetcher.toggle_cache).to eq("features" => []) +- end +- end ++ cache_creator.save! toggles.to_json + +- context 'when backup file does not exist' do +- before do +- File.delete(Unleash.configuration.backup_file) if File.exist?(Unleash.configuration.backup_file) + WebMock.stub_request(:get, "http://toggle-fetcher-test-url/client/features").to_return(status: 500) + end + +- it 'returns an empty toggle_cache' do +- expect(toggle_fetcher.toggle_cache).to eq(nil) +- end +- end +- +- context 'segments are present' do +- it 'loads a segement map correctly' do +- expect(toggle_fetcher.toggle_cache["segments"].count).to eq 1 +- end +- end +- +- context 'segments are not present' do +- before do +- WebMock.stub_request(:get, "http://toggle-fetcher-test-url/client/features") +- .to_return(status: 200, +- body: { +- "version": 1, +- "features": [] +- }.to_json, +- headers: {}) +- end +- +- it 'loads an empty segment map' do +- expect(toggle_fetcher.toggle_cache["segments"].count).to eq 0 ++ it 'reads the backup file for values' do ++ enabled = Unleash.engine.enabled?('Feature.A', {}) ++ expect(enabled).to eq(true) + end + end + end diff --git a/lib/unleash.rb b/lib/unleash.rb index 9a6f1c7e..260cf40e 100644 --- a/lib/unleash.rb +++ b/lib/unleash.rb @@ -1,6 +1,5 @@ require 'unleash/version' require 'unleash/configuration' -require 'unleash/strategies' require 'unleash/context' require 'unleash/client' require 'logger' @@ -9,7 +8,7 @@ module Unleash TIME_RESOLUTION = 3 class << self - attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :segment_cache, :logger + attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :segment_cache, :logger, :engine end self.configuration = Unleash::Configuration.new @@ -26,6 +25,6 @@ def self.configure end def self.strategies - self.configuration.strategies + nil end end diff --git a/lib/unleash/activation_strategy.rb b/lib/unleash/activation_strategy.rb deleted file mode 100644 index 29feb0c6..00000000 --- a/lib/unleash/activation_strategy.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Unleash - class ActivationStrategy - attr_accessor :name, :params, :constraints, :disabled - - def initialize(name, params, constraints = []) - self.name = name - self.disabled = false - - if params.is_a?(Hash) - self.params = params - elsif params.nil? - self.params = {} - else - Unleash.logger.warn "Invalid params provided for ActivationStrategy (params:#{params})" - self.params = {} - end - - if constraints.is_a?(Array) && constraints.each{ |c| c.is_a?(Constraint) } - self.constraints = constraints - else - Unleash.logger.warn "Invalid constraints provided for ActivationStrategy (contraints: #{constraints})" - self.disabled = true - self.constraints = [] - end - end - - def matches_context?(context) - self.constraints.any?{ |c| c.matches_context? context } - end - end -end diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 2191cafb..4b53300f 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -17,6 +17,7 @@ def initialize(*opts) Unleash.logger = Unleash.configuration.logger.clone Unleash.logger.level = Unleash.configuration.log_level + Unleash.engine = UnleashEngine.new Unleash.toggle_fetcher = Unleash::ToggleFetcher.new if Unleash.configuration.disable_client @@ -40,16 +41,18 @@ def is_enabled?(feature, context = nil, default_value_param = false, &fallback_b default_value_param end - toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first + Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}" - if toggle_as_hash.nil? + toggle_enabled = Unleash&.engine&.enabled?(feature, context) + if toggle_enabled.nil? Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found" + Unleash&.engine&.count_toggle(feature, false) return default_value end - toggle = Unleash::FeatureToggle.new(toggle_as_hash, Unleash&.segment_cache) + Unleash&.engine&.count_toggle(feature, toggle_enabled) - toggle.is_enabled?(context) + toggle_enabled end def is_disabled?(feature, context = nil, default_value_param = true, &fallback_blk) @@ -74,22 +77,20 @@ def if_disabled(feature, context = nil, default_value = true, &blk) def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant) Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}" - toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first - - if toggle_as_hash.nil? - Unleash.logger.debug "Unleash::Client.get_variant feature: #{feature} not found" - return fallback_variant + toggle_enabled = Unleash&.engine&.enabled?(feature, context) + if toggle_enabled.nil? + Unleash&.engine&.count_toggle(feature, false) + else + Unleash&.engine&.count_toggle(feature, toggle_enabled) end - toggle = Unleash::FeatureToggle.new(toggle_as_hash) - variant = toggle.get_variant(context, fallback_variant) - - if variant.nil? - Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found" + variant_response = Unleash&.engine.get_variant(feature, context) + if variant_response.code < 0 + Unleash&.engine&.count_variant(feature, fallback_variant.name) return fallback_variant end - - # TODO: Add to README: name, payload, enabled (bool) + variant = variant_response.variant + Unleash&.engine&.count_variant(feature, variant.name) variant end @@ -118,7 +119,7 @@ def info 'appName': Unleash.configuration.app_name, 'instanceId': Unleash.configuration.instance_id, 'sdkVersion': "unleash-client-ruby:" + Unleash::VERSION, - 'strategies': Unleash.strategies.keys, + 'strategies': nil, 'started': Time.now.iso8601(Unleash::TIME_RESOLUTION), 'interval': Unleash.configuration.metrics_interval_in_millis } @@ -137,7 +138,6 @@ def start_toggle_fetcher end def start_metrics - Unleash.toggle_metrics = Unleash::Metrics.new Unleash.reporter = Unleash::MetricsReporter.new self.metrics_scheduled_executor = Unleash::ScheduledExecutor.new( 'MetricsReporter', diff --git a/lib/unleash/configuration.rb b/lib/unleash/configuration.rb index 4f432005..dddd90f7 100644 --- a/lib/unleash/configuration.rb +++ b/lib/unleash/configuration.rb @@ -40,9 +40,9 @@ def metrics_interval_in_millis def validate! return if self.disable_client - raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? || self.url.nil? + raise ArgumentError, "app_name is a required parameter." if self.app_name.nil? - validate_custom_http_headers!(self.custom_http_headers) + validate_custom_http_headers!(self.custom_http_headers) unless self.url.nil? end def refresh_backup_file! @@ -96,7 +96,7 @@ def set_defaults self.backup_file = nil self.log_level = Logger::WARN self.bootstrap_config = nil - self.strategies = Unleash::Strategies.new + self.strategies = nil self.custom_http_headers = {} end diff --git a/lib/unleash/constraint.rb b/lib/unleash/constraint.rb deleted file mode 100644 index 51607c12..00000000 --- a/lib/unleash/constraint.rb +++ /dev/null @@ -1,115 +0,0 @@ -require 'date' -module Unleash - class Constraint - attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive - - OPERATORS = { - IN: ->(context_v, constraint_v){ constraint_v.include? context_v.to_s }, - NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v.to_s }, - STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } }, - STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } }, - STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } }, - NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } }, - NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } }, - NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } }, - NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } }, - NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } }, - DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } }, - DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } }, - SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } }, - SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } }, - SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } }, - FALLBACK_VALIDATOR: ->(_context_v, _constraint_v){ false } - }.freeze - - LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze - - def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false) - raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String) - - unless OPERATORS.include? operator.to_sym - Unleash.logger.warn "Operator #{operator} is not a supported operator, " \ - "falling back to FALLBACK_VALIDATOR which skips this constraint." - operator = "FALLBACK_VALIDATOR" - end - self.log_inconsistent_constraint_configuration(operator.to_sym, value) - - self.context_name = context_name - self.operator = operator.to_sym - self.value = value - self.inverted = !!inverted - self.case_insensitive = !!case_insensitive - end - - def matches_context?(context) - Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" - return false if context.nil? - - match = matches_constraint?(context) - self.inverted ? !match : match - rescue KeyError - Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \ - found on the context" - false - end - - def self.on_valid_date(val1, val2) - val1 = DateTime.parse(val1) - val2 = DateTime.parse(val2) - yield(val1, val2) - rescue ArgumentError - Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \ - or constraint_value (#{val2}) into a date. Returning false!" - false - end - - def self.on_valid_float(val1, val2) - val1 = Float(val1) - val2 = Float(val2) - yield(val1, val2) - rescue ArgumentError - Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \ - or constraint_value (#{val2}) into a number. Returning false!" - false - end - - def self.on_valid_version(val1, val2) - val1 = Gem::Version.new(val1) - val2 = Gem::Version.new(val2) - yield(val1, val2) - rescue ArgumentError - Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \ - or constraint_value (#{val2}) into a version. Return false!" - false - end - - # This should be a private method but for some reason this fails on Ruby 2.5 - def log_inconsistent_constraint_configuration(operator, value) - Unleash.logger.warn "value is a String, operator is expecting an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String) - Unleash.logger.warn "value is an Array, operator is expecting a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array) - end - - private - - def matches_constraint?(context) - Unleash.logger.debug "Unleash::Constraint matches_constraint? value: #{self.value} operator: #{self.operator} " \ - " context.get_by_name(#{self.context_name})" - - unless OPERATORS.include?(self.operator) - Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false." - false - end - - # when the operator is NOT_IN and there is no data, return true. In all other cases the operator doesn't match. - return self.operator == :NOT_IN unless context.include?(self.context_name) - - v = self.value.dup - context_value = context.get_by_name(self.context_name) - - v.map!(&:upcase) if self.case_insensitive - context_value.upcase! if self.case_insensitive - - OPERATORS[self.operator].call(context_value, v) - end - end -end diff --git a/lib/unleash/context.rb b/lib/unleash/context.rb index 98ba467c..9e235ce4 100644 --- a/lib/unleash/context.rb +++ b/lib/unleash/context.rb @@ -23,6 +23,22 @@ def to_s ",app_name=#{@app_name},environment=#{@environment}>" end + def as_json + { + appName: self.app_name, + environment: self.environment, + userId: self.user_id, + sessionId: self.session_id, + remoteAddress: self.remote_address, + currentTime: self.current_time, + properties: self.properties + } + end + + def to_json(*options) + as_json(*options).to_json(*options) + end + def to_h ATTRS.map{ |attr| [attr, self.send(attr)] }.to_h.merge(properties: @properties) end diff --git a/lib/unleash/feature_toggle.rb b/lib/unleash/feature_toggle.rb index ec064b34..02020e54 100644 --- a/lib/unleash/feature_toggle.rb +++ b/lib/unleash/feature_toggle.rb @@ -1,187 +1,10 @@ -require 'unleash/activation_strategy' -require 'unleash/constraint' require 'unleash/variant_definition' require 'unleash/variant' -require 'unleash/strategy/util' -require 'securerandom' module Unleash class FeatureToggle - attr_accessor :name, :enabled, :strategies, :variant_definitions - - def initialize(params = {}, segment_map = {}) - params = {} if params.nil? - - self.name = params.fetch('name', nil) - self.enabled = params.fetch('enabled', false) - - self.strategies = initialize_strategies(params, segment_map) - self.variant_definitions = initialize_variant_definitions(params) - end - - def to_s - "" - end - - def is_enabled?(context) - result = am_enabled?(context) - - choice = result ? :yes : :no - Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics - - result - end - - def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant) - raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant' - - context = ensure_valid_context(context) - - toggle_enabled = am_enabled?(context) - variant = resolve_variant(context, toggle_enabled) - - choice = toggle_enabled ? :yes : :no - Unleash.toggle_metrics.increment_variant(self.name, choice, variant.name) unless Unleash.configuration.disable_metrics - variant - end - def self.disabled_variant Unleash::Variant.new(name: 'disabled', enabled: false) end - - private - - def resolve_variant(context, toggle_enabled) - return Unleash::FeatureToggle.disabled_variant unless toggle_enabled - return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0 - - variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness) - end - - def resolve_stickiness - self.variant_definitions&.map(&:stickiness)&.compact&.first || "default" - end - - # only check if it is enabled, do not do metrics - def am_enabled?(context) - result = - if self.enabled - self.strategies.empty? || - self.strategies.any? do |s| - strategy_enabled?(s, context) && strategy_constraint_matches?(s, context) - end - else - false - end - - Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \ - "and Strategies combined with contraints returned #{result})" - - result - end - - def strategy_enabled?(strategy, context) - r = Unleash.strategies.fetch(strategy.name).is_enabled?(strategy.params, context) - Unleash.logger.debug "Unleash::FeatureToggle.strategy_enabled? Strategy #{strategy.name} returned #{r} with context: #{context}" - r - end - - def strategy_constraint_matches?(strategy, context) - return false if strategy.disabled - - strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) } - end - - def sum_variant_defs_weights - self.variant_definitions.map(&:weight).reduce(0, :+) - end - - def variant_salt(context, stickiness = "default") - begin - return context.get_by_name(stickiness) if !context.nil? && stickiness != "default" - rescue KeyError - Unleash.logger.warn "Custom stickiness key (#{stickiness}) not found in the provided context #{context}. " \ - "Falling back to default behavior." - end - return context.user_id unless context&.user_id.to_s.empty? - return context.session_id unless context&.session_id.to_s.empty? - return context.remote_address unless context&.remote_address.to_s.empty? - - SecureRandom.random_number - end - - def variant_from_override_match(context) - variant = self.variant_definitions.find{ |vd| vd.override_matches_context?(context) } - return nil if variant.nil? - - Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload) - end - - def variant_from_weights(context, stickiness) - variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context, stickiness), self.name, sum_variant_defs_weights) - prev_weights = 0 - - variant_definition = self.variant_definitions - .find do |v| - res = (prev_weights + v.weight >= variant_weight) - prev_weights += v.weight - res - end - return self.disabled_variant if variant_definition.nil? - - Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload) - end - - def ensure_valid_context(context) - unless ['NilClass', 'Unleash::Context'].include? context.class.name - Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, " \ - "please use Unleash::Context. Context set to nil." - context = nil - end - context - end - - def initialize_strategies(params, segment_map) - params.fetch('strategies', []) - .select{ |s| s.has_key?('name') && Unleash.strategies.includes?(s['name']) } - .map do |s| - ActivationStrategy.new( - s['name'], - s['parameters'], - resolve_constraints(s, segment_map) - ) - end || [] - end - - def resolve_constraints(strategy, segment_map) - segment_constraints = (strategy["segments"] || []).map do |segment_id| - segment_map[segment_id]&.fetch("constraints") - end - (strategy.fetch("constraints", []) + segment_constraints).flatten.map do |constraint| - return nil if constraint.nil? - - Constraint.new( - constraint.fetch('contextName'), - constraint.fetch('operator'), - constraint.fetch('value', nil) || constraint.fetch('values', nil), - inverted: constraint.fetch('inverted', false), - case_insensitive: constraint.fetch('caseInsensitive', false) - ) - end - end - - def initialize_variant_definitions(params) - (params.fetch('variants', []) || []) - .select{ |v| v.is_a?(Hash) && v.has_key?('name') } - .map do |v| - VariantDefinition.new( - v.fetch('name', ''), - v.fetch('weight', 0), - v.fetch('payload', nil), - v.fetch('stickiness', nil), - v.fetch('overrides', []) - ) - end || [] - end end end diff --git a/lib/unleash/metrics.rb b/lib/unleash/metrics.rb deleted file mode 100644 index 6342ade4..00000000 --- a/lib/unleash/metrics.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Unleash - class Metrics - attr_accessor :features, :features_lock - - def initialize - self.features = {} - self.features_lock = Mutex.new - end - - def to_s - self.features_lock.synchronize do - return self.features.to_json - end - end - - def increment(feature, choice) - raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice - - self.features_lock.synchronize do - self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature - self.features[feature][choice] += 1 - end - end - - def increment_variant(feature, choice, variant) - self.features_lock.synchronize do - self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature - self.features[feature][choice] += 1 - self.features[feature]['variants'] = {} unless self.features[feature].include? 'variants' - self.features[feature]['variants'][variant] = 0 unless self.features[feature]['variants'].include? variant - self.features[feature]['variants'][variant] += 1 - end - end - - def reset - self.features_lock.synchronize do - self.features = {} - end - end - end -end diff --git a/lib/unleash/metrics_reporter.rb b/lib/unleash/metrics_reporter.rb index fc1e7cae..4cd4340b 100755 --- a/lib/unleash/metrics_reporter.rb +++ b/lib/unleash/metrics_reporter.rb @@ -1,5 +1,4 @@ require 'unleash/configuration' -require 'unleash/metrics' require 'net/http' require 'json' require 'time' @@ -15,22 +14,17 @@ def initialize end def generate_report - now = Time.now - - start = self.last_time - stop = now - self.last_time = now - + puts "Making report" + metrics = Unleash&.engine&.get_metrics() + if metrics.nil? || metrics.empty? + puts "nothing here" + return nil + end report = { 'appName': Unleash.configuration.app_name, 'instanceId': Unleash.configuration.instance_id, - 'bucket': { - 'start': start.iso8601(Unleash::TIME_RESOLUTION), - 'stop': stop.iso8601(Unleash::TIME_RESOLUTION), - 'toggles': Unleash.toggle_metrics.features - } + 'bucket': metrics } - Unleash.toggle_metrics.reset report end @@ -38,13 +32,14 @@ def generate_report def post Unleash.logger.debug "post() Report" - if bucket_empty? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes... + bucket = self.generate_report + if bucket.nil? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes... Unleash.logger.debug "Report not posted to server, as it would have been empty. (and has been empty for up to 10 min)" return end - response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, self.generate_report.to_json) + response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, bucket.to_json) if ['200', '202'].include? response.code Unleash.logger.debug "Report sent to unleash server successfully. Server responded with http code #{response.code}" @@ -54,9 +49,5 @@ def post end private - - def bucket_empty? - Unleash.toggle_metrics.features.empty? - end end end diff --git a/lib/unleash/strategies.rb b/lib/unleash/strategies.rb deleted file mode 100644 index 842af4f5..00000000 --- a/lib/unleash/strategies.rb +++ /dev/null @@ -1,80 +0,0 @@ -require 'unleash/strategy/base' -Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path } - -module Unleash - class Strategies - def initialize - @strategies = {} - register_strategies - end - - def keys - @strategies.keys - end - - def includes?(name) - @strategies.has_key?(name.to_s) - end - - def fetch(name) - raise Unleash::Strategy::NotImplemented, "Strategy is not implemented" unless (strategy = @strategies[name.to_s]) - - strategy - end - - def add(strategy) - @strategies[strategy.name] = strategy - end - - def []=(key, strategy) - warn_deprecated_registration(strategy, 'modifying Unleash::STRATEGIES') - @strategies[key.to_s] = strategy - end - - def [](key) - @strategies[key.to_s] - end - - def register_strategies - register_base_strategies - register_custom_strategies - end - - protected - - # Deprecated: Use Unleash.configuration to add custom strategies - def register_custom_strategies - Unleash::Strategy.constants - .select{ |c| Unleash::Strategy.const_get(c).is_a? Class } - .reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) } # Reject abstract classes - .map{ |c| Object.const_get("Unleash::Strategy::#{c}") } - .reject{ |c| DEFAULT_STRATEGIES.include?(c) } # Reject base classes - .each do |c| - strategy = c.new - warn_deprecated_registration(strategy, 'adding custom class into Unleash::Strategy namespace') - self.add(strategy) - end - end - - def register_base_strategies - DEFAULT_STRATEGIES.each{ |c| self.add(c.new) } - end - - DEFAULT_STRATEGIES = [ - Unleash::Strategy::ApplicationHostname, - Unleash::Strategy::Default, - Unleash::Strategy::FlexibleRollout, - Unleash::Strategy::GradualRolloutRandom, - Unleash::Strategy::GradualRolloutSessionId, - Unleash::Strategy::GradualRolloutUserId, - Unleash::Strategy::RemoteAddress, - Unleash::Strategy::UserWithId - ].freeze - - def warn_deprecated_registration(strategy, method) - warn "[DEPRECATED] Registering custom Unleash strategy by #{method} is deprecated. - Please use Unleash configuration to register custom strategy: " \ - "`Unleash.configure {|c| c.strategies.add(#{strategy.class.name}.new) }`" - end - end -end diff --git a/lib/unleash/strategy/application_hostname.rb b/lib/unleash/strategy/application_hostname.rb deleted file mode 100644 index f5fd578a..00000000 --- a/lib/unleash/strategy/application_hostname.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'socket' - -module Unleash - module Strategy - class ApplicationHostname < Base - attr_accessor :hostname - - PARAM = 'hostnames'.freeze - - def initialize - self.hostname = Socket.gethostname || 'undefined' - end - - def name - 'applicationHostname' - end - - # need: :params['hostnames'] - def is_enabled?(params = {}, _context = nil) - return false unless params.is_a?(Hash) && params.has_key?(PARAM) - - params[PARAM].split(",").map(&:strip).map(&:downcase).include?(self.hostname) - end - end - end -end diff --git a/lib/unleash/strategy/base.rb b/lib/unleash/strategy/base.rb deleted file mode 100644 index 3e3a0f07..00000000 --- a/lib/unleash/strategy/base.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Unleash - module Strategy - class NotImplemented < RuntimeError - end - - class Base - def name - raise NotImplemented, "Strategy is not implemented" - end - - def is_enabled?(_params = {}, _context = nil) - raise NotImplemented, "Strategy is not implemented" - end - end - end -end diff --git a/lib/unleash/strategy/default.rb b/lib/unleash/strategy/default.rb deleted file mode 100644 index d22cdbe8..00000000 --- a/lib/unleash/strategy/default.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Unleash - module Strategy - class Default < Base - def name - 'default' - end - - def is_enabled?(_params = {}, _context = nil) - true - end - end - end -end diff --git a/lib/unleash/strategy/flexible_rollout.rb b/lib/unleash/strategy/flexible_rollout.rb deleted file mode 100644 index edb8256c..00000000 --- a/lib/unleash/strategy/flexible_rollout.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'unleash/strategy/util' - -module Unleash - module Strategy - class FlexibleRollout < Base - def name - 'flexibleRollout' - end - - # need: params['percentage'] - def is_enabled?(params = {}, context = nil) - return false unless params.is_a?(Hash) - return false unless context.instance_of?(Unleash::Context) - - stickiness = params.fetch('stickiness', 'default') - stickiness_id = resolve_stickiness(stickiness, context) - - begin - percentage = Integer(params.fetch('rollout', 0)) - percentage = 0 if percentage > 100 || percentage.negative? - rescue ArgumentError - return false - end - - group_id = params.fetch('groupId', '') - normalized_number = Util.get_normalized_number(stickiness_id, group_id) - - return false if stickiness_id.nil? - - (percentage.positive? && normalized_number <= percentage) - end - - private - - def random - Random.rand(0..100) - end - - def resolve_stickiness(stickiness, context) - case stickiness - when 'random' - random - when 'default' - context.user_id || context.session_id || random - else - begin - context.get_by_name(stickiness) - rescue KeyError - nil - end - end - end - end - end -end diff --git a/lib/unleash/strategy/gradual_rollout_random.rb b/lib/unleash/strategy/gradual_rollout_random.rb deleted file mode 100644 index 61d0784c..00000000 --- a/lib/unleash/strategy/gradual_rollout_random.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'unleash/strategy/util' - -module Unleash - module Strategy - class GradualRolloutRandom < Base - def name - 'gradualRolloutRandom' - end - - # need: params['percentage'] - def is_enabled?(params = {}, _context = nil) - return false unless params.is_a?(Hash) && params.has_key?('percentage') - - begin - percentage = Integer(params['percentage'] || 0) - rescue ArgumentError - return false - end - - (percentage >= Random.rand(1..100)) - end - end - end -end diff --git a/lib/unleash/strategy/gradual_rollout_sessionid.rb b/lib/unleash/strategy/gradual_rollout_sessionid.rb deleted file mode 100644 index 0f2a553c..00000000 --- a/lib/unleash/strategy/gradual_rollout_sessionid.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'unleash/strategy/util' - -module Unleash - module Strategy - class GradualRolloutSessionId < Base - def name - 'gradualRolloutSessionId' - end - - # need: params['percentage'], params['groupId'], context.user_id, - def is_enabled?(params = {}, context = nil) - return false unless params.is_a?(Hash) && params.has_key?('percentage') - return false unless context.instance_of?(Unleash::Context) - return false if context.session_id.nil? || context.session_id.empty? - - percentage = Integer(params['percentage'] || 0) - (percentage.positive? && Util.get_normalized_number(context.session_id, params['groupId'] || "") <= percentage) - end - end - end -end diff --git a/lib/unleash/strategy/gradual_rollout_userid.rb b/lib/unleash/strategy/gradual_rollout_userid.rb deleted file mode 100644 index 1aa3c052..00000000 --- a/lib/unleash/strategy/gradual_rollout_userid.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'unleash/strategy/util' - -module Unleash - module Strategy - class GradualRolloutUserId < Base - def name - 'gradualRolloutUserId' - end - - # need: params['percentage'], params['groupId'], context.user_id, - def is_enabled?(params = {}, context = nil, _constraints = []) - return false unless params.is_a?(Hash) && params.has_key?('percentage') - return false unless context.instance_of?(Unleash::Context) - return false if context.user_id.nil? || context.user_id.empty? - - percentage = Integer(params['percentage'] || 0) - (percentage.positive? && Util.get_normalized_number(context.user_id, params['groupId'] || "") <= percentage) - end - end - end -end diff --git a/lib/unleash/strategy/remote_address.rb b/lib/unleash/strategy/remote_address.rb deleted file mode 100644 index d2223118..00000000 --- a/lib/unleash/strategy/remote_address.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Unleash - module Strategy - class RemoteAddress < Base - PARAM = 'IPs'.freeze - - def name - 'remoteAddress' - end - - # need: params['IPs'], context.remote_address - def is_enabled?(params = {}, context = nil) - return false unless params.is_a?(Hash) && params.has_key?(PARAM) - return false unless params.fetch(PARAM, nil).is_a? String - return false unless context.instance_of?(Unleash::Context) - - remote_address = ipaddr_or_nil_from_str(context.remote_address) - - params[PARAM] - .split(',') - .map(&:strip) - .map{ |ipblock| ipaddr_or_nil_from_str(ipblock) } - .compact - .map{ |ipb| ipb.include? remote_address } - .any? - end - - private - - def ipaddr_or_nil_from_str(ip) - IPAddr.new(ip) - rescue StandardError - nil - end - end - end -end diff --git a/lib/unleash/strategy/user_with_id.rb b/lib/unleash/strategy/user_with_id.rb deleted file mode 100644 index c20a75b3..00000000 --- a/lib/unleash/strategy/user_with_id.rb +++ /dev/null @@ -1,20 +0,0 @@ -module Unleash - module Strategy - class UserWithId < Base - PARAM = 'userIds'.freeze - - def name - 'userWithId' - end - - # requires: params['userIds'], context.user_id, - def is_enabled?(params = {}, context = nil) - return false unless params.is_a?(Hash) && params.has_key?(PARAM) - return false unless params.fetch(PARAM, nil).is_a? String - return false unless context.instance_of?(Unleash::Context) - - params[PARAM].split(",").map(&:strip).include?(context.user_id) - end - end - end -end diff --git a/lib/unleash/strategy/util.rb b/lib/unleash/strategy/util.rb deleted file mode 100644 index a00ade85..00000000 --- a/lib/unleash/strategy/util.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'murmurhash3' - -module Unleash - module Strategy - module Util - module_function - - NORMALIZER = 100 - - # convert the two strings () into a number between 1 and base (100 by default) - def get_normalized_number(identifier, group_id, base = NORMALIZER) - MurmurHash3::V32.str_hash("#{group_id}:#{identifier}") % base + 1 - end - end - end -end diff --git a/lib/unleash/toggle_fetcher.rb b/lib/unleash/toggle_fetcher.rb index 2b33f4d3..41361ef3 100755 --- a/lib/unleash/toggle_fetcher.rb +++ b/lib/unleash/toggle_fetcher.rb @@ -2,14 +2,14 @@ require 'unleash/bootstrap/handler' require 'net/http' require 'json' +require 'unleash_engine' module Unleash class ToggleFetcher - attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache + attr_accessor :toggle_engine, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache def initialize self.etag = nil - self.toggle_cache = nil self.segment_cache = nil self.toggle_lock = Mutex.new self.toggle_resource = ConditionVariable.new @@ -35,8 +35,8 @@ def initialize def toggles self.toggle_lock.synchronize do # wait for resource, only if it is null - self.toggle_resource.wait(self.toggle_lock) if self.toggle_cache.nil? - return self.toggle_cache + self.toggle_resource.wait(self.toggle_lock) if self.toggle_engine.nil? + return self.toggle_engine end end @@ -55,16 +55,16 @@ def fetch end self.etag = response['ETag'] - features = get_features(response.body) + engine = get_engine(response.body) # always synchronize with the local cache when fetching: - synchronize_with_local_cache!(features) + synchronize_with_local_cache!(engine) update_running_client! - save! + save! response.body end - def save! + def save!(toggle_data) Unleash.logger.debug "Will save toggles to disk now" backup_file = Unleash.configuration.backup_file @@ -72,7 +72,7 @@ def save! self.toggle_lock.synchronize do File.open(backup_file_tmp, "w") do |file| - file.write(self.toggle_cache.to_json) + file.write(toggle_data) end File.rename(backup_file_tmp, backup_file) end @@ -84,10 +84,10 @@ def save! private - def synchronize_with_local_cache!(features) - if self.toggle_cache != features + def synchronize_with_local_cache!(engine) + if self.toggle_engine != engine self.toggle_lock.synchronize do - self.toggle_cache = features + self.toggle_engine = engine end # notify all threads waiting for this resource to no longer wait @@ -96,10 +96,8 @@ def synchronize_with_local_cache!(features) end def update_running_client! - if Unleash.toggles != self.toggles["features"] || Unleash.segment_cache != self.toggles["segments"] - Unleash.logger.info "Updating toggles to main client, there has been a change in the server." - Unleash.toggles = self.toggles["features"] - Unleash.segment_cache = self.toggles["segments"] + if Unleash.engine != self.toggle_engine + Unleash.engine = self.toggle_engine end end @@ -121,7 +119,7 @@ def read! def bootstrap bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles - synchronize_with_local_cache! get_features bootstrap_payload + synchronize_with_local_cache! get_engine bootstrap_payload update_running_client! # reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again @@ -134,10 +132,15 @@ def build_segment_map(segments_array) segments_array.map{ |segment| [segment["id"], segment] }.to_h end + def get_engine(response_body) + engine = UnleashEngine.new + engine.take_state(response_body) + engine + end + # @param response_body [String] def get_features(response_body) response_hash = JSON.parse(response_body) - if response_hash['version'] >= 1 return { "features" => response_hash["features"], "segments" => build_segment_map(response_hash["segments"]) } end diff --git a/spec/unleash/activation_strategy_spec.rb b/spec/unleash/activation_strategy_spec.rb deleted file mode 100644 index 812dbe08..00000000 --- a/spec/unleash/activation_strategy_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'unleash/constraint' - -RSpec.describe Unleash::ActivationStrategy do - before do - Unleash.configuration = Unleash::Configuration.new - Unleash.logger = Unleash.configuration.logger - end - - let(:name) { 'test name' } - - describe '#initialize' do - context 'with correct payload' do - let(:params) { Hash.new(test: true) } - let(:constraints) { [Unleash::Constraint.new("constraint_name", "IN", ["value"])] } - - it 'initializes with correct attributes' do - expect(Unleash.logger).to_not receive(:warn) - - strategy = Unleash::ActivationStrategy.new(name, params, constraints) - - expect(strategy.name).to eq name - expect(strategy.params).to eq params - expect(strategy.constraints).to eq constraints - end - end - - context 'with incorrect payload' do - let(:params) { 'bad_params' } - let(:constraints) { [] } - - it 'initializes with correct attributes and logs warning' do - expect(Unleash.logger).to receive(:warn) - - strategy = Unleash::ActivationStrategy.new(name, params, constraints) - - expect(strategy.name).to eq name - expect(strategy.params).to eq({}) - expect(strategy.constraints).to eq(constraints) - end - end - end -end diff --git a/spec/unleash/client_specification_spec.rb b/spec/unleash/client_specification_spec.rb index 621ed4f8..a45ffbe2 100644 --- a/spec/unleash/client_specification_spec.rb +++ b/spec/unleash/client_specification_spec.rb @@ -10,37 +10,30 @@ DEFAULT_VARIANT = Unleash::Variant.new(name: 'unknown', enabled: false).freeze before do - Unleash.configuration = Unleash::Configuration.new Unleash.logger = Unleash.configuration.logger Unleash.logger.level = Unleash.configuration.log_level - Unleash.toggles = [] - Unleash.toggle_metrics = {} - - # Do not test metrics: - Unleash.configuration.disable_metrics = true end if File.exist?(SPECIFICATION_PATH + '/index.json') JSON.parse(File.read(SPECIFICATION_PATH + '/index.json')).each do |test_file| describe "for #{test_file}" do current_test_set = JSON.parse(File.read(SPECIFICATION_PATH + '/' + test_file)) + context "with #{current_test_set.fetch('name')} " do - # name = current_test_set.fetch('name', '') tests = current_test_set.fetch('tests', []) state = current_test_set.fetch('state', {}) - state_features = state.fetch('features', []) - state_segments = state.fetch('segments', []).map{ |segment| [segment["id"], segment] }.to_h - - let(:unleash_toggles) { state_features } - tests.each do |test| it "test that #{test['description']}" do - test_toggle = unleash_toggles.select{ |t| t.fetch('name', '') == test.fetch('toggleName') }.first - - toggle = Unleash::FeatureToggle.new(test_toggle, state_segments) context = Unleash::Context.new(test['context']) - toggle_result = toggle.is_enabled?(context) + unleash = Unleash::Client.new( + app_name: 'bootstrap-test', + instance_id: 'local-test-cli', + disable_client: true, + disable_metrics: true, + bootstrap_config: Unleash::Bootstrap::Configuration.new(data: current_test_set.fetch('state', {}).to_json) + ) + toggle_result = unleash.is_enabled?(test.fetch('toggleName'), context) expect(toggle_result).to eq(test['expectedResult']) end @@ -49,14 +42,21 @@ variant_tests = current_test_set.fetch('variantTests', []) variant_tests.each do |test| it "test that #{test['description']}" do - test_toggle = unleash_toggles.select{ |t| t.fetch('name', '') == test.fetch('toggleName') }.first - - toggle = Unleash::FeatureToggle.new(test_toggle, state_segments) context = Unleash::Context.new(test['context']) - variant = toggle.get_variant(context, DEFAULT_VARIANT) + unleash = Unleash::Client.new( + app_name: 'bootstrap-test', + instance_id: 'local-test-cli', + disable_client: true, + disable_metrics: true, + bootstrap_config: Unleash::Bootstrap::Configuration.new(data: current_test_set.fetch('state', {}).to_json) + ) + variant = unleash.get_variant(test.fetch('toggleName'), context) + expectedResult = test['expectedResult'] - expect(variant).to eq(Unleash::Variant.new(test['expectedResult'])) + expect(variant.name).to eq(expectedResult['name']) + expect(variant.enabled).to eq(expectedResult['enabled']) + expect(variant.payload).to eq(expectedResult['payload']) end end end diff --git a/spec/unleash/constraint_spec.rb b/spec/unleash/constraint_spec.rb deleted file mode 100644 index 38c19091..00000000 --- a/spec/unleash/constraint_spec.rb +++ /dev/null @@ -1,458 +0,0 @@ -RSpec.describe Unleash::Constraint do - before do - Unleash.configuration = Unleash::Configuration.new - Unleash.logger = Unleash.configuration.logger - end - - describe '#is_enabled?' do - it 'matches based on property IN value' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - env: 'dev' - } - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('env', 'IN', ['dev']) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'IN', ['dev', 'pre']) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'NOT_IN', ['dev', 'pre']) - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('env', 'NOT_IN', ['pre', 'prod']) - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on property NOT_IN value' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.2', - properties: { - env: 'dev' - } - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('env', 'NOT_IN', ['dev']) - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('env', 'NOT_IN', ['dev', 'pre']) - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('env', 'NOT_IN', ['pre', 'prod']) - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on a value NOT_IN in a not existing context field' do - context_params = { - properties: {} - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('env', 'NOT_IN', ['anything']) - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on user_id IN/NOT_IN user_id' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.3', - properties: { - fancy: 'polarbear' - } - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('user_id', 'IN', ['123', '456']) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('user_id', 'IN', ['456', '789']) - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['123', '456']) - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['456', '789']) - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on user_id IN/NOT_IN user_id with user_id as int' do - context_params = { - user_id: 123 - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('user_id', 'IN', ['123', '456']) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('user_id', 'IN', ['456', '789']) - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['123', '456']) - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['456', '789']) - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on property STR_STARTS_WITH value' do - context_params = { - properties: { - env: 'development' - } - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['dev']) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['development']) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['ment']) - expect(constraint.matches_context?(context)).to be false - end - - it 'matches based on property STR_ENDS_WITH value' do - context_params = { - properties: { - env: 'development' - } - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['ment']) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['development']) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['dev']) - expect(constraint.matches_context?(context)).to be false - end - - it 'matches based on property STR_CONTAINS value' do - context_params = { - properties: { - env: 'development' - } - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['ment']) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['dev']) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['development']) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['DEVELOPMENT']) - expect(constraint.matches_context?(context)).to be false - end - - it 'matches based on property NUM_EQ value' do - context_params = { - properties: { - distance: '0.3' - } - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('distance', 'NUM_EQ', '0.3') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('distance', 'NUM_EQ', '0.2') - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('distance', 'NUM_EQ', (0.1 + 0.2).to_s) - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on property NUM_LT value' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - distance: '3.141' - } - } - context = Unleash::Context.new(context_params) - - constraint = Unleash::Constraint.new('distance', 'NUM_LT', '2.718') - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('distance', 'NUM_LT', '3.141') - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('distance', 'NUM_LT', '6.282') - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on property NUM_LTE value' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - distance: '3.141' - } - } - context = Unleash::Context.new(context_params) - - constraint = Unleash::Constraint.new('distance', 'NUM_LTE', '2.718') - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('distance', 'NUM_LTE', '3.141') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('distance', 'NUM_LTE', '6.282') - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on property NUM_GT value' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - distance: '3.141' - } - } - context = Unleash::Context.new(context_params) - - constraint = Unleash::Constraint.new('distance', 'NUM_GT', '2.718') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('distance', 'NUM_GT', '3.141') - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('distance', 'NUM_GT', '6.282') - expect(constraint.matches_context?(context)).to be false - end - - it 'matches based on property NUM_GTE value' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - distance: '3.141' - } - } - context = Unleash::Context.new(context_params) - - constraint = Unleash::Constraint.new('distance', 'NUM_GTE', '2.718') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('distance', 'NUM_GTE', '3.141') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('distance', 'NUM_GTE', '6.282') - expect(constraint.matches_context?(context)).to be false - end - - it 'matches based on property SEMVER_EQ value' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - env: '3.1.41-beta' - } - } - context = Unleash::Context.new(context_params) - - constraint = Unleash::Constraint.new('env', 'SEMVER_EQ', '3.1.41-beta') - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on property SEMVER_GT value' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - env: '3.1.41-gamma' - } - } - context = Unleash::Context.new(context_params) - - constraint = Unleash::Constraint.new('env', 'SEMVER_GT', '3.1.41-beta') - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on property SEMVER_LT value' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - env: '3.1.41-alpha' - } - } - context = Unleash::Context.new(context_params) - - constraint = Unleash::Constraint.new('env', 'SEMVER_LT', '3.1.41-beta') - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on property DATE_AFTER value' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - currentTime: '2022-01-30T13:00:00.000Z' - } - context = Unleash::Context.new(context_params) - - constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-29T13:00:00.000Z') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-29T13:00:00Z') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-29T13:00Z') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59:59.999999Z') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59:59.999Z') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59:59') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59') - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T13:00:00.000Z') - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-31T13:00:00.000Z') - expect(constraint.matches_context?(context)).to be false - end - - it 'matches based on property DATE_BEFORE value' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - currentTime: '2022-01-30T13:00:00.000Z' - } - context = Unleash::Context.new(context_params) - - constraint = Unleash::Constraint.new('currentTime', 'DATE_BEFORE', '2022-01-29T13:00:00.000Z') - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('currentTime', 'DATE_BEFORE', '2022-01-31T13:00:00.000Z') - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on case insensitive property when operator is uppercased' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - env: 'development' - } - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['DEV'], case_insensitive: true) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['MENT'], case_insensitive: true) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['LOP'], case_insensitive: true) - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on case insensitive property when context is uppercased' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - env: 'DEVELOPMENT' - } - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['dev'], case_insensitive: true) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['ment'], case_insensitive: true) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['lop'], case_insensitive: true) - expect(constraint.matches_context?(context)).to be true - end - - it 'matches based on inverted property' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - env: 'development' - } - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['dev'], inverted: true) - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['ment'], inverted: true) - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['lop'], inverted: true) - expect(constraint.matches_context?(context)).to be false - end - - it 'gracefully handles invalid constraint operators' do - context_params = { - user_id: '123', - session_id: 'verylongsesssionid', - remote_address: '127.0.0.1', - properties: { - env: 'development' - } - } - context = Unleash::Context.new(context_params) - constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', 'dev', inverted: true) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', ['dev'], inverted: true) - expect(constraint.matches_context?(context)).to be true - - constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', 'dev') - expect(constraint.matches_context?(context)).to be false - - constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', ['dev']) - expect(constraint.matches_context?(context)).to be false - end - - it 'warns about constraint construction for invalid value types for operator' do - array_constraints = ['STR_CONTAINS', 'STR_ENDS_WITH', 'STR_STARTS_WITH', 'IN', 'NOT_IN'] - - array_constraints.each do |operator_name| - expect(Unleash.logger).to receive(:warn).with("value is a String, operator is expecting an Array") - Unleash::Constraint.new('env', operator_name, '') - end - - string_constraints = ['NUM_EQ', 'NUM_GT', 'NUM_GTE', 'NUM_LT', 'NUM_LTE', - 'DATE_AFTER', 'DATE_BEFORE', 'SEMVER_EQ', 'SEMVER_GT', 'SEMVER_LT'] - string_constraints.each do |operator_name| - expect(Unleash.logger).to receive(:warn).with("value is an Array, operator is expecting a String") - Unleash::Constraint.new('env', operator_name, []) - end - end - end - - it 'does resolves to false rather than crashing when passed a nil context' do - constraint = Unleash::Constraint.new('anything', 'NUM_GTE', '6.282') - expect(constraint.matches_context?(nil)).to be false - end -end diff --git a/spec/unleash/feature_toggle_spec.rb b/spec/unleash/feature_toggle_spec.rb deleted file mode 100644 index a95f2436..00000000 --- a/spec/unleash/feature_toggle_spec.rb +++ /dev/null @@ -1,663 +0,0 @@ -require 'logger' -require 'unleash' -require 'unleash/configuration' -require 'unleash/context' -require 'unleash/feature_toggle' -require 'unleash/variant' - -RSpec.describe Unleash::FeatureToggle do - before do - Unleash.configuration = Unleash::Configuration.new - Unleash.logger = Unleash.configuration.logger - Unleash.logger.level = Unleash.configuration.log_level - Unleash.logger.level = Logger::ERROR - Unleash.toggles = [] - Unleash.toggle_metrics = {} - - # Do not test metrics: - Unleash.configuration.disable_metrics = true - end - - describe 'FeatureToggle with empty strategies' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "test", - "enabled" => true, - "strategies" => [], - "variants" => nil - ) - end - - it 'should return true if enabled' do - context = Unleash::Context.new(user_id: 1) - expect(feature_toggle.is_enabled?(context)).to be_truthy - end - end - - describe 'FeatureToggle with empty strategies and disabled toggle' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "Test.userid", - "description" => nil, - "enabled" => false, - "strategies" => [], - "variants" => nil, - "createdAt" => "2019-01-24T10:41:45.236Z" - ) - end - - it 'should return false if disabled' do - context = Unleash::Context.new(user_id: 1) - expect(feature_toggle.is_enabled?(context)).to be_falsey - end - end - - describe 'FeatureToggle with userId strategy and enabled toggle' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "Test.userid", - "description" => nil, - "enabled" => true, - "strategies" => [ - { - "name" => "userWithId", - "parameters" => { - "userIds" => "12345" - } - } - ], - "variants" => nil, - "createdAt" => "2019-01-24T10:41:45.236Z" - ) - end - - it 'should return true if enabled and user_id is matched' do - context = Unleash::Context.new(user_id: "12345") - expect(feature_toggle.is_enabled?(context)).to be_truthy - end - - it 'should return false if enabled and user_id is unmatched' do - context = Unleash::Context.new(user_id: "54321") - expect(feature_toggle.is_enabled?(context)).to be_falsey - end - end - - describe 'FeatureToggle with userId strategy and disabled toggle' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "Test.userid", - "description" => nil, - "enabled" => false, - "strategies" => [ - { - "name" => "userWithId", - "parameters" => { - "userIds" => "12345" - } - } - ], - "variants" => nil, - "createdAt" => "2019-01-24T10:41:45.236Z" - ) - end - - it 'should return false if disabled and user_id matched' do - context = Unleash::Context.new(user_id: "12345") - expect(feature_toggle.is_enabled?(context)).to be_falsey - end - - it 'should return false if disabled and user_id unmatched' do - context = Unleash::Context.new(user_id: "54321") - expect(feature_toggle.is_enabled?(context)).to be_falsey - end - end - - describe 'FeatureToggle with variants' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "Test.variants", - "description" => nil, - "enabled" => true, - "strategies" => [ - { - "name" => "default" - } - ], - "variants" => [ - { - "name" => "variant1", - "weight" => 50, - "stickiness" => "default" - }, - { - "name" => "variant2", - "weight" => 50, - "stickiness" => "default" - } - ], - "createdAt" => "2019-01-24T10:41:45.236Z" - ) - end - - let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } - - it 'should return variant1 for user_id:1' do - context = Unleash::Context.new(user_id: 10) - expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( - name: "variant1", - enabled: true, - payload: nil - ) - end - - it 'should return variant2 for user_id:2' do - context = Unleash::Context.new(user_id: 2) - expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( - name: "variant2", - enabled: true, - payload: nil - ) - end - - xit 'should return false if default is false.' do - context = Unleash::Context.new(user_id: 2) - expect(feature_toggle.get_variant(context, default_variant)).to be_falsey - end - end - - describe 'FeatureToggle including weightless variants' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "Test.variants", - "description" => nil, - "enabled" => true, - "strategies" => [ - { - "name" => "default" - } - ], - "variants" => [ - { - "name" => "variantA", - "weight" => 0, - "stickiness" => "default" - }, - { - "name" => "variantB", - "weight" => 10, - "stickiness" => "default" - }, - { - "name" => "variantC", - "weight" => 20, - "stickiness" => "default" - } - ], - "createdAt" => "2019-01-24T10:41:45.236Z" - ) - end - - let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } - - it 'should return variantC for user_id:1' do - context = Unleash::Context.new(user_id: 10) - expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( - name: "variantC", - enabled: true, - payload: nil - ) - end - - it 'should return variantB for user_id:2' do - context = Unleash::Context.new(user_id: 2) - expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( - name: "variantB", - enabled: true, - payload: nil - ) - end - end - - describe 'FeatureToggle with variants which have all zero weight' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "Test.variants", - "description" => nil, - "enabled" => true, - "strategies" => [ - { - "name" => "default" - } - ], - "variants" => [ - { - "name" => "variantA", - "weight" => 0 - }, - { - "name" => "variantB", - "weight" => 0 - } - ], - "createdAt" => "2019-01-24T10:41:45.236Z" - ) - end - let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } - - it 'should return disabled for user_id:1' do - context = Unleash::Context.new(user_id: 10) - expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( - name: "disabled", - enabled: false, - payload: nil - ) - end - - it 'should return disabled for user_id:2' do - context = Unleash::Context.new(user_id: 2) - expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( - name: "disabled", - enabled: false, - payload: nil - ) - end - end - - describe 'FeatureToggle with variants that have a variant override' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "Test.variants", - "description" => nil, - "enabled" => true, - "strategies" => [ - { - "name" => "default" - } - ], - "variants" => [ - { - "name" => "variant1", - "weight" => 50, - "stickiness" => "default", - "payload" => { - "type" => "string", - "value" => "val1" - }, - "overrides" => [{ - "contextName" => "userId", - "values" => ["132", "61"] - }] - }, - { - "name" => "variant2", - "weight" => 50, - "stickiness" => "default", - "payload" => { - "type" => "string", - "value" => "val2" - } - } - ], - "createdAt" => "2019-01-24T10:41:45.236Z" - ) - end - - it 'should return variant1 for user_id:61 from override' do - context = Unleash::Context.new(user_id: 61) - expect(feature_toggle.get_variant(context)).to have_attributes( - name: "variant1", - enabled: true, - payload: { "type" => "string", "value" => "val1" } - ) - end - - it 'should return variant1 for user_id:132 from override' do - context = Unleash::Context.new("userId" => 132) - expect(feature_toggle.get_variant(context)).to have_attributes( - name: "variant1", - enabled: true, - payload: { "type" => "string", "value" => "val1" } - ) - end - - it 'should return variant2 for user_id:60' do - context = Unleash::Context.new(user_id: 60) - expect(feature_toggle.get_variant(context)).to have_attributes( - name: "variant2", - enabled: true, - payload: { "type" => "string", "value" => "val2" } - ) - end - - it 'get_variant_with_matching_override should for user_id:61' do - # NOTE: Use send method, as we are testing a private method - context = Unleash::Context.new(user_id: 61) - expect(feature_toggle.send(:variant_from_override_match, context)).to have_attributes( - name: "variant1", - payload: { "type" => "string", "value" => "val1" } - ) - end - end - - describe 'FeatureToggle with no variants' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "Test.variants", - "description" => nil, - "enabled" => true, - "strategies" => [ - { - "name" => "default" - } - ], - "variants" => [], - "createdAt" => "2019-01-24T10:41:45.236Z" - ) - end - let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } - - it 'should return disabled for user_id:1' do - context = Unleash::Context.new(user_id: 10) - expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( - name: "disabled", - enabled: false, - payload: nil - ) - end - - it 'should return disabled for user_id:2' do - context = Unleash::Context.new(user_id: 2) - expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( - name: "disabled", - enabled: false, - payload: nil - ) - end - - it 'should return an enabled fallback when the fallback is specified' do - context = Unleash::Context.new(user_id: 2) - expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( - name: "disabled", - enabled: false, - payload: nil - ) - end - end - - describe 'FeatureToggle with invalid default_variant' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "Test.variants", - "description" => nil, - "enabled" => true, - "strategies" => [ - { - "name" => "default" - } - ], - "variants" => [], - "createdAt" => "2019-01-24T10:41:45.236Z" - ) - end - let(:valid_default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } - let(:invalid_default_variant) { Hash.new(name: 'unknown', default: true) } - - it 'should raise an error for an invalid fallback variant' do - expect{ feature_toggle.get_variant(nil, invalid_default_variant) }.to raise_error(ArgumentError) - end - - it 'should not raise an error for a valid fallback variant' do - expect{ feature_toggle.get_variant(nil, valid_default_variant) }.to_not raise_error - end - end - - describe 'FeatureToggle default Strategy with two constraints' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "Test.userid", - "description" => "Play with strategy constraints", - "enabled" => true, - "strategies" => [ - { - "constraints" => [ - { - "contextName" => "environment", - "operator" => "IN", - "values" => [ - "dev" - ] - }, - { - "contextName" => "userId", - "operator" => "IN", - "values" => ["123"] - } - ], - "name" => "default", - "parameters" => {} - } - ] - ) - end - - it 'should return true if it matches all constraints' do - context = Unleash::Context.new(user_id: "123", environment: "dev") - expect(feature_toggle.is_enabled?(context)).to be_truthy - end - - it 'should return false if it does not match all constraints (env)' do - context = Unleash::Context.new(user_id: "123", environment: "prod") - expect(feature_toggle.is_enabled?(context)).to be_falsey - end - - it 'should return false if it does not match all constraints (user_id)' do - context = Unleash::Context.new(user_id: "11", environment: "dev") - expect(feature_toggle.is_enabled?(context)).to be_falsey - end - - it 'should return false if it does not match any constraint (env and user_id)' do - context = Unleash::Context.new(user_id: "11", environment: "prod") - expect(feature_toggle.is_enabled?(context)).to be_falsey - end - end - - describe 'FeatureToggle default Strategy with one constraint' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "Demo", - "description" => "Play with strategy constraints", - "enabled" => true, - "strategies" => [ - { - "constraints" => [ - { - "contextName" => "environment", - "operator" => "IN", - "values" => [ - "dev" - ] - } - ], - "name" => "default", - "parameters" => {} - } - ] - ) - end - - it 'should return true if it matches the constraint' do - context = Unleash::Context.new(user_id: "123", environment: "dev") - expect(feature_toggle.is_enabled?(context)).to be_truthy - end - - it 'should return false if it does not match the constraint' do - context = Unleash::Context.new(user_id: "123", environment: "prod") - expect(feature_toggle.is_enabled?(context)).to be_falsey - end - end - - describe 'disabled_variant' do - it 'returns disabled variant' do - ret = described_class.disabled_variant - expect(ret.enabled).to be false - expect(ret.name).to eq 'disabled' - end - end - - describe 'FeatureToggle variant with custom stickiness' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "toggleName", - "description" => nil, - "enabled" => true, - "variants" => [ - { - "name" => "variant1", - "weight" => 25, - "stickiness" => "organization" - }, - { - "name" => "variant2", - "weight" => 25, - "stickiness" => "organization" - }, - { - "name" => "variant3", - "weight" => 25, - "stickiness" => "organization" - }, - { - "name" => "variant4", - "weight" => 25, - "stickiness" => "organization" - } - - ], - "createdAt" => "2019-01-24T10:41:45.236Z" - ) - end - - it 'should return variant1 organization 726' do - context = Unleash::Context.new( - properties: { - organization: '726' - } - ) - - expect(feature_toggle.get_variant(context)).to have_attributes( - name: "variant1", - enabled: true - ) - end - - it 'should return variant2 organization 48' do - context = Unleash::Context.new( - properties: { - organization: '48' - } - ) - - expect(feature_toggle.get_variant(context)).to have_attributes( - name: "variant2", - enabled: true - ) - end - - it 'should return variant3 organization 381' do - context = Unleash::Context.new( - properties: { - organization: '381' - } - ) - - expect(feature_toggle.get_variant(context)).to have_attributes( - name: "variant3", - enabled: true - ) - end - - it 'should return variant4 organization 222' do - context = Unleash::Context.new( - properties: { - organization: '222' - } - ) - - expect(feature_toggle.get_variant(context)).to have_attributes( - name: "variant4", - enabled: true - ) - end - - it 'should work with a nil context' do - variant = feature_toggle.get_variant(nil) - - expect(variant.name).to match(/variant\d/) - expect(variant.enabled).to be true - expect(variant).to be_a_kind_of(Unleash::Variant) - end - end - - describe 'FeatureToggle Variant with payload and custom stickiness' do - let(:feature_toggle) do - Unleash::FeatureToggle.new( - "name" => "featureVariantX", - "description" => nil, - "enabled" => true, - "strategies" => [ - { "name" => "default" } - ], - "variants" => [ - { - "name" => "default-value", - "payload" => { - "type" => "string", - "value" => "payloadData" - }, - "stickiness" => "custom_context_attribute", - "weight" => 100, - "weightType" => "variable" - } - ] - ) - end - - let(:expected_variant) do - { - name: "default-value", - enabled: true, - payload: { - "type" => "string", - "value" => "payloadData" - } - } - end - - it 'should return the one variant, when the context correctly contains the custom stickiness parameter' do - context = Unleash::Context.new( - properties: { - default: 'foo', - custom_context_attribute: 'uniqueContextValue' - } - ) - expect(feature_toggle.get_variant(context)).to have_attributes(expected_variant) - end - - it 'should return the one variant, with context that is nil' do - expect(feature_toggle.get_variant(nil)).to have_attributes(expected_variant) - end - - it 'should return the one variant, even when the contexts do not contain the stickiness parameter' do - [ - nil, - Unleash::Context.new, - Unleash::Context.new(user_id: '123'), - Unleash::Context.new(session_id: '123'), - Unleash::Context.new(remote_address: '127.0.0.1'), - Unleash::Context.new(properties: { not_custom_context_attribute: 'foo' }) - ].each do |context| - expect(feature_toggle.get_variant(context)).to have_attributes(expected_variant) - end - end - end -end diff --git a/spec/unleash/metrics_reporter_spec.rb b/spec/unleash/metrics_reporter_spec.rb index cd5bd4fb..858a0fb1 100644 --- a/spec/unleash/metrics_reporter_spec.rb +++ b/spec/unleash/metrics_reporter_spec.rb @@ -27,24 +27,26 @@ config.instance_id = 'rspec/test' config.disable_client = true end - Unleash.toggle_metrics = Unleash::Metrics.new + Unleash.engine = UnleashEngine.new - Unleash.toggle_metrics.increment('featureA', :yes) - Unleash.toggle_metrics.increment('featureA', :yes) - Unleash.toggle_metrics.increment('featureA', :yes) - Unleash.toggle_metrics.increment('featureA', :no) - Unleash.toggle_metrics.increment('featureA', :no) - Unleash.toggle_metrics.increment('featureB', :yes) + Unleash.engine.count_toggle('featureA', true) + Unleash.engine.count_toggle('featureA', true) + Unleash.engine.count_toggle('featureA', true) + Unleash.engine.count_toggle('featureA', false) + Unleash.engine.count_toggle('featureA', false) + Unleash.engine.count_toggle('featureB', true) report = metrics_reporter.generate_report expect(report[:bucket][:toggles]).to include( - "featureA" => { + :featureA => { no: 2, - yes: 3 + yes: 3, + variants: {} }, - "featureB" => { + :featureB => { no: 0, - yes: 1 + yes: 1, + variants: {} } ) @@ -74,14 +76,14 @@ ) .to_return(status: 200, body: "", headers: {}) - Unleash.toggle_metrics = Unleash::Metrics.new + Unleash.engine = UnleashEngine.new - Unleash.toggle_metrics.increment('featureA', :yes) - Unleash.toggle_metrics.increment('featureA', :yes) - Unleash.toggle_metrics.increment('featureA', :yes) - Unleash.toggle_metrics.increment('featureA', :no) - Unleash.toggle_metrics.increment('featureA', :no) - Unleash.toggle_metrics.increment('featureB', :yes) + Unleash.engine.count_toggle('featureA', true) + Unleash.engine.count_toggle('featureA', true) + Unleash.engine.count_toggle('featureA', true) + Unleash.engine.count_toggle('featureA', false) + Unleash.engine.count_toggle('featureA', false) + Unleash.engine.count_toggle('featureB', true) metrics_reporter.post @@ -105,7 +107,7 @@ end it "does not send a report, if there were no metrics registered/evaluated" do - Unleash.toggle_metrics = Unleash::Metrics.new + Unleash.engine = UnleashEngine.new metrics_reporter.post diff --git a/spec/unleash/metrics_spec.rb b/spec/unleash/metrics_spec.rb deleted file mode 100644 index 2830f9b5..00000000 --- a/spec/unleash/metrics_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -require "rspec/json_expectations" - -RSpec.describe Unleash::Metrics do - let(:metrics) { Unleash::Metrics.new } - - it "counts up correctly" do - metrics.increment('featureA', :yes) - metrics.increment('featureA', :yes) - metrics.increment('featureA', :yes) - metrics.increment('featureA', :no) - metrics.increment('featureA', :no) - - metrics.increment('featureB', :yes) - metrics.increment('featureB', :no) - metrics.increment('featureC', :no) - - expect(metrics.features['featureA'][:yes]).to eq(3) - expect(metrics.features['featureA'][:no]).to eq(2) - expect(metrics.features['featureB'][:yes]).to eq(1) - expect(metrics.features['featureB'][:no]).to eq(1) - expect(metrics.features['featureC'][:yes]).to eq(0) - expect(metrics.features['featureC'][:no]).to eq(1) - end - - it "resets correctly" do - metrics = Unleash::Metrics.new - - metrics.increment('featureA', :yes) - metrics.reset - metrics.increment('featureB', :no) - - expect(metrics.features['featureA']).to be_nil - expect(metrics.features['featureB'][:yes]).to eq(0) - expect(metrics.features['featureB'][:no]).to eq(1) - end - - it "spits out correct JSON" do - metrics.reset - metrics.increment('featureA', :yes) - metrics.increment('featureB', :no) - - expect(metrics.to_s).to include_json( - featureA: { - yes: 1, - no: 0 - }, - featureB: { - no: 1 - } - ) - end - - describe "when dealing with variants" do - it "counts up correctly" do - metrics.increment_variant('featureA', :yes, 'variantA') - metrics.increment_variant('featureA', :yes, 'variantA') - metrics.increment_variant('featureA', :yes, 'variantB') - - expect(metrics.features['featureA'][:yes]).to eq(3) - expect(metrics.features['featureA'][:no]).to eq(0) - expect(metrics.features['featureA']['variants']['variantA']).to eq(2) - expect(metrics.features['featureA']['variants']['variantB']).to eq(1) - end - end - - it "increments feature toggle counter when variant is resolved" do - metrics.increment_variant('featureA', :yes, 'variantA') - - expect(metrics.features['featureA'][:yes]).to eq(1) - expect(metrics.features['featureA'][:no]).to eq(0) - end -end diff --git a/spec/unleash/strategies_spec.rb b/spec/unleash/strategies_spec.rb deleted file mode 100644 index 87b70ff0..00000000 --- a/spec/unleash/strategies_spec.rb +++ /dev/null @@ -1,156 +0,0 @@ -require "spec_helper" - -RSpec.describe Unleash::Strategies do - let(:strategies) { described_class.new } - - # Silence warnings we are triggering in this test - around do |example| - old_verbose = $VERBOSE - $VERBOSE = nil - example.run - ensure - $VERBOSE = old_verbose - end - - describe 'strategies registration' do - let(:default_strategies) do - ['applicationHostname', 'default', 'flexibleRollout', 'gradualRolloutRandom', - 'gradualRolloutSessionId', 'gradualRolloutUserId', 'remoteAddress', - 'userWithId'] - end - - context 'when no custom strategies are defined' do - it 'has default list' do - expect(strategies.keys.sort).to eq(default_strategies) - end - end - - # This block testing previous way of loading strategies, when we dynamically picked up all classes - # defined under `Unleash::Strategy` module - context 'when custom strategy is defined' do - let(:custom_strategy) do - Class.new do - def name - 'myCustomStrategy' - end - end - end - - before do - # Define custom class - Unleash::Strategy.const_set("MyCustomStrategy", custom_strategy) - end - - after do - # Remove custom class so it does not interfere with other tests - Unleash::Strategy.send(:remove_const, :MyCustomStrategy) - end - - it 'includes custom strategy in default list' do - expect(strategies.keys.sort).to eq(default_strategies.concat(['myCustomStrategy']).sort) - end - - it 'warns about deprecated functionality' do - allow(strategies).to receive(:warn) - strategies.send(:register_strategies) - message = '[DEPRECATED] Registering custom Unleash strategy by adding custom class into Unleash::Strategy' - expect(strategies).to have_received(:warn).with(start_with(message)) - end - end - end - - describe '#includes?' do - it 'returns true for available strategy' do - expect(strategies.includes?('gradualRolloutRandom')).to be_truthy - expect(strategies.includes?(:userWithId)).to be_truthy - end - - it 'returns false for missing strategy' do - expect(strategies.includes?(:missing)).to be_falsey - end - end - - describe '#fetch' do - it 'returns available strategy' do - expect(strategies.fetch(:flexibleRollout)).to be_instance_of(Unleash::Strategy::FlexibleRollout) - expect(strategies.fetch('applicationHostname')).to be_instance_of(Unleash::Strategy::ApplicationHostname) - end - - it 'raising error when missing' do - message = 'Strategy is not implemented' - expect { strategies.fetch(:missing) }.to raise_error(Unleash::Strategy::NotImplemented, message) - end - end - - describe '#[]' do - it 'returns available strategy' do - expect(strategies[:flexibleRollout]).to be_instance_of(Unleash::Strategy::FlexibleRollout) - expect(strategies['applicationHostname']).to be_instance_of(Unleash::Strategy::ApplicationHostname) - end - - it 'returns nil when missing strategy' do - expect(strategies[:missing]).to be_nil - end - end - - describe '#add' do - before do - strategies.add(custom_strategy) - end - - context 'when existing strategy is available' do - let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'applicationHostname') } - - it 'overrides previous strategy strategy' do - expect(strategies.includes?('applicationHostname')).to be_truthy - expect(strategies.fetch(:applicationHostname)).to eq(custom_strategy) - expect(strategies.fetch('applicationHostname')).to eq(custom_strategy) - end - end - - context 'when strategy is new' do - let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'test') } - - it 'adds new strategy strategy' do - expect(strategies.includes?('test')).to be_truthy - expect(strategies.fetch(:test)).to eq(custom_strategy) - expect(strategies.fetch('test')).to eq(custom_strategy) - end - end - end - - describe '#[]=' do - let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'strange name') } - - context 'when existing strategy is available' do - let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'applicationHostname') } - - it 'overrides previous strategy strategy' do - strategies[:applicationHostname] = custom_strategy - - expect(strategies.includes?('applicationHostname')).to be_truthy - expect(strategies.fetch(:applicationHostname)).to eq(custom_strategy) - expect(strategies.fetch('applicationHostname')).to eq(custom_strategy) - end - - it 'warns when using this method' do - allow(strategies).to receive(:warn) - strategies[:applicationHostname] = custom_strategy - message = '[DEPRECATED] Registering custom Unleash strategy by modifying Unleash::STRATEGIES' - expect(strategies).to have_received(:warn).with(start_with(message)) - end - end - - context 'when strategy is new' do - before do - strategies['test'] = custom_strategy - end - - it 'adds new strategy strategy' do - expect(strategies.includes?('test')).to be_truthy - expect(strategies.fetch(:test)).to eq(custom_strategy) - expect(strategies.fetch('test')).to eq(custom_strategy) - end - end - end -end diff --git a/spec/unleash/strategy/application_hostname_spec.rb b/spec/unleash/strategy/application_hostname_spec.rb deleted file mode 100644 index 0614533b..00000000 --- a/spec/unleash/strategy/application_hostname_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require "unleash/strategy/application_hostname" - -RSpec.describe Unleash::Strategy::ApplicationHostname do - describe '#is_enabled?' do - let(:strategy) { Unleash::Strategy::ApplicationHostname.new } - - before do - expect(Socket).to receive(:gethostname).and_return("rspechost") - end - - it 'correctly initialize' do - expect(strategy.hostname).to eq("rspechost") - end - - it 'should be enabled with correct params' do - expect(strategy.is_enabled?({ 'hostnames' => 'foo,rspechost,bar' })).to be_truthy - end - - it 'should be disabled with false params' do - expect(strategy.is_enabled?({ 'hostnames' => 'abc,localhost' })).to be_falsey - end - - it 'should be disabled on invalid params' do - expect(strategy.is_enabled?(nil)).to be_falsey - expect(strategy.is_enabled?('string')).to be_falsey - expect(strategy.is_enabled?({})).to be_falsey - end - end -end diff --git a/spec/unleash/strategy/base_spec.rb b/spec/unleash/strategy/base_spec.rb deleted file mode 100644 index 3e04cdeb..00000000 --- a/spec/unleash/strategy/base_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -require "unleash/strategy/base" - -RSpec.describe Unleash::Strategy::Base do - describe '#is_enabled?' do - let(:strategy) { Unleash::Strategy::Base.new } - - it 'raise exception' do - expect{ strategy.is_enabled? }.to raise_exception Unleash::Strategy::NotImplemented - end - end -end diff --git a/spec/unleash/strategy/default_spec.rb b/spec/unleash/strategy/default_spec.rb deleted file mode 100644 index 60a908b3..00000000 --- a/spec/unleash/strategy/default_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -require "unleash/strategy/default" - -RSpec.describe Unleash::Strategy::Default do - describe '#is_enabled?' do - let(:strategy) { Unleash::Strategy::Default.new } - - it 'always returns true' do - expect(strategy.is_enabled?).to be_truthy - end - end -end diff --git a/spec/unleash/strategy/flexible_rollout_spec.rb b/spec/unleash/strategy/flexible_rollout_spec.rb deleted file mode 100644 index d4783b89..00000000 --- a/spec/unleash/strategy/flexible_rollout_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'unleash/strategy/flexible_rollout' - -RSpec.describe Unleash::Strategy::FlexibleRollout do - describe '#is_enabled?' do - let(:strategy) { Unleash::Strategy::FlexibleRollout.new } - let(:unleash_context) { Unleash::Context.new } - - it 'should always be enabled when rollout is set to 100, disabled when set to 0' do - params = { - 'groupId' => 'Demo', - 'rollout' => 100, - 'stickiness' => 'default' - } - - expect(strategy.is_enabled?(params, unleash_context)).to be_truthy - expect(strategy.is_enabled?(params.merge({ 'rollout' => 0 }), unleash_context)).to be_falsey - end - - it 'should behave predictably when based on the normalized_number' do - allow(Unleash::Strategy::Util).to receive(:get_normalized_number).and_return(15) - - params = { - 'groupId' => 'Demo', - 'stickiness' => 'default' - } - - expect(strategy.is_enabled?(params.merge({ 'rollout' => 14 }), unleash_context)).to be_falsey - expect(strategy.is_enabled?(params.merge({ 'rollout' => 15 }), unleash_context)).to be_truthy - expect(strategy.is_enabled?(params.merge({ 'rollout' => 16 }), unleash_context)).to be_truthy - end - - it 'should be enabled when stickiness=customerId and customerId=61 and rollout=10' do - params = { - 'groupId' => 'Demo', - 'rollout' => 10, - 'stickiness' => 'customerId' - } - - custom_context = Unleash::Context.new( - properties: { - customer_id: '61' - } - ) - - expect(strategy.is_enabled?(params, custom_context)).to be_truthy - end - - it 'should be disabled when stickiness=customerId and customerId=63 and rollout=10' do - params = { - 'groupId' => 'Demo', - 'rollout' => 10, - 'stickiness' => 'customerId' - } - - custom_context = Unleash::Context.new( - properties: { - customer_id: '63' - } - ) - - expect(strategy.is_enabled?(params, custom_context)).to be_falsey - end - end -end diff --git a/spec/unleash/strategy/gradual_rollout_random_spec.rb b/spec/unleash/strategy/gradual_rollout_random_spec.rb deleted file mode 100644 index c7ab26b0..00000000 --- a/spec/unleash/strategy/gradual_rollout_random_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require "unleash/strategy/gradual_rollout_random" - -RSpec.describe Unleash::Strategy::GradualRolloutRandom do - describe '#is_enabled?' do - let(:strategy) { Unleash::Strategy::GradualRolloutRandom.new } - - before do - # Random.rand always returns 15, so it is not really random in our tests. - allow(Random).to receive(:rand).and_return(15) - end - - it 'return true when percentage set (20) is over the returned random value (15)' do - expect(strategy.is_enabled?({ 'percentage' => '20' })).to be_truthy - expect(strategy.is_enabled?({ 'percentage' => 20 })).to be_truthy - expect(strategy.is_enabled?({ 'percentage' => 20.0 })).to be_truthy - end - - it 'return false when percentage set (10) is under the returned random value (15)' do - expect(strategy.is_enabled?({ 'percentage' => '10' })).to be_falsey - expect(strategy.is_enabled?({ 'percentage' => 10 })).to be_falsey - expect(strategy.is_enabled?({ 'percentage' => 10.0 })).to be_falsey - end - - it 'return false when percentage is invalid' do - expect(strategy.is_enabled?({ 'percentage' => -1 })).to be_falsey - expect(strategy.is_enabled?({ 'percentage' => nil })).to be_falsey - expect(strategy.is_enabled?({ 'percentage' => 'abc' })).to be_falsey - expect(strategy.is_enabled?('text')).to be_falsey - expect(strategy.is_enabled?(nil)).to be_falsey - end - end -end diff --git a/spec/unleash/strategy/gradual_rollout_sessionid_spec.rb b/spec/unleash/strategy/gradual_rollout_sessionid_spec.rb deleted file mode 100644 index b3d76f3f..00000000 --- a/spec/unleash/strategy/gradual_rollout_sessionid_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require "unleash/strategy/gradual_rollout_sessionid" -require "unleash/strategy/util" - -RSpec.describe Unleash::Strategy::GradualRolloutSessionId do - describe '#is_enabled?' do - let(:strategy) { Unleash::Strategy::GradualRolloutSessionId.new } - let(:unleash_context) { Unleash::Context.new(session_id: 'secretsessionidhashgoeshere') } - let(:percentage) { Unleash::Strategy::Util.get_normalized_number(unleash_context.session_id, "") } - - it 'return true when percentage set is gt the number returned by the hash function' do - expect(strategy.is_enabled?({ 'percentage' => (percentage + 1).to_s }, unleash_context)).to be_truthy - expect(strategy.is_enabled?({ 'percentage' => percentage + 1 }, unleash_context)).to be_truthy - expect(strategy.is_enabled?({ 'percentage' => percentage + 0.1 }, unleash_context)).to be_truthy - end - - it 'return false when percentage set is lt the number returned by the hash function' do - expect(strategy.is_enabled?({ 'percentage' => (percentage - 1).to_s }, unleash_context)).to be_falsey - expect(strategy.is_enabled?({ 'percentage' => percentage - 1 }, unleash_context)).to be_falsey - expect(strategy.is_enabled?({ 'percentage' => percentage - 0.1 }, unleash_context)).to be_falsey - end - end -end diff --git a/spec/unleash/strategy/gradual_rollout_userid_spec.rb b/spec/unleash/strategy/gradual_rollout_userid_spec.rb deleted file mode 100644 index 2ed96134..00000000 --- a/spec/unleash/strategy/gradual_rollout_userid_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "unleash/strategy/gradual_rollout_userid" - -RSpec.describe Unleash::Strategy::GradualRolloutUserId do - describe '#is_enabled?' do - let(:strategy) { Unleash::Strategy::GradualRolloutUserId.new } - let(:unleash_context) { Unleash::Context.new({ 'userId' => 'alice' }) } - let(:percentage) { Unleash::Strategy::Util.get_normalized_number(unleash_context.user_id, "") } - - it 'return true when percentage set is gt the number returned by the hash function' do - expect(strategy.is_enabled?({ 'percentage' => (percentage + 1).to_s }, unleash_context)).to be_truthy - expect(strategy.is_enabled?({ 'percentage' => percentage + 1 }, unleash_context)).to be_truthy - expect(strategy.is_enabled?({ 'percentage' => percentage + 0.1 }, unleash_context)).to be_truthy - end - - it 'return false when percentage set is lt the number returned by the hash function' do - expect(strategy.is_enabled?({ 'percentage' => (percentage - 1).to_s }, unleash_context)).to be_falsey - expect(strategy.is_enabled?({ 'percentage' => percentage - 1 }, unleash_context)).to be_falsey - expect(strategy.is_enabled?({ 'percentage' => percentage - 0.1 }, unleash_context)).to be_falsey - end - end -end diff --git a/spec/unleash/strategy/remote_address_spec.rb b/spec/unleash/strategy/remote_address_spec.rb deleted file mode 100644 index ac3da10f..00000000 --- a/spec/unleash/strategy/remote_address_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -require "unleash/strategy/remote_address" - -RSpec.describe Unleash::Strategy::RemoteAddress do - describe '#is_enabled?' do - let(:strategy) { Unleash::Strategy::RemoteAddress.new } - let(:unleash_context) { Unleash::Context.new({ 'remoteAddress' => '127.0.0.1' }) } - - def context_for_addr(remote_address) - Unleash::Context.new(remote_address: remote_address) - end - - it 'should be enabled with correct params' do - expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, unleash_context)).to be_truthy - - unleash_context2 = Unleash::Context.new - unleash_context2.remote_address = '172.12.0.1' - expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, unleash_context2)).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1, 172.12.0.1 , 127.0.0.1' }, unleash_context2)).to be_truthy - end - - it 'should work with ipv6' do - ips_and_cidrs = '2001:0db8:85a3:0000:0000:8a2e:0370:7300/120,2001:0db8:85a3:0000:0000:8a2e:0370:7520/123' - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:72ff'))).to be_falsey - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7330'))).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7334'))).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:73ff'))).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7400'))).to be_falsey - - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7519'))).to be_falsey - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7520'))).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:753f'))).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7540'))).to be_falsey - end - - it 'should be enabled with correct CIDR params' do - ips_and_cidrs = '192.168.0.0/24,127.0.0.1/32,172.12.0.1' - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, unleash_context)).to be_truthy - - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '172.12.0.1'))).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '127.0.0.1'))).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '127.0.0.1/32'))).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.0'))).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.1'))).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.255'))).to be_truthy - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.192/30'))).to be_truthy - - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '127.0.0.2'))).to be_falsey - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.1.0'))).to be_falsey - expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.1.255'))).to be_falsey - end - - it 'should be disabled with false params' do - expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,172.12.0.1' }, unleash_context)).to be_falsey - end - - it 'should be disabled on invalid params' do - expect(strategy.is_enabled?({ 'ips' => '192.168.0.1,172.12.0.1' }, unleash_context)).to be_falsey - expect(strategy.is_enabled?({ 'IPs' => nil }, unleash_context)).to be_falsey - expect(strategy.is_enabled?({}, unleash_context)).to be_falsey - expect(strategy.is_enabled?('IPs_list', unleash_context)).to be_falsey - end - - it 'should be disabled on invalid contexts' do - expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, Unleash::Context.new)).to be_falsey - expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, nil)).to be_falsey - expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' })).to be_falsey - - expect(strategy.is_enabled?({ 'IPs' => '192.168.x.y,127.0.0.1' }, Unleash::Context.new(remote_address: '192.168.x.y'))).to be_falsey - expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' }, Unleash::Context.new(remote_address: 'foobar'))).to be_falsey - expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' }, Unleash::Context.new(remote_address: '192.168.1.0'))).to be_falsey - expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' }, nil)).to be_falsey - expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' })).to be_falsey - end - - it 'should be enabled for valid params even if other params are invalid' do - expect(strategy.is_enabled?({ 'IPs' => '192.168.x.y,127.0.0.1' }, Unleash::Context.new(remote_address: '127.0.0.1'))).to be_truthy - end - end -end diff --git a/spec/unleash/strategy/user_with_id_spec.rb b/spec/unleash/strategy/user_with_id_spec.rb deleted file mode 100644 index 18e326aa..00000000 --- a/spec/unleash/strategy/user_with_id_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require "unleash/strategy/user_with_id" -require "unleash/context" - -RSpec.describe Unleash::Strategy::UserWithId do - describe '#is_enabled?' do - let(:strategy) { Unleash::Strategy::UserWithId.new } - - context 'with string params' do - let(:unleash_context) { Unleash::Context.new({ 'userId' => 'bob' }) } - - it 'should be enabled with correct params' do - expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, unleash_context)).to be_truthy - - unleash_context2 = Unleash::Context.new - unleash_context2.user_id = 'alice' - expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, unleash_context2)).to be_truthy - end - - it 'should be enabled with correct can include spaces' do - expect(strategy.is_enabled?({ 'userIds' => ' alice ,bob,carol,dave' }, unleash_context)).to be_truthy - end - - it 'should be disabled with false params' do - expect(strategy.is_enabled?({ 'userIds' => 'alice,dave' }, unleash_context)).to be_falsey - end - - it 'should be disabled on invalid params' do - expect(strategy.is_enabled?({ 'userIds' => nil }, unleash_context)).to be_falsey - expect(strategy.is_enabled?({}, unleash_context)).to be_falsey - expect(strategy.is_enabled?('string', unleash_context)).to be_falsey - expect(strategy.is_enabled?(nil, unleash_context)).to be_falsey - end - - it 'should be disabled on invalid contexts' do - expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, Unleash::Context.new)).to be_falsey - expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, nil)).to be_falsey - expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' })).to be_falsey - end - end - - context 'with int params' do - let(:user_id) { 123 } - let(:unleash_context) { Unleash::Context.new({ 'userId' => user_id }) } - - it 'should be enabled with correct params' do - expect(strategy.is_enabled?({ 'userIds' => '1,2,123' }, unleash_context)).to be_truthy - - unleash_context2 = Unleash::Context.new(user_id: 1) - expect(strategy.is_enabled?({ 'userIds' => '1,2,123' }, unleash_context2)).to be_truthy - end - - it 'should be enabled with correct can include spaces' do - expect(strategy.is_enabled?({ 'userIds' => ' 1 ,2, 123 ,200 ' }, unleash_context)).to be_truthy - end - - it 'should be disabled with false params' do - expect(strategy.is_enabled?({ 'userIds' => '1,2' }, unleash_context)).to be_falsey - end - end - end -end diff --git a/spec/unleash/strategy/util_spec.rb b/spec/unleash/strategy/util_spec.rb deleted file mode 100644 index 234b160c..00000000 --- a/spec/unleash/strategy/util_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require "unleash/strategy/util" - -RSpec.describe Unleash::Strategy::Util do - describe '.get_normalized_number' do - it "returns correct values" do - expect(Unleash::Strategy::Util.get_normalized_number('123', 'gr1')).to eq(73) - expect(Unleash::Strategy::Util.get_normalized_number('999', 'groupX')).to eq(25) - end - end -end diff --git a/spec/unleash/toggle_fetcher_spec.rb b/spec/unleash/toggle_fetcher_spec.rb index 38210510..97ae8cdc 100644 --- a/spec/unleash/toggle_fetcher_spec.rb +++ b/spec/unleash/toggle_fetcher_spec.rb @@ -55,25 +55,24 @@ end describe '#save!' do - context 'when toggle_cache generation fails' do - before do - allow(toggle_fetcher).to receive(:toggle_cache).and_raise(StandardError) - end - - it 'swallows the error' do - expect { toggle_fetcher.save! }.not_to raise_error - end - end - context 'when toggle_cache with content is saved' do - before do - toggle_fetcher.toggle_cache = { features: [] } - end - it 'creates a file with toggle_cache in JSON' do - toggle_fetcher.save! + toggles = { + version: 2, + features: [ + { + name: "Feature.A", + description: "Enabled toggle", + enabled: true, + strategies: [{ + "name": "default" + }] + }, + ] + } + toggle_fetcher.save! toggles.to_json expect(File.exist?(Unleash.configuration.backup_file)).to eq(true) - expect(File.read(Unleash.configuration.backup_file)).to eq('{"features":[]}') + expect(File.read(Unleash.configuration.backup_file)).to eq('{"version":2,"features":[{"name":"Feature.A","description":"Enabled toggle","enabled":true,"strategies":[{"name":"default"}]}]}') end end end @@ -83,47 +82,28 @@ before do # manually create a stub cache on disk, so we can test that we read it correctly later. cache_creator = described_class.new - cache_creator.toggle_cache = { features: [] } - cache_creator.save! - - WebMock.stub_request(:get, "http://toggle-fetcher-test-url/client/features").to_return(status: 500) - end + toggles = { + version: 2, + features: [ + { + name: "Feature.A", + description: "Enabled toggle", + enabled: true, + strategies: [{ + "name": "default" + }] + }, + ] + } - it 'reads the backup file for values' do - expect(toggle_fetcher.toggle_cache).to eq("features" => []) - end - end + cache_creator.save! toggles.to_json - context 'when backup file does not exist' do - before do - File.delete(Unleash.configuration.backup_file) if File.exist?(Unleash.configuration.backup_file) WebMock.stub_request(:get, "http://toggle-fetcher-test-url/client/features").to_return(status: 500) end - it 'returns an empty toggle_cache' do - expect(toggle_fetcher.toggle_cache).to eq(nil) - end - end - - context 'segments are present' do - it 'loads a segement map correctly' do - expect(toggle_fetcher.toggle_cache["segments"].count).to eq 1 - end - end - - context 'segments are not present' do - before do - WebMock.stub_request(:get, "http://toggle-fetcher-test-url/client/features") - .to_return(status: 200, - body: { - "version": 1, - "features": [] - }.to_json, - headers: {}) - end - - it 'loads an empty segment map' do - expect(toggle_fetcher.toggle_cache["segments"].count).to eq 0 + it 'reads the backup file for values' do + enabled = Unleash.engine.enabled?('Feature.A', {}) + expect(enabled).to eq(true) end end end From eec81214c433b786831c67757a15314051ef3983 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Mon, 18 Sep 2023 15:36:18 +0200 Subject: [PATCH 02/35] chore: hack in local path for yggdrasil --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index f0bae376..8d75e93f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'unleash-engine', path: '../yggdrasil/ruby-engine' +gem 'unleash-engine', path: '../../yggdrasil/ruby-engine' # Specify your gem's dependencies in unleash-client.gemspec gemspec From 61ad46cc46b76fe022a2f48dd9d191a2a8f08877 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Mon, 23 Oct 2023 10:15:05 +0200 Subject: [PATCH 03/35] chore: trim out diff that shouldn't have been checked in --- diff | 3231 ---------------------------------------------------------- 1 file changed, 3231 deletions(-) delete mode 100644 diff diff --git a/diff b/diff deleted file mode 100644 index 83e3777b..00000000 --- a/diff +++ /dev/null @@ -1,3231 +0,0 @@ -diff --git a/Gemfile b/Gemfile -index 17fb50c..f0bae37 100644 ---- a/Gemfile -+++ b/Gemfile -@@ -1,4 +1,6 @@ - source 'https://rubygems.org' - -+gem 'unleash-engine', path: '../yggdrasil/ruby-engine' -+ - # Specify your gem's dependencies in unleash-client.gemspec - gemspec -diff --git a/lib/unleash.rb b/lib/unleash.rb -index 9a6f1c7..260cf40 100644 ---- a/lib/unleash.rb -+++ b/lib/unleash.rb -@@ -1,6 +1,5 @@ - require 'unleash/version' - require 'unleash/configuration' --require 'unleash/strategies' - require 'unleash/context' - require 'unleash/client' - require 'logger' -@@ -9,7 +8,7 @@ module Unleash - TIME_RESOLUTION = 3 - - class << self -- attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :segment_cache, :logger -+ attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :segment_cache, :logger, :engine - end - - self.configuration = Unleash::Configuration.new -@@ -26,6 +25,6 @@ module Unleash - end - - def self.strategies -- self.configuration.strategies -+ nil - end - end -diff --git a/lib/unleash/activation_strategy.rb b/lib/unleash/activation_strategy.rb -deleted file mode 100644 -index 29feb0c..0000000 ---- a/lib/unleash/activation_strategy.rb -+++ /dev/null -@@ -1,31 +0,0 @@ --module Unleash -- class ActivationStrategy -- attr_accessor :name, :params, :constraints, :disabled -- -- def initialize(name, params, constraints = []) -- self.name = name -- self.disabled = false -- -- if params.is_a?(Hash) -- self.params = params -- elsif params.nil? -- self.params = {} -- else -- Unleash.logger.warn "Invalid params provided for ActivationStrategy (params:#{params})" -- self.params = {} -- end -- -- if constraints.is_a?(Array) && constraints.each{ |c| c.is_a?(Constraint) } -- self.constraints = constraints -- else -- Unleash.logger.warn "Invalid constraints provided for ActivationStrategy (contraints: #{constraints})" -- self.disabled = true -- self.constraints = [] -- end -- end -- -- def matches_context?(context) -- self.constraints.any?{ |c| c.matches_context? context } -- end -- end --end -diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb -index 2191caf..4b53300 100644 ---- a/lib/unleash/client.rb -+++ b/lib/unleash/client.rb -@@ -17,6 +17,7 @@ module Unleash - - Unleash.logger = Unleash.configuration.logger.clone - Unleash.logger.level = Unleash.configuration.log_level -+ Unleash.engine = UnleashEngine.new - - Unleash.toggle_fetcher = Unleash::ToggleFetcher.new - if Unleash.configuration.disable_client -@@ -40,16 +41,18 @@ module Unleash - default_value_param - end - -- toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first -+ Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}" - -- if toggle_as_hash.nil? -+ toggle_enabled = Unleash&.engine&.enabled?(feature, context) -+ if toggle_enabled.nil? - Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found" -+ Unleash&.engine&.count_toggle(feature, false) - return default_value - end - -- toggle = Unleash::FeatureToggle.new(toggle_as_hash, Unleash&.segment_cache) -+ Unleash&.engine&.count_toggle(feature, toggle_enabled) - -- toggle.is_enabled?(context) -+ toggle_enabled - end - - def is_disabled?(feature, context = nil, default_value_param = true, &fallback_blk) -@@ -74,22 +77,20 @@ module Unleash - def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant) - Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}" - -- toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first -- -- if toggle_as_hash.nil? -- Unleash.logger.debug "Unleash::Client.get_variant feature: #{feature} not found" -- return fallback_variant -+ toggle_enabled = Unleash&.engine&.enabled?(feature, context) -+ if toggle_enabled.nil? -+ Unleash&.engine&.count_toggle(feature, false) -+ else -+ Unleash&.engine&.count_toggle(feature, toggle_enabled) - end - -- toggle = Unleash::FeatureToggle.new(toggle_as_hash) -- variant = toggle.get_variant(context, fallback_variant) -- -- if variant.nil? -- Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found" -+ variant_response = Unleash&.engine.get_variant(feature, context) -+ if variant_response.code < 0 -+ Unleash&.engine&.count_variant(feature, fallback_variant.name) - return fallback_variant - end -- -- # TODO: Add to README: name, payload, enabled (bool) -+ variant = variant_response.variant -+ Unleash&.engine&.count_variant(feature, variant.name) - - variant - end -@@ -118,7 +119,7 @@ module Unleash - 'appName': Unleash.configuration.app_name, - 'instanceId': Unleash.configuration.instance_id, - 'sdkVersion': "unleash-client-ruby:" + Unleash::VERSION, -- 'strategies': Unleash.strategies.keys, -+ 'strategies': nil, - 'started': Time.now.iso8601(Unleash::TIME_RESOLUTION), - 'interval': Unleash.configuration.metrics_interval_in_millis - } -@@ -137,7 +138,6 @@ module Unleash - end - - def start_metrics -- Unleash.toggle_metrics = Unleash::Metrics.new - Unleash.reporter = Unleash::MetricsReporter.new - self.metrics_scheduled_executor = Unleash::ScheduledExecutor.new( - 'MetricsReporter', -diff --git a/lib/unleash/configuration.rb b/lib/unleash/configuration.rb -index 4f43200..dddd90f 100644 ---- a/lib/unleash/configuration.rb -+++ b/lib/unleash/configuration.rb -@@ -40,9 +40,9 @@ module Unleash - def validate! - return if self.disable_client - -- raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? || self.url.nil? -+ raise ArgumentError, "app_name is a required parameter." if self.app_name.nil? - -- validate_custom_http_headers!(self.custom_http_headers) -+ validate_custom_http_headers!(self.custom_http_headers) unless self.url.nil? - end - - def refresh_backup_file! -@@ -96,7 +96,7 @@ module Unleash - self.backup_file = nil - self.log_level = Logger::WARN - self.bootstrap_config = nil -- self.strategies = Unleash::Strategies.new -+ self.strategies = nil - - self.custom_http_headers = {} - end -diff --git a/lib/unleash/constraint.rb b/lib/unleash/constraint.rb -deleted file mode 100644 -index 51607c1..0000000 ---- a/lib/unleash/constraint.rb -+++ /dev/null -@@ -1,115 +0,0 @@ --require 'date' --module Unleash -- class Constraint -- attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive -- -- OPERATORS = { -- IN: ->(context_v, constraint_v){ constraint_v.include? context_v.to_s }, -- NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v.to_s }, -- STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } }, -- STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } }, -- STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } }, -- NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } }, -- NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } }, -- NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } }, -- NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } }, -- NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } }, -- DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } }, -- DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } }, -- SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } }, -- SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } }, -- SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } }, -- FALLBACK_VALIDATOR: ->(_context_v, _constraint_v){ false } -- }.freeze -- -- LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze -- -- def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false) -- raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String) -- -- unless OPERATORS.include? operator.to_sym -- Unleash.logger.warn "Operator #{operator} is not a supported operator, " \ -- "falling back to FALLBACK_VALIDATOR which skips this constraint." -- operator = "FALLBACK_VALIDATOR" -- end -- self.log_inconsistent_constraint_configuration(operator.to_sym, value) -- -- self.context_name = context_name -- self.operator = operator.to_sym -- self.value = value -- self.inverted = !!inverted -- self.case_insensitive = !!case_insensitive -- end -- -- def matches_context?(context) -- Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" -- return false if context.nil? -- -- match = matches_constraint?(context) -- self.inverted ? !match : match -- rescue KeyError -- Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \ -- found on the context" -- false -- end -- -- def self.on_valid_date(val1, val2) -- val1 = DateTime.parse(val1) -- val2 = DateTime.parse(val2) -- yield(val1, val2) -- rescue ArgumentError -- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \ -- or constraint_value (#{val2}) into a date. Returning false!" -- false -- end -- -- def self.on_valid_float(val1, val2) -- val1 = Float(val1) -- val2 = Float(val2) -- yield(val1, val2) -- rescue ArgumentError -- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \ -- or constraint_value (#{val2}) into a number. Returning false!" -- false -- end -- -- def self.on_valid_version(val1, val2) -- val1 = Gem::Version.new(val1) -- val2 = Gem::Version.new(val2) -- yield(val1, val2) -- rescue ArgumentError -- Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \ -- or constraint_value (#{val2}) into a version. Return false!" -- false -- end -- -- # This should be a private method but for some reason this fails on Ruby 2.5 -- def log_inconsistent_constraint_configuration(operator, value) -- Unleash.logger.warn "value is a String, operator is expecting an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String) -- Unleash.logger.warn "value is an Array, operator is expecting a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array) -- end -- -- private -- -- def matches_constraint?(context) -- Unleash.logger.debug "Unleash::Constraint matches_constraint? value: #{self.value} operator: #{self.operator} " \ -- " context.get_by_name(#{self.context_name})" -- -- unless OPERATORS.include?(self.operator) -- Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false." -- false -- end -- -- # when the operator is NOT_IN and there is no data, return true. In all other cases the operator doesn't match. -- return self.operator == :NOT_IN unless context.include?(self.context_name) -- -- v = self.value.dup -- context_value = context.get_by_name(self.context_name) -- -- v.map!(&:upcase) if self.case_insensitive -- context_value.upcase! if self.case_insensitive -- -- OPERATORS[self.operator].call(context_value, v) -- end -- end --end -diff --git a/lib/unleash/context.rb b/lib/unleash/context.rb -index 98ba467..9e235ce 100644 ---- a/lib/unleash/context.rb -+++ b/lib/unleash/context.rb -@@ -23,6 +23,22 @@ module Unleash - ",app_name=#{@app_name},environment=#{@environment}>" - end - -+ def as_json -+ { -+ appName: self.app_name, -+ environment: self.environment, -+ userId: self.user_id, -+ sessionId: self.session_id, -+ remoteAddress: self.remote_address, -+ currentTime: self.current_time, -+ properties: self.properties -+ } -+ end -+ -+ def to_json(*options) -+ as_json(*options).to_json(*options) -+ end -+ - def to_h - ATTRS.map{ |attr| [attr, self.send(attr)] }.to_h.merge(properties: @properties) - end -diff --git a/lib/unleash/feature_toggle.rb b/lib/unleash/feature_toggle.rb -index ec064b3..02020e5 100644 ---- a/lib/unleash/feature_toggle.rb -+++ b/lib/unleash/feature_toggle.rb -@@ -1,187 +1,10 @@ --require 'unleash/activation_strategy' --require 'unleash/constraint' - require 'unleash/variant_definition' - require 'unleash/variant' --require 'unleash/strategy/util' --require 'securerandom' - - module Unleash - class FeatureToggle -- attr_accessor :name, :enabled, :strategies, :variant_definitions -- -- def initialize(params = {}, segment_map = {}) -- params = {} if params.nil? -- -- self.name = params.fetch('name', nil) -- self.enabled = params.fetch('enabled', false) -- -- self.strategies = initialize_strategies(params, segment_map) -- self.variant_definitions = initialize_variant_definitions(params) -- end -- -- def to_s -- "" -- end -- -- def is_enabled?(context) -- result = am_enabled?(context) -- -- choice = result ? :yes : :no -- Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics -- -- result -- end -- -- def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant) -- raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant' -- -- context = ensure_valid_context(context) -- -- toggle_enabled = am_enabled?(context) -- variant = resolve_variant(context, toggle_enabled) -- -- choice = toggle_enabled ? :yes : :no -- Unleash.toggle_metrics.increment_variant(self.name, choice, variant.name) unless Unleash.configuration.disable_metrics -- variant -- end -- - def self.disabled_variant - Unleash::Variant.new(name: 'disabled', enabled: false) - end -- -- private -- -- def resolve_variant(context, toggle_enabled) -- return Unleash::FeatureToggle.disabled_variant unless toggle_enabled -- return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0 -- -- variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness) -- end -- -- def resolve_stickiness -- self.variant_definitions&.map(&:stickiness)&.compact&.first || "default" -- end -- -- # only check if it is enabled, do not do metrics -- def am_enabled?(context) -- result = -- if self.enabled -- self.strategies.empty? || -- self.strategies.any? do |s| -- strategy_enabled?(s, context) && strategy_constraint_matches?(s, context) -- end -- else -- false -- end -- -- Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \ -- "and Strategies combined with contraints returned #{result})" -- -- result -- end -- -- def strategy_enabled?(strategy, context) -- r = Unleash.strategies.fetch(strategy.name).is_enabled?(strategy.params, context) -- Unleash.logger.debug "Unleash::FeatureToggle.strategy_enabled? Strategy #{strategy.name} returned #{r} with context: #{context}" -- r -- end -- -- def strategy_constraint_matches?(strategy, context) -- return false if strategy.disabled -- -- strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) } -- end -- -- def sum_variant_defs_weights -- self.variant_definitions.map(&:weight).reduce(0, :+) -- end -- -- def variant_salt(context, stickiness = "default") -- begin -- return context.get_by_name(stickiness) if !context.nil? && stickiness != "default" -- rescue KeyError -- Unleash.logger.warn "Custom stickiness key (#{stickiness}) not found in the provided context #{context}. " \ -- "Falling back to default behavior." -- end -- return context.user_id unless context&.user_id.to_s.empty? -- return context.session_id unless context&.session_id.to_s.empty? -- return context.remote_address unless context&.remote_address.to_s.empty? -- -- SecureRandom.random_number -- end -- -- def variant_from_override_match(context) -- variant = self.variant_definitions.find{ |vd| vd.override_matches_context?(context) } -- return nil if variant.nil? -- -- Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload) -- end -- -- def variant_from_weights(context, stickiness) -- variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context, stickiness), self.name, sum_variant_defs_weights) -- prev_weights = 0 -- -- variant_definition = self.variant_definitions -- .find do |v| -- res = (prev_weights + v.weight >= variant_weight) -- prev_weights += v.weight -- res -- end -- return self.disabled_variant if variant_definition.nil? -- -- Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload) -- end -- -- def ensure_valid_context(context) -- unless ['NilClass', 'Unleash::Context'].include? context.class.name -- Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, " \ -- "please use Unleash::Context. Context set to nil." -- context = nil -- end -- context -- end -- -- def initialize_strategies(params, segment_map) -- params.fetch('strategies', []) -- .select{ |s| s.has_key?('name') && Unleash.strategies.includes?(s['name']) } -- .map do |s| -- ActivationStrategy.new( -- s['name'], -- s['parameters'], -- resolve_constraints(s, segment_map) -- ) -- end || [] -- end -- -- def resolve_constraints(strategy, segment_map) -- segment_constraints = (strategy["segments"] || []).map do |segment_id| -- segment_map[segment_id]&.fetch("constraints") -- end -- (strategy.fetch("constraints", []) + segment_constraints).flatten.map do |constraint| -- return nil if constraint.nil? -- -- Constraint.new( -- constraint.fetch('contextName'), -- constraint.fetch('operator'), -- constraint.fetch('value', nil) || constraint.fetch('values', nil), -- inverted: constraint.fetch('inverted', false), -- case_insensitive: constraint.fetch('caseInsensitive', false) -- ) -- end -- end -- -- def initialize_variant_definitions(params) -- (params.fetch('variants', []) || []) -- .select{ |v| v.is_a?(Hash) && v.has_key?('name') } -- .map do |v| -- VariantDefinition.new( -- v.fetch('name', ''), -- v.fetch('weight', 0), -- v.fetch('payload', nil), -- v.fetch('stickiness', nil), -- v.fetch('overrides', []) -- ) -- end || [] -- end - end - end -diff --git a/lib/unleash/metrics.rb b/lib/unleash/metrics.rb -deleted file mode 100644 -index 6342ade..0000000 ---- a/lib/unleash/metrics.rb -+++ /dev/null -@@ -1,41 +0,0 @@ --module Unleash -- class Metrics -- attr_accessor :features, :features_lock -- -- def initialize -- self.features = {} -- self.features_lock = Mutex.new -- end -- -- def to_s -- self.features_lock.synchronize do -- return self.features.to_json -- end -- end -- -- def increment(feature, choice) -- raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice -- -- self.features_lock.synchronize do -- self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature -- self.features[feature][choice] += 1 -- end -- end -- -- def increment_variant(feature, choice, variant) -- self.features_lock.synchronize do -- self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature -- self.features[feature][choice] += 1 -- self.features[feature]['variants'] = {} unless self.features[feature].include? 'variants' -- self.features[feature]['variants'][variant] = 0 unless self.features[feature]['variants'].include? variant -- self.features[feature]['variants'][variant] += 1 -- end -- end -- -- def reset -- self.features_lock.synchronize do -- self.features = {} -- end -- end -- end --end -diff --git a/lib/unleash/metrics_reporter.rb b/lib/unleash/metrics_reporter.rb -index fc1e7ca..4cd4340 100755 ---- a/lib/unleash/metrics_reporter.rb -+++ b/lib/unleash/metrics_reporter.rb -@@ -1,5 +1,4 @@ - require 'unleash/configuration' --require 'unleash/metrics' - require 'net/http' - require 'json' - require 'time' -@@ -15,22 +14,17 @@ module Unleash - end - - def generate_report -- now = Time.now -- -- start = self.last_time -- stop = now -- self.last_time = now -- -+ puts "Making report" -+ metrics = Unleash&.engine&.get_metrics() -+ if metrics.nil? || metrics.empty? -+ puts "nothing here" -+ return nil -+ end - report = { - 'appName': Unleash.configuration.app_name, - 'instanceId': Unleash.configuration.instance_id, -- 'bucket': { -- 'start': start.iso8601(Unleash::TIME_RESOLUTION), -- 'stop': stop.iso8601(Unleash::TIME_RESOLUTION), -- 'toggles': Unleash.toggle_metrics.features -- } -+ 'bucket': metrics - } -- Unleash.toggle_metrics.reset - - report - end -@@ -38,13 +32,14 @@ module Unleash - def post - Unleash.logger.debug "post() Report" - -- if bucket_empty? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes... -+ bucket = self.generate_report -+ if bucket.nil? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes... - Unleash.logger.debug "Report not posted to server, as it would have been empty. (and has been empty for up to 10 min)" - - return - end - -- response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, self.generate_report.to_json) -+ response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, bucket.to_json) - - if ['200', '202'].include? response.code - Unleash.logger.debug "Report sent to unleash server successfully. Server responded with http code #{response.code}" -@@ -54,9 +49,5 @@ module Unleash - end - - private -- -- def bucket_empty? -- Unleash.toggle_metrics.features.empty? -- end - end - end -diff --git a/lib/unleash/strategies.rb b/lib/unleash/strategies.rb -deleted file mode 100644 -index 842af4f..0000000 ---- a/lib/unleash/strategies.rb -+++ /dev/null -@@ -1,80 +0,0 @@ --require 'unleash/strategy/base' --Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path } -- --module Unleash -- class Strategies -- def initialize -- @strategies = {} -- register_strategies -- end -- -- def keys -- @strategies.keys -- end -- -- def includes?(name) -- @strategies.has_key?(name.to_s) -- end -- -- def fetch(name) -- raise Unleash::Strategy::NotImplemented, "Strategy is not implemented" unless (strategy = @strategies[name.to_s]) -- -- strategy -- end -- -- def add(strategy) -- @strategies[strategy.name] = strategy -- end -- -- def []=(key, strategy) -- warn_deprecated_registration(strategy, 'modifying Unleash::STRATEGIES') -- @strategies[key.to_s] = strategy -- end -- -- def [](key) -- @strategies[key.to_s] -- end -- -- def register_strategies -- register_base_strategies -- register_custom_strategies -- end -- -- protected -- -- # Deprecated: Use Unleash.configuration to add custom strategies -- def register_custom_strategies -- Unleash::Strategy.constants -- .select{ |c| Unleash::Strategy.const_get(c).is_a? Class } -- .reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) } # Reject abstract classes -- .map{ |c| Object.const_get("Unleash::Strategy::#{c}") } -- .reject{ |c| DEFAULT_STRATEGIES.include?(c) } # Reject base classes -- .each do |c| -- strategy = c.new -- warn_deprecated_registration(strategy, 'adding custom class into Unleash::Strategy namespace') -- self.add(strategy) -- end -- end -- -- def register_base_strategies -- DEFAULT_STRATEGIES.each{ |c| self.add(c.new) } -- end -- -- DEFAULT_STRATEGIES = [ -- Unleash::Strategy::ApplicationHostname, -- Unleash::Strategy::Default, -- Unleash::Strategy::FlexibleRollout, -- Unleash::Strategy::GradualRolloutRandom, -- Unleash::Strategy::GradualRolloutSessionId, -- Unleash::Strategy::GradualRolloutUserId, -- Unleash::Strategy::RemoteAddress, -- Unleash::Strategy::UserWithId -- ].freeze -- -- def warn_deprecated_registration(strategy, method) -- warn "[DEPRECATED] Registering custom Unleash strategy by #{method} is deprecated. -- Please use Unleash configuration to register custom strategy: " \ -- "`Unleash.configure {|c| c.strategies.add(#{strategy.class.name}.new) }`" -- end -- end --end -diff --git a/lib/unleash/strategy/application_hostname.rb b/lib/unleash/strategy/application_hostname.rb -deleted file mode 100644 -index f5fd578..0000000 ---- a/lib/unleash/strategy/application_hostname.rb -+++ /dev/null -@@ -1,26 +0,0 @@ --require 'socket' -- --module Unleash -- module Strategy -- class ApplicationHostname < Base -- attr_accessor :hostname -- -- PARAM = 'hostnames'.freeze -- -- def initialize -- self.hostname = Socket.gethostname || 'undefined' -- end -- -- def name -- 'applicationHostname' -- end -- -- # need: :params['hostnames'] -- def is_enabled?(params = {}, _context = nil) -- return false unless params.is_a?(Hash) && params.has_key?(PARAM) -- -- params[PARAM].split(",").map(&:strip).map(&:downcase).include?(self.hostname) -- end -- end -- end --end -diff --git a/lib/unleash/strategy/base.rb b/lib/unleash/strategy/base.rb -deleted file mode 100644 -index 3e3a0f0..0000000 ---- a/lib/unleash/strategy/base.rb -+++ /dev/null -@@ -1,16 +0,0 @@ --module Unleash -- module Strategy -- class NotImplemented < RuntimeError -- end -- -- class Base -- def name -- raise NotImplemented, "Strategy is not implemented" -- end -- -- def is_enabled?(_params = {}, _context = nil) -- raise NotImplemented, "Strategy is not implemented" -- end -- end -- end --end -diff --git a/lib/unleash/strategy/default.rb b/lib/unleash/strategy/default.rb -deleted file mode 100644 -index d22cdbe..0000000 ---- a/lib/unleash/strategy/default.rb -+++ /dev/null -@@ -1,13 +0,0 @@ --module Unleash -- module Strategy -- class Default < Base -- def name -- 'default' -- end -- -- def is_enabled?(_params = {}, _context = nil) -- true -- end -- end -- end --end -diff --git a/lib/unleash/strategy/flexible_rollout.rb b/lib/unleash/strategy/flexible_rollout.rb -deleted file mode 100644 -index edb8256..0000000 ---- a/lib/unleash/strategy/flexible_rollout.rb -+++ /dev/null -@@ -1,55 +0,0 @@ --require 'unleash/strategy/util' -- --module Unleash -- module Strategy -- class FlexibleRollout < Base -- def name -- 'flexibleRollout' -- end -- -- # need: params['percentage'] -- def is_enabled?(params = {}, context = nil) -- return false unless params.is_a?(Hash) -- return false unless context.instance_of?(Unleash::Context) -- -- stickiness = params.fetch('stickiness', 'default') -- stickiness_id = resolve_stickiness(stickiness, context) -- -- begin -- percentage = Integer(params.fetch('rollout', 0)) -- percentage = 0 if percentage > 100 || percentage.negative? -- rescue ArgumentError -- return false -- end -- -- group_id = params.fetch('groupId', '') -- normalized_number = Util.get_normalized_number(stickiness_id, group_id) -- -- return false if stickiness_id.nil? -- -- (percentage.positive? && normalized_number <= percentage) -- end -- -- private -- -- def random -- Random.rand(0..100) -- end -- -- def resolve_stickiness(stickiness, context) -- case stickiness -- when 'random' -- random -- when 'default' -- context.user_id || context.session_id || random -- else -- begin -- context.get_by_name(stickiness) -- rescue KeyError -- nil -- end -- end -- end -- end -- end --end -diff --git a/lib/unleash/strategy/gradual_rollout_random.rb b/lib/unleash/strategy/gradual_rollout_random.rb -deleted file mode 100644 -index 61d0784..0000000 ---- a/lib/unleash/strategy/gradual_rollout_random.rb -+++ /dev/null -@@ -1,24 +0,0 @@ --require 'unleash/strategy/util' -- --module Unleash -- module Strategy -- class GradualRolloutRandom < Base -- def name -- 'gradualRolloutRandom' -- end -- -- # need: params['percentage'] -- def is_enabled?(params = {}, _context = nil) -- return false unless params.is_a?(Hash) && params.has_key?('percentage') -- -- begin -- percentage = Integer(params['percentage'] || 0) -- rescue ArgumentError -- return false -- end -- -- (percentage >= Random.rand(1..100)) -- end -- end -- end --end -diff --git a/lib/unleash/strategy/gradual_rollout_sessionid.rb b/lib/unleash/strategy/gradual_rollout_sessionid.rb -deleted file mode 100644 -index 0f2a553..0000000 ---- a/lib/unleash/strategy/gradual_rollout_sessionid.rb -+++ /dev/null -@@ -1,21 +0,0 @@ --require 'unleash/strategy/util' -- --module Unleash -- module Strategy -- class GradualRolloutSessionId < Base -- def name -- 'gradualRolloutSessionId' -- end -- -- # need: params['percentage'], params['groupId'], context.user_id, -- def is_enabled?(params = {}, context = nil) -- return false unless params.is_a?(Hash) && params.has_key?('percentage') -- return false unless context.instance_of?(Unleash::Context) -- return false if context.session_id.nil? || context.session_id.empty? -- -- percentage = Integer(params['percentage'] || 0) -- (percentage.positive? && Util.get_normalized_number(context.session_id, params['groupId'] || "") <= percentage) -- end -- end -- end --end -diff --git a/lib/unleash/strategy/gradual_rollout_userid.rb b/lib/unleash/strategy/gradual_rollout_userid.rb -deleted file mode 100644 -index 1aa3c05..0000000 ---- a/lib/unleash/strategy/gradual_rollout_userid.rb -+++ /dev/null -@@ -1,21 +0,0 @@ --require 'unleash/strategy/util' -- --module Unleash -- module Strategy -- class GradualRolloutUserId < Base -- def name -- 'gradualRolloutUserId' -- end -- -- # need: params['percentage'], params['groupId'], context.user_id, -- def is_enabled?(params = {}, context = nil, _constraints = []) -- return false unless params.is_a?(Hash) && params.has_key?('percentage') -- return false unless context.instance_of?(Unleash::Context) -- return false if context.user_id.nil? || context.user_id.empty? -- -- percentage = Integer(params['percentage'] || 0) -- (percentage.positive? && Util.get_normalized_number(context.user_id, params['groupId'] || "") <= percentage) -- end -- end -- end --end -diff --git a/lib/unleash/strategy/remote_address.rb b/lib/unleash/strategy/remote_address.rb -deleted file mode 100644 -index d222311..0000000 ---- a/lib/unleash/strategy/remote_address.rb -+++ /dev/null -@@ -1,36 +0,0 @@ --module Unleash -- module Strategy -- class RemoteAddress < Base -- PARAM = 'IPs'.freeze -- -- def name -- 'remoteAddress' -- end -- -- # need: params['IPs'], context.remote_address -- def is_enabled?(params = {}, context = nil) -- return false unless params.is_a?(Hash) && params.has_key?(PARAM) -- return false unless params.fetch(PARAM, nil).is_a? String -- return false unless context.instance_of?(Unleash::Context) -- -- remote_address = ipaddr_or_nil_from_str(context.remote_address) -- -- params[PARAM] -- .split(',') -- .map(&:strip) -- .map{ |ipblock| ipaddr_or_nil_from_str(ipblock) } -- .compact -- .map{ |ipb| ipb.include? remote_address } -- .any? -- end -- -- private -- -- def ipaddr_or_nil_from_str(ip) -- IPAddr.new(ip) -- rescue StandardError -- nil -- end -- end -- end --end -diff --git a/lib/unleash/strategy/user_with_id.rb b/lib/unleash/strategy/user_with_id.rb -deleted file mode 100644 -index c20a75b..0000000 ---- a/lib/unleash/strategy/user_with_id.rb -+++ /dev/null -@@ -1,20 +0,0 @@ --module Unleash -- module Strategy -- class UserWithId < Base -- PARAM = 'userIds'.freeze -- -- def name -- 'userWithId' -- end -- -- # requires: params['userIds'], context.user_id, -- def is_enabled?(params = {}, context = nil) -- return false unless params.is_a?(Hash) && params.has_key?(PARAM) -- return false unless params.fetch(PARAM, nil).is_a? String -- return false unless context.instance_of?(Unleash::Context) -- -- params[PARAM].split(",").map(&:strip).include?(context.user_id) -- end -- end -- end --end -diff --git a/lib/unleash/strategy/util.rb b/lib/unleash/strategy/util.rb -deleted file mode 100644 -index a00ade8..0000000 ---- a/lib/unleash/strategy/util.rb -+++ /dev/null -@@ -1,16 +0,0 @@ --require 'murmurhash3' -- --module Unleash -- module Strategy -- module Util -- module_function -- -- NORMALIZER = 100 -- -- # convert the two strings () into a number between 1 and base (100 by default) -- def get_normalized_number(identifier, group_id, base = NORMALIZER) -- MurmurHash3::V32.str_hash("#{group_id}:#{identifier}") % base + 1 -- end -- end -- end --end -diff --git a/lib/unleash/toggle_fetcher.rb b/lib/unleash/toggle_fetcher.rb -index 2b33f4d..41361ef 100755 ---- a/lib/unleash/toggle_fetcher.rb -+++ b/lib/unleash/toggle_fetcher.rb -@@ -2,14 +2,14 @@ require 'unleash/configuration' - require 'unleash/bootstrap/handler' - require 'net/http' - require 'json' -+require 'unleash_engine' - - module Unleash - class ToggleFetcher -- attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache -+ attr_accessor :toggle_engine, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache - - def initialize - self.etag = nil -- self.toggle_cache = nil - self.segment_cache = nil - self.toggle_lock = Mutex.new - self.toggle_resource = ConditionVariable.new -@@ -35,8 +35,8 @@ module Unleash - def toggles - self.toggle_lock.synchronize do - # wait for resource, only if it is null -- self.toggle_resource.wait(self.toggle_lock) if self.toggle_cache.nil? -- return self.toggle_cache -+ self.toggle_resource.wait(self.toggle_lock) if self.toggle_engine.nil? -+ return self.toggle_engine - end - end - -@@ -55,16 +55,16 @@ module Unleash - end - - self.etag = response['ETag'] -- features = get_features(response.body) -+ engine = get_engine(response.body) - - # always synchronize with the local cache when fetching: -- synchronize_with_local_cache!(features) -+ synchronize_with_local_cache!(engine) - - update_running_client! -- save! -+ save! response.body - end - -- def save! -+ def save!(toggle_data) - Unleash.logger.debug "Will save toggles to disk now" - - backup_file = Unleash.configuration.backup_file -@@ -72,7 +72,7 @@ module Unleash - - self.toggle_lock.synchronize do - File.open(backup_file_tmp, "w") do |file| -- file.write(self.toggle_cache.to_json) -+ file.write(toggle_data) - end - File.rename(backup_file_tmp, backup_file) - end -@@ -84,10 +84,10 @@ module Unleash - - private - -- def synchronize_with_local_cache!(features) -- if self.toggle_cache != features -+ def synchronize_with_local_cache!(engine) -+ if self.toggle_engine != engine - self.toggle_lock.synchronize do -- self.toggle_cache = features -+ self.toggle_engine = engine - end - - # notify all threads waiting for this resource to no longer wait -@@ -96,10 +96,8 @@ module Unleash - end - - def update_running_client! -- if Unleash.toggles != self.toggles["features"] || Unleash.segment_cache != self.toggles["segments"] -- Unleash.logger.info "Updating toggles to main client, there has been a change in the server." -- Unleash.toggles = self.toggles["features"] -- Unleash.segment_cache = self.toggles["segments"] -+ if Unleash.engine != self.toggle_engine -+ Unleash.engine = self.toggle_engine - end - end - -@@ -121,7 +119,7 @@ module Unleash - - def bootstrap - bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles -- synchronize_with_local_cache! get_features bootstrap_payload -+ synchronize_with_local_cache! get_engine bootstrap_payload - update_running_client! - - # reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again -@@ -134,10 +132,15 @@ module Unleash - segments_array.map{ |segment| [segment["id"], segment] }.to_h - end - -+ def get_engine(response_body) -+ engine = UnleashEngine.new -+ engine.take_state(response_body) -+ engine -+ end -+ - # @param response_body [String] - def get_features(response_body) - response_hash = JSON.parse(response_body) -- - if response_hash['version'] >= 1 - return { "features" => response_hash["features"], "segments" => build_segment_map(response_hash["segments"]) } - end -diff --git a/spec/unleash/activation_strategy_spec.rb b/spec/unleash/activation_strategy_spec.rb -deleted file mode 100644 -index 812dbe0..0000000 ---- a/spec/unleash/activation_strategy_spec.rb -+++ /dev/null -@@ -1,42 +0,0 @@ --require 'unleash/constraint' -- --RSpec.describe Unleash::ActivationStrategy do -- before do -- Unleash.configuration = Unleash::Configuration.new -- Unleash.logger = Unleash.configuration.logger -- end -- -- let(:name) { 'test name' } -- -- describe '#initialize' do -- context 'with correct payload' do -- let(:params) { Hash.new(test: true) } -- let(:constraints) { [Unleash::Constraint.new("constraint_name", "IN", ["value"])] } -- -- it 'initializes with correct attributes' do -- expect(Unleash.logger).to_not receive(:warn) -- -- strategy = Unleash::ActivationStrategy.new(name, params, constraints) -- -- expect(strategy.name).to eq name -- expect(strategy.params).to eq params -- expect(strategy.constraints).to eq constraints -- end -- end -- -- context 'with incorrect payload' do -- let(:params) { 'bad_params' } -- let(:constraints) { [] } -- -- it 'initializes with correct attributes and logs warning' do -- expect(Unleash.logger).to receive(:warn) -- -- strategy = Unleash::ActivationStrategy.new(name, params, constraints) -- -- expect(strategy.name).to eq name -- expect(strategy.params).to eq({}) -- expect(strategy.constraints).to eq(constraints) -- end -- end -- end --end -diff --git a/spec/unleash/client_specification_spec.rb b/spec/unleash/client_specification_spec.rb -index 621ed4f..a45ffbe 100644 ---- a/spec/unleash/client_specification_spec.rb -+++ b/spec/unleash/client_specification_spec.rb -@@ -10,37 +10,30 @@ RSpec.describe Unleash::Client do - DEFAULT_VARIANT = Unleash::Variant.new(name: 'unknown', enabled: false).freeze - - before do -- Unleash.configuration = Unleash::Configuration.new - Unleash.logger = Unleash.configuration.logger - Unleash.logger.level = Unleash.configuration.log_level -- Unleash.toggles = [] -- Unleash.toggle_metrics = {} -- -- # Do not test metrics: -- Unleash.configuration.disable_metrics = true - end - - if File.exist?(SPECIFICATION_PATH + '/index.json') - JSON.parse(File.read(SPECIFICATION_PATH + '/index.json')).each do |test_file| - describe "for #{test_file}" do - current_test_set = JSON.parse(File.read(SPECIFICATION_PATH + '/' + test_file)) -+ - context "with #{current_test_set.fetch('name')} " do -- # name = current_test_set.fetch('name', '') - tests = current_test_set.fetch('tests', []) - state = current_test_set.fetch('state', {}) -- state_features = state.fetch('features', []) -- state_segments = state.fetch('segments', []).map{ |segment| [segment["id"], segment] }.to_h -- -- let(:unleash_toggles) { state_features } -- - tests.each do |test| - it "test that #{test['description']}" do -- test_toggle = unleash_toggles.select{ |t| t.fetch('name', '') == test.fetch('toggleName') }.first -- -- toggle = Unleash::FeatureToggle.new(test_toggle, state_segments) - context = Unleash::Context.new(test['context']) - -- toggle_result = toggle.is_enabled?(context) -+ unleash = Unleash::Client.new( -+ app_name: 'bootstrap-test', -+ instance_id: 'local-test-cli', -+ disable_client: true, -+ disable_metrics: true, -+ bootstrap_config: Unleash::Bootstrap::Configuration.new(data: current_test_set.fetch('state', {}).to_json) -+ ) -+ toggle_result = unleash.is_enabled?(test.fetch('toggleName'), context) - - expect(toggle_result).to eq(test['expectedResult']) - end -@@ -49,14 +42,21 @@ RSpec.describe Unleash::Client do - variant_tests = current_test_set.fetch('variantTests', []) - variant_tests.each do |test| - it "test that #{test['description']}" do -- test_toggle = unleash_toggles.select{ |t| t.fetch('name', '') == test.fetch('toggleName') }.first -- -- toggle = Unleash::FeatureToggle.new(test_toggle, state_segments) - context = Unleash::Context.new(test['context']) - -- variant = toggle.get_variant(context, DEFAULT_VARIANT) -+ unleash = Unleash::Client.new( -+ app_name: 'bootstrap-test', -+ instance_id: 'local-test-cli', -+ disable_client: true, -+ disable_metrics: true, -+ bootstrap_config: Unleash::Bootstrap::Configuration.new(data: current_test_set.fetch('state', {}).to_json) -+ ) -+ variant = unleash.get_variant(test.fetch('toggleName'), context) -+ expectedResult = test['expectedResult'] - -- expect(variant).to eq(Unleash::Variant.new(test['expectedResult'])) -+ expect(variant.name).to eq(expectedResult['name']) -+ expect(variant.enabled).to eq(expectedResult['enabled']) -+ expect(variant.payload).to eq(expectedResult['payload']) - end - end - end -diff --git a/spec/unleash/constraint_spec.rb b/spec/unleash/constraint_spec.rb -deleted file mode 100644 -index 38c1909..0000000 ---- a/spec/unleash/constraint_spec.rb -+++ /dev/null -@@ -1,458 +0,0 @@ --RSpec.describe Unleash::Constraint do -- before do -- Unleash.configuration = Unleash::Configuration.new -- Unleash.logger = Unleash.configuration.logger -- end -- -- describe '#is_enabled?' do -- it 'matches based on property IN value' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- env: 'dev' -- } -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('env', 'IN', ['dev']) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'IN', ['dev', 'pre']) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['dev', 'pre']) -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['pre', 'prod']) -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on property NOT_IN value' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.2', -- properties: { -- env: 'dev' -- } -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['dev']) -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['dev', 'pre']) -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['pre', 'prod']) -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on a value NOT_IN in a not existing context field' do -- context_params = { -- properties: {} -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('env', 'NOT_IN', ['anything']) -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on user_id IN/NOT_IN user_id' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.3', -- properties: { -- fancy: 'polarbear' -- } -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('user_id', 'IN', ['123', '456']) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('user_id', 'IN', ['456', '789']) -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['123', '456']) -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['456', '789']) -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on user_id IN/NOT_IN user_id with user_id as int' do -- context_params = { -- user_id: 123 -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('user_id', 'IN', ['123', '456']) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('user_id', 'IN', ['456', '789']) -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['123', '456']) -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('user_id', 'NOT_IN', ['456', '789']) -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on property STR_STARTS_WITH value' do -- context_params = { -- properties: { -- env: 'development' -- } -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['dev']) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['development']) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['ment']) -- expect(constraint.matches_context?(context)).to be false -- end -- -- it 'matches based on property STR_ENDS_WITH value' do -- context_params = { -- properties: { -- env: 'development' -- } -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['ment']) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['development']) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['dev']) -- expect(constraint.matches_context?(context)).to be false -- end -- -- it 'matches based on property STR_CONTAINS value' do -- context_params = { -- properties: { -- env: 'development' -- } -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['ment']) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['dev']) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['development']) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['DEVELOPMENT']) -- expect(constraint.matches_context?(context)).to be false -- end -- -- it 'matches based on property NUM_EQ value' do -- context_params = { -- properties: { -- distance: '0.3' -- } -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('distance', 'NUM_EQ', '0.3') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('distance', 'NUM_EQ', '0.2') -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('distance', 'NUM_EQ', (0.1 + 0.2).to_s) -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on property NUM_LT value' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- distance: '3.141' -- } -- } -- context = Unleash::Context.new(context_params) -- -- constraint = Unleash::Constraint.new('distance', 'NUM_LT', '2.718') -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('distance', 'NUM_LT', '3.141') -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('distance', 'NUM_LT', '6.282') -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on property NUM_LTE value' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- distance: '3.141' -- } -- } -- context = Unleash::Context.new(context_params) -- -- constraint = Unleash::Constraint.new('distance', 'NUM_LTE', '2.718') -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('distance', 'NUM_LTE', '3.141') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('distance', 'NUM_LTE', '6.282') -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on property NUM_GT value' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- distance: '3.141' -- } -- } -- context = Unleash::Context.new(context_params) -- -- constraint = Unleash::Constraint.new('distance', 'NUM_GT', '2.718') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('distance', 'NUM_GT', '3.141') -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('distance', 'NUM_GT', '6.282') -- expect(constraint.matches_context?(context)).to be false -- end -- -- it 'matches based on property NUM_GTE value' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- distance: '3.141' -- } -- } -- context = Unleash::Context.new(context_params) -- -- constraint = Unleash::Constraint.new('distance', 'NUM_GTE', '2.718') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('distance', 'NUM_GTE', '3.141') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('distance', 'NUM_GTE', '6.282') -- expect(constraint.matches_context?(context)).to be false -- end -- -- it 'matches based on property SEMVER_EQ value' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- env: '3.1.41-beta' -- } -- } -- context = Unleash::Context.new(context_params) -- -- constraint = Unleash::Constraint.new('env', 'SEMVER_EQ', '3.1.41-beta') -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on property SEMVER_GT value' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- env: '3.1.41-gamma' -- } -- } -- context = Unleash::Context.new(context_params) -- -- constraint = Unleash::Constraint.new('env', 'SEMVER_GT', '3.1.41-beta') -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on property SEMVER_LT value' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- env: '3.1.41-alpha' -- } -- } -- context = Unleash::Context.new(context_params) -- -- constraint = Unleash::Constraint.new('env', 'SEMVER_LT', '3.1.41-beta') -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on property DATE_AFTER value' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- currentTime: '2022-01-30T13:00:00.000Z' -- } -- context = Unleash::Context.new(context_params) -- -- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-29T13:00:00.000Z') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-29T13:00:00Z') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-29T13:00Z') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59:59.999999Z') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59:59.999Z') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59:59') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T12:59') -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-30T13:00:00.000Z') -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('currentTime', 'DATE_AFTER', '2022-01-31T13:00:00.000Z') -- expect(constraint.matches_context?(context)).to be false -- end -- -- it 'matches based on property DATE_BEFORE value' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- currentTime: '2022-01-30T13:00:00.000Z' -- } -- context = Unleash::Context.new(context_params) -- -- constraint = Unleash::Constraint.new('currentTime', 'DATE_BEFORE', '2022-01-29T13:00:00.000Z') -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('currentTime', 'DATE_BEFORE', '2022-01-31T13:00:00.000Z') -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on case insensitive property when operator is uppercased' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- env: 'development' -- } -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['DEV'], case_insensitive: true) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['MENT'], case_insensitive: true) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['LOP'], case_insensitive: true) -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on case insensitive property when context is uppercased' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- env: 'DEVELOPMENT' -- } -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['dev'], case_insensitive: true) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['ment'], case_insensitive: true) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['lop'], case_insensitive: true) -- expect(constraint.matches_context?(context)).to be true -- end -- -- it 'matches based on inverted property' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- env: 'development' -- } -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('env', 'STR_STARTS_WITH', ['dev'], inverted: true) -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('env', 'STR_ENDS_WITH', ['ment'], inverted: true) -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('env', 'STR_CONTAINS', ['lop'], inverted: true) -- expect(constraint.matches_context?(context)).to be false -- end -- -- it 'gracefully handles invalid constraint operators' do -- context_params = { -- user_id: '123', -- session_id: 'verylongsesssionid', -- remote_address: '127.0.0.1', -- properties: { -- env: 'development' -- } -- } -- context = Unleash::Context.new(context_params) -- constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', 'dev', inverted: true) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', ['dev'], inverted: true) -- expect(constraint.matches_context?(context)).to be true -- -- constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', 'dev') -- expect(constraint.matches_context?(context)).to be false -- -- constraint = Unleash::Constraint.new('env', 'NOT_A_VALID_OPERATOR', ['dev']) -- expect(constraint.matches_context?(context)).to be false -- end -- -- it 'warns about constraint construction for invalid value types for operator' do -- array_constraints = ['STR_CONTAINS', 'STR_ENDS_WITH', 'STR_STARTS_WITH', 'IN', 'NOT_IN'] -- -- array_constraints.each do |operator_name| -- expect(Unleash.logger).to receive(:warn).with("value is a String, operator is expecting an Array") -- Unleash::Constraint.new('env', operator_name, '') -- end -- -- string_constraints = ['NUM_EQ', 'NUM_GT', 'NUM_GTE', 'NUM_LT', 'NUM_LTE', -- 'DATE_AFTER', 'DATE_BEFORE', 'SEMVER_EQ', 'SEMVER_GT', 'SEMVER_LT'] -- string_constraints.each do |operator_name| -- expect(Unleash.logger).to receive(:warn).with("value is an Array, operator is expecting a String") -- Unleash::Constraint.new('env', operator_name, []) -- end -- end -- end -- -- it 'does resolves to false rather than crashing when passed a nil context' do -- constraint = Unleash::Constraint.new('anything', 'NUM_GTE', '6.282') -- expect(constraint.matches_context?(nil)).to be false -- end --end -diff --git a/spec/unleash/feature_toggle_spec.rb b/spec/unleash/feature_toggle_spec.rb -deleted file mode 100644 -index a95f243..0000000 ---- a/spec/unleash/feature_toggle_spec.rb -+++ /dev/null -@@ -1,663 +0,0 @@ --require 'logger' --require 'unleash' --require 'unleash/configuration' --require 'unleash/context' --require 'unleash/feature_toggle' --require 'unleash/variant' -- --RSpec.describe Unleash::FeatureToggle do -- before do -- Unleash.configuration = Unleash::Configuration.new -- Unleash.logger = Unleash.configuration.logger -- Unleash.logger.level = Unleash.configuration.log_level -- Unleash.logger.level = Logger::ERROR -- Unleash.toggles = [] -- Unleash.toggle_metrics = {} -- -- # Do not test metrics: -- Unleash.configuration.disable_metrics = true -- end -- -- describe 'FeatureToggle with empty strategies' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "test", -- "enabled" => true, -- "strategies" => [], -- "variants" => nil -- ) -- end -- -- it 'should return true if enabled' do -- context = Unleash::Context.new(user_id: 1) -- expect(feature_toggle.is_enabled?(context)).to be_truthy -- end -- end -- -- describe 'FeatureToggle with empty strategies and disabled toggle' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "Test.userid", -- "description" => nil, -- "enabled" => false, -- "strategies" => [], -- "variants" => nil, -- "createdAt" => "2019-01-24T10:41:45.236Z" -- ) -- end -- -- it 'should return false if disabled' do -- context = Unleash::Context.new(user_id: 1) -- expect(feature_toggle.is_enabled?(context)).to be_falsey -- end -- end -- -- describe 'FeatureToggle with userId strategy and enabled toggle' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "Test.userid", -- "description" => nil, -- "enabled" => true, -- "strategies" => [ -- { -- "name" => "userWithId", -- "parameters" => { -- "userIds" => "12345" -- } -- } -- ], -- "variants" => nil, -- "createdAt" => "2019-01-24T10:41:45.236Z" -- ) -- end -- -- it 'should return true if enabled and user_id is matched' do -- context = Unleash::Context.new(user_id: "12345") -- expect(feature_toggle.is_enabled?(context)).to be_truthy -- end -- -- it 'should return false if enabled and user_id is unmatched' do -- context = Unleash::Context.new(user_id: "54321") -- expect(feature_toggle.is_enabled?(context)).to be_falsey -- end -- end -- -- describe 'FeatureToggle with userId strategy and disabled toggle' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "Test.userid", -- "description" => nil, -- "enabled" => false, -- "strategies" => [ -- { -- "name" => "userWithId", -- "parameters" => { -- "userIds" => "12345" -- } -- } -- ], -- "variants" => nil, -- "createdAt" => "2019-01-24T10:41:45.236Z" -- ) -- end -- -- it 'should return false if disabled and user_id matched' do -- context = Unleash::Context.new(user_id: "12345") -- expect(feature_toggle.is_enabled?(context)).to be_falsey -- end -- -- it 'should return false if disabled and user_id unmatched' do -- context = Unleash::Context.new(user_id: "54321") -- expect(feature_toggle.is_enabled?(context)).to be_falsey -- end -- end -- -- describe 'FeatureToggle with variants' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "Test.variants", -- "description" => nil, -- "enabled" => true, -- "strategies" => [ -- { -- "name" => "default" -- } -- ], -- "variants" => [ -- { -- "name" => "variant1", -- "weight" => 50, -- "stickiness" => "default" -- }, -- { -- "name" => "variant2", -- "weight" => 50, -- "stickiness" => "default" -- } -- ], -- "createdAt" => "2019-01-24T10:41:45.236Z" -- ) -- end -- -- let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } -- -- it 'should return variant1 for user_id:1' do -- context = Unleash::Context.new(user_id: 10) -- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( -- name: "variant1", -- enabled: true, -- payload: nil -- ) -- end -- -- it 'should return variant2 for user_id:2' do -- context = Unleash::Context.new(user_id: 2) -- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( -- name: "variant2", -- enabled: true, -- payload: nil -- ) -- end -- -- xit 'should return false if default is false.' do -- context = Unleash::Context.new(user_id: 2) -- expect(feature_toggle.get_variant(context, default_variant)).to be_falsey -- end -- end -- -- describe 'FeatureToggle including weightless variants' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "Test.variants", -- "description" => nil, -- "enabled" => true, -- "strategies" => [ -- { -- "name" => "default" -- } -- ], -- "variants" => [ -- { -- "name" => "variantA", -- "weight" => 0, -- "stickiness" => "default" -- }, -- { -- "name" => "variantB", -- "weight" => 10, -- "stickiness" => "default" -- }, -- { -- "name" => "variantC", -- "weight" => 20, -- "stickiness" => "default" -- } -- ], -- "createdAt" => "2019-01-24T10:41:45.236Z" -- ) -- end -- -- let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } -- -- it 'should return variantC for user_id:1' do -- context = Unleash::Context.new(user_id: 10) -- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( -- name: "variantC", -- enabled: true, -- payload: nil -- ) -- end -- -- it 'should return variantB for user_id:2' do -- context = Unleash::Context.new(user_id: 2) -- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( -- name: "variantB", -- enabled: true, -- payload: nil -- ) -- end -- end -- -- describe 'FeatureToggle with variants which have all zero weight' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "Test.variants", -- "description" => nil, -- "enabled" => true, -- "strategies" => [ -- { -- "name" => "default" -- } -- ], -- "variants" => [ -- { -- "name" => "variantA", -- "weight" => 0 -- }, -- { -- "name" => "variantB", -- "weight" => 0 -- } -- ], -- "createdAt" => "2019-01-24T10:41:45.236Z" -- ) -- end -- let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } -- -- it 'should return disabled for user_id:1' do -- context = Unleash::Context.new(user_id: 10) -- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( -- name: "disabled", -- enabled: false, -- payload: nil -- ) -- end -- -- it 'should return disabled for user_id:2' do -- context = Unleash::Context.new(user_id: 2) -- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( -- name: "disabled", -- enabled: false, -- payload: nil -- ) -- end -- end -- -- describe 'FeatureToggle with variants that have a variant override' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "Test.variants", -- "description" => nil, -- "enabled" => true, -- "strategies" => [ -- { -- "name" => "default" -- } -- ], -- "variants" => [ -- { -- "name" => "variant1", -- "weight" => 50, -- "stickiness" => "default", -- "payload" => { -- "type" => "string", -- "value" => "val1" -- }, -- "overrides" => [{ -- "contextName" => "userId", -- "values" => ["132", "61"] -- }] -- }, -- { -- "name" => "variant2", -- "weight" => 50, -- "stickiness" => "default", -- "payload" => { -- "type" => "string", -- "value" => "val2" -- } -- } -- ], -- "createdAt" => "2019-01-24T10:41:45.236Z" -- ) -- end -- -- it 'should return variant1 for user_id:61 from override' do -- context = Unleash::Context.new(user_id: 61) -- expect(feature_toggle.get_variant(context)).to have_attributes( -- name: "variant1", -- enabled: true, -- payload: { "type" => "string", "value" => "val1" } -- ) -- end -- -- it 'should return variant1 for user_id:132 from override' do -- context = Unleash::Context.new("userId" => 132) -- expect(feature_toggle.get_variant(context)).to have_attributes( -- name: "variant1", -- enabled: true, -- payload: { "type" => "string", "value" => "val1" } -- ) -- end -- -- it 'should return variant2 for user_id:60' do -- context = Unleash::Context.new(user_id: 60) -- expect(feature_toggle.get_variant(context)).to have_attributes( -- name: "variant2", -- enabled: true, -- payload: { "type" => "string", "value" => "val2" } -- ) -- end -- -- it 'get_variant_with_matching_override should for user_id:61' do -- # NOTE: Use send method, as we are testing a private method -- context = Unleash::Context.new(user_id: 61) -- expect(feature_toggle.send(:variant_from_override_match, context)).to have_attributes( -- name: "variant1", -- payload: { "type" => "string", "value" => "val1" } -- ) -- end -- end -- -- describe 'FeatureToggle with no variants' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "Test.variants", -- "description" => nil, -- "enabled" => true, -- "strategies" => [ -- { -- "name" => "default" -- } -- ], -- "variants" => [], -- "createdAt" => "2019-01-24T10:41:45.236Z" -- ) -- end -- let(:default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } -- -- it 'should return disabled for user_id:1' do -- context = Unleash::Context.new(user_id: 10) -- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( -- name: "disabled", -- enabled: false, -- payload: nil -- ) -- end -- -- it 'should return disabled for user_id:2' do -- context = Unleash::Context.new(user_id: 2) -- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( -- name: "disabled", -- enabled: false, -- payload: nil -- ) -- end -- -- it 'should return an enabled fallback when the fallback is specified' do -- context = Unleash::Context.new(user_id: 2) -- expect(feature_toggle.get_variant(context, default_variant)).to have_attributes( -- name: "disabled", -- enabled: false, -- payload: nil -- ) -- end -- end -- -- describe 'FeatureToggle with invalid default_variant' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "Test.variants", -- "description" => nil, -- "enabled" => true, -- "strategies" => [ -- { -- "name" => "default" -- } -- ], -- "variants" => [], -- "createdAt" => "2019-01-24T10:41:45.236Z" -- ) -- end -- let(:valid_default_variant) { Unleash::Variant.new(name: 'unknown', default: true) } -- let(:invalid_default_variant) { Hash.new(name: 'unknown', default: true) } -- -- it 'should raise an error for an invalid fallback variant' do -- expect{ feature_toggle.get_variant(nil, invalid_default_variant) }.to raise_error(ArgumentError) -- end -- -- it 'should not raise an error for a valid fallback variant' do -- expect{ feature_toggle.get_variant(nil, valid_default_variant) }.to_not raise_error -- end -- end -- -- describe 'FeatureToggle default Strategy with two constraints' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "Test.userid", -- "description" => "Play with strategy constraints", -- "enabled" => true, -- "strategies" => [ -- { -- "constraints" => [ -- { -- "contextName" => "environment", -- "operator" => "IN", -- "values" => [ -- "dev" -- ] -- }, -- { -- "contextName" => "userId", -- "operator" => "IN", -- "values" => ["123"] -- } -- ], -- "name" => "default", -- "parameters" => {} -- } -- ] -- ) -- end -- -- it 'should return true if it matches all constraints' do -- context = Unleash::Context.new(user_id: "123", environment: "dev") -- expect(feature_toggle.is_enabled?(context)).to be_truthy -- end -- -- it 'should return false if it does not match all constraints (env)' do -- context = Unleash::Context.new(user_id: "123", environment: "prod") -- expect(feature_toggle.is_enabled?(context)).to be_falsey -- end -- -- it 'should return false if it does not match all constraints (user_id)' do -- context = Unleash::Context.new(user_id: "11", environment: "dev") -- expect(feature_toggle.is_enabled?(context)).to be_falsey -- end -- -- it 'should return false if it does not match any constraint (env and user_id)' do -- context = Unleash::Context.new(user_id: "11", environment: "prod") -- expect(feature_toggle.is_enabled?(context)).to be_falsey -- end -- end -- -- describe 'FeatureToggle default Strategy with one constraint' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "Demo", -- "description" => "Play with strategy constraints", -- "enabled" => true, -- "strategies" => [ -- { -- "constraints" => [ -- { -- "contextName" => "environment", -- "operator" => "IN", -- "values" => [ -- "dev" -- ] -- } -- ], -- "name" => "default", -- "parameters" => {} -- } -- ] -- ) -- end -- -- it 'should return true if it matches the constraint' do -- context = Unleash::Context.new(user_id: "123", environment: "dev") -- expect(feature_toggle.is_enabled?(context)).to be_truthy -- end -- -- it 'should return false if it does not match the constraint' do -- context = Unleash::Context.new(user_id: "123", environment: "prod") -- expect(feature_toggle.is_enabled?(context)).to be_falsey -- end -- end -- -- describe 'disabled_variant' do -- it 'returns disabled variant' do -- ret = described_class.disabled_variant -- expect(ret.enabled).to be false -- expect(ret.name).to eq 'disabled' -- end -- end -- -- describe 'FeatureToggle variant with custom stickiness' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "toggleName", -- "description" => nil, -- "enabled" => true, -- "variants" => [ -- { -- "name" => "variant1", -- "weight" => 25, -- "stickiness" => "organization" -- }, -- { -- "name" => "variant2", -- "weight" => 25, -- "stickiness" => "organization" -- }, -- { -- "name" => "variant3", -- "weight" => 25, -- "stickiness" => "organization" -- }, -- { -- "name" => "variant4", -- "weight" => 25, -- "stickiness" => "organization" -- } -- -- ], -- "createdAt" => "2019-01-24T10:41:45.236Z" -- ) -- end -- -- it 'should return variant1 organization 726' do -- context = Unleash::Context.new( -- properties: { -- organization: '726' -- } -- ) -- -- expect(feature_toggle.get_variant(context)).to have_attributes( -- name: "variant1", -- enabled: true -- ) -- end -- -- it 'should return variant2 organization 48' do -- context = Unleash::Context.new( -- properties: { -- organization: '48' -- } -- ) -- -- expect(feature_toggle.get_variant(context)).to have_attributes( -- name: "variant2", -- enabled: true -- ) -- end -- -- it 'should return variant3 organization 381' do -- context = Unleash::Context.new( -- properties: { -- organization: '381' -- } -- ) -- -- expect(feature_toggle.get_variant(context)).to have_attributes( -- name: "variant3", -- enabled: true -- ) -- end -- -- it 'should return variant4 organization 222' do -- context = Unleash::Context.new( -- properties: { -- organization: '222' -- } -- ) -- -- expect(feature_toggle.get_variant(context)).to have_attributes( -- name: "variant4", -- enabled: true -- ) -- end -- -- it 'should work with a nil context' do -- variant = feature_toggle.get_variant(nil) -- -- expect(variant.name).to match(/variant\d/) -- expect(variant.enabled).to be true -- expect(variant).to be_a_kind_of(Unleash::Variant) -- end -- end -- -- describe 'FeatureToggle Variant with payload and custom stickiness' do -- let(:feature_toggle) do -- Unleash::FeatureToggle.new( -- "name" => "featureVariantX", -- "description" => nil, -- "enabled" => true, -- "strategies" => [ -- { "name" => "default" } -- ], -- "variants" => [ -- { -- "name" => "default-value", -- "payload" => { -- "type" => "string", -- "value" => "payloadData" -- }, -- "stickiness" => "custom_context_attribute", -- "weight" => 100, -- "weightType" => "variable" -- } -- ] -- ) -- end -- -- let(:expected_variant) do -- { -- name: "default-value", -- enabled: true, -- payload: { -- "type" => "string", -- "value" => "payloadData" -- } -- } -- end -- -- it 'should return the one variant, when the context correctly contains the custom stickiness parameter' do -- context = Unleash::Context.new( -- properties: { -- default: 'foo', -- custom_context_attribute: 'uniqueContextValue' -- } -- ) -- expect(feature_toggle.get_variant(context)).to have_attributes(expected_variant) -- end -- -- it 'should return the one variant, with context that is nil' do -- expect(feature_toggle.get_variant(nil)).to have_attributes(expected_variant) -- end -- -- it 'should return the one variant, even when the contexts do not contain the stickiness parameter' do -- [ -- nil, -- Unleash::Context.new, -- Unleash::Context.new(user_id: '123'), -- Unleash::Context.new(session_id: '123'), -- Unleash::Context.new(remote_address: '127.0.0.1'), -- Unleash::Context.new(properties: { not_custom_context_attribute: 'foo' }) -- ].each do |context| -- expect(feature_toggle.get_variant(context)).to have_attributes(expected_variant) -- end -- end -- end --end -diff --git a/spec/unleash/metrics_reporter_spec.rb b/spec/unleash/metrics_reporter_spec.rb -index cd5bd4f..858a0fb 100644 ---- a/spec/unleash/metrics_reporter_spec.rb -+++ b/spec/unleash/metrics_reporter_spec.rb -@@ -27,24 +27,26 @@ RSpec.describe Unleash::MetricsReporter do - config.instance_id = 'rspec/test' - config.disable_client = true - end -- Unleash.toggle_metrics = Unleash::Metrics.new -+ Unleash.engine = UnleashEngine.new - -- Unleash.toggle_metrics.increment('featureA', :yes) -- Unleash.toggle_metrics.increment('featureA', :yes) -- Unleash.toggle_metrics.increment('featureA', :yes) -- Unleash.toggle_metrics.increment('featureA', :no) -- Unleash.toggle_metrics.increment('featureA', :no) -- Unleash.toggle_metrics.increment('featureB', :yes) -+ Unleash.engine.count_toggle('featureA', true) -+ Unleash.engine.count_toggle('featureA', true) -+ Unleash.engine.count_toggle('featureA', true) -+ Unleash.engine.count_toggle('featureA', false) -+ Unleash.engine.count_toggle('featureA', false) -+ Unleash.engine.count_toggle('featureB', true) - - report = metrics_reporter.generate_report - expect(report[:bucket][:toggles]).to include( -- "featureA" => { -+ :featureA => { - no: 2, -- yes: 3 -+ yes: 3, -+ variants: {} - }, -- "featureB" => { -+ :featureB => { - no: 0, -- yes: 1 -+ yes: 1, -+ variants: {} - } - ) - -@@ -74,14 +76,14 @@ RSpec.describe Unleash::MetricsReporter do - ) - .to_return(status: 200, body: "", headers: {}) - -- Unleash.toggle_metrics = Unleash::Metrics.new -+ Unleash.engine = UnleashEngine.new - -- Unleash.toggle_metrics.increment('featureA', :yes) -- Unleash.toggle_metrics.increment('featureA', :yes) -- Unleash.toggle_metrics.increment('featureA', :yes) -- Unleash.toggle_metrics.increment('featureA', :no) -- Unleash.toggle_metrics.increment('featureA', :no) -- Unleash.toggle_metrics.increment('featureB', :yes) -+ Unleash.engine.count_toggle('featureA', true) -+ Unleash.engine.count_toggle('featureA', true) -+ Unleash.engine.count_toggle('featureA', true) -+ Unleash.engine.count_toggle('featureA', false) -+ Unleash.engine.count_toggle('featureA', false) -+ Unleash.engine.count_toggle('featureB', true) - - metrics_reporter.post - -@@ -105,7 +107,7 @@ RSpec.describe Unleash::MetricsReporter do - end - - it "does not send a report, if there were no metrics registered/evaluated" do -- Unleash.toggle_metrics = Unleash::Metrics.new -+ Unleash.engine = UnleashEngine.new - - metrics_reporter.post - -diff --git a/spec/unleash/metrics_spec.rb b/spec/unleash/metrics_spec.rb -deleted file mode 100644 -index 2830f9b..0000000 ---- a/spec/unleash/metrics_spec.rb -+++ /dev/null -@@ -1,72 +0,0 @@ --require "rspec/json_expectations" -- --RSpec.describe Unleash::Metrics do -- let(:metrics) { Unleash::Metrics.new } -- -- it "counts up correctly" do -- metrics.increment('featureA', :yes) -- metrics.increment('featureA', :yes) -- metrics.increment('featureA', :yes) -- metrics.increment('featureA', :no) -- metrics.increment('featureA', :no) -- -- metrics.increment('featureB', :yes) -- metrics.increment('featureB', :no) -- metrics.increment('featureC', :no) -- -- expect(metrics.features['featureA'][:yes]).to eq(3) -- expect(metrics.features['featureA'][:no]).to eq(2) -- expect(metrics.features['featureB'][:yes]).to eq(1) -- expect(metrics.features['featureB'][:no]).to eq(1) -- expect(metrics.features['featureC'][:yes]).to eq(0) -- expect(metrics.features['featureC'][:no]).to eq(1) -- end -- -- it "resets correctly" do -- metrics = Unleash::Metrics.new -- -- metrics.increment('featureA', :yes) -- metrics.reset -- metrics.increment('featureB', :no) -- -- expect(metrics.features['featureA']).to be_nil -- expect(metrics.features['featureB'][:yes]).to eq(0) -- expect(metrics.features['featureB'][:no]).to eq(1) -- end -- -- it "spits out correct JSON" do -- metrics.reset -- metrics.increment('featureA', :yes) -- metrics.increment('featureB', :no) -- -- expect(metrics.to_s).to include_json( -- featureA: { -- yes: 1, -- no: 0 -- }, -- featureB: { -- no: 1 -- } -- ) -- end -- -- describe "when dealing with variants" do -- it "counts up correctly" do -- metrics.increment_variant('featureA', :yes, 'variantA') -- metrics.increment_variant('featureA', :yes, 'variantA') -- metrics.increment_variant('featureA', :yes, 'variantB') -- -- expect(metrics.features['featureA'][:yes]).to eq(3) -- expect(metrics.features['featureA'][:no]).to eq(0) -- expect(metrics.features['featureA']['variants']['variantA']).to eq(2) -- expect(metrics.features['featureA']['variants']['variantB']).to eq(1) -- end -- end -- -- it "increments feature toggle counter when variant is resolved" do -- metrics.increment_variant('featureA', :yes, 'variantA') -- -- expect(metrics.features['featureA'][:yes]).to eq(1) -- expect(metrics.features['featureA'][:no]).to eq(0) -- end --end -diff --git a/spec/unleash/strategies_spec.rb b/spec/unleash/strategies_spec.rb -deleted file mode 100644 -index 87b70ff..0000000 ---- a/spec/unleash/strategies_spec.rb -+++ /dev/null -@@ -1,156 +0,0 @@ --require "spec_helper" -- --RSpec.describe Unleash::Strategies do -- let(:strategies) { described_class.new } -- -- # Silence warnings we are triggering in this test -- around do |example| -- old_verbose = $VERBOSE -- $VERBOSE = nil -- example.run -- ensure -- $VERBOSE = old_verbose -- end -- -- describe 'strategies registration' do -- let(:default_strategies) do -- ['applicationHostname', 'default', 'flexibleRollout', 'gradualRolloutRandom', -- 'gradualRolloutSessionId', 'gradualRolloutUserId', 'remoteAddress', -- 'userWithId'] -- end -- -- context 'when no custom strategies are defined' do -- it 'has default list' do -- expect(strategies.keys.sort).to eq(default_strategies) -- end -- end -- -- # This block testing previous way of loading strategies, when we dynamically picked up all classes -- # defined under `Unleash::Strategy` module -- context 'when custom strategy is defined' do -- let(:custom_strategy) do -- Class.new do -- def name -- 'myCustomStrategy' -- end -- end -- end -- -- before do -- # Define custom class -- Unleash::Strategy.const_set("MyCustomStrategy", custom_strategy) -- end -- -- after do -- # Remove custom class so it does not interfere with other tests -- Unleash::Strategy.send(:remove_const, :MyCustomStrategy) -- end -- -- it 'includes custom strategy in default list' do -- expect(strategies.keys.sort).to eq(default_strategies.concat(['myCustomStrategy']).sort) -- end -- -- it 'warns about deprecated functionality' do -- allow(strategies).to receive(:warn) -- strategies.send(:register_strategies) -- message = '[DEPRECATED] Registering custom Unleash strategy by adding custom class into Unleash::Strategy' -- expect(strategies).to have_received(:warn).with(start_with(message)) -- end -- end -- end -- -- describe '#includes?' do -- it 'returns true for available strategy' do -- expect(strategies.includes?('gradualRolloutRandom')).to be_truthy -- expect(strategies.includes?(:userWithId)).to be_truthy -- end -- -- it 'returns false for missing strategy' do -- expect(strategies.includes?(:missing)).to be_falsey -- end -- end -- -- describe '#fetch' do -- it 'returns available strategy' do -- expect(strategies.fetch(:flexibleRollout)).to be_instance_of(Unleash::Strategy::FlexibleRollout) -- expect(strategies.fetch('applicationHostname')).to be_instance_of(Unleash::Strategy::ApplicationHostname) -- end -- -- it 'raising error when missing' do -- message = 'Strategy is not implemented' -- expect { strategies.fetch(:missing) }.to raise_error(Unleash::Strategy::NotImplemented, message) -- end -- end -- -- describe '#[]' do -- it 'returns available strategy' do -- expect(strategies[:flexibleRollout]).to be_instance_of(Unleash::Strategy::FlexibleRollout) -- expect(strategies['applicationHostname']).to be_instance_of(Unleash::Strategy::ApplicationHostname) -- end -- -- it 'returns nil when missing strategy' do -- expect(strategies[:missing]).to be_nil -- end -- end -- -- describe '#add' do -- before do -- strategies.add(custom_strategy) -- end -- -- context 'when existing strategy is available' do -- let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'applicationHostname') } -- -- it 'overrides previous strategy strategy' do -- expect(strategies.includes?('applicationHostname')).to be_truthy -- expect(strategies.fetch(:applicationHostname)).to eq(custom_strategy) -- expect(strategies.fetch('applicationHostname')).to eq(custom_strategy) -- end -- end -- -- context 'when strategy is new' do -- let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'test') } -- -- it 'adds new strategy strategy' do -- expect(strategies.includes?('test')).to be_truthy -- expect(strategies.fetch(:test)).to eq(custom_strategy) -- expect(strategies.fetch('test')).to eq(custom_strategy) -- end -- end -- end -- -- describe '#[]=' do -- let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'strange name') } -- -- context 'when existing strategy is available' do -- let(:custom_strategy) { instance_double(Unleash::Strategy::Base, name: 'applicationHostname') } -- -- it 'overrides previous strategy strategy' do -- strategies[:applicationHostname] = custom_strategy -- -- expect(strategies.includes?('applicationHostname')).to be_truthy -- expect(strategies.fetch(:applicationHostname)).to eq(custom_strategy) -- expect(strategies.fetch('applicationHostname')).to eq(custom_strategy) -- end -- -- it 'warns when using this method' do -- allow(strategies).to receive(:warn) -- strategies[:applicationHostname] = custom_strategy -- message = '[DEPRECATED] Registering custom Unleash strategy by modifying Unleash::STRATEGIES' -- expect(strategies).to have_received(:warn).with(start_with(message)) -- end -- end -- -- context 'when strategy is new' do -- before do -- strategies['test'] = custom_strategy -- end -- -- it 'adds new strategy strategy' do -- expect(strategies.includes?('test')).to be_truthy -- expect(strategies.fetch(:test)).to eq(custom_strategy) -- expect(strategies.fetch('test')).to eq(custom_strategy) -- end -- end -- end --end -diff --git a/spec/unleash/strategy/application_hostname_spec.rb b/spec/unleash/strategy/application_hostname_spec.rb -deleted file mode 100644 -index 0614533..0000000 ---- a/spec/unleash/strategy/application_hostname_spec.rb -+++ /dev/null -@@ -1,29 +0,0 @@ --require "unleash/strategy/application_hostname" -- --RSpec.describe Unleash::Strategy::ApplicationHostname do -- describe '#is_enabled?' do -- let(:strategy) { Unleash::Strategy::ApplicationHostname.new } -- -- before do -- expect(Socket).to receive(:gethostname).and_return("rspechost") -- end -- -- it 'correctly initialize' do -- expect(strategy.hostname).to eq("rspechost") -- end -- -- it 'should be enabled with correct params' do -- expect(strategy.is_enabled?({ 'hostnames' => 'foo,rspechost,bar' })).to be_truthy -- end -- -- it 'should be disabled with false params' do -- expect(strategy.is_enabled?({ 'hostnames' => 'abc,localhost' })).to be_falsey -- end -- -- it 'should be disabled on invalid params' do -- expect(strategy.is_enabled?(nil)).to be_falsey -- expect(strategy.is_enabled?('string')).to be_falsey -- expect(strategy.is_enabled?({})).to be_falsey -- end -- end --end -diff --git a/spec/unleash/strategy/base_spec.rb b/spec/unleash/strategy/base_spec.rb -deleted file mode 100644 -index 3e04cde..0000000 ---- a/spec/unleash/strategy/base_spec.rb -+++ /dev/null -@@ -1,11 +0,0 @@ --require "unleash/strategy/base" -- --RSpec.describe Unleash::Strategy::Base do -- describe '#is_enabled?' do -- let(:strategy) { Unleash::Strategy::Base.new } -- -- it 'raise exception' do -- expect{ strategy.is_enabled? }.to raise_exception Unleash::Strategy::NotImplemented -- end -- end --end -diff --git a/spec/unleash/strategy/default_spec.rb b/spec/unleash/strategy/default_spec.rb -deleted file mode 100644 -index 60a908b..0000000 ---- a/spec/unleash/strategy/default_spec.rb -+++ /dev/null -@@ -1,11 +0,0 @@ --require "unleash/strategy/default" -- --RSpec.describe Unleash::Strategy::Default do -- describe '#is_enabled?' do -- let(:strategy) { Unleash::Strategy::Default.new } -- -- it 'always returns true' do -- expect(strategy.is_enabled?).to be_truthy -- end -- end --end -diff --git a/spec/unleash/strategy/flexible_rollout_spec.rb b/spec/unleash/strategy/flexible_rollout_spec.rb -deleted file mode 100644 -index d4783b8..0000000 ---- a/spec/unleash/strategy/flexible_rollout_spec.rb -+++ /dev/null -@@ -1,64 +0,0 @@ --require 'unleash/strategy/flexible_rollout' -- --RSpec.describe Unleash::Strategy::FlexibleRollout do -- describe '#is_enabled?' do -- let(:strategy) { Unleash::Strategy::FlexibleRollout.new } -- let(:unleash_context) { Unleash::Context.new } -- -- it 'should always be enabled when rollout is set to 100, disabled when set to 0' do -- params = { -- 'groupId' => 'Demo', -- 'rollout' => 100, -- 'stickiness' => 'default' -- } -- -- expect(strategy.is_enabled?(params, unleash_context)).to be_truthy -- expect(strategy.is_enabled?(params.merge({ 'rollout' => 0 }), unleash_context)).to be_falsey -- end -- -- it 'should behave predictably when based on the normalized_number' do -- allow(Unleash::Strategy::Util).to receive(:get_normalized_number).and_return(15) -- -- params = { -- 'groupId' => 'Demo', -- 'stickiness' => 'default' -- } -- -- expect(strategy.is_enabled?(params.merge({ 'rollout' => 14 }), unleash_context)).to be_falsey -- expect(strategy.is_enabled?(params.merge({ 'rollout' => 15 }), unleash_context)).to be_truthy -- expect(strategy.is_enabled?(params.merge({ 'rollout' => 16 }), unleash_context)).to be_truthy -- end -- -- it 'should be enabled when stickiness=customerId and customerId=61 and rollout=10' do -- params = { -- 'groupId' => 'Demo', -- 'rollout' => 10, -- 'stickiness' => 'customerId' -- } -- -- custom_context = Unleash::Context.new( -- properties: { -- customer_id: '61' -- } -- ) -- -- expect(strategy.is_enabled?(params, custom_context)).to be_truthy -- end -- -- it 'should be disabled when stickiness=customerId and customerId=63 and rollout=10' do -- params = { -- 'groupId' => 'Demo', -- 'rollout' => 10, -- 'stickiness' => 'customerId' -- } -- -- custom_context = Unleash::Context.new( -- properties: { -- customer_id: '63' -- } -- ) -- -- expect(strategy.is_enabled?(params, custom_context)).to be_falsey -- end -- end --end -diff --git a/spec/unleash/strategy/gradual_rollout_random_spec.rb b/spec/unleash/strategy/gradual_rollout_random_spec.rb -deleted file mode 100644 -index c7ab26b..0000000 ---- a/spec/unleash/strategy/gradual_rollout_random_spec.rb -+++ /dev/null -@@ -1,32 +0,0 @@ --require "unleash/strategy/gradual_rollout_random" -- --RSpec.describe Unleash::Strategy::GradualRolloutRandom do -- describe '#is_enabled?' do -- let(:strategy) { Unleash::Strategy::GradualRolloutRandom.new } -- -- before do -- # Random.rand always returns 15, so it is not really random in our tests. -- allow(Random).to receive(:rand).and_return(15) -- end -- -- it 'return true when percentage set (20) is over the returned random value (15)' do -- expect(strategy.is_enabled?({ 'percentage' => '20' })).to be_truthy -- expect(strategy.is_enabled?({ 'percentage' => 20 })).to be_truthy -- expect(strategy.is_enabled?({ 'percentage' => 20.0 })).to be_truthy -- end -- -- it 'return false when percentage set (10) is under the returned random value (15)' do -- expect(strategy.is_enabled?({ 'percentage' => '10' })).to be_falsey -- expect(strategy.is_enabled?({ 'percentage' => 10 })).to be_falsey -- expect(strategy.is_enabled?({ 'percentage' => 10.0 })).to be_falsey -- end -- -- it 'return false when percentage is invalid' do -- expect(strategy.is_enabled?({ 'percentage' => -1 })).to be_falsey -- expect(strategy.is_enabled?({ 'percentage' => nil })).to be_falsey -- expect(strategy.is_enabled?({ 'percentage' => 'abc' })).to be_falsey -- expect(strategy.is_enabled?('text')).to be_falsey -- expect(strategy.is_enabled?(nil)).to be_falsey -- end -- end --end -diff --git a/spec/unleash/strategy/gradual_rollout_sessionid_spec.rb b/spec/unleash/strategy/gradual_rollout_sessionid_spec.rb -deleted file mode 100644 -index b3d76f3..0000000 ---- a/spec/unleash/strategy/gradual_rollout_sessionid_spec.rb -+++ /dev/null -@@ -1,22 +0,0 @@ --require "unleash/strategy/gradual_rollout_sessionid" --require "unleash/strategy/util" -- --RSpec.describe Unleash::Strategy::GradualRolloutSessionId do -- describe '#is_enabled?' do -- let(:strategy) { Unleash::Strategy::GradualRolloutSessionId.new } -- let(:unleash_context) { Unleash::Context.new(session_id: 'secretsessionidhashgoeshere') } -- let(:percentage) { Unleash::Strategy::Util.get_normalized_number(unleash_context.session_id, "") } -- -- it 'return true when percentage set is gt the number returned by the hash function' do -- expect(strategy.is_enabled?({ 'percentage' => (percentage + 1).to_s }, unleash_context)).to be_truthy -- expect(strategy.is_enabled?({ 'percentage' => percentage + 1 }, unleash_context)).to be_truthy -- expect(strategy.is_enabled?({ 'percentage' => percentage + 0.1 }, unleash_context)).to be_truthy -- end -- -- it 'return false when percentage set is lt the number returned by the hash function' do -- expect(strategy.is_enabled?({ 'percentage' => (percentage - 1).to_s }, unleash_context)).to be_falsey -- expect(strategy.is_enabled?({ 'percentage' => percentage - 1 }, unleash_context)).to be_falsey -- expect(strategy.is_enabled?({ 'percentage' => percentage - 0.1 }, unleash_context)).to be_falsey -- end -- end --end -diff --git a/spec/unleash/strategy/gradual_rollout_userid_spec.rb b/spec/unleash/strategy/gradual_rollout_userid_spec.rb -deleted file mode 100644 -index 2ed9613..0000000 ---- a/spec/unleash/strategy/gradual_rollout_userid_spec.rb -+++ /dev/null -@@ -1,21 +0,0 @@ --require "unleash/strategy/gradual_rollout_userid" -- --RSpec.describe Unleash::Strategy::GradualRolloutUserId do -- describe '#is_enabled?' do -- let(:strategy) { Unleash::Strategy::GradualRolloutUserId.new } -- let(:unleash_context) { Unleash::Context.new({ 'userId' => 'alice' }) } -- let(:percentage) { Unleash::Strategy::Util.get_normalized_number(unleash_context.user_id, "") } -- -- it 'return true when percentage set is gt the number returned by the hash function' do -- expect(strategy.is_enabled?({ 'percentage' => (percentage + 1).to_s }, unleash_context)).to be_truthy -- expect(strategy.is_enabled?({ 'percentage' => percentage + 1 }, unleash_context)).to be_truthy -- expect(strategy.is_enabled?({ 'percentage' => percentage + 0.1 }, unleash_context)).to be_truthy -- end -- -- it 'return false when percentage set is lt the number returned by the hash function' do -- expect(strategy.is_enabled?({ 'percentage' => (percentage - 1).to_s }, unleash_context)).to be_falsey -- expect(strategy.is_enabled?({ 'percentage' => percentage - 1 }, unleash_context)).to be_falsey -- expect(strategy.is_enabled?({ 'percentage' => percentage - 0.1 }, unleash_context)).to be_falsey -- end -- end --end -diff --git a/spec/unleash/strategy/remote_address_spec.rb b/spec/unleash/strategy/remote_address_spec.rb -deleted file mode 100644 -index ac3da10..0000000 ---- a/spec/unleash/strategy/remote_address_spec.rb -+++ /dev/null -@@ -1,79 +0,0 @@ --require "unleash/strategy/remote_address" -- --RSpec.describe Unleash::Strategy::RemoteAddress do -- describe '#is_enabled?' do -- let(:strategy) { Unleash::Strategy::RemoteAddress.new } -- let(:unleash_context) { Unleash::Context.new({ 'remoteAddress' => '127.0.0.1' }) } -- -- def context_for_addr(remote_address) -- Unleash::Context.new(remote_address: remote_address) -- end -- -- it 'should be enabled with correct params' do -- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, unleash_context)).to be_truthy -- -- unleash_context2 = Unleash::Context.new -- unleash_context2.remote_address = '172.12.0.1' -- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, unleash_context2)).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1, 172.12.0.1 , 127.0.0.1' }, unleash_context2)).to be_truthy -- end -- -- it 'should work with ipv6' do -- ips_and_cidrs = '2001:0db8:85a3:0000:0000:8a2e:0370:7300/120,2001:0db8:85a3:0000:0000:8a2e:0370:7520/123' -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:72ff'))).to be_falsey -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7330'))).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7334'))).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:73ff'))).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7400'))).to be_falsey -- -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7519'))).to be_falsey -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7520'))).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:753f'))).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, context_for_addr('2001:0db8:85a3:0000:0000:8a2e:0370:7540'))).to be_falsey -- end -- -- it 'should be enabled with correct CIDR params' do -- ips_and_cidrs = '192.168.0.0/24,127.0.0.1/32,172.12.0.1' -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, unleash_context)).to be_truthy -- -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '172.12.0.1'))).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '127.0.0.1'))).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '127.0.0.1/32'))).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.0'))).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.1'))).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.255'))).to be_truthy -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.0.192/30'))).to be_truthy -- -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '127.0.0.2'))).to be_falsey -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.1.0'))).to be_falsey -- expect(strategy.is_enabled?({ 'IPs' => ips_and_cidrs }, Unleash::Context.new(remote_address: '192.168.1.255'))).to be_falsey -- end -- -- it 'should be disabled with false params' do -- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,172.12.0.1' }, unleash_context)).to be_falsey -- end -- -- it 'should be disabled on invalid params' do -- expect(strategy.is_enabled?({ 'ips' => '192.168.0.1,172.12.0.1' }, unleash_context)).to be_falsey -- expect(strategy.is_enabled?({ 'IPs' => nil }, unleash_context)).to be_falsey -- expect(strategy.is_enabled?({}, unleash_context)).to be_falsey -- expect(strategy.is_enabled?('IPs_list', unleash_context)).to be_falsey -- end -- -- it 'should be disabled on invalid contexts' do -- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, Unleash::Context.new)).to be_falsey -- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' }, nil)).to be_falsey -- expect(strategy.is_enabled?({ 'IPs' => '192.168.0.1,127.0.0.1,172.12.0.1' })).to be_falsey -- -- expect(strategy.is_enabled?({ 'IPs' => '192.168.x.y,127.0.0.1' }, Unleash::Context.new(remote_address: '192.168.x.y'))).to be_falsey -- expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' }, Unleash::Context.new(remote_address: 'foobar'))).to be_falsey -- expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' }, Unleash::Context.new(remote_address: '192.168.1.0'))).to be_falsey -- expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' }, nil)).to be_falsey -- expect(strategy.is_enabled?({ 'IPs' => 'foobar,abc/32' })).to be_falsey -- end -- -- it 'should be enabled for valid params even if other params are invalid' do -- expect(strategy.is_enabled?({ 'IPs' => '192.168.x.y,127.0.0.1' }, Unleash::Context.new(remote_address: '127.0.0.1'))).to be_truthy -- end -- end --end -diff --git a/spec/unleash/strategy/user_with_id_spec.rb b/spec/unleash/strategy/user_with_id_spec.rb -deleted file mode 100644 -index 18e326a..0000000 ---- a/spec/unleash/strategy/user_with_id_spec.rb -+++ /dev/null -@@ -1,61 +0,0 @@ --require "unleash/strategy/user_with_id" --require "unleash/context" -- --RSpec.describe Unleash::Strategy::UserWithId do -- describe '#is_enabled?' do -- let(:strategy) { Unleash::Strategy::UserWithId.new } -- -- context 'with string params' do -- let(:unleash_context) { Unleash::Context.new({ 'userId' => 'bob' }) } -- -- it 'should be enabled with correct params' do -- expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, unleash_context)).to be_truthy -- -- unleash_context2 = Unleash::Context.new -- unleash_context2.user_id = 'alice' -- expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, unleash_context2)).to be_truthy -- end -- -- it 'should be enabled with correct can include spaces' do -- expect(strategy.is_enabled?({ 'userIds' => ' alice ,bob,carol,dave' }, unleash_context)).to be_truthy -- end -- -- it 'should be disabled with false params' do -- expect(strategy.is_enabled?({ 'userIds' => 'alice,dave' }, unleash_context)).to be_falsey -- end -- -- it 'should be disabled on invalid params' do -- expect(strategy.is_enabled?({ 'userIds' => nil }, unleash_context)).to be_falsey -- expect(strategy.is_enabled?({}, unleash_context)).to be_falsey -- expect(strategy.is_enabled?('string', unleash_context)).to be_falsey -- expect(strategy.is_enabled?(nil, unleash_context)).to be_falsey -- end -- -- it 'should be disabled on invalid contexts' do -- expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, Unleash::Context.new)).to be_falsey -- expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' }, nil)).to be_falsey -- expect(strategy.is_enabled?({ 'userIds' => 'alice,bob,carol,dave' })).to be_falsey -- end -- end -- -- context 'with int params' do -- let(:user_id) { 123 } -- let(:unleash_context) { Unleash::Context.new({ 'userId' => user_id }) } -- -- it 'should be enabled with correct params' do -- expect(strategy.is_enabled?({ 'userIds' => '1,2,123' }, unleash_context)).to be_truthy -- -- unleash_context2 = Unleash::Context.new(user_id: 1) -- expect(strategy.is_enabled?({ 'userIds' => '1,2,123' }, unleash_context2)).to be_truthy -- end -- -- it 'should be enabled with correct can include spaces' do -- expect(strategy.is_enabled?({ 'userIds' => ' 1 ,2, 123 ,200 ' }, unleash_context)).to be_truthy -- end -- -- it 'should be disabled with false params' do -- expect(strategy.is_enabled?({ 'userIds' => '1,2' }, unleash_context)).to be_falsey -- end -- end -- end --end -diff --git a/spec/unleash/strategy/util_spec.rb b/spec/unleash/strategy/util_spec.rb -deleted file mode 100644 -index 234b160..0000000 ---- a/spec/unleash/strategy/util_spec.rb -+++ /dev/null -@@ -1,10 +0,0 @@ --require "unleash/strategy/util" -- --RSpec.describe Unleash::Strategy::Util do -- describe '.get_normalized_number' do -- it "returns correct values" do -- expect(Unleash::Strategy::Util.get_normalized_number('123', 'gr1')).to eq(73) -- expect(Unleash::Strategy::Util.get_normalized_number('999', 'groupX')).to eq(25) -- end -- end --end -diff --git a/spec/unleash/toggle_fetcher_spec.rb b/spec/unleash/toggle_fetcher_spec.rb -index 3821051..97ae8cd 100644 ---- a/spec/unleash/toggle_fetcher_spec.rb -+++ b/spec/unleash/toggle_fetcher_spec.rb -@@ -55,25 +55,24 @@ RSpec.describe Unleash::ToggleFetcher do - end - - describe '#save!' do -- context 'when toggle_cache generation fails' do -- before do -- allow(toggle_fetcher).to receive(:toggle_cache).and_raise(StandardError) -- end -- -- it 'swallows the error' do -- expect { toggle_fetcher.save! }.not_to raise_error -- end -- end -- - context 'when toggle_cache with content is saved' do -- before do -- toggle_fetcher.toggle_cache = { features: [] } -- end -- - it 'creates a file with toggle_cache in JSON' do -- toggle_fetcher.save! -+ toggles = { -+ version: 2, -+ features: [ -+ { -+ name: "Feature.A", -+ description: "Enabled toggle", -+ enabled: true, -+ strategies: [{ -+ "name": "default" -+ }] -+ }, -+ ] -+ } -+ toggle_fetcher.save! toggles.to_json - expect(File.exist?(Unleash.configuration.backup_file)).to eq(true) -- expect(File.read(Unleash.configuration.backup_file)).to eq('{"features":[]}') -+ expect(File.read(Unleash.configuration.backup_file)).to eq('{"version":2,"features":[{"name":"Feature.A","description":"Enabled toggle","enabled":true,"strategies":[{"name":"default"}]}]}') - end - end - end -@@ -83,47 +82,28 @@ RSpec.describe Unleash::ToggleFetcher do - before do - # manually create a stub cache on disk, so we can test that we read it correctly later. - cache_creator = described_class.new -- cache_creator.toggle_cache = { features: [] } -- cache_creator.save! -- -- WebMock.stub_request(:get, "http://toggle-fetcher-test-url/client/features").to_return(status: 500) -- end -+ toggles = { -+ version: 2, -+ features: [ -+ { -+ name: "Feature.A", -+ description: "Enabled toggle", -+ enabled: true, -+ strategies: [{ -+ "name": "default" -+ }] -+ }, -+ ] -+ } - -- it 'reads the backup file for values' do -- expect(toggle_fetcher.toggle_cache).to eq("features" => []) -- end -- end -+ cache_creator.save! toggles.to_json - -- context 'when backup file does not exist' do -- before do -- File.delete(Unleash.configuration.backup_file) if File.exist?(Unleash.configuration.backup_file) - WebMock.stub_request(:get, "http://toggle-fetcher-test-url/client/features").to_return(status: 500) - end - -- it 'returns an empty toggle_cache' do -- expect(toggle_fetcher.toggle_cache).to eq(nil) -- end -- end -- -- context 'segments are present' do -- it 'loads a segement map correctly' do -- expect(toggle_fetcher.toggle_cache["segments"].count).to eq 1 -- end -- end -- -- context 'segments are not present' do -- before do -- WebMock.stub_request(:get, "http://toggle-fetcher-test-url/client/features") -- .to_return(status: 200, -- body: { -- "version": 1, -- "features": [] -- }.to_json, -- headers: {}) -- end -- -- it 'loads an empty segment map' do -- expect(toggle_fetcher.toggle_cache["segments"].count).to eq 0 -+ it 'reads the backup file for values' do -+ enabled = Unleash.engine.enabled?('Feature.A', {}) -+ expect(enabled).to eq(true) - end - end - end From 5797508a148c52dafdbf17e39829a6e10b070b95 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Fri, 3 Nov 2023 11:43:01 +0200 Subject: [PATCH 04/35] wip: implement custom strategies, this definitely needs a rebase --- lib/unleash.rb | 3 +- lib/unleash/client.rb | 13 ++++--- lib/unleash/configuration.rb | 2 +- lib/unleash/strategies.rb | 14 +++++++ lib/unleash/variant.rb | 2 +- spec/unleash/client_spec.rb | 46 +++++++++++++++++++++++ spec/unleash/client_specification_spec.rb | 5 +-- spec/unleash_spec.rb | 23 ++++++++++++ 8 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 lib/unleash/strategies.rb diff --git a/lib/unleash.rb b/lib/unleash.rb index 260cf40e..e4ae10a8 100644 --- a/lib/unleash.rb +++ b/lib/unleash.rb @@ -1,5 +1,6 @@ require 'unleash/version' require 'unleash/configuration' +require 'unleash/strategies' require 'unleash/context' require 'unleash/client' require 'logger' @@ -25,6 +26,6 @@ def self.configure end def self.strategies - nil + self.configuration.strategies end end diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 4b53300f..32a246d5 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -18,6 +18,7 @@ def initialize(*opts) Unleash.logger = Unleash.configuration.logger.clone Unleash.logger.level = Unleash.configuration.log_level Unleash.engine = UnleashEngine.new + Unleash.engine.register_custom_strategies(Unleash.configuration.strategies.strategies) Unleash.toggle_fetcher = Unleash::ToggleFetcher.new if Unleash.configuration.disable_client @@ -84,12 +85,14 @@ def get_variant(feature, context = Unleash::Context.new, fallback_variant = disa Unleash&.engine&.count_toggle(feature, toggle_enabled) end - variant_response = Unleash&.engine.get_variant(feature, context) - if variant_response.code < 0 - Unleash&.engine&.count_variant(feature, fallback_variant.name) - return fallback_variant + resolved_variant = Unleash&.engine.get_variant(feature, context) + puts "Got my variant #{resolved_variant}" + if !resolved_variant.nil? + resolved_variant = Variant.new(resolved_variant) end - variant = variant_response.variant + + variant = resolved_variant || fallback_variant + Unleash&.engine&.count_variant(feature, variant.name) variant diff --git a/lib/unleash/configuration.rb b/lib/unleash/configuration.rb index dddd90f7..5321c917 100644 --- a/lib/unleash/configuration.rb +++ b/lib/unleash/configuration.rb @@ -96,7 +96,7 @@ def set_defaults self.backup_file = nil self.log_level = Logger::WARN self.bootstrap_config = nil - self.strategies = nil + self.strategies = Unleash::Strategies.new self.custom_http_headers = {} end diff --git a/lib/unleash/strategies.rb b/lib/unleash/strategies.rb new file mode 100644 index 00000000..ec7e4e0f --- /dev/null +++ b/lib/unleash/strategies.rb @@ -0,0 +1,14 @@ +module Unleash + class Strategies + + attr_accessor :strategies + + def initialize + @strategies = [] + end + + def register(strategy) + @strategies << strategy + end + end +end \ No newline at end of file diff --git a/lib/unleash/variant.rb b/lib/unleash/variant.rb index 6379ac7d..bf6f09b3 100644 --- a/lib/unleash/variant.rb +++ b/lib/unleash/variant.rb @@ -8,7 +8,7 @@ def initialize(params = {}) self.name = params.values_at('name', :name).compact.first self.enabled = params.values_at('enabled', :enabled).compact.first || false self.payload = params.values_at('payload', :payload).compact.first - + puts "I HATE YOU RUBY #{params[:payload]}" raise ArgumentError, "Variant requires a name." if self.name.nil? end diff --git a/spec/unleash/client_spec.rb b/spec/unleash/client_spec.rb index f98032fa..622ea529 100644 --- a/spec/unleash/client_spec.rb +++ b/spec/unleash/client_spec.rb @@ -628,4 +628,50 @@ end end end + + it "should use custom strategies during evaluation" do + bootstrap_values = '{ + "version": 1, + "features": [ + { + "name": "featureX", + "enabled": true, + "strategies": [{ "name": "customStrategy" }] + } + ] + }' + + class TestStrategy + attr_reader :name + + def initialize(name) + @name = name + end + + def enabled?(params, context) + puts "OKAY I'm CALLING THE HOOK" + context[:userId] == "123" + end + end + + Unleash.configure do |config| + config.app_name = 'my-test-app' + config.instance_id = 'rspec/test' + config.disable_client = true + config.disable_metrics = true + config.bootstrap_config = Unleash::Bootstrap::Configuration.new({ 'data' => bootstrap_values }) + config.strategies.register(TestStrategy.new('customStrategy')) + end + + context_params = { + user_id: '123', + } + unleash_context = Unleash::Context.new(context_params) + + unleash_client = Unleash::Client.new + expect( + unleash_client.is_enabled?('featureX', unleash_context) + ).to be false + + end end diff --git a/spec/unleash/client_specification_spec.rb b/spec/unleash/client_specification_spec.rb index a45ffbe2..5839d3ef 100644 --- a/spec/unleash/client_specification_spec.rb +++ b/spec/unleash/client_specification_spec.rb @@ -52,11 +52,8 @@ bootstrap_config: Unleash::Bootstrap::Configuration.new(data: current_test_set.fetch('state', {}).to_json) ) variant = unleash.get_variant(test.fetch('toggleName'), context) - expectedResult = test['expectedResult'] - expect(variant.name).to eq(expectedResult['name']) - expect(variant.enabled).to eq(expectedResult['enabled']) - expect(variant.payload).to eq(expectedResult['payload']) + expect(variant).to eq(Unleash::Variant.new(test['expectedResult'])) end end end diff --git a/spec/unleash_spec.rb b/spec/unleash_spec.rb index 7ccbe3ec..5e5d63a1 100644 --- a/spec/unleash_spec.rb +++ b/spec/unleash_spec.rb @@ -23,4 +23,27 @@ expect(described_class.strategies).to eq(Unleash.configuration.strategies) end end + + it "mount custom strategies correctly" do + class TestStrategy + attr_reader :name + + def initialize(name) + @name = name + end + + def enabled?(params, context) + params["gerkhins"] == "yes" + end + end + + Unleash.configure do |config| + config.app_name = 'rspec_test' + config.strategies.register(TestStrategy.new) + end + + custom_strategy = Unleash.configuration.strategies.strategies.find { |strategy| strategy.name == 'customStrategy' } + + expect(custom_strategy).to be_instance_of(CustomStrategy) + end end From 4b3a598dfde9d9047b094d6edccb428ea99c4c68 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Fri, 10 Nov 2023 09:39:33 +0200 Subject: [PATCH 05/35] green tests! --- lib/unleash/client.rb | 2 +- lib/unleash/toggle_fetcher.rb | 44 ++++++++--------------------- lib/unleash/variant.rb | 1 - spec/unleash/client_spec.rb | 12 ++++---- spec/unleash/toggle_fetcher_spec.rb | 23 +++++++++------ spec/unleash_spec.rb | 8 +++--- 6 files changed, 38 insertions(+), 52 deletions(-) diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 32a246d5..225feb92 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -20,7 +20,7 @@ def initialize(*opts) Unleash.engine = UnleashEngine.new Unleash.engine.register_custom_strategies(Unleash.configuration.strategies.strategies) - Unleash.toggle_fetcher = Unleash::ToggleFetcher.new + Unleash.toggle_fetcher = Unleash::ToggleFetcher.new Unleash.engine if Unleash.configuration.disable_client Unleash.logger.warn "Unleash::Client is disabled! Will only return default (or bootstrapped if available) results!" Unleash.logger.warn "Unleash::Client is disabled! Metrics and MetricsReporter are also disabled!" diff --git a/lib/unleash/toggle_fetcher.rb b/lib/unleash/toggle_fetcher.rb index 41361ef3..eca2e88a 100755 --- a/lib/unleash/toggle_fetcher.rb +++ b/lib/unleash/toggle_fetcher.rb @@ -8,7 +8,8 @@ module Unleash class ToggleFetcher attr_accessor :toggle_engine, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache - def initialize + def initialize(engine) + self.toggle_engine = engine self.etag = nil self.segment_cache = nil self.toggle_lock = Mutex.new @@ -55,10 +56,9 @@ def fetch end self.etag = response['ETag'] - engine = get_engine(response.body) # always synchronize with the local cache when fetching: - synchronize_with_local_cache!(engine) + synchronize_with_local_cache!(response.body) update_running_client! save! response.body @@ -84,15 +84,13 @@ def save!(toggle_data) private - def synchronize_with_local_cache!(engine) - if self.toggle_engine != engine - self.toggle_lock.synchronize do - self.toggle_engine = engine - end - - # notify all threads waiting for this resource to no longer wait - self.toggle_resource.broadcast + def synchronize_with_local_cache!(toggle_data) + self.toggle_lock.synchronize do + self.toggle_engine.take_state(toggle_data) end + + # notify all threads waiting for this resource to no longer wait + self.toggle_resource.broadcast end def update_running_client! @@ -105,9 +103,8 @@ def read! Unleash.logger.debug "read!()" backup_file = Unleash.configuration.backup_file return nil unless File.exist?(backup_file) - - backup_as_hash = JSON.parse(File.read(backup_file)) - synchronize_with_local_cache!(backup_as_hash) + backup_data = File.read(backup_file) + synchronize_with_local_cache!(backup_data) update_running_client! rescue IOError => e Unleash.logger.error "Unable to read the backup_file: #{e}" @@ -119,7 +116,7 @@ def read! def bootstrap bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles - synchronize_with_local_cache! get_engine bootstrap_payload + synchronize_with_local_cache! bootstrap_payload update_running_client! # reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again @@ -131,22 +128,5 @@ def build_segment_map(segments_array) segments_array.map{ |segment| [segment["id"], segment] }.to_h end - - def get_engine(response_body) - engine = UnleashEngine.new - engine.take_state(response_body) - engine - end - - # @param response_body [String] - def get_features(response_body) - response_hash = JSON.parse(response_body) - if response_hash['version'] >= 1 - return { "features" => response_hash["features"], "segments" => build_segment_map(response_hash["segments"]) } - end - - raise NotImplemented, "Version of features provided by unleash server" \ - " is unsupported by this client." - end end end diff --git a/lib/unleash/variant.rb b/lib/unleash/variant.rb index bf6f09b3..3bb5c530 100644 --- a/lib/unleash/variant.rb +++ b/lib/unleash/variant.rb @@ -8,7 +8,6 @@ def initialize(params = {}) self.name = params.values_at('name', :name).compact.first self.enabled = params.values_at('enabled', :enabled).compact.first || false self.payload = params.values_at('payload', :payload).compact.first - puts "I HATE YOU RUBY #{params[:payload]}" raise ArgumentError, "Variant requires a name." if self.name.nil? end diff --git a/spec/unleash/client_spec.rb b/spec/unleash/client_spec.rb index 622ea529..39c70780 100644 --- a/spec/unleash/client_spec.rb +++ b/spec/unleash/client_spec.rb @@ -648,9 +648,8 @@ def initialize(name) @name = name end - def enabled?(params, context) - puts "OKAY I'm CALLING THE HOOK" - context[:userId] == "123" + def enabled?(_params, context) + context.user_id == "123" end end @@ -664,14 +663,17 @@ def enabled?(params, context) end context_params = { - user_id: '123', + user_id: '123' } unleash_context = Unleash::Context.new(context_params) unleash_client = Unleash::Client.new expect( unleash_client.is_enabled?('featureX', unleash_context) - ).to be false + ).to be true + expect( + unleash_client.is_enabled?('featureX', Unleash::Context.new({})) + ).to be false end end diff --git a/spec/unleash/toggle_fetcher_spec.rb b/spec/unleash/toggle_fetcher_spec.rb index 97ae8cdc..191cacbc 100644 --- a/spec/unleash/toggle_fetcher_spec.rb +++ b/spec/unleash/toggle_fetcher_spec.rb @@ -1,5 +1,5 @@ RSpec.describe Unleash::ToggleFetcher do - subject(:toggle_fetcher) { Unleash::ToggleFetcher.new } + subject(:toggle_fetcher) { Unleash::ToggleFetcher.new UnleashEngine.new } before do Unleash.configure do |config| @@ -65,9 +65,9 @@ description: "Enabled toggle", enabled: true, strategies: [{ - "name": "default" + "name": "default" }] - }, + } ] } toggle_fetcher.save! toggles.to_json @@ -78,10 +78,11 @@ end describe '.new' do + let(:engine) { UnleashEngine.new } context 'when there are problems fetching toggles' do before do - # manually create a stub cache on disk, so we can test that we read it correctly later. - cache_creator = described_class.new + backup_file = Unleash.configuration.backup_file + toggles = { version: 2, features: [ @@ -90,19 +91,23 @@ description: "Enabled toggle", enabled: true, strategies: [{ - "name": "default" + "name": "default" }] - }, + } ] } - cache_creator.save! toggles.to_json + # manually create a stub cache on disk, so we can test that we read it correctly later. + File.open(backup_file, "w") do |file| + file.write(toggles.to_json) + end WebMock.stub_request(:get, "http://toggle-fetcher-test-url/client/features").to_return(status: 500) + _toggle_fetcher = described_class.new engine # we new up a new toggle fetcher so that engine is synced end it 'reads the backup file for values' do - enabled = Unleash.engine.enabled?('Feature.A', {}) + enabled = engine.enabled?('Feature.A', {}) expect(enabled).to eq(true) end end diff --git a/spec/unleash_spec.rb b/spec/unleash_spec.rb index 5e5d63a1..738fdcd8 100644 --- a/spec/unleash_spec.rb +++ b/spec/unleash_spec.rb @@ -24,7 +24,7 @@ end end - it "mount custom strategies correctly" do + it "should mount custom strategies correctly" do class TestStrategy attr_reader :name @@ -32,18 +32,18 @@ def initialize(name) @name = name end - def enabled?(params, context) + def enabled?(params, _context) params["gerkhins"] == "yes" end end Unleash.configure do |config| config.app_name = 'rspec_test' - config.strategies.register(TestStrategy.new) + config.strategies.register(TestStrategy.new("customStrategy")) end custom_strategy = Unleash.configuration.strategies.strategies.find { |strategy| strategy.name == 'customStrategy' } - expect(custom_strategy).to be_instance_of(CustomStrategy) + expect(custom_strategy).to be_instance_of(TestStrategy) end end From 211fcdf0240fa17fe79172e99026e3021bb100ab Mon Sep 17 00:00:00 2001 From: sighphyre Date: Wed, 15 Nov 2023 12:20:32 +0200 Subject: [PATCH 06/35] chore: use published package rather than a symlink --- Gemfile | 2 +- lib/unleash/client.rb | 2 +- lib/unleash/toggle_fetcher.rb | 2 +- spec/unleash/metrics_reporter_spec.rb | 6 +++--- spec/unleash/toggle_fetcher_spec.rb | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 8d75e93f..feb83754 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'unleash-engine', path: '../../yggdrasil/ruby-engine' +gem 'yggdrasil-engine', '~> 0.0.2' # Specify your gem's dependencies in unleash-client.gemspec gemspec diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 225feb92..84ccb881 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -17,7 +17,7 @@ def initialize(*opts) Unleash.logger = Unleash.configuration.logger.clone Unleash.logger.level = Unleash.configuration.log_level - Unleash.engine = UnleashEngine.new + Unleash.engine = YggdrasilEngine.new Unleash.engine.register_custom_strategies(Unleash.configuration.strategies.strategies) Unleash.toggle_fetcher = Unleash::ToggleFetcher.new Unleash.engine diff --git a/lib/unleash/toggle_fetcher.rb b/lib/unleash/toggle_fetcher.rb index eca2e88a..bc580b4a 100755 --- a/lib/unleash/toggle_fetcher.rb +++ b/lib/unleash/toggle_fetcher.rb @@ -2,7 +2,7 @@ require 'unleash/bootstrap/handler' require 'net/http' require 'json' -require 'unleash_engine' +require 'yggdrasil_engine' module Unleash class ToggleFetcher diff --git a/spec/unleash/metrics_reporter_spec.rb b/spec/unleash/metrics_reporter_spec.rb index 858a0fb1..0a79e9f9 100644 --- a/spec/unleash/metrics_reporter_spec.rb +++ b/spec/unleash/metrics_reporter_spec.rb @@ -27,7 +27,7 @@ config.instance_id = 'rspec/test' config.disable_client = true end - Unleash.engine = UnleashEngine.new + Unleash.engine = YggdrasilEngine.new Unleash.engine.count_toggle('featureA', true) Unleash.engine.count_toggle('featureA', true) @@ -76,7 +76,7 @@ ) .to_return(status: 200, body: "", headers: {}) - Unleash.engine = UnleashEngine.new + Unleash.engine = YggdrasilEngine.new Unleash.engine.count_toggle('featureA', true) Unleash.engine.count_toggle('featureA', true) @@ -107,7 +107,7 @@ end it "does not send a report, if there were no metrics registered/evaluated" do - Unleash.engine = UnleashEngine.new + Unleash.engine = YggdrasilEngine.new metrics_reporter.post diff --git a/spec/unleash/toggle_fetcher_spec.rb b/spec/unleash/toggle_fetcher_spec.rb index 191cacbc..8d6fbd82 100644 --- a/spec/unleash/toggle_fetcher_spec.rb +++ b/spec/unleash/toggle_fetcher_spec.rb @@ -1,5 +1,5 @@ RSpec.describe Unleash::ToggleFetcher do - subject(:toggle_fetcher) { Unleash::ToggleFetcher.new UnleashEngine.new } + subject(:toggle_fetcher) { Unleash::ToggleFetcher.new YggdrasilEngine.new } before do Unleash.configure do |config| @@ -78,7 +78,7 @@ end describe '.new' do - let(:engine) { UnleashEngine.new } + let(:engine) { YggdrasilEngine.new } context 'when there are problems fetching toggles' do before do backup_file = Unleash.configuration.backup_file From a0707def3a5aafd72ee180d62897b33496c37d60 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Wed, 15 Nov 2023 21:44:34 +0200 Subject: [PATCH 07/35] wip: trim some unneeded strings and apply format --- lib/unleash/client.rb | 3 ++- lib/unleash/metrics_reporter.rb | 3 +-- lib/unleash/strategies.rb | 19 +++++++++---------- lib/unleash/toggle_fetcher.rb | 1 + 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 84ccb881..ab940fdc 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -86,7 +86,6 @@ def get_variant(feature, context = Unleash::Context.new, fallback_variant = disa end resolved_variant = Unleash&.engine.get_variant(feature, context) - puts "Got my variant #{resolved_variant}" if !resolved_variant.nil? resolved_variant = Variant.new(resolved_variant) end @@ -95,6 +94,8 @@ def get_variant(feature, context = Unleash::Context.new, fallback_variant = disa Unleash&.engine&.count_variant(feature, variant.name) + # TODO: Add to README: name, payload, enabled (bool) + variant end diff --git a/lib/unleash/metrics_reporter.rb b/lib/unleash/metrics_reporter.rb index 4cd4340b..70bfb52a 100755 --- a/lib/unleash/metrics_reporter.rb +++ b/lib/unleash/metrics_reporter.rb @@ -14,12 +14,11 @@ def initialize end def generate_report - puts "Making report" metrics = Unleash&.engine&.get_metrics() if metrics.nil? || metrics.empty? - puts "nothing here" return nil end + report = { 'appName': Unleash.configuration.app_name, 'instanceId': Unleash.configuration.instance_id, diff --git a/lib/unleash/strategies.rb b/lib/unleash/strategies.rb index ec7e4e0f..7e7778e2 100644 --- a/lib/unleash/strategies.rb +++ b/lib/unleash/strategies.rb @@ -1,14 +1,13 @@ module Unleash - class Strategies + class Strategies + attr_accessor :strategies - attr_accessor :strategies - - def initialize - @strategies = [] - end + def initialize + @strategies = [] + end - def register(strategy) - @strategies << strategy - end + def register(strategy) + @strategies << strategy end -end \ No newline at end of file + end +end diff --git a/lib/unleash/toggle_fetcher.rb b/lib/unleash/toggle_fetcher.rb index bc580b4a..3e82f4e7 100755 --- a/lib/unleash/toggle_fetcher.rb +++ b/lib/unleash/toggle_fetcher.rb @@ -103,6 +103,7 @@ def read! Unleash.logger.debug "read!()" backup_file = Unleash.configuration.backup_file return nil unless File.exist?(backup_file) + backup_data = File.read(backup_file) synchronize_with_local_cache!(backup_data) update_running_client! From c39fe95d2555aee4f17b43451812cc53cbd6f6a8 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Wed, 15 Nov 2023 22:10:17 +0200 Subject: [PATCH 08/35] wip: trim some unneeded nil checks --- lib/unleash/client.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index ab940fdc..d7936307 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -44,14 +44,14 @@ def is_enabled?(feature, context = nil, default_value_param = false, &fallback_b Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}" - toggle_enabled = Unleash&.engine&.enabled?(feature, context) + toggle_enabled = Unleash.engine.enabled?(feature, context) if toggle_enabled.nil? Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found" - Unleash&.engine&.count_toggle(feature, false) + Unleash.engine.count_toggle(feature, false) return default_value end - Unleash&.engine&.count_toggle(feature, toggle_enabled) + Unleash.engine&.count_toggle(feature, toggle_enabled) toggle_enabled end From 5df50673b7f51e31a02117d318a4353b82ad3c2e Mon Sep 17 00:00:00 2001 From: sighphyre Date: Wed, 15 Nov 2023 22:19:03 +0200 Subject: [PATCH 09/35] wip: move default_variant to Variant --- lib/unleash/variant.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/unleash/variant.rb b/lib/unleash/variant.rb index 3bb5c530..d6884ed5 100644 --- a/lib/unleash/variant.rb +++ b/lib/unleash/variant.rb @@ -18,5 +18,9 @@ def to_s def ==(other) self.name == other.name && self.enabled == other.enabled && self.payload == other.payload end + + def self.disabled_variant + Variant.new(name: 'disabled', enabled: false) + end end end From f6071b9868f0ca608642b7390ceee61f9f95c03c Mon Sep 17 00:00:00 2001 From: sighphyre Date: Wed, 15 Nov 2023 22:23:48 +0200 Subject: [PATCH 10/35] wip: more null checks removed --- lib/unleash/client.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index d7936307..582aab1e 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -51,7 +51,7 @@ def is_enabled?(feature, context = nil, default_value_param = false, &fallback_b return default_value end - Unleash.engine&.count_toggle(feature, toggle_enabled) + Unleash.engine.count_toggle(feature, toggle_enabled) toggle_enabled end @@ -78,21 +78,21 @@ def if_disabled(feature, context = nil, default_value = true, &blk) def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant) Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}" - toggle_enabled = Unleash&.engine&.enabled?(feature, context) + toggle_enabled = Unleash.engine.enabled?(feature, context) if toggle_enabled.nil? - Unleash&.engine&.count_toggle(feature, false) + Unleash.engine.count_toggle(feature, false) else - Unleash&.engine&.count_toggle(feature, toggle_enabled) + Unleash.engine.count_toggle(feature, toggle_enabled) end - resolved_variant = Unleash&.engine.get_variant(feature, context) + resolved_variant = Unleash.engine.get_variant(feature, context) if !resolved_variant.nil? resolved_variant = Variant.new(resolved_variant) end variant = resolved_variant || fallback_variant - Unleash&.engine&.count_variant(feature, variant.name) + Unleash.engine.count_variant(feature, variant.name) # TODO: Add to README: name, payload, enabled (bool) From 88b46c7ff88ed9815f507361ef3ab98ae3e6c13b Mon Sep 17 00:00:00 2001 From: sighphyre Date: Wed, 15 Nov 2023 22:32:02 +0200 Subject: [PATCH 11/35] wip: make internal public api for custom strategies match existing api --- lib/unleash/strategies.rb | 2 +- spec/unleash/client_spec.rb | 2 +- spec/unleash_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/unleash/strategies.rb b/lib/unleash/strategies.rb index 7e7778e2..3365cf47 100644 --- a/lib/unleash/strategies.rb +++ b/lib/unleash/strategies.rb @@ -6,7 +6,7 @@ def initialize @strategies = [] end - def register(strategy) + def add(strategy) @strategies << strategy end end diff --git a/spec/unleash/client_spec.rb b/spec/unleash/client_spec.rb index 39c70780..925108db 100644 --- a/spec/unleash/client_spec.rb +++ b/spec/unleash/client_spec.rb @@ -659,7 +659,7 @@ def enabled?(_params, context) config.disable_client = true config.disable_metrics = true config.bootstrap_config = Unleash::Bootstrap::Configuration.new({ 'data' => bootstrap_values }) - config.strategies.register(TestStrategy.new('customStrategy')) + config.strategies.add(TestStrategy.new('customStrategy')) end context_params = { diff --git a/spec/unleash_spec.rb b/spec/unleash_spec.rb index 738fdcd8..f05a457f 100644 --- a/spec/unleash_spec.rb +++ b/spec/unleash_spec.rb @@ -39,7 +39,7 @@ def enabled?(params, _context) Unleash.configure do |config| config.app_name = 'rspec_test' - config.strategies.register(TestStrategy.new("customStrategy")) + config.strategies.add(TestStrategy.new("customStrategy")) end custom_strategy = Unleash.configuration.strategies.strategies.find { |strategy| strategy.name == 'customStrategy' } From 7f55e6bcc608f3dec5d9bde269ab90d276c9e036 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Wed, 15 Nov 2023 22:41:40 +0200 Subject: [PATCH 12/35] wip: remove class and references to FeatureToggle.default_variant --- lib/unleash/client.rb | 5 +++-- lib/unleash/feature_toggle.rb | 10 ---------- 2 files changed, 3 insertions(+), 12 deletions(-) delete mode 100644 lib/unleash/feature_toggle.rb diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 582aab1e..61e9b7e6 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -2,7 +2,8 @@ require 'unleash/toggle_fetcher' require 'unleash/metrics_reporter' require 'unleash/scheduled_executor' -require 'unleash/feature_toggle' +require 'unleash/variant' +require 'unleash/variant_definition' require 'unleash/util/http' require 'logger' require 'time' @@ -167,7 +168,7 @@ def register end def disabled_variant - @disabled_variant ||= Unleash::FeatureToggle.disabled_variant + @disabled_variant ||= Unleash::Variant.disabled_variant end def first_fetch_is_eager diff --git a/lib/unleash/feature_toggle.rb b/lib/unleash/feature_toggle.rb deleted file mode 100644 index 02020e54..00000000 --- a/lib/unleash/feature_toggle.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'unleash/variant_definition' -require 'unleash/variant' - -module Unleash - class FeatureToggle - def self.disabled_variant - Unleash::Variant.new(name: 'disabled', enabled: false) - end - end -end From 5851724d87bcc0428cf849c7392c369c11431a23 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Wed, 15 Nov 2023 22:44:34 +0200 Subject: [PATCH 13/35] wip: remove new log message --- lib/unleash/client.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 61e9b7e6..1a752b06 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -43,8 +43,6 @@ def is_enabled?(feature, context = nil, default_value_param = false, &fallback_b default_value_param end - Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}" - toggle_enabled = Unleash.engine.enabled?(feature, context) if toggle_enabled.nil? Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found" From 49925d55f8d627c050a3f058c8e7ee1c6fecfbd6 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Thu, 16 Nov 2023 00:13:32 +0200 Subject: [PATCH 14/35] wip: bring custom strategies closer to existing api --- lib/unleash/client.rb | 2 +- lib/unleash/strategies.rb | 20 ++++++++++++++++++-- spec/unleash_spec.rb | 11 ++++------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 1a752b06..86f58bba 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -19,7 +19,7 @@ def initialize(*opts) Unleash.logger = Unleash.configuration.logger.clone Unleash.logger.level = Unleash.configuration.log_level Unleash.engine = YggdrasilEngine.new - Unleash.engine.register_custom_strategies(Unleash.configuration.strategies.strategies) + Unleash.engine.register_custom_strategies(Unleash.configuration.strategies.custom_strategies) Unleash.toggle_fetcher = Unleash::ToggleFetcher.new Unleash.engine if Unleash.configuration.disable_client diff --git a/lib/unleash/strategies.rb b/lib/unleash/strategies.rb index 3365cf47..49796a49 100644 --- a/lib/unleash/strategies.rb +++ b/lib/unleash/strategies.rb @@ -1,13 +1,29 @@ module Unleash + class DefaultOverrideError < RuntimeError + end + class Strategies attr_accessor :strategies def initialize - @strategies = [] + @strategies = {} + end + + def includes?(name) + @strategies.has_key?(name.to_s) || DEFAULT_STRATEGIES.include?(name.to_s) end def add(strategy) - @strategies << strategy + raise DefaultOverrideError, "Cannot override a default strategy" if DEFAULT_STRATEGIES.include?(strategy.name) + + @strategies[strategy.name] = strategy + end + + def custom_strategies + @strategies.values end + + DEFAULT_STRATEGIES = ['applicationHostname', 'default', 'flexibleRollout', 'gradualRolloutRandom', 'gradualRolloutSessionId', + 'gradualRolloutUserId', 'remoteAddress', 'userWithId'].freeze end end diff --git a/spec/unleash_spec.rb b/spec/unleash_spec.rb index f05a457f..ed003de5 100644 --- a/spec/unleash_spec.rb +++ b/spec/unleash_spec.rb @@ -26,10 +26,8 @@ it "should mount custom strategies correctly" do class TestStrategy - attr_reader :name - - def initialize(name) - @name = name + def name + 'customStrategy' end def enabled?(params, _context) @@ -42,8 +40,7 @@ def enabled?(params, _context) config.strategies.add(TestStrategy.new("customStrategy")) end - custom_strategy = Unleash.configuration.strategies.strategies.find { |strategy| strategy.name == 'customStrategy' } - - expect(custom_strategy).to be_instance_of(TestStrategy) + expect(Unleash.configuration.strategies.includes?('customStrategy')).to eq(true) + expect(Unleash.configuration.strategies.includes?('nonExistingStrategy')).to eq(false) end end From 0116bef8a311b828c134c16e138bc85a632a5bf1 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Thu, 16 Nov 2023 00:53:35 +0200 Subject: [PATCH 15/35] wip: remove unused parameters and functions and take rubocop suggestion --- lib/unleash/client.rb | 4 +--- spec/unleash/client_specification_spec.rb | 5 ----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 86f58bba..14fbe9fe 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -85,9 +85,7 @@ def get_variant(feature, context = Unleash::Context.new, fallback_variant = disa end resolved_variant = Unleash.engine.get_variant(feature, context) - if !resolved_variant.nil? - resolved_variant = Variant.new(resolved_variant) - end + resolved_variant = Variant.new(resolved_variant) unless resolved_variant.nil? variant = resolved_variant || fallback_variant diff --git a/spec/unleash/client_specification_spec.rb b/spec/unleash/client_specification_spec.rb index 5839d3ef..07c926d1 100644 --- a/spec/unleash/client_specification_spec.rb +++ b/spec/unleash/client_specification_spec.rb @@ -21,14 +21,11 @@ context "with #{current_test_set.fetch('name')} " do tests = current_test_set.fetch('tests', []) - state = current_test_set.fetch('state', {}) tests.each do |test| it "test that #{test['description']}" do context = Unleash::Context.new(test['context']) unleash = Unleash::Client.new( - app_name: 'bootstrap-test', - instance_id: 'local-test-cli', disable_client: true, disable_metrics: true, bootstrap_config: Unleash::Bootstrap::Configuration.new(data: current_test_set.fetch('state', {}).to_json) @@ -45,8 +42,6 @@ context = Unleash::Context.new(test['context']) unleash = Unleash::Client.new( - app_name: 'bootstrap-test', - instance_id: 'local-test-cli', disable_client: true, disable_metrics: true, bootstrap_config: Unleash::Bootstrap::Configuration.new(data: current_test_set.fetch('state', {}).to_json) From 766d39245e183fa119bb6f5929d20ab8b143ac9c Mon Sep 17 00:00:00 2001 From: sighphyre Date: Thu, 16 Nov 2023 01:02:11 +0200 Subject: [PATCH 16/35] wip: remove :toggles, :toggle_metrics, and :segment_cache from Unleash namespace and remove usage of these throughout the bodebase --- lib/unleash.rb | 2 +- spec/unleash/client_spec.rb | 1 - spec/unleash/metrics_reporter_spec.rb | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/unleash.rb b/lib/unleash.rb index e4ae10a8..cf586d5c 100644 --- a/lib/unleash.rb +++ b/lib/unleash.rb @@ -9,7 +9,7 @@ module Unleash TIME_RESOLUTION = 3 class << self - attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :segment_cache, :logger, :engine + attr_accessor :configuration, :toggle_fetcher, :reporter, :logger, :engine end self.configuration = Unleash::Configuration.new diff --git a/spec/unleash/client_spec.rb b/spec/unleash/client_spec.rb index 925108db..5a378063 100644 --- a/spec/unleash/client_spec.rb +++ b/spec/unleash/client_spec.rb @@ -570,7 +570,6 @@ ) .to_return(status: 200, body: body, headers: {}) - Unleash.toggles = [] Unleash.configure do |config| config.url = 'http://test-url/' config.app_name = 'my-test-app' diff --git a/spec/unleash/metrics_reporter_spec.rb b/spec/unleash/metrics_reporter_spec.rb index 0a79e9f9..3259c52d 100644 --- a/spec/unleash/metrics_reporter_spec.rb +++ b/spec/unleash/metrics_reporter_spec.rb @@ -8,8 +8,6 @@ Unleash.logger = Unleash.configuration.logger Unleash.logger.level = Unleash.configuration.log_level # Unleash.logger.level = Logger::DEBUG - Unleash.toggles = [] - Unleash.toggle_metrics = {} Unleash.configuration.url = 'http://test-url/' Unleash.configuration.app_name = 'my-test-app' From 6530299c8d77b4129fe81fa88e81831108ef88f0 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Thu, 16 Nov 2023 01:29:04 +0200 Subject: [PATCH 17/35] wip: refactor get_variant method and restore logging --- lib/unleash/client.rb | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 14fbe9fe..cbafcc42 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -79,15 +79,22 @@ def get_variant(feature, context = Unleash::Context.new, fallback_variant = disa toggle_enabled = Unleash.engine.enabled?(feature, context) if toggle_enabled.nil? + Unleash.logger.debug "Unleash::Client.get_variant feature: #{feature} not found" Unleash.engine.count_toggle(feature, false) - else - Unleash.engine.count_toggle(feature, toggle_enabled) + return fallback_variant end - resolved_variant = Unleash.engine.get_variant(feature, context) - resolved_variant = Variant.new(resolved_variant) unless resolved_variant.nil? + Unleash.engine.count_toggle(feature, toggle_enabled) + + variant = Unleash.engine.get_variant(feature, context) + + if variant.nil? + Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found" + Unleash.engine.count_variant(feature, "disabled") + return fallback_variant + end - variant = resolved_variant || fallback_variant + variant = Variant.new(variant) Unleash.engine.count_variant(feature, variant.name) From c367b0b24035cb46d0c242a78d400ae597b5185f Mon Sep 17 00:00:00 2001 From: sighphyre Date: Thu, 16 Nov 2023 01:38:27 +0200 Subject: [PATCH 18/35] chore: apply some rubocop lints --- lib/unleash/client.rb | 2 ++ lib/unleash/metrics_reporter.rb | 10 ++-------- spec/unleash/metrics_reporter_spec.rb | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index cbafcc42..97372491 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -12,6 +12,7 @@ module Unleash class Client attr_accessor :fetcher_scheduled_executor, :metrics_scheduled_executor + # rubocop:disable Metrics/AbcSize def initialize(*opts) Unleash.configuration = Unleash::Configuration.new(*opts) unless opts.empty? Unleash.configuration.validate! @@ -33,6 +34,7 @@ def initialize(*opts) start_toggle_fetcher start_metrics unless Unleash.configuration.disable_metrics end + # rubocop:enable Metrics/AbcSize def is_enabled?(feature, context = nil, default_value_param = false, &fallback_blk) Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}" diff --git a/lib/unleash/metrics_reporter.rb b/lib/unleash/metrics_reporter.rb index 70bfb52a..3f7b5f30 100755 --- a/lib/unleash/metrics_reporter.rb +++ b/lib/unleash/metrics_reporter.rb @@ -15,17 +15,13 @@ def initialize def generate_report metrics = Unleash&.engine&.get_metrics() - if metrics.nil? || metrics.empty? - return nil - end + return nil if metrics.nil? || metrics.empty? - report = { + { 'appName': Unleash.configuration.app_name, 'instanceId': Unleash.configuration.instance_id, 'bucket': metrics } - - report end def post @@ -46,7 +42,5 @@ def post Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}." end end - - private end end diff --git a/spec/unleash/metrics_reporter_spec.rb b/spec/unleash/metrics_reporter_spec.rb index 3259c52d..4d64f7a8 100644 --- a/spec/unleash/metrics_reporter_spec.rb +++ b/spec/unleash/metrics_reporter_spec.rb @@ -36,12 +36,12 @@ report = metrics_reporter.generate_report expect(report[:bucket][:toggles]).to include( - :featureA => { + featureA: { no: 2, yes: 3, variants: {} }, - :featureB => { + featureB: { no: 0, yes: 1, variants: {} From 1cd34b2ad5e75b9aafad68f2f0295bc6f93231a6 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Thu, 16 Nov 2023 02:22:16 +0200 Subject: [PATCH 19/35] wip: minor refactor to toggle fethcer --- lib/unleash/toggle_fetcher.rb | 34 +++++------------------------ spec/unleash/toggle_fetcher_spec.rb | 27 ++++++++--------------- 2 files changed, 14 insertions(+), 47 deletions(-) diff --git a/lib/unleash/toggle_fetcher.rb b/lib/unleash/toggle_fetcher.rb index 3e82f4e7..12702a10 100755 --- a/lib/unleash/toggle_fetcher.rb +++ b/lib/unleash/toggle_fetcher.rb @@ -6,12 +6,11 @@ module Unleash class ToggleFetcher - attr_accessor :toggle_engine, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache + attr_accessor :toggle_engine, :toggle_lock, :toggle_resource, :etag, :retry_count def initialize(engine) self.toggle_engine = engine self.etag = nil - self.segment_cache = nil self.toggle_lock = Mutex.new self.toggle_resource = ConditionVariable.new self.retry_count = 0 @@ -33,14 +32,6 @@ def initialize(engine) # once initialized, somewhere else you will want to start a loop with fetch() end - def toggles - self.toggle_lock.synchronize do - # wait for resource, only if it is null - self.toggle_resource.wait(self.toggle_lock) if self.toggle_engine.nil? - return self.toggle_engine - end - end - # rename to refresh_from_server! ?? def fetch Unleash.logger.debug "fetch()" @@ -58,9 +49,8 @@ def fetch self.etag = response['ETag'] # always synchronize with the local cache when fetching: - synchronize_with_local_cache!(response.body) + update_engine_state!(response.body) - update_running_client! save! response.body end @@ -84,7 +74,7 @@ def save!(toggle_data) private - def synchronize_with_local_cache!(toggle_data) + def update_engine_state!(toggle_data) self.toggle_lock.synchronize do self.toggle_engine.take_state(toggle_data) end @@ -93,20 +83,13 @@ def synchronize_with_local_cache!(toggle_data) self.toggle_resource.broadcast end - def update_running_client! - if Unleash.engine != self.toggle_engine - Unleash.engine = self.toggle_engine - end - end - def read! Unleash.logger.debug "read!()" backup_file = Unleash.configuration.backup_file return nil unless File.exist?(backup_file) backup_data = File.read(backup_file) - synchronize_with_local_cache!(backup_data) - update_running_client! + update_engine_state!(backup_data) rescue IOError => e Unleash.logger.error "Unable to read the backup_file: #{e}" rescue JSON::ParserError => e @@ -117,17 +100,10 @@ def read! def bootstrap bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles - synchronize_with_local_cache! bootstrap_payload - update_running_client! + update_engine_state! bootstrap_payload # reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again Unleash.configuration.bootstrap_config = nil end - - def build_segment_map(segments_array) - return {} if segments_array.nil? - - segments_array.map{ |segment| [segment["id"], segment] }.to_h - end end end diff --git a/spec/unleash/toggle_fetcher_spec.rb b/spec/unleash/toggle_fetcher_spec.rb index 8d6fbd82..7e7b9aad 100644 --- a/spec/unleash/toggle_fetcher_spec.rb +++ b/spec/unleash/toggle_fetcher_spec.rb @@ -54,25 +54,16 @@ File.delete(Unleash.configuration.backup_file) if File.exist?(Unleash.configuration.backup_file) end - describe '#save!' do - context 'when toggle_cache with content is saved' do + describe '#fetch!' do + let(:engine) { YggdrasilEngine.new } + + context 'when fetching toggles succeds' do + before do + _toggle_fetcher = described_class.new engine + end it 'creates a file with toggle_cache in JSON' do - toggles = { - version: 2, - features: [ - { - name: "Feature.A", - description: "Enabled toggle", - enabled: true, - strategies: [{ - "name": "default" - }] - } - ] - } - toggle_fetcher.save! toggles.to_json - expect(File.exist?(Unleash.configuration.backup_file)).to eq(true) - expect(File.read(Unleash.configuration.backup_file)).to eq('{"version":2,"features":[{"name":"Feature.A","description":"Enabled toggle","enabled":true,"strategies":[{"name":"default"}]}]}') + backup_file = Unleash.configuration.backup_file + expect(File.exist?(backup_file)).to eq(true) end end end From 5b5bb206fb1a98d704a882df9da7bff91c6160ee Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Fri, 16 Feb 2024 10:18:02 +0200 Subject: [PATCH 20/35] wip: bump yggdrasil --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index feb83754..543bcdb4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'yggdrasil-engine', '~> 0.0.2' +gem 'yggdrasil-engine', '~> 0.0.3.pre.beta.3' # Specify your gem's dependencies in unleash-client.gemspec gemspec From 8f0c0388d3cc459d15c74a05cfd8e53846610f8f Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Mon, 19 Feb 2024 15:10:19 +0200 Subject: [PATCH 21/35] chore: bump ygg version --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 543bcdb4..afc1d3f3 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'yggdrasil-engine', '~> 0.0.3.pre.beta.3' +gem 'yggdrasil-engine', '~> 0.0.3.beta.12' # Specify your gem's dependencies in unleash-client.gemspec gemspec From 3122986b8f8d8d0295288ed5238f65762bba473f Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Tue, 20 Feb 2024 09:56:18 +0200 Subject: [PATCH 22/35] wip: include version of ygg that contains gem for jruby9.2 --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index afc1d3f3..0405ef82 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'yggdrasil-engine', '~> 0.0.3.beta.12' +gem 'yggdrasil-engine', "~> 0.0.5.beta.2" # Specify your gem's dependencies in unleash-client.gemspec gemspec From 3e44bc3dc28a60dd6f02222a82268f48a03e47f8 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Tue, 20 Feb 2024 11:44:04 +0200 Subject: [PATCH 23/35] chore: bump ygg to a version that should include macos universal artifacts --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 0405ef82..e531d03b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'yggdrasil-engine', "~> 0.0.5.beta.2" +gem 'yggdrasil-engine', "~> 0.0.5.beta.3" # Specify your gem's dependencies in unleash-client.gemspec gemspec From 05c83cedf049cb18d5f581209362a44184ed1c68 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Tue, 20 Feb 2024 23:47:08 +0200 Subject: [PATCH 24/35] chore: bump yggdrasil version --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index e531d03b..e0669e75 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'yggdrasil-engine', "~> 0.0.5.beta.3" +gem 'yggdrasil-engine', "~> 0.0.5.beta.8" # Specify your gem's dependencies in unleash-client.gemspec gemspec From e0d37af5109fd92aea4631d8876184c7f655594a Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Tue, 20 Feb 2024 23:59:09 +0200 Subject: [PATCH 25/35] chore: bump ygg again --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index e0669e75..e3619de4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'yggdrasil-engine', "~> 0.0.5.beta.8" +gem 'yggdrasil-engine', "~> 0.0.5.beta.9" # Specify your gem's dependencies in unleash-client.gemspec gemspec From 156356c53a07c5936ab58f894760545f81dbf299 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Wed, 21 Feb 2024 11:31:42 +0200 Subject: [PATCH 26/35] chore: exclude spec folder in coveralls --- spec/spec_helper.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5bbfcff0..86c567a7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,7 +13,9 @@ ] ) -SimpleCov.start +SimpleCov.start do + add_filter '/spec/' +end require "bundler/setup" require "unleash" From 9be1f29e16e4f078906376b895361d3b818e7bad Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Wed, 21 Feb 2024 12:01:21 +0200 Subject: [PATCH 27/35] chore: remove variant override, don't need it anymore --- lib/unleash/client.rb | 1 - lib/unleash/variant_definition.rb | 26 ---------------- lib/unleash/variant_override.rb | 44 --------------------------- spec/unleash/variant_override_spec.rb | 26 ---------------- 4 files changed, 97 deletions(-) delete mode 100644 lib/unleash/variant_definition.rb delete mode 100644 lib/unleash/variant_override.rb delete mode 100644 spec/unleash/variant_override_spec.rb diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 97372491..4ac840d2 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -3,7 +3,6 @@ require 'unleash/metrics_reporter' require 'unleash/scheduled_executor' require 'unleash/variant' -require 'unleash/variant_definition' require 'unleash/util/http' require 'logger' require 'time' diff --git a/lib/unleash/variant_definition.rb b/lib/unleash/variant_definition.rb deleted file mode 100644 index d9cecad0..00000000 --- a/lib/unleash/variant_definition.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'unleash/variant_override' - -module Unleash - class VariantDefinition - attr_accessor :name, :weight, :payload, :overrides, :stickiness - - def initialize(name, weight = 0, payload = nil, stickiness = nil, overrides = []) # rubocop:disable Metrics/ParameterLists - self.name = name - self.weight = weight - self.payload = payload - self.stickiness = stickiness - self.overrides = (overrides || []) - .select{ |v| v.is_a?(Hash) && v.has_key?('contextName') } - .map{ |v| VariantOverride.new(v.fetch('contextName', ''), v.fetch('values', [])) } || [] - end - - def override_matches_context?(context) - self.overrides.select{ |o| o.matches_context?(context) }.first - end - - def to_s - "" - end - end -end diff --git a/lib/unleash/variant_override.rb b/lib/unleash/variant_override.rb deleted file mode 100644 index bac429a2..00000000 --- a/lib/unleash/variant_override.rb +++ /dev/null @@ -1,44 +0,0 @@ -module Unleash - class VariantOverride - attr_accessor :context_name, :values - - def initialize(context_name, values = []) - self.context_name = context_name - self.values = values || [] - - validate - end - - def to_s - "" - end - - def matches_context?(context) - raise ArgumentError, 'context must be of class Unleash::Context' unless context.instance_of?(Unleash::Context) - - context_value = - case self.context_name - when 'userId' - context.user_id - when 'sessionId' - context.session_id - when 'remoteAddress' - context.remote_address - else - context.properties.fetch(self.context_name, nil) - end - - Unleash.logger.debug "VariantOverride: context_name: #{context_name} context_value: #{context_value}" - - self.values.include? context_value.to_s - end - - private - - def validate - raise ArgumentError, 'context_name must be a String' unless self.context_name.is_a?(String) - raise ArgumentError, 'values must be an Array of strings' unless self.values.is_a?(Array) \ - && self.values.reject{ |v| v.is_a?(String) }.empty? - end - end -end diff --git a/spec/unleash/variant_override_spec.rb b/spec/unleash/variant_override_spec.rb deleted file mode 100644 index d498b21d..00000000 --- a/spec/unleash/variant_override_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -RSpec.describe Unleash::VariantOverride do - context 'parameters correctly assigned in initialization' - - it 'should raise exception if instanciated with invalid parameters' do - expect{ Unleash::VariantOverride.new(name: 'userId', values: ['123', '61']) }.to raise_error(ArgumentError) - end - - describe 'Simple VariantOverride with userId parameter set' do - let(:variant_override) { Unleash::VariantOverride.new('userId', ['123', '61']) } - - it 'matching context should return true' do - context = Unleash::Context.new(user_id: '61') - expect(variant_override.matches_context?(context)).to be true - end - - it 'matching context should return true' do - context = Unleash::Context.new(user_id: '123') - expect(variant_override.matches_context?(context)).to be true - end - - it 'NOT matching context should return false' do - context = Unleash::Context.new(user_id: '0') - expect(variant_override.matches_context?(context)).to be false - end - end -end From 8e64f842c613a400cf5498394dd8560a02fedf00 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Wed, 21 Feb 2024 12:23:48 +0200 Subject: [PATCH 28/35] chore: disable some coverage that I don't think makes sense --- lib/unleash/metrics_reporter.rb | 2 ++ lib/unleash/toggle_fetcher.rb | 6 ++++++ lib/unleash/variant.rb | 2 ++ 3 files changed, 10 insertions(+) diff --git a/lib/unleash/metrics_reporter.rb b/lib/unleash/metrics_reporter.rb index 3f7b5f30..7d5b6d0c 100755 --- a/lib/unleash/metrics_reporter.rb +++ b/lib/unleash/metrics_reporter.rb @@ -39,7 +39,9 @@ def post if ['200', '202'].include? response.code Unleash.logger.debug "Report sent to unleash server successfully. Server responded with http code #{response.code}" else + # :nocov: Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}." + # :nocov: end end end diff --git a/lib/unleash/toggle_fetcher.rb b/lib/unleash/toggle_fetcher.rb index 12702a10..1cc0f4fa 100755 --- a/lib/unleash/toggle_fetcher.rb +++ b/lib/unleash/toggle_fetcher.rb @@ -91,11 +91,17 @@ def read! backup_data = File.read(backup_file) update_engine_state!(backup_data) rescue IOError => e + # :nocov: Unleash.logger.error "Unable to read the backup_file: #{e}" + # :nocov: rescue JSON::ParserError => e + # :nocov: Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}" + # :nocov: rescue StandardError => e + # :nocov: Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}" + # :nocov: end def bootstrap diff --git a/lib/unleash/variant.rb b/lib/unleash/variant.rb index d6884ed5..c2203c55 100644 --- a/lib/unleash/variant.rb +++ b/lib/unleash/variant.rb @@ -12,7 +12,9 @@ def initialize(params = {}) end def to_s + # :nocov: "" + # :nocov: end def ==(other) From cf89cc537de163ecdfa56d5378735fcc7f3b0083 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Wed, 21 Feb 2024 13:54:49 +0200 Subject: [PATCH 29/35] fix: make strategies report correct data shape during registration --- lib/unleash/client.rb | 2 +- lib/unleash/strategies.rb | 6 ++++++ lib/unleash/variant.rb | 1 + spec/unleash/client_specification_spec.rb | 1 - 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index 4ac840d2..dc455d89 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -128,7 +128,7 @@ def info 'appName': Unleash.configuration.app_name, 'instanceId': Unleash.configuration.instance_id, 'sdkVersion': "unleash-client-ruby:" + Unleash::VERSION, - 'strategies': nil, + 'strategies': Unleash.strategies.known_strategies, 'started': Time.now.iso8601(Unleash::TIME_RESOLUTION), 'interval': Unleash.configuration.metrics_interval_in_millis } diff --git a/lib/unleash/strategies.rb b/lib/unleash/strategies.rb index 49796a49..b2b2c558 100644 --- a/lib/unleash/strategies.rb +++ b/lib/unleash/strategies.rb @@ -23,6 +23,12 @@ def custom_strategies @strategies.values end + def known_strategies + @strategies.keys.map { |key| { name: key } } + end + + private + DEFAULT_STRATEGIES = ['applicationHostname', 'default', 'flexibleRollout', 'gradualRolloutRandom', 'gradualRolloutSessionId', 'gradualRolloutUserId', 'remoteAddress', 'userWithId'].freeze end diff --git a/lib/unleash/variant.rb b/lib/unleash/variant.rb index c2203c55..b5eb45c1 100644 --- a/lib/unleash/variant.rb +++ b/lib/unleash/variant.rb @@ -8,6 +8,7 @@ def initialize(params = {}) self.name = params.values_at('name', :name).compact.first self.enabled = params.values_at('enabled', :enabled).compact.first || false self.payload = params.values_at('payload', :payload).compact.first + raise ArgumentError, "Variant requires a name." if self.name.nil? end diff --git a/spec/unleash/client_specification_spec.rb b/spec/unleash/client_specification_spec.rb index 07c926d1..8dc66604 100644 --- a/spec/unleash/client_specification_spec.rb +++ b/spec/unleash/client_specification_spec.rb @@ -18,7 +18,6 @@ JSON.parse(File.read(SPECIFICATION_PATH + '/index.json')).each do |test_file| describe "for #{test_file}" do current_test_set = JSON.parse(File.read(SPECIFICATION_PATH + '/' + test_file)) - context "with #{current_test_set.fetch('name')} " do tests = current_test_set.fetch('tests', []) tests.each do |test| From 9dc382ef576e29ab66c63bac18f2768814995a9f Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Wed, 21 Feb 2024 14:11:35 +0200 Subject: [PATCH 30/35] chore: clean up gem dependency --- Gemfile | 2 -- lib/unleash/strategies.rb | 4 +--- unleash-client.gemspec | 1 + 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index e3619de4..17fb50c6 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,4 @@ source 'https://rubygems.org' -gem 'yggdrasil-engine', "~> 0.0.5.beta.9" - # Specify your gem's dependencies in unleash-client.gemspec gemspec diff --git a/lib/unleash/strategies.rb b/lib/unleash/strategies.rb index b2b2c558..a18d9311 100644 --- a/lib/unleash/strategies.rb +++ b/lib/unleash/strategies.rb @@ -24,11 +24,9 @@ def custom_strategies end def known_strategies - @strategies.keys.map { |key| { name: key } } + @strategies.keys.map{ |key| { name: key } } end - private - DEFAULT_STRATEGIES = ['applicationHostname', 'default', 'flexibleRollout', 'gradualRolloutRandom', 'gradualRolloutSessionId', 'gradualRolloutUserId', 'remoteAddress', 'userWithId'].freeze end diff --git a/unleash-client.gemspec b/unleash-client.gemspec index ec156341..ff7718a3 100644 --- a/unleash-client.gemspec +++ b/unleash-client.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.5" spec.add_dependency "murmurhash3", "~> 0.1.7" + spec.add_dependency "yggdrasil-engine", "~> 0.0.5.beta.9" spec.add_development_dependency "bundler", "~> 2.1" spec.add_development_dependency "rake", "~> 12.3" From 950eb8a0f400c90fd98169e8a06458b82bb44895 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Tue, 30 Jul 2024 11:40:50 +0200 Subject: [PATCH 31/35] chore: no longer save on shutdown, there's no point in doing that --- lib/unleash/client.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/unleash/client.rb b/lib/unleash/client.rb index dc455d89..af88d9c8 100644 --- a/lib/unleash/client.rb +++ b/lib/unleash/client.rb @@ -107,7 +107,6 @@ def get_variant(feature, context = Unleash::Context.new, fallback_variant = disa # safe shutdown: also flush metrics to server and toggles to disk def shutdown unless Unleash.configuration.disable_client - Unleash.toggle_fetcher.save! Unleash.reporter.post unless Unleash.configuration.disable_metrics shutdown! end From 3fb4688342fabf0ced4bb3e6f322ff0b5229606f Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Thu, 5 Sep 2024 10:18:41 +0200 Subject: [PATCH 32/35] fix: convert all context values to strings prior to send to ygg --- lib/unleash/context.rb | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/lib/unleash/context.rb b/lib/unleash/context.rb index 36553599..352b9831 100644 --- a/lib/unleash/context.rb +++ b/lib/unleash/context.rb @@ -7,31 +7,31 @@ class Context def initialize(params = {}) raise ArgumentError, "Unleash::Context must be initialized with a hash." unless params.is_a?(Hash) - self.app_name = value_for('appName', params, Unleash&.configuration&.app_name) - self.environment = value_for('environment', params, Unleash&.configuration&.environment || 'default') - self.user_id = value_for('userId', params)&.to_s - self.session_id = value_for('sessionId', params) - self.remote_address = value_for('remoteAddress', params) - self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s) - - properties = value_for('properties', params) + self.app_name = value_for("appName", params, Unleash&.configuration&.app_name) + self.environment = value_for("environment", params, Unleash&.configuration&.environment || "default") + self.user_id = value_for("userId", params)&.to_s + self.session_id = value_for("sessionId", params) + self.remote_address = value_for("remoteAddress", params) + self.current_time = value_for("currentTime", params, Time.now.utc.iso8601.to_s) + + properties = value_for("properties", params) self.properties = properties.is_a?(Hash) ? properties.transform_keys(&:to_sym) : {} end def to_s "" + ",app_name=#{@app_name},environment=#{@environment},current_time=#{@current_time}>" end def as_json { - appName: self.app_name, - environment: self.environment, - userId: self.user_id, - sessionId: self.session_id, - remoteAddress: self.remote_address, - currentTime: self.current_time, - properties: self.properties + appName: to_safe_value(self.app_name), + environment: to_safe_value(self.environment), + userId: to_safe_value(self.user_id), + sessionId: to_safe_value(self.session_id), + remoteAddress: to_safe_value(self.remote_address), + currentTime: to_safe_value(self.current_time), + properties: self.properties.transform_values{ |value| to_safe_value(value) } } end @@ -68,6 +68,16 @@ def value_for(key, params, default_value = nil) params.values_at(key, key.to_sym, underscore(key), underscore(key).to_sym).compact.first || default_value end + def to_safe_value(value) + return nil if value.nil? + + if value.is_a?(Time) + value.utc.iso8601 + else + value.to_s + end + end + # converts CamelCase to snake_case def underscore(camel_cased_word) camel_cased_word.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase From 0518b74f07a7a04f246ec547fe7fb94f5cd242ec Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Thu, 5 Sep 2024 14:52:51 +0200 Subject: [PATCH 33/35] chore: bump ygg to test macos --- unleash-client.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unleash-client.gemspec b/unleash-client.gemspec index 025e375d..8b2d9575 100644 --- a/unleash-client.gemspec +++ b/unleash-client.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.6" spec.add_dependency "murmurhash3", "~> 0.1.7" - spec.add_dependency "yggdrasil-engine", "~> 0.0.5.beta.9" + spec.add_dependency "yggdrasil-engine", "~> 0.0.5.beta.12" spec.add_development_dependency "bundler", "~> 2.1" spec.add_development_dependency "rake", "~> 12.3" From 92642d6d00f05a192d396a01819ad12c970a62e8 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Fri, 20 Sep 2024 15:37:22 +0200 Subject: [PATCH 34/35] chore: bump yggdrasil version --- unleash-client.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unleash-client.gemspec b/unleash-client.gemspec index 8b2d9575..9b986539 100644 --- a/unleash-client.gemspec +++ b/unleash-client.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.6" spec.add_dependency "murmurhash3", "~> 0.1.7" - spec.add_dependency "yggdrasil-engine", "~> 0.0.5.beta.12" + spec.add_dependency "yggdrasil-engine", "~> 0.0.5.beta.13" spec.add_development_dependency "bundler", "~> 2.1" spec.add_development_dependency "rake", "~> 12.3" From d4d7a8bf925c211348a247bd1f4c08f6bea070f2 Mon Sep 17 00:00:00 2001 From: Simon Hornby Date: Mon, 23 Sep 2024 08:41:34 +0200 Subject: [PATCH 35/35] chore: bump ygg version --- unleash-client.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unleash-client.gemspec b/unleash-client.gemspec index 9b986539..96a4948c 100644 --- a/unleash-client.gemspec +++ b/unleash-client.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.6" spec.add_dependency "murmurhash3", "~> 0.1.7" - spec.add_dependency "yggdrasil-engine", "~> 0.0.5.beta.13" + spec.add_dependency "yggdrasil-engine", "~> 0.0.5.beta.14" spec.add_development_dependency "bundler", "~> 2.1" spec.add_development_dependency "rake", "~> 12.3"