Module: Subflag::Rails::TargetingEngine

Defined in:
lib/subflag/rails/targeting_engine.rb

Overview

Evaluates targeting rules against evaluation contexts.

Rules are arrays of { "value" => X, "conditions" => ... } hashes. First matching rule wins. Falls back to flag's default value if nothing matches.

Examples:

Rule structure

[
  {
    "value" => "100",
    "conditions" => {
      "type" => "OR",
      "conditions" => [
        { "attribute" => "email", "operator" => "ENDS_WITH", "value" => "@company.com" },
        { "attribute" => "role", "operator" => "IN", "value" => ["admin", "developer"] }
      ]
    }
  }
]

Constant Summary collapse

OPERATORS =
%w[
  EQUALS NOT_EQUALS IN NOT_IN
  CONTAINS NOT_CONTAINS STARTS_WITH ENDS_WITH
  GREATER_THAN LESS_THAN GREATER_THAN_OR_EQUAL LESS_THAN_OR_EQUAL
  MATCHES
].freeze

Class Method Summary collapse

Class Method Details

.evaluate(rules, context, flag_key: nil) ⇒ String?

Evaluate targeting rules against a context

Parameters:

  • rules (Array<Hash>, nil)

    Array of targeting rules with values

  • context (Hash, nil)

    Evaluation context with attributes

  • flag_key (String, nil) (defaults to: nil)

    The flag key (required for percentage rollouts)

Returns:

  • (String, nil)

    The matched rule's value, or nil if no match



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
# File 'lib/subflag/rails/targeting_engine.rb', line 41

def evaluate(rules, context, flag_key: nil)
  return nil if rules.nil? || rules.empty?
  return nil if context.nil? || context.empty?

  normalized_context = normalize_context(context)
  targeting_key = extract_targeting_key(normalized_context)

  rules.each do |rule|
    rule = rule.transform_keys(&:to_s)
    conditions = rule["conditions"]
    percentage = rule["percentage"]

    segment_matches = if conditions.nil? || conditions.empty?
                        true
                      else
                        evaluate_rule(conditions, normalized_context)
                      end

    next unless segment_matches

    if percentage
      next unless targeting_key && flag_key
      next unless evaluate_percentage(targeting_key, flag_key, percentage)
    end

    return rule["value"]
  end

  nil
end

.evaluate_percentage(targeting_key, flag_key, percentage) ⇒ Boolean

Evaluate percentage rollout using MurmurHash3

Parameters:

  • targeting_key (String)

    Unique identifier for the context

  • flag_key (String)

    The flag being evaluated

  • percentage (Integer)

    Target percentage (0-100)

Returns:

  • (Boolean)

    true if context falls within percentage



78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/subflag/rails/targeting_engine.rb', line 78

def evaluate_percentage(targeting_key, flag_key, percentage)
  percentage = percentage.to_i
  return false if percentage <= 0
  return true if percentage >= 100

  hash_input = "#{targeting_key}:#{flag_key}"
  hash_bytes = MurmurHash3::V128.str_hash(hash_input)
  hash_code = [hash_bytes[0]].pack("L").unpack1("l")
  bucket = hash_code.abs % 100

  bucket < percentage
end