hackle-ruby-sdk 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hackle/client.rb +186 -87
  3. data/lib/hackle/config.rb +59 -17
  4. data/lib/hackle/decision.rb +113 -0
  5. data/lib/hackle/event.rb +89 -0
  6. data/lib/hackle/internal/clock/clock.rb +47 -0
  7. data/lib/hackle/internal/concurrent/executors.rb +20 -0
  8. data/lib/hackle/internal/concurrent/schedule/scheduler.rb +12 -0
  9. data/lib/hackle/internal/concurrent/schedule/timer_scheduler.rb +30 -0
  10. data/lib/hackle/internal/config/parameter_config.rb +50 -0
  11. data/lib/hackle/internal/core/hackle_core.rb +182 -0
  12. data/lib/hackle/{decision → internal/evaluation/bucketer}/bucketer.rb +17 -15
  13. data/lib/hackle/internal/evaluation/evaluator/contextual/contextual_evaluator.rb +29 -0
  14. data/lib/hackle/internal/evaluation/evaluator/delegating/delegating_evaluator.rb +26 -0
  15. data/lib/hackle/internal/evaluation/evaluator/evaluator.rb +117 -0
  16. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory.rb +67 -0
  17. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluator.rb +172 -0
  18. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_flow_evaluator.rb +241 -0
  19. data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_resolver.rb +166 -0
  20. data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_determiner.rb +48 -0
  21. data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_evaluator.rb +174 -0
  22. data/lib/hackle/internal/evaluation/flow/evaluation_flow.rb +49 -0
  23. data/lib/hackle/internal/evaluation/flow/flow_evaluator.rb +11 -0
  24. data/lib/hackle/internal/evaluation/match/condition/condition_matcher.rb +11 -0
  25. data/lib/hackle/internal/evaluation/match/condition/condition_matcher_factory.rb +53 -0
  26. data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_condition_matcher.rb +29 -0
  27. data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_evaluator_matcher.rb +135 -0
  28. data/lib/hackle/internal/evaluation/match/condition/segment/segment_condition_matcher.rb +67 -0
  29. data/lib/hackle/internal/evaluation/match/condition/user/user_condition_matcher.rb +44 -0
  30. data/lib/hackle/internal/evaluation/match/operator/operator_matcher.rb +185 -0
  31. data/lib/hackle/internal/evaluation/match/operator/operator_matcher_factory.rb +31 -0
  32. data/lib/hackle/internal/evaluation/match/target/target_matcher.rb +31 -0
  33. data/lib/hackle/internal/evaluation/match/value/value_matcher.rb +96 -0
  34. data/lib/hackle/internal/evaluation/match/value/value_matcher_factory.rb +28 -0
  35. data/lib/hackle/internal/evaluation/match/value/value_operator_matcher.rb +59 -0
  36. data/lib/hackle/internal/event/user_event.rb +187 -0
  37. data/lib/hackle/internal/event/user_event_dispatcher.rb +156 -0
  38. data/lib/hackle/internal/event/user_event_factory.rb +58 -0
  39. data/lib/hackle/internal/event/user_event_processor.rb +181 -0
  40. data/lib/hackle/internal/http/http.rb +28 -0
  41. data/lib/hackle/internal/http/http_client.rb +48 -0
  42. data/lib/hackle/internal/identifiers/identifier_builder.rb +67 -0
  43. data/lib/hackle/internal/logger/logger.rb +31 -0
  44. data/lib/hackle/internal/model/action.rb +57 -0
  45. data/lib/hackle/internal/model/bucket.rb +58 -0
  46. data/lib/hackle/internal/model/container.rb +47 -0
  47. data/lib/hackle/internal/model/decision_reason.rb +31 -0
  48. data/lib/hackle/{models → internal/model}/event_type.rb +5 -8
  49. data/lib/hackle/internal/model/experiment.rb +194 -0
  50. data/lib/hackle/internal/model/parameter_configuration.rb +19 -0
  51. data/lib/hackle/internal/model/remote_config_parameter.rb +76 -0
  52. data/lib/hackle/internal/model/sdk.rb +23 -0
  53. data/lib/hackle/internal/model/segment.rb +61 -0
  54. data/lib/hackle/internal/model/target.rb +203 -0
  55. data/lib/hackle/internal/model/target_rule.rb +19 -0
  56. data/lib/hackle/internal/model/targeting.rb +45 -0
  57. data/lib/hackle/internal/model/value_type.rb +75 -0
  58. data/lib/hackle/internal/model/variation.rb +27 -0
  59. data/lib/hackle/internal/model/version.rb +153 -0
  60. data/lib/hackle/internal/properties/properties_builder.rb +101 -0
  61. data/lib/hackle/internal/user/hackle_user.rb +74 -0
  62. data/lib/hackle/internal/user/hackle_user_resolver.rb +27 -0
  63. data/lib/hackle/internal/workspace/http_workspace_fetcher.rb +50 -0
  64. data/lib/hackle/internal/workspace/polling_workspace_fetcher.rb +62 -0
  65. data/lib/hackle/internal/workspace/workspace.rb +353 -0
  66. data/lib/hackle/internal/workspace/workspace_fetcher.rb +18 -0
  67. data/lib/hackle/remote_config.rb +55 -0
  68. data/lib/hackle/user.rb +124 -0
  69. data/lib/hackle/version.rb +1 -11
  70. data/lib/hackle.rb +4 -69
  71. metadata +123 -53
  72. data/.gitignore +0 -11
  73. data/.rspec +0 -2
  74. data/.travis.yml +0 -7
  75. data/Gemfile +0 -6
  76. data/README.md +0 -33
  77. data/Rakefile +0 -6
  78. data/hackle-ruby-sdk.gemspec +0 -29
  79. data/lib/hackle/decision/decider.rb +0 -69
  80. data/lib/hackle/events/event_dispatcher.rb +0 -96
  81. data/lib/hackle/events/event_processor.rb +0 -126
  82. data/lib/hackle/events/user_event.rb +0 -61
  83. data/lib/hackle/http/http.rb +0 -37
  84. data/lib/hackle/models/bucket.rb +0 -26
  85. data/lib/hackle/models/event.rb +0 -26
  86. data/lib/hackle/models/experiment.rb +0 -69
  87. data/lib/hackle/models/slot.rb +0 -22
  88. data/lib/hackle/models/user.rb +0 -24
  89. data/lib/hackle/models/variation.rb +0 -21
  90. data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
  91. data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -47
  92. data/lib/hackle/workspaces/workspace.rb +0 -100
@@ -0,0 +1,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