Class: Growthbook::Conditions

Inherits:
Object
  • Object
show all
Defined in:
lib/growthbook/conditions.rb

Overview

internal use only Utils for condition evaluation

Class Method Summary collapse

Class Method Details

.compare(val1, val2) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
# File 'lib/growthbook/conditions.rb', line 107

def self.compare(val1, val2)
  if val1.is_a?(Numeric) || val2.is_a?(Numeric)
    val1 = val1.is_a?(Numeric) ? val1 : val1.to_f
    val2 = val2.is_a?(Numeric) ? val2 : val2.to_f
  end

  return 1 if val1 > val2
  return -1 if val1 < val2

  0
end

.elem_match(condition, attribute_value) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/growthbook/conditions.rb', line 94

def self.elem_match(condition, attribute_value)
  return false unless attribute_value.is_a? Array

  attribute_value.each do |item|
    if operator_object?(condition)
      return true if eval_condition_value(condition, item)
    elsif eval_condition(item, condition)
      return true
    end
  end
  false
end

.eval_and(attributes, conditions) ⇒ Object



44
45
46
47
48
49
# File 'lib/growthbook/conditions.rb', line 44

def self.eval_and(attributes, conditions)
  conditions.each do |condition|
    return false unless eval_condition(attributes, condition)
  end
  true
end

.eval_condition(attributes, condition) ⇒ Object

Evaluate a targeting conditions hash against an attributes hash Both attributes and conditions only have string keys (no symbols)



11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/growthbook/conditions.rb', line 11

def self.eval_condition(attributes, condition)
  return eval_or(attributes, condition['$or']) if condition.key?('$or')
  return !eval_or(attributes, condition['$nor']) if condition.key?('$nor')
  return eval_and(attributes, condition['$and']) if condition.key?('$and')
  return !eval_condition(attributes, condition['$not']) if condition.key?('$not')

  condition.each do |key, value|
    return false unless eval_condition_value(value, get_path(attributes, key))
  end

  true
end

.eval_condition_value(condition_value, attribute_value) ⇒ Object



84
85
86
87
88
89
90
91
92
# File 'lib/growthbook/conditions.rb', line 84

def self.eval_condition_value(condition_value, attribute_value)
  if condition_value.is_a?(Hash) && operator_object?(condition_value)
    condition_value.each do |key, value|
      return false unless eval_operator_condition(key, attribute_value, value)
    end
    return true
  end
  condition_value.to_json == attribute_value.to_json
end

.eval_operator_condition(operator, attribute_value, condition_value) ⇒ Object



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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/growthbook/conditions.rb', line 119

def self.eval_operator_condition(operator, attribute_value, condition_value)
  case operator
  when '$veq'
    padded_version_string(attribute_value) == padded_version_string(condition_value)
  when '$vne'
    padded_version_string(attribute_value) != padded_version_string(condition_value)
  when '$vgt'
    padded_version_string(attribute_value) > padded_version_string(condition_value)
  when '$vgte'
    padded_version_string(attribute_value) >= padded_version_string(condition_value)
  when '$vlt'
    padded_version_string(attribute_value) < padded_version_string(condition_value)
  when '$vlte'
    padded_version_string(attribute_value) <= padded_version_string(condition_value)
  when '$eq'
    begin
      compare(attribute_value, condition_value).zero?
    rescue StandardError
      false
    end
  when '$ne'
    begin
      compare(attribute_value, condition_value) != 0
    rescue StandardError
      false
    end
  when '$lt'
    begin
      compare(attribute_value, condition_value).negative?
    rescue StandardError
      false
    end
  when '$lte'
    begin
      compare(attribute_value, condition_value) <= 0
    rescue StandardError
      false
    end
  when '$gt'
    begin
      compare(attribute_value, condition_value).positive?
    rescue StandardError
      false
    end
  when '$gte'
    begin
      compare(attribute_value, condition_value) >= 0
    rescue StandardError
      false
    end
  when '$regex'
    silence_warnings do
      re = Regexp.new(condition_value)
      !!attribute_value.match(re)
    rescue StandardError
      false
    end
  when '$in'
    return false unless condition_value.is_a?(Array)

    in?(attribute_value, condition_value)
  when '$nin'
    return false unless condition_value.is_a?(Array)

    !in?(attribute_value, condition_value)
  when '$elemMatch'
    elem_match(condition_value, attribute_value)
  when '$size'
    return false unless attribute_value.is_a? Array

    eval_condition_value(condition_value, attribute_value.length)
  when '$all'
    return false unless attribute_value.is_a? Array

    condition_value.each do |condition|
      passed = false
      attribute_value.each do |attr|
        passed = true if eval_condition_value(condition, attr)
      end
      return false unless passed
    end
    true
  when '$exists'
    exists = !attribute_value.nil?
    if condition_value
      exists
    else
      !exists
    end
  when '$type'
    condition_value == get_type(attribute_value)
  when '$not'
    !eval_condition_value(condition_value, attribute_value)
  else
    false
  end
end

.eval_or(attributes, conditions) ⇒ Object



35
36
37
38
39
40
41
42
# File 'lib/growthbook/conditions.rb', line 35

def self.eval_or(attributes, conditions)
  return true if conditions.length <= 0

  conditions.each do |condition|
    return true if eval_condition(attributes, condition)
  end
  false
end

.get_path(attributes, path) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/growthbook/conditions.rb', line 69

def self.get_path(attributes, path)
  path = path.to_s if path.is_a?(Symbol)

  parts = path.split('.')
  current = attributes

  parts.each do |value|
    return nil unless current.is_a?(Hash) && current&.key?(value)

    current = current[value]
  end

  current
end

.get_type(attribute_value) ⇒ Object



58
59
60
61
62
63
64
65
66
67
# File 'lib/growthbook/conditions.rb', line 58

def self.get_type(attribute_value)
  return 'string' if attribute_value.is_a? String
  return 'number' if attribute_value.is_a? Integer
  return 'number' if attribute_value.is_a? Float
  return 'boolean' if [true, false].include?(attribute_value)
  return 'array' if attribute_value.is_a? Array
  return 'null' if attribute_value.nil?

  'object'
end

.in?(actual, expected) ⇒ Boolean

Returns:

  • (Boolean)


234
235
236
237
238
# File 'lib/growthbook/conditions.rb', line 234

def self.in?(actual, expected)
  return expected.include?(actual) unless actual.is_a?(Array)

  (actual & expected).any?
end

.operator_object?(obj) ⇒ Boolean

Returns:

  • (Boolean)


51
52
53
54
55
56
# File 'lib/growthbook/conditions.rb', line 51

def self.operator_object?(obj)
  obj.each do |key, _value|
    return false if key[0] != '$'
  end
  true
end

.padded_version_string(input) ⇒ Object



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/growthbook/conditions.rb', line 217

def self.padded_version_string(input)
  # Remove build info and leading `v` if any
  # Split version into parts (both core version numbers and pre-release tags)
  # "v1.2.3-rc.1+build123" -> ["1","2","3","rc","1"]
  parts = input.gsub(/(^v|\+.*$)/, '').split(/[-.]/)

  # If it's SemVer without a pre-release, add `~` to the end
  # ["1","0","0"] -> ["1","0","0","~"]
  # "~" is the largest ASCII character, so this will make "1.0.0" greater than "1.0.0-beta" for example
  parts << '~' if parts.length == 3

  # Left pad each numeric part with spaces so string comparisons will work ("9">"10", but " 9"<"10")
  parts.map do |part|
    /^[0-9]+$/.match?(part) ? part.rjust(5, ' ') : part
  end.join('-')
end

.parse_condition(condition) ⇒ Object

Helper function to ensure conditions only have string keys (no symbols)



25
26
27
28
29
30
31
32
33
# File 'lib/growthbook/conditions.rb', line 25

def self.parse_condition(condition)
  case condition
  when Array
    return condition.map { |v| parse_condition(v) }
  when Hash
    return condition.to_h { |k, v| [k.to_s, parse_condition(v)] }
  end
  condition
end

.silence_warningsObject

Sets $VERBOSE for the duration of the block and back to its original value afterwards. Used for testing invalid regexes.



242
243
244
245
246
247
248
# File 'lib/growthbook/conditions.rb', line 242

def self.silence_warnings
  old_verbose = $VERBOSE
  $VERBOSE = nil
  yield
ensure
  $VERBOSE = old_verbose
end