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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ module Scheduler
5
+ def schedule_periodically(interval_seconds, task) end
6
+ end
7
+
8
+ module ScheduledJob
9
+ def cancel
10
+ end
11
+ end
12
+ end
@@ -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
@@ -5,34 +5,36 @@ require 'murmurhash3'
5
5
  module Hackle
6
6
  class Bucketer
7
7
 
8
+ # @param hasher [Hasher]
9
+ def initialize(hasher:)
10
+ # @type [Hasher]
11
+ @hasher = hasher
12
+ end
13
+
8
14
  # @param bucket [Bucket]
9
- # @param user [User]
10
- #
15
+ # @param identifier [String]
11
16
  # @return [Slot, nil]
12
- def bucketing(bucket:, user:)
13
- slot_number = calculate_slot_number(
14
- seed: bucket.seed,
15
- slot_size: bucket.slot_size,
16
- user_id: user.id
17
- )
18
- bucket.get_slot(slot_number: slot_number)
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)
19
20
  end
20
21
 
21
22
  # @param seed [Integer]
22
23
  # @param slot_size [Integer]
23
- # @param user_id [String]
24
- #
24
+ # @param value [String]
25
25
  # @return [Integer]
26
- def calculate_slot_number(seed:, slot_size:, user_id:)
27
- hash_value = hash(data: user_id, seed: seed)
26
+ def calculate_slot_number(seed:, slot_size:, value:)
27
+ hash_value = @hasher.hash(value, seed)
28
28
  hash_value.abs % slot_size
29
29
  end
30
30
 
31
+ end
32
+
33
+ class Hasher
31
34
  # @param data [String]
32
35
  # @param seed [Integer]
33
- #
34
36
  # @return [Integer]
35
- def hash(data:, seed:)
37
+ def hash(data, seed)
36
38
  unsigned_value = MurmurHash3::V32.str_hash(data, seed)
37
39
  if (unsigned_value & 0x80000000).zero?
38
40
  unsigned_value
@@ -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
@@ -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