1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
|
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 = {})
params = {} if params.nil?
self.name = params.fetch('name', nil)
self.enabled = params.fetch('enabled', false)
self.strategies = initialize_strategies(params)
self.variant_definitions = initialize_variant_definitions(params)
end
def to_s
"<FeatureToggle: name=#{name},enabled=#{enabled},strategies=#{strategies},variant_definitions=#{variant_definitions}>"
end
def is_enabled?(context, default_result)
result = am_enabled?(context, default_result)
choice = result ? :yes : :no
Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
result
end
def get_variant(context, fallback_variant = 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)
return disabled_variant unless self.enabled && am_enabled?(context, true)
return disabled_variant if sum_variant_defs_weights <= 0
variant = variant_from_override_match(context)
variant = variant_from_weights(context) if variant.nil?
Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics
variant
end
private
# only check if it is enabled, do not do metrics
def am_enabled?(context, default_result)
result =
if self.enabled
self.strategies.empty? ||
self.strategies.any? do |s|
strategy_enabled?(s, context) && strategy_constraint_matches?(s, context)
end
else
default_result
end
Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} " \
"and Strategies combined with contraints returned #{result})"
result
end
def strategy_enabled?(strategy, context)
r = Unleash::STRATEGIES.fetch(strategy.name.to_sym, :unknown).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)
strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
end
def disabled_variant
Unleash::Variant.new(name: 'disabled', enabled: false)
end
def sum_variant_defs_weights
self.variant_definitions.map(&:weight).reduce(0, :+)
end
def variant_salt(context)
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)
variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context), 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 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)
params.fetch('strategies', [])
.select{ |s| s.has_key?('name') && Unleash::STRATEGIES.has_key?(s['name'].to_sym) }
.map do |s|
ActivationStrategy.new(
s['name'],
s['parameters'],
(s['constraints'] || []).map do |c|
Constraint.new(
c.fetch('contextName'),
c.fetch('operator'),
c.fetch('values')
)
end
)
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('overrides', [])
)
end || []
end
end
end
|