optimizely-sdk 3.10.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +202 -202
  3. data/lib/optimizely/audience.rb +127 -97
  4. data/lib/optimizely/bucketer.rb +156 -156
  5. data/lib/optimizely/condition_tree_evaluator.rb +123 -123
  6. data/lib/optimizely/config/datafile_project_config.rb +539 -552
  7. data/lib/optimizely/config/proxy_config.rb +34 -34
  8. data/lib/optimizely/config_manager/async_scheduler.rb +95 -95
  9. data/lib/optimizely/config_manager/http_project_config_manager.rb +330 -329
  10. data/lib/optimizely/config_manager/project_config_manager.rb +24 -24
  11. data/lib/optimizely/config_manager/static_project_config_manager.rb +53 -52
  12. data/lib/optimizely/decide/optimizely_decide_option.rb +28 -28
  13. data/lib/optimizely/decide/optimizely_decision.rb +60 -60
  14. data/lib/optimizely/decide/optimizely_decision_message.rb +26 -26
  15. data/lib/optimizely/decision_service.rb +563 -563
  16. data/lib/optimizely/error_handler.rb +39 -39
  17. data/lib/optimizely/event/batch_event_processor.rb +235 -234
  18. data/lib/optimizely/event/entity/conversion_event.rb +44 -43
  19. data/lib/optimizely/event/entity/decision.rb +38 -38
  20. data/lib/optimizely/event/entity/event_batch.rb +86 -86
  21. data/lib/optimizely/event/entity/event_context.rb +50 -50
  22. data/lib/optimizely/event/entity/impression_event.rb +48 -47
  23. data/lib/optimizely/event/entity/snapshot.rb +33 -33
  24. data/lib/optimizely/event/entity/snapshot_event.rb +48 -48
  25. data/lib/optimizely/event/entity/user_event.rb +22 -22
  26. data/lib/optimizely/event/entity/visitor.rb +36 -35
  27. data/lib/optimizely/event/entity/visitor_attribute.rb +38 -37
  28. data/lib/optimizely/event/event_factory.rb +156 -155
  29. data/lib/optimizely/event/event_processor.rb +25 -25
  30. data/lib/optimizely/event/forwarding_event_processor.rb +44 -43
  31. data/lib/optimizely/event/user_event_factory.rb +88 -88
  32. data/lib/optimizely/event_builder.rb +221 -228
  33. data/lib/optimizely/event_dispatcher.rb +71 -71
  34. data/lib/optimizely/exceptions.rb +135 -139
  35. data/lib/optimizely/helpers/constants.rb +415 -397
  36. data/lib/optimizely/helpers/date_time_utils.rb +30 -30
  37. data/lib/optimizely/helpers/event_tag_utils.rb +132 -132
  38. data/lib/optimizely/helpers/group.rb +31 -31
  39. data/lib/optimizely/helpers/http_utils.rb +65 -64
  40. data/lib/optimizely/helpers/validator.rb +183 -183
  41. data/lib/optimizely/helpers/variable_type.rb +67 -67
  42. data/lib/optimizely/logger.rb +46 -45
  43. data/lib/optimizely/notification_center.rb +174 -176
  44. data/lib/optimizely/optimizely_config.rb +271 -272
  45. data/lib/optimizely/optimizely_factory.rb +181 -181
  46. data/lib/optimizely/optimizely_user_context.rb +204 -179
  47. data/lib/optimizely/params.rb +31 -31
  48. data/lib/optimizely/project_config.rb +99 -91
  49. data/lib/optimizely/semantic_version.rb +166 -166
  50. data/lib/optimizely/{custom_attribute_condition_evaluator.rb → user_condition_evaluator.rb} +391 -369
  51. data/lib/optimizely/user_profile_service.rb +35 -35
  52. data/lib/optimizely/version.rb +21 -21
  53. data/lib/optimizely.rb +1130 -1145
  54. metadata +13 -13
@@ -1,156 +1,156 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Copyright 2016-2017, 2019-2021 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
- require 'murmurhash3'
19
- require_relative 'helpers/group'
20
-
21
- module Optimizely
22
- class Bucketer
23
- # Optimizely bucketing algorithm that evenly distributes visitors.
24
-
25
- BUCKETING_ID_TEMPLATE = '%<bucketing_id>s%<entity_id>s'
26
- HASH_SEED = 1
27
- MAX_HASH_VALUE = 2**32
28
- MAX_TRAFFIC_VALUE = 10_000
29
- UNSIGNED_MAX_32_BIT_VALUE = 0xFFFFFFFF
30
-
31
- def initialize(logger)
32
- # Bucketer init method to set bucketing seed and logger.
33
- # logger - Optional component which provides a log method to log messages.
34
- @logger = logger
35
- @bucket_seed = HASH_SEED
36
- end
37
-
38
- def bucket(project_config, experiment, bucketing_id, user_id)
39
- # Determines ID of variation to be shown for a given experiment key and user ID.
40
- #
41
- # project_config - Instance of ProjectConfig
42
- # experiment - Experiment or Rollout rule for which visitor is to be bucketed.
43
- # bucketing_id - String A customer-assigned value used to generate the bucketing key
44
- # user_id - String ID for user.
45
- #
46
- # Returns variation in which visitor with ID user_id has been placed. Nil if no variation.
47
- return nil, [] if experiment.nil?
48
-
49
- decide_reasons = []
50
-
51
- # check if experiment is in a group; if so, check if user is bucketed into specified experiment
52
- # this will not affect evaluation of rollout rules.
53
- experiment_id = experiment['id']
54
- experiment_key = experiment['key']
55
- group_id = experiment['groupId']
56
- if group_id
57
- group = project_config.group_id_map.fetch(group_id)
58
- if Helpers::Group.random_policy?(group)
59
- traffic_allocations = group.fetch('trafficAllocation')
60
- bucketed_experiment_id, find_bucket_reasons = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
61
- decide_reasons.push(*find_bucket_reasons)
62
-
63
- # return if the user is not bucketed into any experiment
64
- unless bucketed_experiment_id
65
- message = "User '#{user_id}' is in no experiment."
66
- @logger.log(Logger::INFO, message)
67
- decide_reasons.push(message)
68
- return nil, decide_reasons
69
- end
70
-
71
- # return if the user is bucketed into a different experiment than the one specified
72
- if bucketed_experiment_id != experiment_id
73
- message = "User '#{user_id}' is not in experiment '#{experiment_key}' of group #{group_id}."
74
- @logger.log(Logger::INFO, message)
75
- decide_reasons.push(message)
76
- return nil, decide_reasons
77
- end
78
-
79
- # continue bucketing if the user is bucketed into the experiment specified
80
- message = "User '#{user_id}' is in experiment '#{experiment_key}' of group #{group_id}."
81
- @logger.log(Logger::INFO, message)
82
- decide_reasons.push(message)
83
- end
84
- end
85
-
86
- traffic_allocations = experiment['trafficAllocation']
87
- variation_id, find_bucket_reasons = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
88
- decide_reasons.push(*find_bucket_reasons)
89
-
90
- if variation_id && variation_id != ''
91
- variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
92
- return variation, decide_reasons
93
- end
94
-
95
- # Handle the case when the traffic range is empty due to sticky bucketing
96
- if variation_id == ''
97
- message = 'Bucketed into an empty traffic range. Returning nil.'
98
- @logger.log(Logger::DEBUG, message)
99
- decide_reasons.push(message)
100
- end
101
-
102
- [nil, decide_reasons]
103
- end
104
-
105
- def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
106
- # Helper function to find the matching entity ID for a given bucketing value in a list of traffic allocations.
107
- #
108
- # bucketing_id - String A customer-assigned value user to generate bucketing key
109
- # user_id - String ID for user
110
- # parent_id - String entity ID to use for bucketing ID
111
- # traffic_allocations - Array of traffic allocations
112
- #
113
- # Returns and array of two values where first value is the entity ID corresponding to the provided bucket value
114
- # or nil if no match is found. The second value contains the array of reasons stating how the deicision was taken
115
- decide_reasons = []
116
- bucketing_key = format(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
117
- bucket_value = generate_bucket_value(bucketing_key)
118
-
119
- message = "Assigned bucket #{bucket_value} to user '#{user_id}' with bucketing ID: '#{bucketing_id}'."
120
- @logger.log(Logger::DEBUG, message)
121
-
122
- traffic_allocations.each do |traffic_allocation|
123
- current_end_of_range = traffic_allocation['endOfRange']
124
- if bucket_value < current_end_of_range
125
- entity_id = traffic_allocation['entityId']
126
- return entity_id, decide_reasons
127
- end
128
- end
129
-
130
- [nil, decide_reasons]
131
- end
132
-
133
- private
134
-
135
- def generate_bucket_value(bucketing_key)
136
- # Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE).
137
- #
138
- # bucketing_key - String - Value used to generate bucket value
139
- #
140
- # Returns bucket value corresponding to the provided bucketing key.
141
-
142
- ratio = generate_unsigned_hash_code_32_bit(bucketing_key).to_f / MAX_HASH_VALUE
143
- (ratio * MAX_TRAFFIC_VALUE).to_i
144
- end
145
-
146
- def generate_unsigned_hash_code_32_bit(bucketing_key)
147
- # Helper function to retreive hash code
148
- #
149
- # bucketing_key - String - Value used for the key of the murmur hash
150
- #
151
- # Returns hash code which is a 32 bit unsigned integer.
152
-
153
- MurmurHash3::V32.str_hash(bucketing_key, @bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE
154
- end
155
- end
156
- end
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2016-2017, 2019-2021 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
+ require 'murmurhash3'
19
+ require_relative 'helpers/group'
20
+
21
+ module Optimizely
22
+ class Bucketer
23
+ # Optimizely bucketing algorithm that evenly distributes visitors.
24
+
25
+ BUCKETING_ID_TEMPLATE = '%<bucketing_id>s%<entity_id>s'
26
+ HASH_SEED = 1
27
+ MAX_HASH_VALUE = 2**32
28
+ MAX_TRAFFIC_VALUE = 10_000
29
+ UNSIGNED_MAX_32_BIT_VALUE = 0xFFFFFFFF
30
+
31
+ def initialize(logger)
32
+ # Bucketer init method to set bucketing seed and logger.
33
+ # logger - Optional component which provides a log method to log messages.
34
+ @logger = logger
35
+ @bucket_seed = HASH_SEED
36
+ end
37
+
38
+ def bucket(project_config, experiment, bucketing_id, user_id)
39
+ # Determines ID of variation to be shown for a given experiment key and user ID.
40
+ #
41
+ # project_config - Instance of ProjectConfig
42
+ # experiment - Experiment or Rollout rule for which visitor is to be bucketed.
43
+ # bucketing_id - String A customer-assigned value used to generate the bucketing key
44
+ # user_id - String ID for user.
45
+ #
46
+ # Returns variation in which visitor with ID user_id has been placed. Nil if no variation.
47
+ return nil, [] if experiment.nil?
48
+
49
+ decide_reasons = []
50
+
51
+ # check if experiment is in a group; if so, check if user is bucketed into specified experiment
52
+ # this will not affect evaluation of rollout rules.
53
+ experiment_id = experiment['id']
54
+ experiment_key = experiment['key']
55
+ group_id = experiment['groupId']
56
+ if group_id
57
+ group = project_config.group_id_map.fetch(group_id)
58
+ if Helpers::Group.random_policy?(group)
59
+ traffic_allocations = group.fetch('trafficAllocation')
60
+ bucketed_experiment_id, find_bucket_reasons = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
61
+ decide_reasons.push(*find_bucket_reasons)
62
+
63
+ # return if the user is not bucketed into any experiment
64
+ unless bucketed_experiment_id
65
+ message = "User '#{user_id}' is in no experiment."
66
+ @logger.log(Logger::INFO, message)
67
+ decide_reasons.push(message)
68
+ return nil, decide_reasons
69
+ end
70
+
71
+ # return if the user is bucketed into a different experiment than the one specified
72
+ if bucketed_experiment_id != experiment_id
73
+ message = "User '#{user_id}' is not in experiment '#{experiment_key}' of group #{group_id}."
74
+ @logger.log(Logger::INFO, message)
75
+ decide_reasons.push(message)
76
+ return nil, decide_reasons
77
+ end
78
+
79
+ # continue bucketing if the user is bucketed into the experiment specified
80
+ message = "User '#{user_id}' is in experiment '#{experiment_key}' of group #{group_id}."
81
+ @logger.log(Logger::INFO, message)
82
+ decide_reasons.push(message)
83
+ end
84
+ end
85
+
86
+ traffic_allocations = experiment['trafficAllocation']
87
+ variation_id, find_bucket_reasons = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
88
+ decide_reasons.push(*find_bucket_reasons)
89
+
90
+ if variation_id && variation_id != ''
91
+ variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
92
+ return variation, decide_reasons
93
+ end
94
+
95
+ # Handle the case when the traffic range is empty due to sticky bucketing
96
+ if variation_id == ''
97
+ message = 'Bucketed into an empty traffic range. Returning nil.'
98
+ @logger.log(Logger::DEBUG, message)
99
+ decide_reasons.push(message)
100
+ end
101
+
102
+ [nil, decide_reasons]
103
+ end
104
+
105
+ def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
106
+ # Helper function to find the matching entity ID for a given bucketing value in a list of traffic allocations.
107
+ #
108
+ # bucketing_id - String A customer-assigned value user to generate bucketing key
109
+ # user_id - String ID for user
110
+ # parent_id - String entity ID to use for bucketing ID
111
+ # traffic_allocations - Array of traffic allocations
112
+ #
113
+ # Returns and array of two values where first value is the entity ID corresponding to the provided bucket value
114
+ # or nil if no match is found. The second value contains the array of reasons stating how the deicision was taken
115
+ decide_reasons = []
116
+ bucketing_key = format(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
117
+ bucket_value = generate_bucket_value(bucketing_key)
118
+
119
+ message = "Assigned bucket #{bucket_value} to user '#{user_id}' with bucketing ID: '#{bucketing_id}'."
120
+ @logger.log(Logger::DEBUG, message)
121
+
122
+ traffic_allocations.each do |traffic_allocation|
123
+ current_end_of_range = traffic_allocation['endOfRange']
124
+ if bucket_value < current_end_of_range
125
+ entity_id = traffic_allocation['entityId']
126
+ return entity_id, decide_reasons
127
+ end
128
+ end
129
+
130
+ [nil, decide_reasons]
131
+ end
132
+
133
+ private
134
+
135
+ def generate_bucket_value(bucketing_key)
136
+ # Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE).
137
+ #
138
+ # bucketing_key - String - Value used to generate bucket value
139
+ #
140
+ # Returns bucket value corresponding to the provided bucketing key.
141
+
142
+ ratio = generate_unsigned_hash_code_32_bit(bucketing_key).to_f / MAX_HASH_VALUE
143
+ (ratio * MAX_TRAFFIC_VALUE).to_i
144
+ end
145
+
146
+ def generate_unsigned_hash_code_32_bit(bucketing_key)
147
+ # Helper function to retreive hash code
148
+ #
149
+ # bucketing_key - String - Value used for the key of the murmur hash
150
+ #
151
+ # Returns hash code which is a 32 bit unsigned integer.
152
+
153
+ MurmurHash3::V32.str_hash(bucketing_key, @bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE
154
+ end
155
+ end
156
+ end
@@ -1,123 +1,123 @@
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
- OPERATORS = [AND_CONDITION, OR_CONDITION, NOT_CONDITION].freeze
32
-
33
- module_function
34
-
35
- def evaluate(conditions, leaf_evaluator)
36
- # Top level method to evaluate audience conditions.
37
- #
38
- # conditions - Nested array of and/or conditions.
39
- # Example: ['and', operand_1, ['or', operand_2, operand_3]]
40
- #
41
- # leaf_evaluator - Function which will be called to evaluate leaf condition values.
42
- #
43
- # Returns boolean if the given user attributes match/don't match the given conditions,
44
- # nil if the given conditions are invalid or can't be evaluated.
45
-
46
- if conditions.is_a? Array
47
- first_operator = conditions[0]
48
- rest_of_conditions = conditions[1..-1]
49
-
50
- # Operator to apply is not explicit - assume 'or'
51
- unless EVALUATORS_BY_OPERATOR_TYPE.include?(conditions[0])
52
- first_operator = OR_CONDITION
53
- rest_of_conditions = conditions
54
- end
55
-
56
- return send(EVALUATORS_BY_OPERATOR_TYPE[first_operator], rest_of_conditions, leaf_evaluator)
57
- end
58
-
59
- leaf_evaluator.call(conditions)
60
- end
61
-
62
- def and_evaluator(conditions, leaf_evaluator)
63
- # Evaluates an array of conditions as if the evaluator had been applied
64
- # to each entry and the results AND-ed together.
65
- #
66
- # conditions - Array of conditions ex: [operand_1, operand_2]
67
- #
68
- # leaf_evaluator - Function which will be called to evaluate leaf condition values.
69
- #
70
- # Returns boolean if the user attributes match/don't match the given conditions,
71
- # nil if the user attributes and conditions can't be evaluated.
72
-
73
- found_nil = false
74
- conditions.each do |condition|
75
- result = evaluate(condition, leaf_evaluator)
76
- return result if result == false
77
-
78
- found_nil = true if result.nil?
79
- end
80
-
81
- found_nil ? nil : true
82
- end
83
-
84
- def not_evaluator(single_condition, leaf_evaluator)
85
- # Evaluates an array of conditions as if the evaluator had been applied
86
- # to a single entry and NOT was applied to the result.
87
- #
88
- # single_condition - Array of a single condition ex: [operand_1]
89
- #
90
- # leaf_evaluator - Function which will be called to evaluate leaf condition values.
91
- #
92
- # Returns boolean if the user attributes match/don't match the given conditions,
93
- # nil if the user attributes and conditions can't be evaluated.
94
-
95
- return nil if single_condition.empty?
96
-
97
- result = evaluate(single_condition[0], leaf_evaluator)
98
- result.nil? ? nil : !result
99
- end
100
-
101
- def or_evaluator(conditions, leaf_evaluator)
102
- # Evaluates an array of conditions as if the evaluator had been applied
103
- # to each entry and the results OR-ed together.
104
- #
105
- # conditions - Array of conditions ex: [operand_1, operand_2]
106
- #
107
- # leaf_evaluator - Function which will be called to evaluate leaf condition values.
108
- #
109
- # Returns boolean if the user attributes match/don't match the given conditions,
110
- # nil if the user attributes and conditions can't be evaluated.
111
-
112
- found_nil = false
113
- conditions.each do |condition|
114
- result = evaluate(condition, leaf_evaluator)
115
- return result if result == true
116
-
117
- found_nil = true if result.nil?
118
- end
119
-
120
- found_nil ? nil : false
121
- end
122
- end
123
- end
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2019, 2022, 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
+ OPERATORS = [AND_CONDITION, OR_CONDITION, NOT_CONDITION].freeze
32
+
33
+ module_function
34
+
35
+ def evaluate(conditions, leaf_evaluator)
36
+ # Top level method to evaluate audience conditions.
37
+ #
38
+ # conditions - Nested array of and/or conditions.
39
+ # Example: ['and', operand_1, ['or', operand_2, operand_3]]
40
+ #
41
+ # leaf_evaluator - Function which will be called to evaluate leaf condition values.
42
+ #
43
+ # Returns boolean if the given user attributes match/don't match the given conditions,
44
+ # nil if the given conditions are invalid or can't be evaluated.
45
+
46
+ if conditions.is_a? Array
47
+ first_operator = conditions[0]
48
+ rest_of_conditions = conditions[1..]
49
+
50
+ # Operator to apply is not explicit - assume 'or'
51
+ unless EVALUATORS_BY_OPERATOR_TYPE.include?(conditions[0])
52
+ first_operator = OR_CONDITION
53
+ rest_of_conditions = conditions
54
+ end
55
+
56
+ return send(EVALUATORS_BY_OPERATOR_TYPE[first_operator], rest_of_conditions, leaf_evaluator)
57
+ end
58
+
59
+ leaf_evaluator.call(conditions)
60
+ end
61
+
62
+ def and_evaluator(conditions, leaf_evaluator)
63
+ # Evaluates an array of conditions as if the evaluator had been applied
64
+ # to each entry and the results AND-ed together.
65
+ #
66
+ # conditions - Array of conditions ex: [operand_1, operand_2]
67
+ #
68
+ # leaf_evaluator - Function which will be called to evaluate leaf condition values.
69
+ #
70
+ # Returns boolean if the user attributes match/don't match the given conditions,
71
+ # nil if the user attributes and conditions can't be evaluated.
72
+
73
+ found_nil = false
74
+ conditions.each do |condition|
75
+ result = evaluate(condition, leaf_evaluator)
76
+ return result if result == false
77
+
78
+ found_nil = true if result.nil?
79
+ end
80
+
81
+ found_nil ? nil : true
82
+ end
83
+
84
+ def not_evaluator(single_condition, leaf_evaluator)
85
+ # Evaluates an array of conditions as if the evaluator had been applied
86
+ # to a single entry and NOT was applied to the result.
87
+ #
88
+ # single_condition - Array of a single condition ex: [operand_1]
89
+ #
90
+ # leaf_evaluator - Function which will be called to evaluate leaf condition values.
91
+ #
92
+ # Returns boolean if the user attributes match/don't match the given conditions,
93
+ # nil if the user attributes and conditions can't be evaluated.
94
+
95
+ return nil if single_condition.empty?
96
+
97
+ result = evaluate(single_condition[0], leaf_evaluator)
98
+ result.nil? ? nil : !result
99
+ end
100
+
101
+ def or_evaluator(conditions, leaf_evaluator)
102
+ # Evaluates an array of conditions as if the evaluator had been applied
103
+ # to each entry and the results OR-ed together.
104
+ #
105
+ # conditions - Array of conditions ex: [operand_1, operand_2]
106
+ #
107
+ # leaf_evaluator - Function which will be called to evaluate leaf condition values.
108
+ #
109
+ # Returns boolean if the user attributes match/don't match the given conditions,
110
+ # nil if the user attributes and conditions can't be evaluated.
111
+
112
+ found_nil = false
113
+ conditions.each do |condition|
114
+ result = evaluate(condition, leaf_evaluator)
115
+ return result if result == true
116
+
117
+ found_nil = true if result.nil?
118
+ end
119
+
120
+ found_nil ? nil : false
121
+ end
122
+ end
123
+ end