Skip to content

Commit cd16893

Browse files
rashidspmikeproeng37
authored andcommitted
feat (audience match types): Condition Evaluator, Project Config, and Audience combinations PRs consolidation. (#150)
1 parent e848bf5 commit cd16893

16 files changed

+1932
-336
lines changed

.travis.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ branch:
22
only:
33
- master
44
language: ruby
5+
cache: bundler
56
rvm:
67
- 2.3.7
78
- 2.4.4
89
- 2.5.1
9-
before_install: gem install bundler
10+
before_install:
11+
- gem update --system
12+
- gem install bundler
1013
install:
1114
- bundle install
1215
script: "bundle exec rake spec"

lib/optimizely/audience.rb

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
#
4-
# Copyright 2016-2017, Optimizely and contributors
4+
# Copyright 2016-2017, 2019, Optimizely and contributors
55
#
66
# Licensed under the Apache License, Version 2.0 (the "License");
77
# you may not use this file except in compliance with the License.
@@ -16,7 +16,8 @@
1616
# limitations under the License.
1717
#
1818
require 'json'
19-
require_relative './condition'
19+
require_relative './custom_attribute_condition_evaluator'
20+
require_relative 'condition_tree_evaluator'
2021

2122
module Optimizely
2223
module Audience
@@ -30,25 +31,32 @@ def user_in_experiment?(config, experiment, attributes)
3031
# attributes - Hash representing user attributes which will be used in determining if
3132
# the audience conditions are met.
3233
#
33-
# Returns boolean representing if user satisfies audience conditions for any of the audiences or not.
34+
# Returns boolean representing if user satisfies audience conditions for the audiences or not.
3435

35-
audience_ids = experiment['audienceIds']
36+
audience_conditions = experiment['audienceConditions'] || experiment['audienceIds']
3637

3738
# Return true if there are no audiences
38-
return true if audience_ids.empty?
39+
return true if audience_conditions.empty?
3940

40-
# Return false if there are audiences but no attributes
41-
return false unless attributes
41+
attributes ||= {}
4242

43-
# Return true if any one of the audience conditions are met
44-
@condition_evaluator = ConditionEvaluator.new(attributes)
45-
audience_ids.each do |audience_id|
43+
custom_attr_condition_evaluator = CustomAttributeConditionEvaluator.new(attributes)
44+
45+
evaluate_custom_attr = lambda do |condition|
46+
return custom_attr_condition_evaluator.evaluate(condition)
47+
end
48+
49+
evaluate_audience = lambda do |audience_id|
4650
audience = config.get_audience_from_id(audience_id)
51+
return nil unless audience
52+
4753
audience_conditions = audience['conditions']
48-
audience_conditions = JSON.parse(audience_conditions)
49-
return true if @condition_evaluator.evaluate(audience_conditions)
54+
audience_conditions = JSON.parse(audience_conditions) if audience_conditions.is_a?(String)
55+
return ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_custom_attr)
5056
end
5157

58+
return true if ConditionTreeEvaluator.evaluate(audience_conditions, evaluate_audience)
59+
5260
false
5361
end
5462
end

lib/optimizely/condition.rb

Lines changed: 0 additions & 136 deletions
This file was deleted.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# frozen_string_literal: true
2+
3+
#
4+
# Copyright 2019, Optimizely and contributors
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
module Optimizely
19+
module ConditionTreeEvaluator
20+
# Operator types
21+
AND_CONDITION = 'and'
22+
OR_CONDITION = 'or'
23+
NOT_CONDITION = 'not'
24+
25+
EVALUATORS_BY_OPERATOR_TYPE = {
26+
AND_CONDITION => :and_evaluator,
27+
OR_CONDITION => :or_evaluator,
28+
NOT_CONDITION => :not_evaluator
29+
}.freeze
30+
31+
module_function
32+
33+
def evaluate(conditions, leaf_evaluator)
34+
# Top level method to evaluate audience conditions.
35+
#
36+
# conditions - Nested array of and/or conditions.
37+
# Example: ['and', operand_1, ['or', operand_2, operand_3]]
38+
#
39+
# leaf_evaluator - Function which will be called to evaluate leaf condition values.
40+
#
41+
# Returns boolean if the given user attributes match/don't match the given conditions,
42+
# nil if the given conditions are invalid or can't be evaluated.
43+
44+
if conditions.is_a? Array
45+
first_operator = conditions[0]
46+
rest_of_conditions = conditions[1..-1]
47+
48+
# Operator to apply is not explicit - assume 'or'
49+
unless EVALUATORS_BY_OPERATOR_TYPE.include?(conditions[0])
50+
first_operator = OR_CONDITION
51+
rest_of_conditions = conditions
52+
end
53+
54+
return send(EVALUATORS_BY_OPERATOR_TYPE[first_operator], rest_of_conditions, leaf_evaluator)
55+
end
56+
57+
leaf_evaluator.call(conditions)
58+
end
59+
60+
def and_evaluator(conditions, leaf_evaluator)
61+
# Evaluates an array of conditions as if the evaluator had been applied
62+
# to each entry and the results AND-ed together.
63+
#
64+
# conditions - Array of conditions ex: [operand_1, operand_2]
65+
#
66+
# leaf_evaluator - Function which will be called to evaluate leaf condition values.
67+
#
68+
# Returns boolean if the user attributes match/don't match the given conditions,
69+
# nil if the user attributes and conditions can't be evaluated.
70+
71+
found_nil = false
72+
conditions.each do |condition|
73+
result = evaluate(condition, leaf_evaluator)
74+
return result if result == false
75+
76+
found_nil = true if result.nil?
77+
end
78+
79+
found_nil ? nil : true
80+
end
81+
82+
def not_evaluator(single_condition, leaf_evaluator)
83+
# Evaluates an array of conditions as if the evaluator had been applied
84+
# to a single entry and NOT was applied to the result.
85+
#
86+
# single_condition - Array of a single condition ex: [operand_1]
87+
#
88+
# leaf_evaluator - Function which will be called to evaluate leaf condition values.
89+
#
90+
# Returns boolean if the user attributes match/don't match the given conditions,
91+
# nil if the user attributes and conditions can't be evaluated.
92+
93+
return nil if single_condition.empty?
94+
95+
result = evaluate(single_condition[0], leaf_evaluator)
96+
result.nil? ? nil : !result
97+
end
98+
99+
def or_evaluator(conditions, leaf_evaluator)
100+
# Evaluates an array of conditions as if the evaluator had been applied
101+
# to each entry and the results OR-ed together.
102+
#
103+
# conditions - Array of conditions ex: [operand_1, operand_2]
104+
#
105+
# leaf_evaluator - Function which will be called to evaluate leaf condition values.
106+
#
107+
# Returns boolean if the user attributes match/don't match the given conditions,
108+
# nil if the user attributes and conditions can't be evaluated.
109+
110+
found_nil = false
111+
conditions.each do |condition|
112+
result = evaluate(condition, leaf_evaluator)
113+
return result if result == true
114+
115+
found_nil = true if result.nil?
116+
end
117+
118+
found_nil ? nil : false
119+
end
120+
end
121+
end

0 commit comments

Comments
 (0)