Module: ToggleCraft::Utils

Defined in:
lib/togglecraft/utils.rb

Constant Summary collapse

OPERATORS =

Operators supported for rule evaluation

%w[
  equals not_equals contains not_contains starts_with ends_with
  in not_in gt gte lt lte between regex
  semver_eq semver_gt semver_gte semver_lt semver_lte
].freeze

Class Method Summary collapse

Class Method Details

.compare_semver(version1, version2) ⇒ Integer

Compare semantic versions Returns -1 if version1 < version2, 0 if equal, 1 if version1 > version2

Parameters:

  • version1 (String)

    First version

  • version2 (String)

    Second version

Returns:

  • (Integer)

    -1, 0, or 1



56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/togglecraft/utils.rb', line 56

def self.compare_semver(version1, version2)
  v1_parts = version1.to_s.split('.').map(&:to_i)
  v2_parts = version2.to_s.split('.').map(&:to_i)

  3.times do |i|
    part1 = v1_parts[i] || 0
    part2 = v2_parts[i] || 0

    return 1 if part1 > part2
    return -1 if part1 < part2
  end

  0
end

.create_cache_key(flag_key, context) ⇒ String

Create a unique key for caching based on flag and context

Parameters:

  • flag_key (String)

    The flag key

  • context (Hash)

    The evaluation context

Returns:

  • (String)

    A unique cache key



86
87
88
89
# File 'lib/togglecraft/utils.rb', line 86

def self.create_cache_key(flag_key, context)
  user_id = context.dig(:user, :id) || context.dig(:user, :email) || 'anonymous'
  "#{flag_key}:#{user_id}"
end

.evaluate_operator(attribute_value, operator, condition_values) ⇒ Boolean

Evaluate a condition operator Supports 14 different operators for flexible targeting rules

Parameters:

  • attribute_value (Object)

    The actual value from context

  • operator (String)

    The operator to use

  • condition_values (Array)

    The values to compare against

Returns:

  • (Boolean)

    Whether the condition passes



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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/togglecraft/utils.rb', line 98

def self.evaluate_operator(attribute_value, operator, condition_values)
  # Handle nil/null values
  return %w[not_equals not_in].include?(operator) if attribute_value.nil?

  first_value = condition_values[0]
  attribute_str = attribute_value.to_s.downcase
  value_str = first_value.to_s.downcase

  case operator
  when 'equals'
    attribute_str == value_str

  when 'not_equals'
    attribute_str != value_str

  when 'contains'
    attribute_str.include?(value_str)

  when 'not_contains'
    !attribute_str.include?(value_str)

  when 'starts_with'
    attribute_str.start_with?(value_str)

  when 'ends_with'
    attribute_str.end_with?(value_str)

  when 'in'
    condition_values.any? { |v| v.to_s.downcase == attribute_str }

  when 'not_in'
    condition_values.none? { |v| v.to_s.downcase == attribute_str }

  when 'gt'
    attribute_value.to_f > first_value.to_f

  when 'gte'
    attribute_value.to_f >= first_value.to_f

  when 'lt'
    attribute_value.to_f < first_value.to_f

  when 'lte'
    attribute_value.to_f <= first_value.to_f

  when 'between'
    return false if condition_values.length < 2

    num = attribute_value.to_f
    num.between?(condition_values[0].to_f, condition_values[1].to_f)

  when 'regex'
    begin
      regex = Regexp.new(first_value.to_s)
      regex.match?(attribute_value.to_s)
    rescue RegexpError
      false
    end

  when 'semver_eq'
    compare_semver(attribute_value, first_value).zero?

  when 'semver_gt'
    compare_semver(attribute_value, first_value).positive?

  when 'semver_gte'
    compare_semver(attribute_value, first_value) >= 0

  when 'semver_lt'
    compare_semver(attribute_value, first_value).negative?

  when 'semver_lte'
    compare_semver(attribute_value, first_value) <= 0

  else
    # Unknown operator - log warning and return false
    warn "[ToggleCraft] Unknown operator: #{operator}"
    false
  end
end

.generate_jitter(max_jitter_ms = 1500) ⇒ Integer

Generate random jitter delay to prevent thundering herd When many clients receive a version update simultaneously, jitter ensures they don’t all fetch the new version at exactly the same time

Parameters:

  • max_jitter_ms (Integer) (defaults to: 1500)

    Maximum jitter in milliseconds

Returns:

  • (Integer)

    Random delay between 0 and max_jitter_ms



77
78
79
# File 'lib/togglecraft/utils.rb', line 77

def self.generate_jitter(max_jitter_ms = 1500)
  rand(0..max_jitter_ms)
end

.get_nested_property(obj, path) ⇒ Object?

Deep get a nested property from a hash using dot notation Supports both string and symbol keys

Parameters:

  • obj (Hash)

    The hash to query

  • path (String)

    The path to the property (e.g., “user.email”)

Returns:

  • (Object, nil)

    The value at the path or nil



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/togglecraft/utils.rb', line 33

def self.get_nested_property(obj, path)
  return nil unless obj && path
  return nil if path.to_s.strip.empty?

  keys = path.to_s.split('.')
  current = obj

  keys.each do |key|
    return nil if current.nil?

    # Try both symbol and string keys
    current = current[key.to_sym] || current[key.to_s]
  end

  current
end

.hash_key(key) ⇒ Integer

Consistent hashing for percentage rollouts Returns a value between 0 and 100 Mirrors the JavaScript implementation exactly for consistency

Parameters:

  • key (String)

    The key to hash

Returns:

  • (Integer)

    A value between 0 and 100



18
19
20
21
22
23
24
25
# File 'lib/togglecraft/utils.rb', line 18

def self.hash_key(key)
  hash = 0
  key.to_s.each_char do |char|
    hash = ((hash << 5) - hash) + char.ord
    hash &= hash # Convert to 32-bit integer
  end
  hash.abs % 100
end