File: feature_toggle.rb

package info (click to toggle)
ruby-unleash 3.2.5-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 252 kB
  • sloc: ruby: 1,098; makefile: 10; sh: 4
file content (158 lines) | stat: -rw-r--r-- 5,272 bytes parent folder | download
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