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,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/model/value_type'
4
+ require 'hackle/internal/model/decision_reason'
5
+ require 'hackle/internal/properties/properties_builder'
6
+
7
+ module Hackle
8
+ class RemoteConfigEvaluator
9
+ include ContextualEvaluator
10
+
11
+ # @param target_rule_determiner [RemoteConfigTargetRuleDeterminer]
12
+ def initialize(target_rule_determiner:)
13
+ # @type [RemoteConfigTargetRuleDeterminer]
14
+ @target_rule_determiner = target_rule_determiner
15
+ end
16
+
17
+ def supports?(request)
18
+ request.is_a?(RemoteConfigRequest)
19
+ end
20
+
21
+ # @param request [RemoteConfigRequest]
22
+ # @param context [EvaluatorContext]
23
+ # @return [RemoteConfigEvaluation]
24
+ def evaluate_internal(request, context)
25
+ properties_builder = PropertiesBuilder.new
26
+ .add('requestValueType', request.required_type.name)
27
+ .add('requestDefaultValue', request.default_value)
28
+
29
+ if request.user.identifiers[request.parameter.identifier_type].nil?
30
+ return RemoteConfigEvaluation.create_default(
31
+ request, context, DecisionReason::IDENTIFIER_NOT_FOUND, properties_builder
32
+ )
33
+ end
34
+
35
+ target_rule = @target_rule_determiner.determine_or_nil(request, context)
36
+ unless target_rule.nil?
37
+ properties_builder.add('targetRuleKey', target_rule.key)
38
+ properties_builder.add('targetRuleName', target_rule.name)
39
+ return evaluation(request, context, target_rule.value, DecisionReason::TARGET_RULE_MATCH, properties_builder)
40
+ end
41
+
42
+ evaluation(request, context, request.parameter.default_value, DecisionReason::DEFAULT_RULE, properties_builder)
43
+ end
44
+
45
+ private
46
+
47
+ # @param request [RemoteConfigRequest]
48
+ # @param context [EvaluatorContext]
49
+ # @param parameter_value [RemoteConfigValue]
50
+ # @param reason [String]
51
+ # @param properties_builder [PropertiesBuilder]
52
+ # @return [RemoteConfigEvaluation]
53
+ def evaluation(request, context, parameter_value, reason, properties_builder)
54
+ if valid?(request.required_type, parameter_value.raw_value)
55
+ RemoteConfigEvaluation.create(request, context, parameter_value.id, parameter_value.raw_value, reason,
56
+ properties_builder)
57
+ else
58
+ RemoteConfigEvaluation.create_default(request, context, DecisionReason::TYPE_MISMATCH, properties_builder)
59
+ end
60
+ end
61
+
62
+ # @param required_type [ValueType]
63
+ # @param value [Object]
64
+ # @return [boolean]
65
+ def valid?(required_type, value)
66
+ case required_type
67
+ when ValueType::NULL
68
+ true
69
+ when ValueType::STRING
70
+ value.is_a?(String)
71
+ when ValueType::NUMBER
72
+ value.is_a?(Numeric)
73
+ when ValueType::BOOLEAN
74
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
75
+ else
76
+ false
77
+ end
78
+ end
79
+ end
80
+
81
+ class RemoteConfigRequest < EvaluatorRequest
82
+ # @return [RemoteConfigParameter]
83
+ attr_reader :parameter
84
+
85
+ # @return [ValueType]
86
+ attr_reader :required_type
87
+
88
+ # @return [Object, nil]
89
+ attr_reader :default_value
90
+
91
+ # @param key [EvaluatorKey]
92
+ # @param workspace [Workspace]
93
+ # @param user [HackleUser]
94
+ # @param parameter [RemoteConfigParameter]
95
+ # @param required_type [ValueType]
96
+ # @param default_value [Object, nil]
97
+ def initialize(key:, workspace:, user:, parameter:, required_type:, default_value:)
98
+ super(key: key, workspace: workspace, user: user)
99
+ @parameter = parameter
100
+ @required_type = required_type
101
+ @default_value = default_value
102
+ end
103
+
104
+ # @param workspace [Workspace]
105
+ # @param user [HackleUser]
106
+ # @param parameter [RemoteConfigParameter]
107
+ # @param required_type [ValueType]
108
+ # @param default_value [Object, nil]
109
+ def self.create(workspace, user, parameter, required_type, default_value)
110
+ RemoteConfigRequest.new(
111
+ key: EvaluatorKey.new(type: 'REMOTE_CONFIG', id: parameter.id),
112
+ workspace: workspace,
113
+ user: user,
114
+ parameter: parameter,
115
+ required_type: required_type,
116
+ default_value: default_value
117
+ )
118
+ end
119
+ end
120
+
121
+ class RemoteConfigEvaluation < EvaluatorEvaluation
122
+ # @return [RemoteConfigParameter]
123
+ attr_reader :parameter
124
+
125
+ # @return [Integer, nil]
126
+ attr_reader :value_id
127
+
128
+ # @return [Object]
129
+ attr_reader :value
130
+
131
+ # @return [Hash<String => Object>]
132
+ attr_reader :properties
133
+
134
+ # @param reason [String]
135
+ # @param target_evaluations [Array<EvaluatorEvaluation>]
136
+ # @param parameter [RemoteConfigParameter]
137
+ # @param value_id [Integer, nil]
138
+ # @param value [Object, nil]
139
+ # @param properties [Hash<String => Object>]
140
+ def initialize(reason:, target_evaluations:, parameter:, value_id:, value:, properties:)
141
+ super(reason: reason, target_evaluations: target_evaluations)
142
+ @parameter = parameter
143
+ @value_id = value_id
144
+ @value = value
145
+ @properties = properties
146
+ end
147
+
148
+ # @param request [RemoteConfigRequest]
149
+ # @param context [EvaluatorContext]
150
+ # @param value_id [Integer, nil]
151
+ # @param value [Object, nil]
152
+ # @param reason [String]
153
+ # @param properties_builder [PropertiesBuilder]
154
+ def self.create(request, context, value_id, value, reason, properties_builder)
155
+ properties_builder.add('returnValue', value)
156
+ RemoteConfigEvaluation.new(
157
+ reason: reason,
158
+ target_evaluations: context.evaluations,
159
+ parameter: request.parameter,
160
+ value_id: value_id,
161
+ value: value,
162
+ properties: properties_builder.build
163
+ )
164
+ end
165
+
166
+ # @param request [RemoteConfigRequest]
167
+ # @param context [EvaluatorContext]
168
+ # @param reason [String]
169
+ # @param properties_builder [PropertiesBuilder]
170
+ def self.create_default(request, context, reason, properties_builder)
171
+ create(request, context, nil, request.default_value, reason, properties_builder)
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ module EvaluationFlow
5
+ # @param request [EvaluatorRequest]
6
+ # @param context [EvaluatorContext]
7
+ # @return [EvaluatorEvaluation, nil]
8
+ def evaluate(request, context) end
9
+
10
+ # @param evaluators [Array<FlowEvaluator>]
11
+ # @return [Hackle::EvaluationFlow]
12
+ def self.create(evaluators)
13
+ flow = End.new
14
+ evaluators.reverse_each do |evaluator|
15
+ flow = Decision.new(evaluator, flow)
16
+ end
17
+ flow
18
+ end
19
+
20
+ class End
21
+ include EvaluationFlow
22
+
23
+ def evaluate(request, context)
24
+ nil
25
+ end
26
+ end
27
+
28
+ class Decision
29
+ include EvaluationFlow
30
+
31
+ # @return [FlowEvaluator]
32
+ attr_reader :evaluator
33
+
34
+ # @return [EvaluationFlow]
35
+ attr_reader :next_flow
36
+
37
+ # @param evaluator [FlowEvaluator]
38
+ # @param next_flow [EvaluationFlow]
39
+ def initialize(evaluator, next_flow)
40
+ @evaluator = evaluator
41
+ @next_flow = next_flow
42
+ end
43
+
44
+ def evaluate(request, context)
45
+ evaluator.evaluate(request, context, next_flow)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ module FlowEvaluator
5
+ # @param request [EvaluatorRequest]
6
+ # @param context [EvaluatorContext]
7
+ # @param next_flow [EvaluationFlow]
8
+ # @return [EvaluatorEvaluation, nil]
9
+ def evaluate(request, context, next_flow) end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ module ConditionMatcher
5
+ # @param request [EvaluatorRequest]
6
+ # @param context [EvaluatorContext]
7
+ # @param condition [TargetCondition]
8
+ # @return [boolean]
9
+ def matches(request, context, condition) end
10
+ end
11
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/evaluation/match/value/value_operator_matcher'
4
+ require 'hackle/internal/evaluation/match/value/value_matcher_factory'
5
+ require 'hackle/internal/evaluation/match/operator/operator_matcher_factory'
6
+ require 'hackle/internal/evaluation/match/condition/user/user_condition_matcher'
7
+ require 'hackle/internal/evaluation/match/condition/segment/segment_condition_matcher'
8
+ require 'hackle/internal/evaluation/match/condition/experiment/experiment_condition_matcher'
9
+ require 'hackle/internal/evaluation/match/condition/experiment/experiment_evaluator_matcher'
10
+
11
+ module Hackle
12
+ class ConditionMatcherFactory
13
+ # @param evaluator [Evaluator]
14
+ def initialize(evaluator:)
15
+ value_operator_matcher = ValueOperatorMatcher.new(
16
+ value_matcher_factory: ValueMatcherFactory.new,
17
+ operator_matcher_factory: OperatorMatcherFactory.new
18
+ )
19
+ @user_condition_matcher = UserConditionMatcher.new(
20
+ user_value_resolver: UserValueResolver.new,
21
+ value_operator_matcher: value_operator_matcher
22
+ )
23
+ @segment_condition_matcher = SegmentConditionMatcher.new(
24
+ segment_matcher: SegmentMatcher.new(user_condition_matcher: @user_condition_matcher)
25
+ )
26
+ @experiment_condition_matcher = ExperimentConditionMatcher.new(
27
+ ab_test_matcher: AbTestEvaluatorMatcher.new(
28
+ evaluator: evaluator,
29
+ value_operator_matcher: value_operator_matcher
30
+ ),
31
+ feature_flag_matcher: FeatureFlagEvaluatorMatcher.new(
32
+ evaluator: evaluator,
33
+ value_operator_matcher: value_operator_matcher
34
+ )
35
+ )
36
+ end
37
+
38
+ # @param key_type [TargetKeyType]
39
+ # @return [ConditionMatcher]
40
+ def get(key_type)
41
+ case key_type
42
+ when TargetKeyType::USER_ID, TargetKeyType::USER_PROPERTY, TargetKeyType::HACKLE_PROPERTY
43
+ @user_condition_matcher
44
+ when TargetKeyType::SEGMENT
45
+ @segment_condition_matcher
46
+ when TargetKeyType::AB_TEST, TargetKeyType::FEATURE_FLAG
47
+ @experiment_condition_matcher
48
+ else
49
+ raise ArgumentError, "unsupported TargetKeyType [#{key_type}]"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/evaluation/match/condition/condition_matcher'
4
+
5
+ module Hackle
6
+ class ExperimentConditionMatcher
7
+ include ConditionMatcher
8
+
9
+ # @param ab_test_matcher [ExperimentEvaluatorMatcher]
10
+ # @param feature_flag_matcher [ExperimentEvaluatorMatcher]
11
+ def initialize(ab_test_matcher:, feature_flag_matcher:)
12
+ # @type [ExperimentEvaluatorMatcher]
13
+ @ab_test_matcher = ab_test_matcher
14
+ # @type [ExperimentEvaluatorMatcher]
15
+ @feature_flag_matcher = feature_flag_matcher
16
+ end
17
+
18
+ def matches(request, context, condition)
19
+ case condition.key.type
20
+ when TargetKeyType::AB_TEST
21
+ @ab_test_matcher.matches(request, context, condition)
22
+ when TargetKeyType::FEATURE_FLAG
23
+ @feature_flag_matcher.matches(request, context, condition)
24
+ else
25
+ raise ArgumentError, "unsupported TargetKeyType [#{condition.key.type}]"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/model/decision_reason'
4
+ require 'hackle/internal/evaluation/evaluator/experiment/experiment_evaluator'
5
+
6
+ module Hackle
7
+ class ExperimentEvaluatorMatcher
8
+ # @param evaluator [Evaluator]
9
+ def initialize(evaluator:)
10
+ # @type [Evaluator]
11
+ @evaluator = evaluator
12
+ end
13
+
14
+ # @param request [EvaluatorRequest]
15
+ # @param context [EvaluatorContext]
16
+ # @param condition [TargetCondition]
17
+ # @return [boolean]
18
+ def matches(request, context, condition)
19
+ key = Integer(condition.key.name, exception: false)
20
+ raise ArgumentError, "invalid key [#{condition.key.type}, #{condition.key.name}]" if key.nil?
21
+
22
+ experiment = experiment_or_nil(request, key)
23
+ return false if experiment.nil?
24
+
25
+ evaluation = get_evaluation_or_nil(context, experiment)
26
+ evaluation = evaluate(request, context, experiment) if evaluation.nil?
27
+
28
+ evaluation_matches(evaluation, condition)
29
+ end
30
+
31
+ private
32
+
33
+ # @param request [EvaluatorRequest]
34
+ # @param context [EvaluatorContext]
35
+ # @param experiment [Experiment]
36
+ # @return [Hackle::ExperimentEvaluation]
37
+ def evaluate(request, context, experiment)
38
+ experiment_request = ExperimentRequest.create_by(request, experiment)
39
+ evaluation = @evaluator.evaluate(experiment_request, context)
40
+ unless evaluation.is_a?(ExperimentEvaluation)
41
+ raise ArgumentError, "unexpected evaluation: #{evaluation.class} (expected: ExperimentEvaluation)"
42
+ end
43
+
44
+ resolved_evaluation = resolve_evaluation(request, evaluation)
45
+ context.add_evaluation(resolved_evaluation)
46
+ resolved_evaluation
47
+ end
48
+
49
+ # @param context [EvaluatorContext]
50
+ # @param experiment [Experiment]
51
+ # @return [ExperimentEvaluation, nil]
52
+ def get_evaluation_or_nil(context, experiment)
53
+ context.evaluations.each do |evaluation|
54
+ return evaluation if evaluation.is_a?(ExperimentEvaluation) && evaluation.experiment.id == experiment.id
55
+ end
56
+ nil
57
+ end
58
+
59
+ # @abstract
60
+ # @param request [EvaluatorRequest]
61
+ # @param key [Integer]
62
+ # @return [Experiment, nil]
63
+ def experiment_or_nil(request, key) end
64
+
65
+ # @abstract
66
+ # @param request [EvaluatorRequest]
67
+ # @param evaluation [ExperimentEvaluation]
68
+ # @return [ExperimentEvaluation]
69
+ def resolve_evaluation(request, evaluation) end
70
+
71
+ # @abstract
72
+ # @param evaluation [ExperimentEvaluation]
73
+ # @param condition [TargetCondition]
74
+ # @return [boolean]
75
+ def evaluation_matches(evaluation, condition) end
76
+ end
77
+
78
+ class AbTestEvaluatorMatcher < ExperimentEvaluatorMatcher
79
+ # @param evaluator [Evaluator]
80
+ # @param value_operator_matcher [ValueOperatorMatcher]
81
+ def initialize(evaluator:, value_operator_matcher:)
82
+ super(evaluator: evaluator)
83
+ # @type [ValueOperatorMatcher]
84
+ @value_operator_matcher = value_operator_matcher
85
+ end
86
+
87
+ def experiment_or_nil(request, key)
88
+ request.workspace.get_experiment_or_nil(key)
89
+ end
90
+
91
+ def resolve_evaluation(request, evaluation)
92
+ if request.is_a?(ExperimentRequest) && evaluation.reason == DecisionReason::TRAFFIC_ALLOCATED
93
+ return evaluation.with(DecisionReason::TRAFFIC_ALLOCATED_BY_TARGETING)
94
+ end
95
+
96
+ evaluation
97
+ end
98
+
99
+ def evaluation_matches(evaluation, condition)
100
+ return false if AB_TEST_MATCHED_REASONS.none?(evaluation.reason)
101
+
102
+ @value_operator_matcher.matches(evaluation.variation_key, condition.match)
103
+ end
104
+
105
+ AB_TEST_MATCHED_REASONS = [
106
+ DecisionReason::OVERRIDDEN,
107
+ DecisionReason::TRAFFIC_ALLOCATED,
108
+ DecisionReason::TRAFFIC_ALLOCATED_BY_TARGETING,
109
+ DecisionReason::EXPERIMENT_COMPLETED
110
+ ].freeze
111
+ end
112
+
113
+ class FeatureFlagEvaluatorMatcher < ExperimentEvaluatorMatcher
114
+ # @param evaluator [Evaluator]
115
+ # @param value_operator_matcher [ValueOperatorMatcher]
116
+ def initialize(evaluator:, value_operator_matcher:)
117
+ super(evaluator: evaluator)
118
+ # @type [ValueOperatorMatcher]
119
+ @value_operator_matcher = value_operator_matcher
120
+ end
121
+
122
+ def experiment_or_nil(request, key)
123
+ request.workspace.get_feature_flag_or_nil(key)
124
+ end
125
+
126
+ def resolve_evaluation(_request, evaluation)
127
+ evaluation
128
+ end
129
+
130
+ def evaluation_matches(evaluation, condition)
131
+ on = evaluation.variation_key != 'A'
132
+ @value_operator_matcher.matches(on, condition.match)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/model/target'
4
+ require 'hackle/internal/evaluation/match/condition/condition_matcher'
5
+
6
+ module Hackle
7
+ class SegmentConditionMatcher
8
+ include ConditionMatcher
9
+
10
+ # @param segment_matcher [SegmentMatcher]
11
+ def initialize(segment_matcher:)
12
+ # @type [SegmentMatcher]
13
+ @segment_matcher = segment_matcher
14
+ end
15
+
16
+ def matches(request, context, condition)
17
+ if condition.key.type != TargetKeyType::SEGMENT
18
+ raise ArgumentError, "unsupported TargetKeyType [#{condition.key.type}]"
19
+ end
20
+
21
+ matches = condition.match.values.any? { |it| value_matches(request, context, it) }
22
+ TargetMatchType.matches(condition.match.type, matches)
23
+ end
24
+
25
+ private
26
+
27
+ # @param request [EvaluatorRequest]
28
+ # @param context [EvaluatorContext]
29
+ # @param value [Object]
30
+ # @return [boolean]
31
+ def value_matches(request, context, value)
32
+ segment_key = value
33
+ raise ArgumentError, "segment key [#{value}]" unless segment_key.is_a?(String)
34
+
35
+ segment = request.workspace.get_segment_or_nil(segment_key)
36
+ raise ArgumentError, "segment [#{segment_key}]" unless segment
37
+
38
+ @segment_matcher.matches(request, context, segment)
39
+ end
40
+ end
41
+
42
+ class SegmentMatcher
43
+ # @param user_condition_matcher [ConditionMatcher]
44
+ def initialize(user_condition_matcher:)
45
+ # @type [ConditionMatcher]
46
+ @user_condition_matcher = user_condition_matcher
47
+ end
48
+
49
+ # @param request [EvaluatorRequest]
50
+ # @param context [EvaluatorContext]
51
+ # @param segment [Segment]
52
+ # @return [boolean]
53
+ def matches(request, context, segment)
54
+ segment.targets.any? { |it| target_matches(request, context, it) }
55
+ end
56
+
57
+ private
58
+
59
+ # @param request [EvaluatorRequest]
60
+ # @param context [EvaluatorContext]
61
+ # @param target [Target]
62
+ # @return [boolean]
63
+ def target_matches(request, context, target)
64
+ target.conditions.all? { |it| @user_condition_matcher.matches(request, context, it) }
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/model/target'
4
+ require 'hackle/internal/evaluation/match/condition/condition_matcher'
5
+
6
+ module Hackle
7
+ class UserConditionMatcher
8
+ include ConditionMatcher
9
+
10
+ # @param user_value_resolver [UserValueResolver]
11
+ # @param value_operator_matcher [ValueOperatorMatcher]
12
+ def initialize(user_value_resolver:, value_operator_matcher:)
13
+ # @type [UserValueResolver]
14
+ @user_value_resolver = user_value_resolver
15
+ # @type [ValueOperatorMatcher]
16
+ @value_operator_matcher = value_operator_matcher
17
+ end
18
+
19
+ def matches(request, context, condition)
20
+ user_value = @user_value_resolver.resolve_or_nil(request.user, condition.key)
21
+ return false if user_value.nil?
22
+
23
+ @value_operator_matcher.matches(user_value, condition.match)
24
+ end
25
+ end
26
+
27
+ class UserValueResolver
28
+ # @param user [HackleUser]
29
+ # @param key [TargetKey]
30
+ # @return [Object, nil]
31
+ def resolve_or_nil(user, key)
32
+ case key.type
33
+ when TargetKeyType::USER_ID
34
+ user.identifiers[key.name]
35
+ when TargetKeyType::USER_PROPERTY
36
+ user.properties[key.name]
37
+ when TargetKeyType::HACKLE_PROPERTY
38
+ nil
39
+ else
40
+ raise ArgumentError, "unsupported TargetKeyType: #{key.type}"
41
+ end
42
+ end
43
+ end
44
+ end