hackle-ruby-sdk 0.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/hackle/client.rb +191 -79
- 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/internal/evaluation/bucketer/bucketer.rb +46 -0
- 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/internal/model/event_type.rb +19 -0
- 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 -32
- metadata +123 -51
- 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/bucketer.rb +0 -30
- data/lib/hackle/decision/decider.rb +0 -54
- data/lib/hackle/events/event.rb +0 -33
- data/lib/hackle/events/event_dispatcher.rb +0 -89
- data/lib/hackle/events/event_processor.rb +0 -115
- data/lib/hackle/http/http.rb +0 -37
- data/lib/hackle/models/bucket.rb +0 -15
- data/lib/hackle/models/event_type.rb +0 -14
- data/lib/hackle/models/experiment.rb +0 -36
- data/lib/hackle/models/slot.rb +0 -15
- data/lib/hackle/models/variation.rb +0 -11
- data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
- data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -44
- data/lib/hackle/workspaces/workspace.rb +0 -87
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent'
|
4
|
+
require 'hackle/internal/concurrent/schedule/scheduler'
|
5
|
+
|
6
|
+
module Hackle
|
7
|
+
class TimerScheduler
|
8
|
+
include Scheduler
|
9
|
+
|
10
|
+
def schedule_periodically(interval_seconds, task)
|
11
|
+
timer_task = Concurrent::TimerTask.new(execution_interval: interval_seconds, interval_type: :fixed_rate) do
|
12
|
+
task.call
|
13
|
+
end
|
14
|
+
timer_task.execute
|
15
|
+
TimerScheduledJob.new(timer_task)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class TimerScheduledJob
|
20
|
+
include ScheduledJob
|
21
|
+
|
22
|
+
def initialize(task)
|
23
|
+
@task = task
|
24
|
+
end
|
25
|
+
|
26
|
+
def cancel
|
27
|
+
@task.shutdown
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hackle
|
4
|
+
class ParameterConfig
|
5
|
+
|
6
|
+
# @return [Hash{String => Object}]
|
7
|
+
attr_reader :parameters
|
8
|
+
|
9
|
+
# @param parameters [Hash{String => Object}]
|
10
|
+
def initialize(parameters)
|
11
|
+
@parameters = parameters
|
12
|
+
end
|
13
|
+
|
14
|
+
@empty = new({})
|
15
|
+
|
16
|
+
# @return [Hackle::ParameterConfig]
|
17
|
+
def self.empty
|
18
|
+
@empty
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(other)
|
22
|
+
other.is_a?(self.class) && other.parameters == parameters
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"Hackle::ParameterConfig(#{parameters})"
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param key [String]
|
30
|
+
# @param default_value [Object, nil]
|
31
|
+
# @return [Object, nil]
|
32
|
+
def get(key, default_value = nil)
|
33
|
+
parameter_value = parameters.fetch(key, default_value)
|
34
|
+
|
35
|
+
return default_value if parameter_value.nil?
|
36
|
+
return parameter_value if default_value.nil?
|
37
|
+
|
38
|
+
case default_value
|
39
|
+
when String
|
40
|
+
parameter_value.is_a?(String) ? parameter_value : default_value
|
41
|
+
when Numeric
|
42
|
+
parameter_value.is_a?(Numeric) ? parameter_value : default_value
|
43
|
+
when TrueClass, FalseClass
|
44
|
+
parameter_value.is_a?(TrueClass) || parameter_value.is_a?(FalseClass) ? parameter_value : default_value
|
45
|
+
else
|
46
|
+
default_value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/internal/event/user_event'
|
4
|
+
require 'hackle/internal/model/decision_reason'
|
5
|
+
require 'hackle/internal/evaluation/evaluator/delegating/delegating_evaluator'
|
6
|
+
require 'hackle/internal/evaluation/match/condition/condition_matcher_factory'
|
7
|
+
require 'hackle/internal/evaluation/match/target/target_matcher'
|
8
|
+
require 'hackle/internal/evaluation/bucketer/bucketer'
|
9
|
+
require 'hackle/internal/evaluation/evaluator/experiment/experiment_evaluator'
|
10
|
+
require 'hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory'
|
11
|
+
require 'hackle/internal/evaluation/evaluator/remoteconfig/remote_config_evaluator'
|
12
|
+
require 'hackle/internal/evaluation/evaluator/remoteconfig/remote_config_determiner'
|
13
|
+
require 'hackle/internal/event/user_event_factory'
|
14
|
+
require 'hackle/internal/clock/clock'
|
15
|
+
|
16
|
+
module Hackle
|
17
|
+
class Core
|
18
|
+
|
19
|
+
# @param experiment_evaluator [ExperimentEvaluator]
|
20
|
+
# @param remote_config_evaluator [RemoteConfigEvaluator]
|
21
|
+
# @param workspace_fetcher [WorkspaceFetcher]
|
22
|
+
# @param event_factory [UserEventFactory]
|
23
|
+
# @param event_processor [UserEventProcessor]
|
24
|
+
# @param clock [Clock]
|
25
|
+
def initialize(
|
26
|
+
experiment_evaluator:,
|
27
|
+
remote_config_evaluator:,
|
28
|
+
workspace_fetcher:,
|
29
|
+
event_factory:,
|
30
|
+
event_processor:,
|
31
|
+
clock:
|
32
|
+
)
|
33
|
+
# @type experiment_evaluator [ExperimentEvaluator]
|
34
|
+
@experiment_evaluator = experiment_evaluator
|
35
|
+
# @type remote_config_evaluator [RemoteConfigEvaluator]
|
36
|
+
@remote_config_evaluator = remote_config_evaluator
|
37
|
+
# @type workspace_fetcher [WorkspaceFetcher]
|
38
|
+
@workspace_fetcher = workspace_fetcher
|
39
|
+
# @type event_factory [UserEventFactory]
|
40
|
+
@event_factory = event_factory
|
41
|
+
# @type event_processor [UserEventProcessor]
|
42
|
+
@event_processor = event_processor
|
43
|
+
# @type clock [Clock]
|
44
|
+
@clock = clock
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param workspace_fetcher [WorkspaceFetcher]
|
48
|
+
# @param event_processor [UserEventProcessor]
|
49
|
+
# @return [Core]
|
50
|
+
def self.create(workspace_fetcher:, event_processor:)
|
51
|
+
delegating_evaluator = DelegatingEvaluator.new
|
52
|
+
|
53
|
+
condition_matcher_factory = ConditionMatcherFactory.new(evaluator: delegating_evaluator)
|
54
|
+
target_matcher = TargetMatcher.new(condition_matcher_factory: condition_matcher_factory)
|
55
|
+
bucketer = Bucketer.new(hasher: Hasher.new)
|
56
|
+
|
57
|
+
experiment_evaluator = ExperimentEvaluator.new(
|
58
|
+
flow_factory: ExperimentEvaluationFlowFactory.new(
|
59
|
+
target_matcher: target_matcher,
|
60
|
+
bucketer: bucketer
|
61
|
+
)
|
62
|
+
)
|
63
|
+
delegating_evaluator.add(experiment_evaluator)
|
64
|
+
|
65
|
+
remote_config_evaluator = RemoteConfigEvaluator.new(
|
66
|
+
target_rule_determiner: RemoteConfigTargetRuleDeterminer.new(
|
67
|
+
matcher: RemoteConfigTargetRuleMatcher.new(
|
68
|
+
target_matcher: target_matcher,
|
69
|
+
bucketer: bucketer
|
70
|
+
)
|
71
|
+
)
|
72
|
+
)
|
73
|
+
delegating_evaluator.add(remote_config_evaluator)
|
74
|
+
|
75
|
+
Core.new(
|
76
|
+
experiment_evaluator: experiment_evaluator,
|
77
|
+
remote_config_evaluator: remote_config_evaluator,
|
78
|
+
workspace_fetcher: workspace_fetcher,
|
79
|
+
event_factory: UserEventFactory.new(clock: SystemClock.instance),
|
80
|
+
event_processor: event_processor,
|
81
|
+
clock: SystemClock.instance
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param experiment_key [Integer]
|
86
|
+
# @param user [HackleUser]
|
87
|
+
# @param default_variation [String]
|
88
|
+
# @return [ExperimentDecision]
|
89
|
+
def experiment(experiment_key, user, default_variation)
|
90
|
+
workspace = @workspace_fetcher.fetch
|
91
|
+
return ExperimentDecision.new(default_variation, DecisionReason::SDK_NOT_READY, ParameterConfig.empty) if workspace.nil?
|
92
|
+
|
93
|
+
experiment = workspace.get_experiment_or_nil(experiment_key)
|
94
|
+
return ExperimentDecision.new(default_variation, DecisionReason::EXPERIMENT_NOT_FOUND, ParameterConfig.empty) if experiment.nil?
|
95
|
+
|
96
|
+
request = ExperimentRequest.create(workspace, user, experiment, default_variation)
|
97
|
+
evaluation = @experiment_evaluator.evaluate(request, Evaluator.context)
|
98
|
+
|
99
|
+
events = @event_factory.create(request, evaluation)
|
100
|
+
events.each do |event|
|
101
|
+
@event_processor.process(event)
|
102
|
+
end
|
103
|
+
|
104
|
+
ExperimentDecision.new(evaluation.variation_key, evaluation.reason, evaluation.parameter_config)
|
105
|
+
end
|
106
|
+
|
107
|
+
# @param feature_key [Integer]
|
108
|
+
# @param user [HackleUser]
|
109
|
+
# @return [FeatureFlagDecision]
|
110
|
+
def feature_flag(feature_key, user)
|
111
|
+
workspace = @workspace_fetcher.fetch
|
112
|
+
return FeatureFlagDecision.new(false, DecisionReason::SDK_NOT_READY, ParameterConfig.empty) if workspace.nil?
|
113
|
+
|
114
|
+
feature_flag = workspace.get_feature_flag_or_nil(feature_key)
|
115
|
+
return FeatureFlagDecision.new(false, DecisionReason::FEATURE_FLAG_NOT_FOUND, ParameterConfig.empty) if feature_flag.nil?
|
116
|
+
|
117
|
+
request = ExperimentRequest.create(workspace, user, feature_flag, 'A')
|
118
|
+
evaluation = @experiment_evaluator.evaluate(request, Evaluator.context)
|
119
|
+
|
120
|
+
events = @event_factory.create(request, evaluation)
|
121
|
+
events.each do |event|
|
122
|
+
@event_processor.process(event)
|
123
|
+
end
|
124
|
+
|
125
|
+
is_on = evaluation.variation_key != 'A'
|
126
|
+
FeatureFlagDecision.new(is_on, evaluation.reason, evaluation.parameter_config)
|
127
|
+
end
|
128
|
+
|
129
|
+
# @param parameter_key [String]
|
130
|
+
# @param user [HackleUser]
|
131
|
+
# @param required_type [ValueType]
|
132
|
+
# @param default_value [Object, nil]
|
133
|
+
def remote_config(parameter_key, user, required_type, default_value)
|
134
|
+
workspace = @workspace_fetcher.fetch
|
135
|
+
return RemoteConfigDecision.new(default_value, DecisionReason::SDK_NOT_READY) if workspace.nil?
|
136
|
+
|
137
|
+
parameter = workspace.get_remote_config_parameter_or_nil(parameter_key)
|
138
|
+
return RemoteConfigDecision.new(default_value, DecisionReason::REMOTE_CONFIG_PARAMETER_NOT_FOUND) if parameter.nil?
|
139
|
+
|
140
|
+
request = RemoteConfigRequest.create(workspace, user, parameter, required_type, default_value)
|
141
|
+
evaluation = @remote_config_evaluator.evaluate(request, Evaluator.context)
|
142
|
+
|
143
|
+
events = @event_factory.create(request, evaluation)
|
144
|
+
events.each do |event|
|
145
|
+
@event_processor.process(event)
|
146
|
+
end
|
147
|
+
|
148
|
+
RemoteConfigDecision.new(evaluation.value, evaluation.reason)
|
149
|
+
end
|
150
|
+
|
151
|
+
# @param event [Event]
|
152
|
+
# @param user [HackleUser]
|
153
|
+
def track(event, user)
|
154
|
+
event_type = event_type(event)
|
155
|
+
@event_processor.process(UserEvent.track(event_type, event, user, @clock.current_millis))
|
156
|
+
end
|
157
|
+
|
158
|
+
def close
|
159
|
+
@workspace_fetcher.stop
|
160
|
+
@event_processor.stop
|
161
|
+
end
|
162
|
+
|
163
|
+
def resume
|
164
|
+
@workspace_fetcher.resume
|
165
|
+
@event_processor.resume
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
# @param event [Event]
|
171
|
+
# @return [EventType]
|
172
|
+
def event_type(event)
|
173
|
+
workspace = @workspace_fetcher.fetch
|
174
|
+
return EventType.new(id: 0, key: event.key) if workspace.nil?
|
175
|
+
|
176
|
+
event_type = workspace.get_event_type_or_nil(event.key)
|
177
|
+
return EventType.new(id: 0, key: event.key) if event_type.nil?
|
178
|
+
|
179
|
+
event_type
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'murmurhash3'
|
4
|
+
|
5
|
+
module Hackle
|
6
|
+
class Bucketer
|
7
|
+
|
8
|
+
# @param hasher [Hasher]
|
9
|
+
def initialize(hasher:)
|
10
|
+
# @type [Hasher]
|
11
|
+
@hasher = hasher
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param bucket [Bucket]
|
15
|
+
# @param identifier [String]
|
16
|
+
# @return [Slot, nil]
|
17
|
+
def bucketing(bucket, identifier)
|
18
|
+
slot_number = calculate_slot_number(seed: bucket.seed, slot_size: bucket.slot_size, value: identifier)
|
19
|
+
bucket.get_slot_or_nil(slot_number)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param seed [Integer]
|
23
|
+
# @param slot_size [Integer]
|
24
|
+
# @param value [String]
|
25
|
+
# @return [Integer]
|
26
|
+
def calculate_slot_number(seed:, slot_size:, value:)
|
27
|
+
hash_value = @hasher.hash(value, seed)
|
28
|
+
hash_value.abs % slot_size
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
class Hasher
|
34
|
+
# @param data [String]
|
35
|
+
# @param seed [Integer]
|
36
|
+
# @return [Integer]
|
37
|
+
def hash(data, seed)
|
38
|
+
unsigned_value = MurmurHash3::V32.str_hash(data, seed)
|
39
|
+
if (unsigned_value & 0x80000000).zero?
|
40
|
+
unsigned_value
|
41
|
+
else
|
42
|
+
-((unsigned_value ^ 0xFFFFFFFF) + 1)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/internal/evaluation/evaluator/evaluator'
|
4
|
+
|
5
|
+
module Hackle
|
6
|
+
module ContextualEvaluator
|
7
|
+
include Evaluator
|
8
|
+
|
9
|
+
# @param request [EvaluatorRequest]
|
10
|
+
# @return [boolean]
|
11
|
+
def supports?(request) end
|
12
|
+
|
13
|
+
# @param request [EvaluatorRequest]
|
14
|
+
# @param context [EvaluatorContext]
|
15
|
+
# @return [EvaluatorEvaluation]
|
16
|
+
def evaluate_internal(request, context) end
|
17
|
+
|
18
|
+
def evaluate(request, context)
|
19
|
+
raise ArgumentError, 'circular evaluation has occurred' if context.request_include?(request)
|
20
|
+
|
21
|
+
context.add_request(request)
|
22
|
+
begin
|
23
|
+
evaluate_internal(request, context)
|
24
|
+
ensure
|
25
|
+
context.remove_request(request)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/internal/evaluation/evaluator/evaluator'
|
4
|
+
|
5
|
+
module Hackle
|
6
|
+
class DelegatingEvaluator
|
7
|
+
include Evaluator
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
# @type [Array<ContextualEvaluator>]
|
11
|
+
@evaluators = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param evaluator [ContextualEvaluator]
|
15
|
+
def add(evaluator)
|
16
|
+
@evaluators << evaluator
|
17
|
+
end
|
18
|
+
|
19
|
+
def evaluate(request, context)
|
20
|
+
evaluator = @evaluators.find { |it| it.supports?(request) }
|
21
|
+
raise ArgumentError, "unsupported EvaluatorRequest [#{request.class}]" if evaluator.nil?
|
22
|
+
|
23
|
+
evaluator.evaluate(request, context)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hackle
|
4
|
+
|
5
|
+
module Evaluator
|
6
|
+
# @param request [EvaluatorRequest]
|
7
|
+
# @param context [EvaluatorContext]
|
8
|
+
# @return [EvaluatorEvaluation]
|
9
|
+
def evaluate(request, context) end
|
10
|
+
|
11
|
+
# @return [EvaluatorContext]
|
12
|
+
def self.context
|
13
|
+
EvaluatorContext.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class EvaluatorRequest
|
18
|
+
|
19
|
+
# @return [EvaluatorKey]
|
20
|
+
attr_reader :key
|
21
|
+
|
22
|
+
# @return [Workspace]
|
23
|
+
attr_reader :workspace
|
24
|
+
|
25
|
+
# @return [HackleUser]
|
26
|
+
attr_reader :user
|
27
|
+
|
28
|
+
# @param key [EvaluatorKey]
|
29
|
+
# @param workspace [Workspace]
|
30
|
+
# @param user [HackleUser]
|
31
|
+
def initialize(key:, workspace:, user:)
|
32
|
+
@key = key
|
33
|
+
@workspace = workspace
|
34
|
+
@user = user
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
other.is_a?(EvaluatorRequest) && key == other.key
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class EvaluatorEvaluation
|
43
|
+
|
44
|
+
# @return [String]
|
45
|
+
attr_reader :reason
|
46
|
+
|
47
|
+
# @return [Array<EvaluatorEvaluation>]
|
48
|
+
attr_reader :target_evaluations
|
49
|
+
|
50
|
+
# @param reason [String]
|
51
|
+
# @param target_evaluations [Array<EvaluatorEvaluation>]
|
52
|
+
def initialize(reason:, target_evaluations:)
|
53
|
+
@reason = reason
|
54
|
+
@target_evaluations = target_evaluations
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class EvaluatorContext
|
59
|
+
|
60
|
+
def initialize
|
61
|
+
# @type [Array<EvaluatorRequest>]
|
62
|
+
@requests = []
|
63
|
+
# @type [Array<EvaluatorEvaluation>]
|
64
|
+
@evaluations = []
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Array<EvaluatorRequest>]
|
68
|
+
def requests
|
69
|
+
@requests.dup
|
70
|
+
end
|
71
|
+
|
72
|
+
# @param request [EvaluatorRequest]
|
73
|
+
# @return [boolean]
|
74
|
+
def request_include?(request)
|
75
|
+
@requests.include?(request)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param request [EvaluatorRequest]
|
79
|
+
def add_request(request)
|
80
|
+
@requests << request
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param request [EvaluatorRequest]
|
84
|
+
def remove_request(request)
|
85
|
+
@requests.delete(request)
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [Array<EvaluatorEvaluation>]
|
89
|
+
def evaluations
|
90
|
+
@evaluations.dup
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param evaluation [EvaluatorEvaluation]
|
94
|
+
def add_evaluation(evaluation)
|
95
|
+
@evaluations << evaluation
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class EvaluatorKey
|
100
|
+
# @return [String]
|
101
|
+
attr_reader :type
|
102
|
+
|
103
|
+
# @return [Integer]
|
104
|
+
attr_reader :id
|
105
|
+
|
106
|
+
# @param type [String]
|
107
|
+
# @param id [Integer]
|
108
|
+
def initialize(type:, id:)
|
109
|
+
@type = type
|
110
|
+
@id = id
|
111
|
+
end
|
112
|
+
|
113
|
+
def ==(other)
|
114
|
+
other.is_a?(EvaluatorKey) && type == other.type && id == other.id
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/internal/model/experiment'
|
4
|
+
require 'hackle/internal/evaluation/flow/evaluation_flow'
|
5
|
+
require 'hackle/internal/evaluation/evaluator/experiment/experiment_resolver'
|
6
|
+
require 'hackle/internal/evaluation/evaluator/experiment/experiment_flow_evaluator'
|
7
|
+
|
8
|
+
module Hackle
|
9
|
+
class ExperimentEvaluationFlowFactory
|
10
|
+
# @param target_matcher [TargetMatcher]
|
11
|
+
# @param bucketer [Bucketer]
|
12
|
+
def initialize(target_matcher:, bucketer:)
|
13
|
+
|
14
|
+
action_resolver = ExperimentActionResolver.new(bucketer: bucketer)
|
15
|
+
override_resolver = ExperimentOverrideResolver.new(
|
16
|
+
target_matcher: target_matcher,
|
17
|
+
action_resolver: action_resolver
|
18
|
+
)
|
19
|
+
container_resolver = ExperimentContainerResolver.new(bucketer: bucketer)
|
20
|
+
target_determiner = ExperimentTargetDeterminer.new(target_matcher: target_matcher)
|
21
|
+
target_rule_determiner = ExperimentTargetRuleDeterminer.new(target_matcher: target_matcher)
|
22
|
+
|
23
|
+
# @type [ExperimentFlow]
|
24
|
+
@ab_test_flow = EvaluationFlow.create(
|
25
|
+
[
|
26
|
+
OverrideExperimentFlowEvaluator.new(override_resolver: override_resolver),
|
27
|
+
IdentifierExperimentFlowEvaluator.new,
|
28
|
+
ContainerExperimentFlowEvaluator.new(container_resolver: container_resolver),
|
29
|
+
TargetExperimentFlowEvaluator.new(target_determiner: target_determiner),
|
30
|
+
DraftExperimentFlowEvaluator.new,
|
31
|
+
PausedExperimentFlowEvaluator.new,
|
32
|
+
CompletedExperimentFlowEvaluator.new,
|
33
|
+
TrafficAllocateExperimentFlowEvaluator.new(action_resolver: action_resolver)
|
34
|
+
]
|
35
|
+
)
|
36
|
+
|
37
|
+
# @type [ExperimentFlow]
|
38
|
+
@feature_flag_flow = EvaluationFlow.create(
|
39
|
+
[
|
40
|
+
DraftExperimentFlowEvaluator.new,
|
41
|
+
PausedExperimentFlowEvaluator.new,
|
42
|
+
CompletedExperimentFlowEvaluator.new,
|
43
|
+
OverrideExperimentFlowEvaluator.new(override_resolver: override_resolver),
|
44
|
+
IdentifierExperimentFlowEvaluator.new,
|
45
|
+
TargetRuleExperimentFlowEvaluator.new(
|
46
|
+
target_rule_determiner: target_rule_determiner,
|
47
|
+
action_resolver: action_resolver
|
48
|
+
),
|
49
|
+
DefaultRuleExperimentFlowEvaluator.new(action_resolver: action_resolver)
|
50
|
+
]
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param experiment_type [ExperimentType]
|
55
|
+
# @return [ExperimentFlow]
|
56
|
+
def get(experiment_type)
|
57
|
+
case experiment_type
|
58
|
+
when ExperimentType::AB_TEST
|
59
|
+
@ab_test_flow
|
60
|
+
when ExperimentType::FEATURE_FLAG
|
61
|
+
@feature_flag_flow
|
62
|
+
else
|
63
|
+
raise ArgumentError, "unsupported ExperimentType [#{experiment_type}]"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|