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.
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