hackle-ruby-sdk 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hackle/client.rb +186 -87
- data/lib/hackle/config.rb +59 -17
- data/lib/hackle/decision.rb +113 -0
- data/lib/hackle/event.rb +89 -0
- data/lib/hackle/internal/clock/clock.rb +47 -0
- data/lib/hackle/internal/concurrent/executors.rb +20 -0
- data/lib/hackle/internal/concurrent/schedule/scheduler.rb +12 -0
- data/lib/hackle/internal/concurrent/schedule/timer_scheduler.rb +30 -0
- data/lib/hackle/internal/config/parameter_config.rb +50 -0
- data/lib/hackle/internal/core/hackle_core.rb +182 -0
- data/lib/hackle/{decision → internal/evaluation/bucketer}/bucketer.rb +17 -15
- data/lib/hackle/internal/evaluation/evaluator/contextual/contextual_evaluator.rb +29 -0
- data/lib/hackle/internal/evaluation/evaluator/delegating/delegating_evaluator.rb +26 -0
- data/lib/hackle/internal/evaluation/evaluator/evaluator.rb +117 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory.rb +67 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluator.rb +172 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_flow_evaluator.rb +241 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_resolver.rb +166 -0
- data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_determiner.rb +48 -0
- data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_evaluator.rb +174 -0
- data/lib/hackle/internal/evaluation/flow/evaluation_flow.rb +49 -0
- data/lib/hackle/internal/evaluation/flow/flow_evaluator.rb +11 -0
- data/lib/hackle/internal/evaluation/match/condition/condition_matcher.rb +11 -0
- data/lib/hackle/internal/evaluation/match/condition/condition_matcher_factory.rb +53 -0
- data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_condition_matcher.rb +29 -0
- data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_evaluator_matcher.rb +135 -0
- data/lib/hackle/internal/evaluation/match/condition/segment/segment_condition_matcher.rb +67 -0
- data/lib/hackle/internal/evaluation/match/condition/user/user_condition_matcher.rb +44 -0
- data/lib/hackle/internal/evaluation/match/operator/operator_matcher.rb +185 -0
- data/lib/hackle/internal/evaluation/match/operator/operator_matcher_factory.rb +31 -0
- data/lib/hackle/internal/evaluation/match/target/target_matcher.rb +31 -0
- data/lib/hackle/internal/evaluation/match/value/value_matcher.rb +96 -0
- data/lib/hackle/internal/evaluation/match/value/value_matcher_factory.rb +28 -0
- data/lib/hackle/internal/evaluation/match/value/value_operator_matcher.rb +59 -0
- data/lib/hackle/internal/event/user_event.rb +187 -0
- data/lib/hackle/internal/event/user_event_dispatcher.rb +156 -0
- data/lib/hackle/internal/event/user_event_factory.rb +58 -0
- data/lib/hackle/internal/event/user_event_processor.rb +181 -0
- data/lib/hackle/internal/http/http.rb +28 -0
- data/lib/hackle/internal/http/http_client.rb +48 -0
- data/lib/hackle/internal/identifiers/identifier_builder.rb +67 -0
- data/lib/hackle/internal/logger/logger.rb +31 -0
- data/lib/hackle/internal/model/action.rb +57 -0
- data/lib/hackle/internal/model/bucket.rb +58 -0
- data/lib/hackle/internal/model/container.rb +47 -0
- data/lib/hackle/internal/model/decision_reason.rb +31 -0
- data/lib/hackle/{models → internal/model}/event_type.rb +5 -8
- data/lib/hackle/internal/model/experiment.rb +194 -0
- data/lib/hackle/internal/model/parameter_configuration.rb +19 -0
- data/lib/hackle/internal/model/remote_config_parameter.rb +76 -0
- data/lib/hackle/internal/model/sdk.rb +23 -0
- data/lib/hackle/internal/model/segment.rb +61 -0
- data/lib/hackle/internal/model/target.rb +203 -0
- data/lib/hackle/internal/model/target_rule.rb +19 -0
- data/lib/hackle/internal/model/targeting.rb +45 -0
- data/lib/hackle/internal/model/value_type.rb +75 -0
- data/lib/hackle/internal/model/variation.rb +27 -0
- data/lib/hackle/internal/model/version.rb +153 -0
- data/lib/hackle/internal/properties/properties_builder.rb +101 -0
- data/lib/hackle/internal/user/hackle_user.rb +74 -0
- data/lib/hackle/internal/user/hackle_user_resolver.rb +27 -0
- data/lib/hackle/internal/workspace/http_workspace_fetcher.rb +50 -0
- data/lib/hackle/internal/workspace/polling_workspace_fetcher.rb +62 -0
- data/lib/hackle/internal/workspace/workspace.rb +353 -0
- data/lib/hackle/internal/workspace/workspace_fetcher.rb +18 -0
- data/lib/hackle/remote_config.rb +55 -0
- data/lib/hackle/user.rb +124 -0
- data/lib/hackle/version.rb +1 -11
- data/lib/hackle.rb +4 -69
- metadata +123 -53
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.travis.yml +0 -7
- data/Gemfile +0 -6
- data/README.md +0 -33
- data/Rakefile +0 -6
- data/hackle-ruby-sdk.gemspec +0 -29
- data/lib/hackle/decision/decider.rb +0 -69
- data/lib/hackle/events/event_dispatcher.rb +0 -96
- data/lib/hackle/events/event_processor.rb +0 -126
- data/lib/hackle/events/user_event.rb +0 -61
- data/lib/hackle/http/http.rb +0 -37
- data/lib/hackle/models/bucket.rb +0 -26
- data/lib/hackle/models/event.rb +0 -26
- data/lib/hackle/models/experiment.rb +0 -69
- data/lib/hackle/models/slot.rb +0 -22
- data/lib/hackle/models/user.rb +0 -24
- data/lib/hackle/models/variation.rb +0 -21
- data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
- data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -47
- data/lib/hackle/workspaces/workspace.rb +0 -100
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hackle/internal/model/value_type'
|
|
4
|
+
require 'hackle/internal/model/decision_reason'
|
|
5
|
+
require 'hackle/internal/properties/properties_builder'
|
|
6
|
+
|
|
7
|
+
module Hackle
|
|
8
|
+
class RemoteConfigEvaluator
|
|
9
|
+
include ContextualEvaluator
|
|
10
|
+
|
|
11
|
+
# @param target_rule_determiner [RemoteConfigTargetRuleDeterminer]
|
|
12
|
+
def initialize(target_rule_determiner:)
|
|
13
|
+
# @type [RemoteConfigTargetRuleDeterminer]
|
|
14
|
+
@target_rule_determiner = target_rule_determiner
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def supports?(request)
|
|
18
|
+
request.is_a?(RemoteConfigRequest)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param request [RemoteConfigRequest]
|
|
22
|
+
# @param context [EvaluatorContext]
|
|
23
|
+
# @return [RemoteConfigEvaluation]
|
|
24
|
+
def evaluate_internal(request, context)
|
|
25
|
+
properties_builder = PropertiesBuilder.new
|
|
26
|
+
.add('requestValueType', request.required_type.name)
|
|
27
|
+
.add('requestDefaultValue', request.default_value)
|
|
28
|
+
|
|
29
|
+
if request.user.identifiers[request.parameter.identifier_type].nil?
|
|
30
|
+
return RemoteConfigEvaluation.create_default(
|
|
31
|
+
request, context, DecisionReason::IDENTIFIER_NOT_FOUND, properties_builder
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
target_rule = @target_rule_determiner.determine_or_nil(request, context)
|
|
36
|
+
unless target_rule.nil?
|
|
37
|
+
properties_builder.add('targetRuleKey', target_rule.key)
|
|
38
|
+
properties_builder.add('targetRuleName', target_rule.name)
|
|
39
|
+
return evaluation(request, context, target_rule.value, DecisionReason::TARGET_RULE_MATCH, properties_builder)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
evaluation(request, context, request.parameter.default_value, DecisionReason::DEFAULT_RULE, properties_builder)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# @param request [RemoteConfigRequest]
|
|
48
|
+
# @param context [EvaluatorContext]
|
|
49
|
+
# @param parameter_value [RemoteConfigValue]
|
|
50
|
+
# @param reason [String]
|
|
51
|
+
# @param properties_builder [PropertiesBuilder]
|
|
52
|
+
# @return [RemoteConfigEvaluation]
|
|
53
|
+
def evaluation(request, context, parameter_value, reason, properties_builder)
|
|
54
|
+
if valid?(request.required_type, parameter_value.raw_value)
|
|
55
|
+
RemoteConfigEvaluation.create(request, context, parameter_value.id, parameter_value.raw_value, reason,
|
|
56
|
+
properties_builder)
|
|
57
|
+
else
|
|
58
|
+
RemoteConfigEvaluation.create_default(request, context, DecisionReason::TYPE_MISMATCH, properties_builder)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param required_type [ValueType]
|
|
63
|
+
# @param value [Object]
|
|
64
|
+
# @return [boolean]
|
|
65
|
+
def valid?(required_type, value)
|
|
66
|
+
case required_type
|
|
67
|
+
when ValueType::NULL
|
|
68
|
+
true
|
|
69
|
+
when ValueType::STRING
|
|
70
|
+
value.is_a?(String)
|
|
71
|
+
when ValueType::NUMBER
|
|
72
|
+
value.is_a?(Numeric)
|
|
73
|
+
when ValueType::BOOLEAN
|
|
74
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
75
|
+
else
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class RemoteConfigRequest < EvaluatorRequest
|
|
82
|
+
# @return [RemoteConfigParameter]
|
|
83
|
+
attr_reader :parameter
|
|
84
|
+
|
|
85
|
+
# @return [ValueType]
|
|
86
|
+
attr_reader :required_type
|
|
87
|
+
|
|
88
|
+
# @return [Object, nil]
|
|
89
|
+
attr_reader :default_value
|
|
90
|
+
|
|
91
|
+
# @param key [EvaluatorKey]
|
|
92
|
+
# @param workspace [Workspace]
|
|
93
|
+
# @param user [HackleUser]
|
|
94
|
+
# @param parameter [RemoteConfigParameter]
|
|
95
|
+
# @param required_type [ValueType]
|
|
96
|
+
# @param default_value [Object, nil]
|
|
97
|
+
def initialize(key:, workspace:, user:, parameter:, required_type:, default_value:)
|
|
98
|
+
super(key: key, workspace: workspace, user: user)
|
|
99
|
+
@parameter = parameter
|
|
100
|
+
@required_type = required_type
|
|
101
|
+
@default_value = default_value
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @param workspace [Workspace]
|
|
105
|
+
# @param user [HackleUser]
|
|
106
|
+
# @param parameter [RemoteConfigParameter]
|
|
107
|
+
# @param required_type [ValueType]
|
|
108
|
+
# @param default_value [Object, nil]
|
|
109
|
+
def self.create(workspace, user, parameter, required_type, default_value)
|
|
110
|
+
RemoteConfigRequest.new(
|
|
111
|
+
key: EvaluatorKey.new(type: 'REMOTE_CONFIG', id: parameter.id),
|
|
112
|
+
workspace: workspace,
|
|
113
|
+
user: user,
|
|
114
|
+
parameter: parameter,
|
|
115
|
+
required_type: required_type,
|
|
116
|
+
default_value: default_value
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class RemoteConfigEvaluation < EvaluatorEvaluation
|
|
122
|
+
# @return [RemoteConfigParameter]
|
|
123
|
+
attr_reader :parameter
|
|
124
|
+
|
|
125
|
+
# @return [Integer, nil]
|
|
126
|
+
attr_reader :value_id
|
|
127
|
+
|
|
128
|
+
# @return [Object]
|
|
129
|
+
attr_reader :value
|
|
130
|
+
|
|
131
|
+
# @return [Hash<String => Object>]
|
|
132
|
+
attr_reader :properties
|
|
133
|
+
|
|
134
|
+
# @param reason [String]
|
|
135
|
+
# @param target_evaluations [Array<EvaluatorEvaluation>]
|
|
136
|
+
# @param parameter [RemoteConfigParameter]
|
|
137
|
+
# @param value_id [Integer, nil]
|
|
138
|
+
# @param value [Object, nil]
|
|
139
|
+
# @param properties [Hash<String => Object>]
|
|
140
|
+
def initialize(reason:, target_evaluations:, parameter:, value_id:, value:, properties:)
|
|
141
|
+
super(reason: reason, target_evaluations: target_evaluations)
|
|
142
|
+
@parameter = parameter
|
|
143
|
+
@value_id = value_id
|
|
144
|
+
@value = value
|
|
145
|
+
@properties = properties
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @param request [RemoteConfigRequest]
|
|
149
|
+
# @param context [EvaluatorContext]
|
|
150
|
+
# @param value_id [Integer, nil]
|
|
151
|
+
# @param value [Object, nil]
|
|
152
|
+
# @param reason [String]
|
|
153
|
+
# @param properties_builder [PropertiesBuilder]
|
|
154
|
+
def self.create(request, context, value_id, value, reason, properties_builder)
|
|
155
|
+
properties_builder.add('returnValue', value)
|
|
156
|
+
RemoteConfigEvaluation.new(
|
|
157
|
+
reason: reason,
|
|
158
|
+
target_evaluations: context.evaluations,
|
|
159
|
+
parameter: request.parameter,
|
|
160
|
+
value_id: value_id,
|
|
161
|
+
value: value,
|
|
162
|
+
properties: properties_builder.build
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @param request [RemoteConfigRequest]
|
|
167
|
+
# @param context [EvaluatorContext]
|
|
168
|
+
# @param reason [String]
|
|
169
|
+
# @param properties_builder [PropertiesBuilder]
|
|
170
|
+
def self.create_default(request, context, reason, properties_builder)
|
|
171
|
+
create(request, context, nil, request.default_value, reason, properties_builder)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hackle
|
|
4
|
+
module EvaluationFlow
|
|
5
|
+
# @param request [EvaluatorRequest]
|
|
6
|
+
# @param context [EvaluatorContext]
|
|
7
|
+
# @return [EvaluatorEvaluation, nil]
|
|
8
|
+
def evaluate(request, context) end
|
|
9
|
+
|
|
10
|
+
# @param evaluators [Array<FlowEvaluator>]
|
|
11
|
+
# @return [Hackle::EvaluationFlow]
|
|
12
|
+
def self.create(evaluators)
|
|
13
|
+
flow = End.new
|
|
14
|
+
evaluators.reverse_each do |evaluator|
|
|
15
|
+
flow = Decision.new(evaluator, flow)
|
|
16
|
+
end
|
|
17
|
+
flow
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class End
|
|
21
|
+
include EvaluationFlow
|
|
22
|
+
|
|
23
|
+
def evaluate(request, context)
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Decision
|
|
29
|
+
include EvaluationFlow
|
|
30
|
+
|
|
31
|
+
# @return [FlowEvaluator]
|
|
32
|
+
attr_reader :evaluator
|
|
33
|
+
|
|
34
|
+
# @return [EvaluationFlow]
|
|
35
|
+
attr_reader :next_flow
|
|
36
|
+
|
|
37
|
+
# @param evaluator [FlowEvaluator]
|
|
38
|
+
# @param next_flow [EvaluationFlow]
|
|
39
|
+
def initialize(evaluator, next_flow)
|
|
40
|
+
@evaluator = evaluator
|
|
41
|
+
@next_flow = next_flow
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def evaluate(request, context)
|
|
45
|
+
evaluator.evaluate(request, context, next_flow)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hackle
|
|
4
|
+
module FlowEvaluator
|
|
5
|
+
# @param request [EvaluatorRequest]
|
|
6
|
+
# @param context [EvaluatorContext]
|
|
7
|
+
# @param next_flow [EvaluationFlow]
|
|
8
|
+
# @return [EvaluatorEvaluation, nil]
|
|
9
|
+
def evaluate(request, context, next_flow) end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hackle
|
|
4
|
+
module ConditionMatcher
|
|
5
|
+
# @param request [EvaluatorRequest]
|
|
6
|
+
# @param context [EvaluatorContext]
|
|
7
|
+
# @param condition [TargetCondition]
|
|
8
|
+
# @return [boolean]
|
|
9
|
+
def matches(request, context, condition) end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hackle/internal/evaluation/match/value/value_operator_matcher'
|
|
4
|
+
require 'hackle/internal/evaluation/match/value/value_matcher_factory'
|
|
5
|
+
require 'hackle/internal/evaluation/match/operator/operator_matcher_factory'
|
|
6
|
+
require 'hackle/internal/evaluation/match/condition/user/user_condition_matcher'
|
|
7
|
+
require 'hackle/internal/evaluation/match/condition/segment/segment_condition_matcher'
|
|
8
|
+
require 'hackle/internal/evaluation/match/condition/experiment/experiment_condition_matcher'
|
|
9
|
+
require 'hackle/internal/evaluation/match/condition/experiment/experiment_evaluator_matcher'
|
|
10
|
+
|
|
11
|
+
module Hackle
|
|
12
|
+
class ConditionMatcherFactory
|
|
13
|
+
# @param evaluator [Evaluator]
|
|
14
|
+
def initialize(evaluator:)
|
|
15
|
+
value_operator_matcher = ValueOperatorMatcher.new(
|
|
16
|
+
value_matcher_factory: ValueMatcherFactory.new,
|
|
17
|
+
operator_matcher_factory: OperatorMatcherFactory.new
|
|
18
|
+
)
|
|
19
|
+
@user_condition_matcher = UserConditionMatcher.new(
|
|
20
|
+
user_value_resolver: UserValueResolver.new,
|
|
21
|
+
value_operator_matcher: value_operator_matcher
|
|
22
|
+
)
|
|
23
|
+
@segment_condition_matcher = SegmentConditionMatcher.new(
|
|
24
|
+
segment_matcher: SegmentMatcher.new(user_condition_matcher: @user_condition_matcher)
|
|
25
|
+
)
|
|
26
|
+
@experiment_condition_matcher = ExperimentConditionMatcher.new(
|
|
27
|
+
ab_test_matcher: AbTestEvaluatorMatcher.new(
|
|
28
|
+
evaluator: evaluator,
|
|
29
|
+
value_operator_matcher: value_operator_matcher
|
|
30
|
+
),
|
|
31
|
+
feature_flag_matcher: FeatureFlagEvaluatorMatcher.new(
|
|
32
|
+
evaluator: evaluator,
|
|
33
|
+
value_operator_matcher: value_operator_matcher
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param key_type [TargetKeyType]
|
|
39
|
+
# @return [ConditionMatcher]
|
|
40
|
+
def get(key_type)
|
|
41
|
+
case key_type
|
|
42
|
+
when TargetKeyType::USER_ID, TargetKeyType::USER_PROPERTY, TargetKeyType::HACKLE_PROPERTY
|
|
43
|
+
@user_condition_matcher
|
|
44
|
+
when TargetKeyType::SEGMENT
|
|
45
|
+
@segment_condition_matcher
|
|
46
|
+
when TargetKeyType::AB_TEST, TargetKeyType::FEATURE_FLAG
|
|
47
|
+
@experiment_condition_matcher
|
|
48
|
+
else
|
|
49
|
+
raise ArgumentError, "unsupported TargetKeyType [#{key_type}]"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_condition_matcher.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hackle/internal/evaluation/match/condition/condition_matcher'
|
|
4
|
+
|
|
5
|
+
module Hackle
|
|
6
|
+
class ExperimentConditionMatcher
|
|
7
|
+
include ConditionMatcher
|
|
8
|
+
|
|
9
|
+
# @param ab_test_matcher [ExperimentEvaluatorMatcher]
|
|
10
|
+
# @param feature_flag_matcher [ExperimentEvaluatorMatcher]
|
|
11
|
+
def initialize(ab_test_matcher:, feature_flag_matcher:)
|
|
12
|
+
# @type [ExperimentEvaluatorMatcher]
|
|
13
|
+
@ab_test_matcher = ab_test_matcher
|
|
14
|
+
# @type [ExperimentEvaluatorMatcher]
|
|
15
|
+
@feature_flag_matcher = feature_flag_matcher
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def matches(request, context, condition)
|
|
19
|
+
case condition.key.type
|
|
20
|
+
when TargetKeyType::AB_TEST
|
|
21
|
+
@ab_test_matcher.matches(request, context, condition)
|
|
22
|
+
when TargetKeyType::FEATURE_FLAG
|
|
23
|
+
@feature_flag_matcher.matches(request, context, condition)
|
|
24
|
+
else
|
|
25
|
+
raise ArgumentError, "unsupported TargetKeyType [#{condition.key.type}]"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_evaluator_matcher.rb
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hackle/internal/model/decision_reason'
|
|
4
|
+
require 'hackle/internal/evaluation/evaluator/experiment/experiment_evaluator'
|
|
5
|
+
|
|
6
|
+
module Hackle
|
|
7
|
+
class ExperimentEvaluatorMatcher
|
|
8
|
+
# @param evaluator [Evaluator]
|
|
9
|
+
def initialize(evaluator:)
|
|
10
|
+
# @type [Evaluator]
|
|
11
|
+
@evaluator = evaluator
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param request [EvaluatorRequest]
|
|
15
|
+
# @param context [EvaluatorContext]
|
|
16
|
+
# @param condition [TargetCondition]
|
|
17
|
+
# @return [boolean]
|
|
18
|
+
def matches(request, context, condition)
|
|
19
|
+
key = Integer(condition.key.name, exception: false)
|
|
20
|
+
raise ArgumentError, "invalid key [#{condition.key.type}, #{condition.key.name}]" if key.nil?
|
|
21
|
+
|
|
22
|
+
experiment = experiment_or_nil(request, key)
|
|
23
|
+
return false if experiment.nil?
|
|
24
|
+
|
|
25
|
+
evaluation = get_evaluation_or_nil(context, experiment)
|
|
26
|
+
evaluation = evaluate(request, context, experiment) if evaluation.nil?
|
|
27
|
+
|
|
28
|
+
evaluation_matches(evaluation, condition)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# @param request [EvaluatorRequest]
|
|
34
|
+
# @param context [EvaluatorContext]
|
|
35
|
+
# @param experiment [Experiment]
|
|
36
|
+
# @return [Hackle::ExperimentEvaluation]
|
|
37
|
+
def evaluate(request, context, experiment)
|
|
38
|
+
experiment_request = ExperimentRequest.create_by(request, experiment)
|
|
39
|
+
evaluation = @evaluator.evaluate(experiment_request, context)
|
|
40
|
+
unless evaluation.is_a?(ExperimentEvaluation)
|
|
41
|
+
raise ArgumentError, "unexpected evaluation: #{evaluation.class} (expected: ExperimentEvaluation)"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
resolved_evaluation = resolve_evaluation(request, evaluation)
|
|
45
|
+
context.add_evaluation(resolved_evaluation)
|
|
46
|
+
resolved_evaluation
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param context [EvaluatorContext]
|
|
50
|
+
# @param experiment [Experiment]
|
|
51
|
+
# @return [ExperimentEvaluation, nil]
|
|
52
|
+
def get_evaluation_or_nil(context, experiment)
|
|
53
|
+
context.evaluations.each do |evaluation|
|
|
54
|
+
return evaluation if evaluation.is_a?(ExperimentEvaluation) && evaluation.experiment.id == experiment.id
|
|
55
|
+
end
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @abstract
|
|
60
|
+
# @param request [EvaluatorRequest]
|
|
61
|
+
# @param key [Integer]
|
|
62
|
+
# @return [Experiment, nil]
|
|
63
|
+
def experiment_or_nil(request, key) end
|
|
64
|
+
|
|
65
|
+
# @abstract
|
|
66
|
+
# @param request [EvaluatorRequest]
|
|
67
|
+
# @param evaluation [ExperimentEvaluation]
|
|
68
|
+
# @return [ExperimentEvaluation]
|
|
69
|
+
def resolve_evaluation(request, evaluation) end
|
|
70
|
+
|
|
71
|
+
# @abstract
|
|
72
|
+
# @param evaluation [ExperimentEvaluation]
|
|
73
|
+
# @param condition [TargetCondition]
|
|
74
|
+
# @return [boolean]
|
|
75
|
+
def evaluation_matches(evaluation, condition) end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class AbTestEvaluatorMatcher < ExperimentEvaluatorMatcher
|
|
79
|
+
# @param evaluator [Evaluator]
|
|
80
|
+
# @param value_operator_matcher [ValueOperatorMatcher]
|
|
81
|
+
def initialize(evaluator:, value_operator_matcher:)
|
|
82
|
+
super(evaluator: evaluator)
|
|
83
|
+
# @type [ValueOperatorMatcher]
|
|
84
|
+
@value_operator_matcher = value_operator_matcher
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def experiment_or_nil(request, key)
|
|
88
|
+
request.workspace.get_experiment_or_nil(key)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def resolve_evaluation(request, evaluation)
|
|
92
|
+
if request.is_a?(ExperimentRequest) && evaluation.reason == DecisionReason::TRAFFIC_ALLOCATED
|
|
93
|
+
return evaluation.with(DecisionReason::TRAFFIC_ALLOCATED_BY_TARGETING)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
evaluation
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def evaluation_matches(evaluation, condition)
|
|
100
|
+
return false if AB_TEST_MATCHED_REASONS.none?(evaluation.reason)
|
|
101
|
+
|
|
102
|
+
@value_operator_matcher.matches(evaluation.variation_key, condition.match)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
AB_TEST_MATCHED_REASONS = [
|
|
106
|
+
DecisionReason::OVERRIDDEN,
|
|
107
|
+
DecisionReason::TRAFFIC_ALLOCATED,
|
|
108
|
+
DecisionReason::TRAFFIC_ALLOCATED_BY_TARGETING,
|
|
109
|
+
DecisionReason::EXPERIMENT_COMPLETED
|
|
110
|
+
].freeze
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class FeatureFlagEvaluatorMatcher < ExperimentEvaluatorMatcher
|
|
114
|
+
# @param evaluator [Evaluator]
|
|
115
|
+
# @param value_operator_matcher [ValueOperatorMatcher]
|
|
116
|
+
def initialize(evaluator:, value_operator_matcher:)
|
|
117
|
+
super(evaluator: evaluator)
|
|
118
|
+
# @type [ValueOperatorMatcher]
|
|
119
|
+
@value_operator_matcher = value_operator_matcher
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def experiment_or_nil(request, key)
|
|
123
|
+
request.workspace.get_feature_flag_or_nil(key)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def resolve_evaluation(_request, evaluation)
|
|
127
|
+
evaluation
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def evaluation_matches(evaluation, condition)
|
|
131
|
+
on = evaluation.variation_key != 'A'
|
|
132
|
+
@value_operator_matcher.matches(on, condition.match)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hackle/internal/model/target'
|
|
4
|
+
require 'hackle/internal/evaluation/match/condition/condition_matcher'
|
|
5
|
+
|
|
6
|
+
module Hackle
|
|
7
|
+
class SegmentConditionMatcher
|
|
8
|
+
include ConditionMatcher
|
|
9
|
+
|
|
10
|
+
# @param segment_matcher [SegmentMatcher]
|
|
11
|
+
def initialize(segment_matcher:)
|
|
12
|
+
# @type [SegmentMatcher]
|
|
13
|
+
@segment_matcher = segment_matcher
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def matches(request, context, condition)
|
|
17
|
+
if condition.key.type != TargetKeyType::SEGMENT
|
|
18
|
+
raise ArgumentError, "unsupported TargetKeyType [#{condition.key.type}]"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
matches = condition.match.values.any? { |it| value_matches(request, context, it) }
|
|
22
|
+
TargetMatchType.matches(condition.match.type, matches)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# @param request [EvaluatorRequest]
|
|
28
|
+
# @param context [EvaluatorContext]
|
|
29
|
+
# @param value [Object]
|
|
30
|
+
# @return [boolean]
|
|
31
|
+
def value_matches(request, context, value)
|
|
32
|
+
segment_key = value
|
|
33
|
+
raise ArgumentError, "segment key [#{value}]" unless segment_key.is_a?(String)
|
|
34
|
+
|
|
35
|
+
segment = request.workspace.get_segment_or_nil(segment_key)
|
|
36
|
+
raise ArgumentError, "segment [#{segment_key}]" unless segment
|
|
37
|
+
|
|
38
|
+
@segment_matcher.matches(request, context, segment)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class SegmentMatcher
|
|
43
|
+
# @param user_condition_matcher [ConditionMatcher]
|
|
44
|
+
def initialize(user_condition_matcher:)
|
|
45
|
+
# @type [ConditionMatcher]
|
|
46
|
+
@user_condition_matcher = user_condition_matcher
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param request [EvaluatorRequest]
|
|
50
|
+
# @param context [EvaluatorContext]
|
|
51
|
+
# @param segment [Segment]
|
|
52
|
+
# @return [boolean]
|
|
53
|
+
def matches(request, context, segment)
|
|
54
|
+
segment.targets.any? { |it| target_matches(request, context, it) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# @param request [EvaluatorRequest]
|
|
60
|
+
# @param context [EvaluatorContext]
|
|
61
|
+
# @param target [Target]
|
|
62
|
+
# @return [boolean]
|
|
63
|
+
def target_matches(request, context, target)
|
|
64
|
+
target.conditions.all? { |it| @user_condition_matcher.matches(request, context, it) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hackle/internal/model/target'
|
|
4
|
+
require 'hackle/internal/evaluation/match/condition/condition_matcher'
|
|
5
|
+
|
|
6
|
+
module Hackle
|
|
7
|
+
class UserConditionMatcher
|
|
8
|
+
include ConditionMatcher
|
|
9
|
+
|
|
10
|
+
# @param user_value_resolver [UserValueResolver]
|
|
11
|
+
# @param value_operator_matcher [ValueOperatorMatcher]
|
|
12
|
+
def initialize(user_value_resolver:, value_operator_matcher:)
|
|
13
|
+
# @type [UserValueResolver]
|
|
14
|
+
@user_value_resolver = user_value_resolver
|
|
15
|
+
# @type [ValueOperatorMatcher]
|
|
16
|
+
@value_operator_matcher = value_operator_matcher
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def matches(request, context, condition)
|
|
20
|
+
user_value = @user_value_resolver.resolve_or_nil(request.user, condition.key)
|
|
21
|
+
return false if user_value.nil?
|
|
22
|
+
|
|
23
|
+
@value_operator_matcher.matches(user_value, condition.match)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class UserValueResolver
|
|
28
|
+
# @param user [HackleUser]
|
|
29
|
+
# @param key [TargetKey]
|
|
30
|
+
# @return [Object, nil]
|
|
31
|
+
def resolve_or_nil(user, key)
|
|
32
|
+
case key.type
|
|
33
|
+
when TargetKeyType::USER_ID
|
|
34
|
+
user.identifiers[key.name]
|
|
35
|
+
when TargetKeyType::USER_PROPERTY
|
|
36
|
+
user.properties[key.name]
|
|
37
|
+
when TargetKeyType::HACKLE_PROPERTY
|
|
38
|
+
nil
|
|
39
|
+
else
|
|
40
|
+
raise ArgumentError, "unsupported TargetKeyType: #{key.type}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|