hackle-ruby-sdk 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hackle/client.rb +186 -87
  3. data/lib/hackle/config.rb +59 -17
  4. data/lib/hackle/decision.rb +113 -0
  5. data/lib/hackle/event.rb +89 -0
  6. data/lib/hackle/internal/clock/clock.rb +47 -0
  7. data/lib/hackle/internal/concurrent/executors.rb +20 -0
  8. data/lib/hackle/internal/concurrent/schedule/scheduler.rb +12 -0
  9. data/lib/hackle/internal/concurrent/schedule/timer_scheduler.rb +30 -0
  10. data/lib/hackle/internal/config/parameter_config.rb +50 -0
  11. data/lib/hackle/internal/core/hackle_core.rb +182 -0
  12. data/lib/hackle/{decision → internal/evaluation/bucketer}/bucketer.rb +17 -15
  13. data/lib/hackle/internal/evaluation/evaluator/contextual/contextual_evaluator.rb +29 -0
  14. data/lib/hackle/internal/evaluation/evaluator/delegating/delegating_evaluator.rb +26 -0
  15. data/lib/hackle/internal/evaluation/evaluator/evaluator.rb +117 -0
  16. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory.rb +67 -0
  17. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluator.rb +172 -0
  18. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_flow_evaluator.rb +241 -0
  19. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_resolver.rb +166 -0
  20. data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_determiner.rb +48 -0
  21. data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_evaluator.rb +174 -0
  22. data/lib/hackle/internal/evaluation/flow/evaluation_flow.rb +49 -0
  23. data/lib/hackle/internal/evaluation/flow/flow_evaluator.rb +11 -0
  24. data/lib/hackle/internal/evaluation/match/condition/condition_matcher.rb +11 -0
  25. data/lib/hackle/internal/evaluation/match/condition/condition_matcher_factory.rb +53 -0
  26. data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_condition_matcher.rb +29 -0
  27. data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_evaluator_matcher.rb +135 -0
  28. data/lib/hackle/internal/evaluation/match/condition/segment/segment_condition_matcher.rb +67 -0
  29. data/lib/hackle/internal/evaluation/match/condition/user/user_condition_matcher.rb +44 -0
  30. data/lib/hackle/internal/evaluation/match/operator/operator_matcher.rb +185 -0
  31. data/lib/hackle/internal/evaluation/match/operator/operator_matcher_factory.rb +31 -0
  32. data/lib/hackle/internal/evaluation/match/target/target_matcher.rb +31 -0
  33. data/lib/hackle/internal/evaluation/match/value/value_matcher.rb +96 -0
  34. data/lib/hackle/internal/evaluation/match/value/value_matcher_factory.rb +28 -0
  35. data/lib/hackle/internal/evaluation/match/value/value_operator_matcher.rb +59 -0
  36. data/lib/hackle/internal/event/user_event.rb +187 -0
  37. data/lib/hackle/internal/event/user_event_dispatcher.rb +156 -0
  38. data/lib/hackle/internal/event/user_event_factory.rb +58 -0
  39. data/lib/hackle/internal/event/user_event_processor.rb +181 -0
  40. data/lib/hackle/internal/http/http.rb +28 -0
  41. data/lib/hackle/internal/http/http_client.rb +48 -0
  42. data/lib/hackle/internal/identifiers/identifier_builder.rb +67 -0
  43. data/lib/hackle/internal/logger/logger.rb +31 -0
  44. data/lib/hackle/internal/model/action.rb +57 -0
  45. data/lib/hackle/internal/model/bucket.rb +58 -0
  46. data/lib/hackle/internal/model/container.rb +47 -0
  47. data/lib/hackle/internal/model/decision_reason.rb +31 -0
  48. data/lib/hackle/{models → internal/model}/event_type.rb +5 -8
  49. data/lib/hackle/internal/model/experiment.rb +194 -0
  50. data/lib/hackle/internal/model/parameter_configuration.rb +19 -0
  51. data/lib/hackle/internal/model/remote_config_parameter.rb +76 -0
  52. data/lib/hackle/internal/model/sdk.rb +23 -0
  53. data/lib/hackle/internal/model/segment.rb +61 -0
  54. data/lib/hackle/internal/model/target.rb +203 -0
  55. data/lib/hackle/internal/model/target_rule.rb +19 -0
  56. data/lib/hackle/internal/model/targeting.rb +45 -0
  57. data/lib/hackle/internal/model/value_type.rb +75 -0
  58. data/lib/hackle/internal/model/variation.rb +27 -0
  59. data/lib/hackle/internal/model/version.rb +153 -0
  60. data/lib/hackle/internal/properties/properties_builder.rb +101 -0
  61. data/lib/hackle/internal/user/hackle_user.rb +74 -0
  62. data/lib/hackle/internal/user/hackle_user_resolver.rb +27 -0
  63. data/lib/hackle/internal/workspace/http_workspace_fetcher.rb +50 -0
  64. data/lib/hackle/internal/workspace/polling_workspace_fetcher.rb +62 -0
  65. data/lib/hackle/internal/workspace/workspace.rb +353 -0
  66. data/lib/hackle/internal/workspace/workspace_fetcher.rb +18 -0
  67. data/lib/hackle/remote_config.rb +55 -0
  68. data/lib/hackle/user.rb +124 -0
  69. data/lib/hackle/version.rb +1 -11
  70. data/lib/hackle.rb +4 -69
  71. metadata +123 -53
  72. data/.gitignore +0 -11
  73. data/.rspec +0 -2
  74. data/.travis.yml +0 -7
  75. data/Gemfile +0 -6
  76. data/README.md +0 -33
  77. data/Rakefile +0 -6
  78. data/hackle-ruby-sdk.gemspec +0 -29
  79. data/lib/hackle/decision/decider.rb +0 -69
  80. data/lib/hackle/events/event_dispatcher.rb +0 -96
  81. data/lib/hackle/events/event_processor.rb +0 -126
  82. data/lib/hackle/events/user_event.rb +0 -61
  83. data/lib/hackle/http/http.rb +0 -37
  84. data/lib/hackle/models/bucket.rb +0 -26
  85. data/lib/hackle/models/event.rb +0 -26
  86. data/lib/hackle/models/experiment.rb +0 -69
  87. data/lib/hackle/models/slot.rb +0 -22
  88. data/lib/hackle/models/user.rb +0 -24
  89. data/lib/hackle/models/variation.rb +0 -21
  90. data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
  91. data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -47
  92. data/lib/hackle/workspaces/workspace.rb +0 -100
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/model/decision_reason'
4
+ require 'hackle/internal/evaluation/evaluator/evaluator'
5
+ require 'hackle/internal/evaluation/evaluator/contextual/contextual_evaluator'
6
+
7
+ module Hackle
8
+ class ExperimentEvaluator
9
+ include ContextualEvaluator
10
+
11
+ # @param flow_factory [ExperimentEvaluationFlowFactory]
12
+ def initialize(flow_factory:)
13
+ # @type [ExperimentEvaluationFlowFactory]
14
+ @flow_factory = flow_factory
15
+ end
16
+
17
+ def supports?(request)
18
+ request.is_a?(ExperimentRequest)
19
+ end
20
+
21
+ # @param request [ExperimentRequest]
22
+ # @param context [EvaluatorContext]
23
+ # @return [ExperimentEvaluation]
24
+ def evaluate_internal(request, context)
25
+ evaluation_flow = @flow_factory.get(request.experiment.type)
26
+ evaluation = evaluation_flow.evaluate(request, context)
27
+ return evaluation unless evaluation.nil?
28
+
29
+ ExperimentEvaluation.create_default(request, context, DecisionReason::TRAFFIC_NOT_ALLOCATED)
30
+ end
31
+ end
32
+
33
+ class ExperimentRequest < EvaluatorRequest
34
+ # @return [Experiment]
35
+ attr_reader :experiment
36
+
37
+ # @return [String]
38
+ attr_reader :default_variation_key
39
+
40
+ # @param key [EvaluatorKey]
41
+ # @param workspace [Workspace]
42
+ # @param user [HackleUser]
43
+ # @param experiment [Experiment]
44
+ # @param default_variation_key [String]
45
+ def initialize(key:, workspace:, user:, experiment:, default_variation_key:)
46
+ super(key: key, workspace: workspace, user: user)
47
+ @experiment = experiment
48
+ @default_variation_key = default_variation_key
49
+ end
50
+
51
+ # @param workspace [Workspace]
52
+ # @param user [HackleUser]
53
+ # @param experiment [Experiment]
54
+ # @param default_variation_key [String]
55
+ # @return [Hackle::ExperimentRequest]
56
+ def self.create(workspace, user, experiment, default_variation_key)
57
+ ExperimentRequest.new(
58
+ key: EvaluatorKey.new(type: 'EXPERIMENT', id: experiment.id),
59
+ workspace: workspace,
60
+ user: user,
61
+ experiment: experiment,
62
+ default_variation_key: default_variation_key
63
+ )
64
+ end
65
+
66
+ # @param request [EvaluatorRequest]
67
+ # @param experiment [Experiment]
68
+ # @return [Hackle::ExperimentRequest]
69
+ def self.create_by(request, experiment)
70
+ ExperimentRequest.new(
71
+ key: EvaluatorKey.new(type: 'EXPERIMENT', id: experiment.id),
72
+ workspace: request.workspace,
73
+ user: request.user,
74
+ experiment: experiment,
75
+ default_variation_key: 'A'
76
+ )
77
+ end
78
+ end
79
+
80
+ class ExperimentEvaluation < EvaluatorEvaluation
81
+ # @return [Experiment]
82
+ attr_reader :experiment
83
+
84
+ # @return [Integer, nil]
85
+ attr_reader :variation_id
86
+
87
+ # @return [String]
88
+ attr_reader :variation_key
89
+
90
+ # @return [ParameterConfiguration, nil]
91
+ attr_reader :config
92
+
93
+ # @param reason [String]
94
+ # @param target_evaluations [Array<EvaluatorEvaluation]
95
+ # @param experiment [Experiment]
96
+ # @param variation_id [Integer, nil]
97
+ # @param variation_key [String]
98
+ # @param config [ParameterConfiguration, nil]
99
+ def initialize(reason:, target_evaluations:, experiment:, variation_id:, variation_key:, config:)
100
+ super(reason: reason, target_evaluations: target_evaluations)
101
+ @experiment = experiment
102
+ @variation_id = variation_id
103
+ @variation_key = variation_key
104
+ @config = config
105
+ end
106
+
107
+ # @return [ParameterConfig]
108
+ def parameter_config
109
+ return ParameterConfig.empty if @config.nil?
110
+
111
+ ParameterConfig.new(@config.parameters)
112
+ end
113
+
114
+ # @param reason [String]
115
+ # @return [Hackle::ExperimentEvaluation]
116
+ def with(reason)
117
+ ExperimentEvaluation.new(
118
+ reason: reason,
119
+ target_evaluations: target_evaluations,
120
+ experiment: experiment,
121
+ variation_id: variation_id,
122
+ variation_key: variation_key,
123
+ config: config
124
+ )
125
+ end
126
+
127
+ # @param request [ExperimentRequest]
128
+ # @param context [EvaluatorContext]
129
+ # @param variation [Variation]
130
+ # @param reason [String]
131
+ # @return [Hackle::ExperimentEvaluation]
132
+ def self.create(request, context, variation, reason)
133
+ configuration = configuration_or_nil(request.workspace, variation)
134
+ ExperimentEvaluation.new(
135
+ reason: reason,
136
+ target_evaluations: context.evaluations,
137
+ experiment: request.experiment,
138
+ variation_id: variation.id,
139
+ variation_key: variation.key,
140
+ config: configuration
141
+ )
142
+ end
143
+
144
+ # @param request [ExperimentRequest]
145
+ # @param context [EvaluatorContext]
146
+ # @param reason [String]
147
+ # @return [Hackle::ExperimentEvaluation]
148
+ def self.create_default(request, context, reason)
149
+ variation = request.experiment.get_variation_or_nil_by_key(request.default_variation_key)
150
+ return create(request, context, variation, reason) unless variation.nil?
151
+
152
+ ExperimentEvaluation.new(
153
+ reason: reason,
154
+ target_evaluations: context.evaluations,
155
+ experiment: request.experiment,
156
+ variation_id: nil,
157
+ variation_key: request.default_variation_key,
158
+ config: nil
159
+ )
160
+ end
161
+
162
+ # @param workspace [Workspace]
163
+ # @param variation [Variation]
164
+ # @return [Hackle::ParameterConfiguration, nil]
165
+ def self.configuration_or_nil(workspace, variation)
166
+ parameter_configuration_id = variation.parameter_configuration_id
167
+ return nil if parameter_configuration_id.nil?
168
+
169
+ workspace.get_parameter_configuration_or_nil(parameter_configuration_id)
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/evaluation/evaluator/experiment/experiment_evaluator'
4
+ require 'hackle/internal/evaluation/flow/evaluation_flow'
5
+ require 'hackle/internal/evaluation/flow/flow_evaluator'
6
+ require 'hackle/internal/model/experiment'
7
+ require 'hackle/internal/model/decision_reason'
8
+
9
+ module Hackle
10
+ module ExperimentFlow
11
+ include EvaluationFlow
12
+
13
+ # @param request [ExperimentRequest]
14
+ # @param context [EvaluatorContext]
15
+ # @return [ExperimentEvaluation, nil]
16
+ def evaluate(request, context) end
17
+ end
18
+
19
+ module ExperimentFlowEvaluator
20
+ include FlowEvaluator
21
+
22
+ # @param request [ExperimentRequest]
23
+ # @param context [EvaluatorContext]
24
+ # @param next_flow [ExperimentFlow]
25
+ # @return [ExperimentEvaluation, nil]
26
+ def evaluate(request, context, next_flow) end
27
+ end
28
+
29
+ class OverrideExperimentFlowEvaluator
30
+ include ExperimentFlowEvaluator
31
+
32
+ # @param override_resolver [ExperimentOverrideResolver]
33
+ def initialize(override_resolver:)
34
+ # @type [ExperimentOverrideResolver]
35
+ @override_resolver = override_resolver
36
+ end
37
+
38
+ def evaluate(request, context, next_flow)
39
+ overridden_variation = @override_resolver.resolve_or_nil(request, context)
40
+ return next_flow.evaluate(request, context) if overridden_variation.nil?
41
+
42
+ case request.experiment.type
43
+ when ExperimentType::AB_TEST
44
+ ExperimentEvaluation.create(request, context, overridden_variation, DecisionReason::OVERRIDDEN)
45
+ when ExperimentType::FEATURE_FLAG
46
+ ExperimentEvaluation.create(request, context, overridden_variation, DecisionReason::INDIVIDUAL_TARGET_MATCH)
47
+ else
48
+ raise ArgumentError, "unsupported experiment type [#{request.experiment.type}]"
49
+ end
50
+ end
51
+ end
52
+
53
+ class DraftExperimentFlowEvaluator
54
+ include ExperimentFlowEvaluator
55
+
56
+ def evaluate(request, context, next_flow)
57
+ return next_flow.evaluate(request, context) if request.experiment.status != ExperimentStatus::DRAFT
58
+
59
+ ExperimentEvaluation.create_default(request, context, DecisionReason::EXPERIMENT_DRAFT)
60
+ end
61
+ end
62
+
63
+ class PausedExperimentFlowEvaluator
64
+ include ExperimentFlowEvaluator
65
+
66
+ def evaluate(request, context, next_flow)
67
+ return next_flow.evaluate(request, context) if request.experiment.status != ExperimentStatus::PAUSED
68
+
69
+ case request.experiment.type
70
+ when ExperimentType::AB_TEST
71
+ ExperimentEvaluation.create_default(request, context, DecisionReason::EXPERIMENT_PAUSED)
72
+ when ExperimentType::FEATURE_FLAG
73
+ ExperimentEvaluation.create_default(request, context, DecisionReason::FEATURE_FLAG_INACTIVE)
74
+ else
75
+ raise ArgumentError, "unsupported experiment type [#{request.experiment.type}]"
76
+ end
77
+ end
78
+ end
79
+
80
+ class CompletedExperimentFlowEvaluator
81
+ include ExperimentFlowEvaluator
82
+
83
+ def evaluate(request, context, next_flow)
84
+ return next_flow.evaluate(request, context) if request.experiment.status != ExperimentStatus::COMPLETED
85
+
86
+ winner_variation = request.experiment.winner_variation_or_nil
87
+ raise ArgumentError, "winner variation [#{request.experiment.id}]" if winner_variation.nil?
88
+
89
+ ExperimentEvaluation.create(request, context, winner_variation, DecisionReason::EXPERIMENT_COMPLETED)
90
+ end
91
+ end
92
+
93
+ class TargetExperimentFlowEvaluator
94
+ include ExperimentFlowEvaluator
95
+
96
+ # @param target_determiner [ExperimentTargetDeterminer]
97
+ def initialize(target_determiner:)
98
+ # @type [ExperimentTargetDeterminer]
99
+ @target_determiner = target_determiner
100
+ end
101
+
102
+ def evaluate(request, context, next_flow)
103
+ if request.experiment.type != ExperimentType::AB_TEST
104
+ raise ArgumentError, "experiment type must be AB_TEST [#{request.experiment.id}]"
105
+ end
106
+
107
+ is_user_in_experiment_target = @target_determiner.user_in_experiment_target?(request, context)
108
+ if is_user_in_experiment_target
109
+ next_flow.evaluate(request, context)
110
+ else
111
+ ExperimentEvaluation.create_default(request, context, DecisionReason::NOT_IN_EXPERIMENT_TARGET)
112
+ end
113
+ end
114
+ end
115
+
116
+ class TrafficAllocateExperimentFlowEvaluator
117
+ include ExperimentFlowEvaluator
118
+
119
+ # @param action_resolver [ExperimentActionResolver]
120
+ def initialize(action_resolver:)
121
+ # @type [ExperimentActionResolver]
122
+ @action_resolver = action_resolver
123
+ end
124
+
125
+ def evaluate(request, context, _next_flow)
126
+ if request.experiment.status != ExperimentStatus::RUNNING
127
+ raise ArgumentError, "experiment status must be RUNNING [#{request.experiment.id}]"
128
+ end
129
+ if request.experiment.type != ExperimentType::AB_TEST
130
+ raise ArgumentError, "experiment type must be AB_TEST [#{request.experiment.id}]"
131
+ end
132
+
133
+ variation = @action_resolver.resolve_or_nil(request, request.experiment.default_rule)
134
+ if variation.nil?
135
+ return ExperimentEvaluation.create_default(request, context, DecisionReason::TRAFFIC_NOT_ALLOCATED)
136
+ end
137
+ if variation.is_dropped
138
+ return ExperimentEvaluation.create_default(request, context, DecisionReason::VARIATION_DROPPED)
139
+ end
140
+
141
+ ExperimentEvaluation.create(request, context, variation, DecisionReason::TRAFFIC_ALLOCATED)
142
+ end
143
+ end
144
+
145
+ class TargetRuleExperimentFlowEvaluator
146
+ include ExperimentFlowEvaluator
147
+
148
+ # @param target_rule_determiner [ExperimentTargetRuleDeterminer]
149
+ # @param action_resolver [ExperimentActionResolver]
150
+ def initialize(target_rule_determiner:, action_resolver:)
151
+ # @type [ExperimentTargetRuleDeterminer]
152
+ @target_rule_determiner = target_rule_determiner
153
+ # @type [ExperimentActionResolver]
154
+ @action_resolver = action_resolver
155
+ end
156
+
157
+ def evaluate(request, context, next_flow)
158
+ if request.experiment.status != ExperimentStatus::RUNNING
159
+ raise ArgumentError, "experiment status must be RUNNING [#{request.experiment.id}]"
160
+ end
161
+ if request.experiment.type != ExperimentType::FEATURE_FLAG
162
+ raise ArgumentError, "experiment type must be FEATURE_FLAG [#{request.experiment.id}]"
163
+ end
164
+
165
+ return next_flow.evaluate(request, context) if request.user.identifiers[request.experiment.identifier_type].nil?
166
+
167
+ target_rule = @target_rule_determiner.determine_target_rule_or_nil(request, context)
168
+ return next_flow.evaluate(request, context) if target_rule.nil?
169
+
170
+ variation = @action_resolver.resolve_or_nil(request, target_rule.action)
171
+ raise ArgumentError, "feature flag must decide the variation [#{request.experiment.id}]" if variation.nil?
172
+
173
+ ExperimentEvaluation.create(request, context, variation, DecisionReason::TARGET_RULE_MATCH)
174
+ end
175
+ end
176
+
177
+ class DefaultRuleExperimentFlowEvaluator
178
+ include ExperimentFlowEvaluator
179
+
180
+ # @param action_resolver [ExperimentActionResolver]
181
+ def initialize(action_resolver:)
182
+ # @type [ExperimentActionResolver]
183
+ @action_resolver = action_resolver
184
+ end
185
+
186
+ def evaluate(request, context, _next_flow)
187
+ if request.experiment.status != ExperimentStatus::RUNNING
188
+ raise ArgumentError, "experiment status must be RUNNING [#{request.experiment.id}]"
189
+ end
190
+ if request.experiment.type != ExperimentType::FEATURE_FLAG
191
+ raise ArgumentError, "experiment type must be FEATURE_FLAG [#{request.experiment.id}]"
192
+ end
193
+
194
+ if request.user.identifiers[request.experiment.identifier_type].nil?
195
+ return ExperimentEvaluation.create_default(request, context, DecisionReason::DEFAULT_RULE)
196
+ end
197
+
198
+ variation = @action_resolver.resolve_or_nil(request, request.experiment.default_rule)
199
+ raise ArgumentError, "feature flag must decide the variation [#{request.experiment.id}]" if variation.nil?
200
+
201
+ ExperimentEvaluation.create(request, context, variation, DecisionReason::DEFAULT_RULE)
202
+ end
203
+ end
204
+
205
+ class ContainerExperimentFlowEvaluator
206
+ include ExperimentFlowEvaluator
207
+
208
+ # @param container_resolver [ExperimentContainerResolver]
209
+ def initialize(container_resolver:)
210
+ # @type [ExperimentContainerResolver]
211
+ @container_resolver = container_resolver
212
+ end
213
+
214
+ def evaluate(request, context, next_flow)
215
+ container_id = request.experiment.container_id
216
+ return next_flow.evaluate(request, context) if container_id.nil?
217
+
218
+ container = request.workspace.get_container_or_nil(container_id)
219
+ raise ArgumentError, "container [#{container_id}]" if container.nil?
220
+
221
+ is_user_in_container_group = @container_resolver.user_in_container_group?(request, container)
222
+ if is_user_in_container_group
223
+ next_flow.evaluate(request, context)
224
+ else
225
+ ExperimentEvaluation.create_default(request, context, DecisionReason::NOT_IN_MUTUAL_EXCLUSION_EXPERIMENT)
226
+ end
227
+ end
228
+ end
229
+
230
+ class IdentifierExperimentFlowEvaluator
231
+ include ExperimentFlowEvaluator
232
+
233
+ def evaluate(request, context, next_flow)
234
+ if request.user.identifiers[request.experiment.identifier_type].nil?
235
+ ExperimentEvaluation.create_default(request, context, DecisionReason::IDENTIFIER_NOT_FOUND)
236
+ else
237
+ next_flow.evaluate(request, context)
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/model/action'
4
+ require 'hackle/internal/evaluation/bucketer/bucketer'
5
+
6
+ module Hackle
7
+ class ExperimentActionResolver
8
+ # @param bucketer [Bucketer]
9
+ def initialize(bucketer:)
10
+ # @type [Bucketer]
11
+ @bucketer = bucketer
12
+ end
13
+
14
+ # @param request [ExperimentRequest]
15
+ # @param action [Action]
16
+ # @return [Hackle::Variation, nil]
17
+ def resolve_or_nil(request, action)
18
+ case action.type
19
+ when ActionType::VARIATION
20
+ resolve_variation(request, action)
21
+ when ActionType::BUCKET
22
+ resolve_bucket(request, action)
23
+ else
24
+ raise ArgumentError, "unsupported ActionType [#{action.type}]"
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # @param request [ExperimentRequest]
31
+ # @param action [Action]
32
+ # @return [Hackle::Variation]
33
+ def resolve_variation(request, action)
34
+ variation_id = action.variation_id
35
+ raise ArgumentError, "action variation [#{request.experiment.id}]" if variation_id.nil?
36
+
37
+ variation = request.experiment.get_variation_or_nil_by_id(variation_id)
38
+ raise ArgumentError, "variation [#{variation_id}]" if variation.nil?
39
+
40
+ variation
41
+ end
42
+
43
+ # @param request [ExperimentRequest]
44
+ # @param action [Action]
45
+ # @return [Hackle::Variation, nil]
46
+ def resolve_bucket(request, action)
47
+ bucket_id = action.bucket_id
48
+ raise ArgumentError, "action bucket [#{request.experiment.id}]" if bucket_id.nil?
49
+
50
+ bucket = request.workspace.get_bucket_or_nil(bucket_id)
51
+ raise ArgumentError, "bucket [#{bucket_id}]" if bucket.nil?
52
+
53
+ identifier = request.user.identifiers[request.experiment.identifier_type]
54
+ return nil if identifier.nil?
55
+
56
+ slot = @bucketer.bucketing(bucket, identifier)
57
+ return nil if slot.nil?
58
+
59
+ request.experiment.get_variation_or_nil_by_id(slot.variation_id)
60
+ end
61
+ end
62
+
63
+ class ExperimentOverrideResolver
64
+ # @param target_matcher [TargetMatcher]
65
+ # @param action_resolver [ExperimentActionResolver]
66
+
67
+ def initialize(target_matcher:, action_resolver:)
68
+ # @type [TargetMatcher]
69
+ @target_matcher = target_matcher
70
+ # @type [ExperimentActionResolver]
71
+ @action_resolver = action_resolver
72
+ end
73
+
74
+ # @param request [ExperimentRequest]
75
+ # @param context [EvaluatorContext]
76
+ # @return [Hackle::Variation, nil]
77
+ def resolve_or_nil(request, context)
78
+ resolve_user_override(request) || resolve_segment_override(request, context)
79
+ end
80
+
81
+ private
82
+
83
+ # @param request [ExperimentRequest]
84
+ # @return [Hackle::Variation, nil]
85
+ def resolve_user_override(request)
86
+ identifier = request.user.identifiers[request.experiment.identifier_type]
87
+ return nil if identifier.nil?
88
+
89
+ overridden_variation_id = request.experiment.user_overrides[identifier]
90
+ return nil if overridden_variation_id.nil?
91
+
92
+ request.experiment.get_variation_or_nil_by_id(overridden_variation_id)
93
+ end
94
+
95
+ # @param request [ExperimentRequest]
96
+ # @param context [EvaluatorContext]
97
+ # @return [Hackle::Variation, nil]
98
+ def resolve_segment_override(request, context)
99
+ overridden_rule = request.experiment.segment_overrides.find do |it|
100
+ @target_matcher.matches(request, context, it.target)
101
+ end
102
+ return nil if overridden_rule.nil?
103
+
104
+ @action_resolver.resolve_or_nil(request, overridden_rule.action)
105
+ end
106
+ end
107
+
108
+ class ExperimentContainerResolver
109
+ # @param bucketer [Bucketer]
110
+ def initialize(bucketer:)
111
+ # @type [Bucketer]
112
+ @bucketer = bucketer
113
+ end
114
+
115
+ # @param request [ExperimentRequest]
116
+ # @param container [Container]
117
+ def user_in_container_group?(request, container)
118
+
119
+ identifier = request.user.identifiers[request.experiment.identifier_type]
120
+ return false if identifier.nil?
121
+
122
+ bucket = request.workspace.get_bucket_or_nil(container.bucket_id)
123
+ raise ArgumentError, "bucket [#{container.bucket_id}]" if bucket.nil?
124
+
125
+ slot = @bucketer.bucketing(bucket, identifier)
126
+ return false if slot.nil?
127
+
128
+ group = container.get_group_or_nil(slot.variation_id)
129
+ raise ArgumentError, "container group [#{slot.variation_id}]" if group.nil?
130
+
131
+ group.experiments.include?(request.experiment.id)
132
+ end
133
+ end
134
+
135
+ class ExperimentTargetDeterminer
136
+
137
+ # @param target_matcher [TargetMatcher]
138
+ def initialize(target_matcher:)
139
+ # @type [TargetMatcher]
140
+ @target_matcher = target_matcher
141
+ end
142
+
143
+ # @param request [ExperimentRequest]
144
+ # @param context [EvaluatorContext]
145
+ def user_in_experiment_target?(request, context)
146
+ return true if request.experiment.target_audiences.empty?
147
+
148
+ request.experiment.target_audiences.any? { |it| @target_matcher.matches(request, context, it) }
149
+ end
150
+ end
151
+
152
+ class ExperimentTargetRuleDeterminer
153
+ # @param target_matcher [TargetMatcher]
154
+ def initialize(target_matcher:)
155
+ # @type [TargetMatcher]
156
+ @target_matcher = target_matcher
157
+ end
158
+
159
+ # @param request [ExperimentRequest]
160
+ # @param context [EvaluatorContext]
161
+ # @return [Hackle::TargetRule, nil]
162
+ def determine_target_rule_or_nil(request, context)
163
+ request.experiment.target_rules.find { |it| @target_matcher.matches(request, context, it.target) }
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ class RemoteConfigTargetRuleDeterminer
5
+
6
+ # @param matcher [RemoteConfigTargetRuleMatcher]
7
+ def initialize(matcher:)
8
+ # @type [RemoteConfigTargetRuleMatcher]
9
+ @matcher = matcher
10
+ end
11
+
12
+ # @param request [RemoteConfigRequest]
13
+ # @param context [EvaluatorContext]
14
+ # @return [RemoteConfigTargetRule, nil]
15
+ def determine_or_nil(request, context)
16
+ request.parameter.target_rules.find { |it| @matcher.matches(request, context, it) }
17
+ end
18
+ end
19
+
20
+ class RemoteConfigTargetRuleMatcher
21
+ # @param target_matcher [TargetMatcher]
22
+ # @param bucketer [Bucketer]
23
+ def initialize(target_matcher:, bucketer:)
24
+ # @type [TargetMatcher]
25
+ @target_matcher = target_matcher
26
+ # @type [Bucketer]
27
+ @bucketer = bucketer
28
+ end
29
+
30
+ # @param request [RemoteConfigRequest]
31
+ # @param context [EvaluatorContext]
32
+ # @param target_rule [RemoteConfigTargetRule]
33
+ # @return [boolean]
34
+ def matches(request, context, target_rule)
35
+ matches = @target_matcher.matches(request, context, target_rule.target)
36
+ return false unless matches
37
+
38
+ identifier = request.user.identifiers[request.parameter.identifier_type]
39
+ return false if identifier.nil?
40
+
41
+ bucket = request.workspace.get_bucket_or_nil(target_rule.bucket_id)
42
+ raise ArgumentError, "bucket [#{target_rule.bucket_id}]" if bucket.nil?
43
+
44
+ allocated = @bucketer.bucketing(bucket, identifier)
45
+ !allocated.nil?
46
+ end
47
+ end
48
+ end