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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2eb9518d11c4980c9a6abf036b8400fad5de381db6c60d2fc98b68e37cda8851
4
- data.tar.gz: faf6536122c01e9a55f95eef4e6497313a7e09e2ff16ea1123ca6c7fdc56470a
3
+ metadata.gz: 572cfbd5a794b4516be482e888dffdbfbf98485ea566ceef44187b8555f754d8
4
+ data.tar.gz: 80c1c73b16728ee50eab4a613c0810f42883db15154a0b697d796e003a222274
5
5
  SHA512:
6
- metadata.gz: cbf193163d0b5926f052e76b7b01e0ca472297ef5da32b2317e90a1f35450cefa6496a72bcd0e9d6c7faf83947b6a9379e07be7111f5fa0a5030b891013c6c1c
7
- data.tar.gz: e9d8ed3a4439684fb0b3308edb0a2e48c36ba041fa8f081e33ddad237ee8a5b546f0c98a1ec05b5c4123f76fe9289bc2ea3e84d9b02b068a3833472eec77691e
6
+ metadata.gz: 9f68e02af4c482fba3d10f8598ffc832240524791ad66e29db644996d128236e594c79602ec37e040ba800e6a77f3bab8eb226e5f336aa852c0b9e60eedbc723
7
+ data.tar.gz: 11eaa326223fd384bc0c0261c1ea3ef6ab7fbff6885570cf62ce3c124cdd0104d63f6525420c5a976c2e14cac932320f07e7aef80f1e0b751be6d1702e11b26d
data/lib/hackle/client.rb CHANGED
@@ -1,127 +1,226 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'hackle/decision/bucketer'
4
- require 'hackle/decision/decider'
5
-
6
- require 'hackle/events/user_event'
7
- require 'hackle/events/event_dispatcher'
8
- require 'hackle/events/event_processor'
9
-
10
- require 'hackle/http/http'
11
-
12
- require 'hackle/models/bucket'
13
- require 'hackle/models/event'
14
- require 'hackle/models/event_type'
15
- require 'hackle/models/experiment'
16
- require 'hackle/models/slot'
17
- require 'hackle/models/user'
18
- require 'hackle/models/variation'
19
-
20
- require 'hackle/workspaces/http_workspace_fetcher'
21
- require 'hackle/workspaces/polling_workspace_fetcher'
22
- require 'hackle/workspaces/workspace'
3
+ require 'hackle/event'
4
+ require 'hackle/decision'
5
+ require 'hackle/config'
6
+ require 'hackle/remote_config'
7
+ require 'hackle/version'
8
+ require 'hackle/internal/concurrent/executors'
9
+ require 'hackle/internal/logger/logger'
10
+ require 'hackle/internal/model/sdk'
11
+ require 'hackle/internal/model/decision_reason'
12
+ require 'hackle/internal/config/parameter_config'
13
+ require 'hackle/internal/workspace/http_workspace_fetcher'
14
+ require 'hackle/internal/workspace/polling_workspace_fetcher'
15
+ require 'hackle/internal/event/user_event_dispatcher'
16
+ require 'hackle/internal/event/user_event_processor'
17
+ require 'hackle/internal/http/http_client'
18
+ require 'hackle/internal/core/hackle_core'
19
+ require 'hackle/internal/user/hackle_user_resolver'
23
20
 
24
21
  module Hackle
25
-
26
22
  #
27
- # A client for Hackle API.
23
+ # The entry point of Hackle SDKs.
28
24
  #
29
25
  class Client
26
+ # @param core [Hackle::Core]
27
+ # @param user_resolver [Hackle::HackleUserResolver]
28
+ def initialize(core:, user_resolver:)
29
+ # @type [Hackle::Core]
30
+ @core = core
31
+
32
+ # @type [Hackle::HackleUserResolver]
33
+ @user_resolver = user_resolver
34
+ end
35
+
36
+ @instance = {}
37
+
38
+ #
39
+ # Instantiates a Hackle client.
40
+ #
41
+ # @param sdk_key [String]
42
+ # @param config [Hackle::Config]
43
+ # @return [Hackle::Client]
44
+ #
45
+ def self.create(sdk_key:, config: Config.builder.build)
46
+ client = @instance[sdk_key]
47
+ unless client.nil?
48
+ client.__resume
49
+ return client
50
+ end
30
51
 
52
+ Log.init(config.logger)
53
+
54
+ sdk = Sdk.new(name: 'ruby-sdk', version: Hackle::VERSION, key: sdk_key)
55
+
56
+ http_workspace_fetcher = HttpWorkspaceFetcher.new(
57
+ http_client: HttpClient.create(base_url: config.sdk_url, sdk: sdk),
58
+ sdk: sdk
59
+ )
60
+ workspace_fetcher = PollingWorkspaceFetcher.new(
61
+ http_workspace_fetcher: http_workspace_fetcher,
62
+ scheduler: Executors.scheduler,
63
+ polling_interval_seconds: 10.0
64
+ )
65
+
66
+ event_dispatcher = UserEventDispatcher.create(
67
+ http_client: HttpClient.create(base_url: config.event_url, sdk: sdk),
68
+ executor: Executors.thread_pool(pool_size: 2, queue_capacity: 64)
69
+ )
70
+
71
+ event_processor = DefaultUserEventProcessor.new(
72
+ queue: SizedQueue.new(10_000),
73
+ event_dispatcher: event_dispatcher,
74
+ event_dispatch_size: 100,
75
+ flush_scheduler: Executors.scheduler,
76
+ flush_interval_seconds: 10.0,
77
+ shutdown_timeout_seconds: 10.0
78
+ )
79
+
80
+ workspace_fetcher.start
81
+ event_processor.start
82
+
83
+ core = Core.create(
84
+ workspace_fetcher: workspace_fetcher,
85
+ event_processor: event_processor
86
+ )
87
+
88
+ new_client = Client.new(
89
+ core: core,
90
+ user_resolver: HackleUserResolver.new
91
+ )
92
+ @instance[sdk_key] = new_client
93
+ new_client
94
+ end
95
+
96
+ #
97
+ # Decide the variation to expose to the user for experiment.
31
98
  #
32
- # Initializes a Hackle client.
99
+ # @param experiment_key [Integer]
100
+ # @param user [Hackle::User]
33
101
  #
34
- # @param config [Config]
35
- # @param workspace_fetcher [PollingWorkspaceFetcher]
36
- # @param event_processor [EventProcessor]
37
- # @param decider [Decider]
102
+ # @return [String] the decided variation for the user
38
103
  #
39
- def initialize(config:, workspace_fetcher:, event_processor:, decider:)
40
- @logger = config.logger
104
+ def variation(experiment_key, user)
105
+ variation_detail(experiment_key, user).variation
106
+ end
41
107
 
42
- # @type [PollingWorkspaceFetcher]
43
- @workspace_fetcher = workspace_fetcher
108
+ #
109
+ # Decide the variation to expose to the user for experiment, and returns an object that
110
+ # describes the way the variation was decided.
111
+ #
112
+ # @param experiment_key [Integer] the unique key of the experiment. MUST NOT be nil.
113
+ # @param user [Hackle::User] the user to participate in the experiment. MUST NOT be nil.
114
+ #
115
+ # @return [Hackle::ExperimentDecision] an object describing the result
116
+ #
117
+ def variation_detail(experiment_key, user)
118
+ unless experiment_key.is_a?(Integer)
119
+ Log.get.warn { "Invalid experiment key: #{experiment_key} (expected: integer)" }
120
+ return ExperimentDecision.new('A', DecisionReason::INVALID_INPUT, ParameterConfig.empty)
121
+ end
44
122
 
45
- # @type [EventProcessor]
46
- @event_processor = event_processor
123
+ hackle_user = @user_resolver.resolve_or_nil(user)
124
+ if hackle_user.nil?
125
+ Log.get.warn { "Invalid hackle user: #{user}" }
126
+ return ExperimentDecision.new('A', DecisionReason::INVALID_INPUT, ParameterConfig.empty)
127
+ end
47
128
 
48
- # @type [Decider]
49
- @decider = decider
129
+ @core.experiment(experiment_key, hackle_user, 'A')
130
+ rescue => e
131
+ Log.get.error { "Unexpected error while deciding variation of experiment[#{experiment_key}]: #{e.inspect}]" }
132
+ ExperimentDecision.new('A', DecisionReason::EXCEPTION, ParameterConfig.empty)
50
133
  end
51
134
 
52
135
  #
53
- # Decide the variation to expose to the user for experiment.
136
+ # Decide whether the feature is turned on to the user.
137
+ #
138
+ # @param feature_key [Integer] the unique key of the feature.
139
+ # @param user [Hackle::User] the user requesting the feature.
140
+ #
141
+ # @return [TrueClass] of the feature is on
142
+ # @return [FalseClass] of the feature is off
143
+ #
144
+ def is_feature_on(feature_key, user)
145
+ feature_flag_detail(feature_key, user).is_on
146
+ end
147
+
54
148
  #
55
- # This method return the control variation 'A' if:
56
- # - The experiment key is invalid
57
- # - The experiment has not started yet
58
- # - The user is not allocated to the experiment
59
- # - The decided variation has been dropped
60
- #
61
- # @param experiment_key [Integer] The unique key of the experiment. MUST NOT be nil.
62
- # @param user [User] the user to participate in the experiment. MUST NOT be nil.
63
- # @param default_variation [String] The default variation of the experiment.
64
- #
65
- # @return [String] The decided variation for the user, or default variation
66
- #
67
- def variation(experiment_key:, user:, default_variation: 'A')
68
-
69
- return default_variation if experiment_key.nil? || !experiment_key.is_a?(Integer)
70
- return default_variation if user.nil? || !user.is_a?(User) || !user.valid?
71
-
72
- workspace = @workspace_fetcher.fetch
73
- return default_variation if workspace.nil?
74
-
75
- experiment = workspace.get_experiment(experiment_key: experiment_key)
76
- return default_variation if experiment.nil?
77
-
78
- decision = @decider.decide(experiment: experiment, user: user)
79
- case decision
80
- when Decision::NotAllocated
81
- default_variation
82
- when Decision::ForcedAllocated
83
- decision.variation_key
84
- when Decision::NaturalAllocated
85
- exposure_event = UserEvent::Exposure.new(user: user, experiment: experiment, variation: decision.variation)
86
- @event_processor.process(event: exposure_event)
87
- decision.variation.key
88
- else
89
- default_variation
149
+ # Decide whether the feature is turned on to the user, and returns an object that
150
+ # describes the way the value was decided.
151
+ #
152
+ # @param feature_key [Integer] the unique key of the feature.
153
+ # @param user [Hackle::User] the user requesting the feature.
154
+ #
155
+ # @return [Hackle::FeatureFlagDecision] an object describing the result
156
+ #
157
+ def feature_flag_detail(feature_key, user)
158
+ unless feature_key.is_a?(Integer)
159
+ Log.get.warn { "Invalid feature key: #{feature_key} (expected: integer)" }
160
+ return FeatureFlagDecision.new(false, DecisionReason::INVALID_INPUT, ParameterConfig.empty)
90
161
  end
91
162
 
163
+ hackle_user = @user_resolver.resolve_or_nil(user)
164
+ if hackle_user.nil?
165
+ Log.get.warn { "Invalid hackle user: #{user}" }
166
+ return FeatureFlagDecision.new(false, DecisionReason::INVALID_INPUT, ParameterConfig.empty)
167
+ end
168
+
169
+ @core.feature_flag(feature_key, hackle_user)
92
170
  rescue => e
93
- @logger.error { "Unexpected error while deciding variation for experiment[#{experiment_key}]. Returning default variation[#{default_variation}]: #{e.inspect}" }
94
- default_variation
171
+ Log.get.error { "Unexpected error while deciding feature flag[#{feature_key}]: #{e.inspect}]" }
172
+ FeatureFlagDecision.new(false, DecisionReason::EXCEPTION, ParameterConfig.empty)
95
173
  end
96
174
 
97
175
  #
98
- # Records the event that occurred by the user.
176
+ # Returns a instance of Hackle::RemoteConfig.
99
177
  #
100
- # @param event [Event] the event that occurred.
101
- # @param user [User] the user that occurred the event.
178
+ # @param user [Hackle::User] the user requesting the remote config.
102
179
  #
103
- def track(event:, user:)
180
+ # @return [Hackle::RemoteConfig]
181
+ #
182
+ def remote_config(user)
183
+ RemoteConfig.new(user: user, user_resolver: @user_resolver, core: @core)
184
+ end
104
185
 
105
- return if event.nil? || !event.is_a?(Event) || !event.valid?
106
- return if user.nil? || !user.is_a?(User) || !user.valid?
186
+ #
187
+ # Records the event that occurred by the user.
188
+ #
189
+ # @param event [Hackle::Event] the event that occurred.
190
+ # @param user [Hackle::User] the user that occurred the event.
191
+ #
192
+ def track(event, user)
193
+ unless event.is_a?(Event)
194
+ Log.get.warn { "Invalid event: #{event} (expected: Hackle::Event)" }
195
+ return
196
+ end
107
197
 
108
- workspace = @workspace_fetcher.fetch
109
- return if workspace.nil?
198
+ unless event.valid?
199
+ Log.get.error { "Invalid event: #{event.error_or_nil}" }
200
+ return
201
+ end
110
202
 
111
- event_type = workspace.get_event_type(event_type_key: event.key)
112
- track_event = UserEvent::Track.new(user: user, event_type: event_type, event: event)
113
- @event_processor.process(event: track_event)
203
+ hackle_user = @user_resolver.resolve_or_nil(user)
204
+ if hackle_user.nil?
205
+ Log.get.warn { "Invalid hackle user: #{user}" }
206
+ return FeatureFlagDecision.new(false, DecisionReason::INVALID_INPUT, ParameterConfig.empty)
207
+ end
114
208
 
209
+ @core.track(event, hackle_user)
115
210
  rescue => e
116
- @logger.error { "Unexpected error while tracking event: #{e.inspect}" }
211
+ Log.get.error { "Unexpected error while tracking event: #{e.inspect}]" }
117
212
  end
118
213
 
119
214
  #
120
215
  # Shutdown the background task and release the resources used for the background task.
216
+ # This should only be called when the application shutdown.
121
217
  #
122
218
  def close
123
- @workspace_fetcher.stop!
124
- @event_processor.stop!
219
+ @core.close
220
+ end
221
+
222
+ def __resume
223
+ @core.resume
125
224
  end
126
225
  end
127
226
  end
data/lib/hackle/config.rb CHANGED
@@ -4,30 +4,72 @@ require 'logger'
4
4
 
5
5
  module Hackle
6
6
  class Config
7
+ # @return [Logger]
8
+ attr_reader :logger
7
9
 
8
- def initialize(options = {})
9
- @logger = options[:logger] || Config.default_logger
10
- @base_uri = options[:base_uri] || Config.default_base_uri
11
- @event_uri = options[:event_uri] || Config.default_event_uri
12
- end
10
+ # @return [String]
11
+ attr_reader :sdk_url
13
12
 
14
- attr_reader :logger
15
- attr_reader :base_uri
16
- attr_reader :event_uri
13
+ # @return [String]
14
+ attr_reader :event_url
17
15
 
18
- def self.default_base_uri
19
- 'https://sdk.hackle.io'
16
+ # @param logger [Logger]
17
+ # @param sdk_url [String]
18
+ # @param event_url [String]
19
+ def initialize(
20
+ logger:,
21
+ sdk_url:,
22
+ event_url:
23
+ )
24
+ @logger = logger
25
+ @sdk_url = sdk_url
26
+ @event_url = event_url
20
27
  end
21
28
 
22
- def self.default_event_uri
23
- 'https://event.hackle.io'
29
+ def self.builder
30
+ Builder.new
24
31
  end
25
32
 
26
- def self.default_logger
27
- if defined?(Rails) && Rails.logger
28
- Rails.logger
29
- else
30
- Logger.new($stdout)
33
+ class Builder
34
+ def initialize
35
+ # noinspection RubyResolve
36
+ @logger = if defined?(Rails) && Rails.logger
37
+ Rails.logger
38
+ else
39
+ Logger.new($stdout)
40
+ end
41
+ @sdk_url = 'https://sdk.hackle.io'
42
+ @event_url = 'https://event.hackle.io'
43
+ end
44
+
45
+ # @param logger [Logger]
46
+ # @return [Hackle::Config::Builder]
47
+ def logger(logger)
48
+ @logger = logger
49
+ self
50
+ end
51
+
52
+ # @param sdk_url [String]
53
+ # @return [Hackle::Config::Builder]
54
+ def sdk_url(sdk_url)
55
+ @sdk_url = sdk_url
56
+ self
57
+ end
58
+
59
+ # @param event_url [String]
60
+ # @return [Hackle::Config::Builder]
61
+ def event_url(event_url)
62
+ @event_url = event_url
63
+ self
64
+ end
65
+
66
+ # @return [Hackle::Config]
67
+ def build
68
+ Config.new(
69
+ logger: @logger,
70
+ sdk_url: @sdk_url,
71
+ event_url: @event_url
72
+ )
31
73
  end
32
74
  end
33
75
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ class ExperimentDecision
5
+
6
+ # @return [String]
7
+ attr_reader :variation
8
+
9
+ # @return [String]
10
+ attr_reader :reason
11
+
12
+ # @return [ParameterConfig]
13
+ attr_reader :config
14
+
15
+ # @param variation [String]
16
+ # @param reason [String]
17
+ # @param config [ParameterConfig]
18
+ def initialize(variation, reason, config)
19
+ @config = config
20
+ @variation = variation
21
+ @reason = reason
22
+ end
23
+
24
+ # @param key [String]
25
+ # @param default_value [Object, nil]
26
+ # @return [Object, nil]
27
+ def get(key, default_value = nil)
28
+ config.get(key, default_value)
29
+ end
30
+
31
+ def ==(other)
32
+ other.is_a?(self.class) &&
33
+ variation == other.variation &&
34
+ reason == other.reason &&
35
+ config == other.config
36
+ end
37
+
38
+ def to_s
39
+ "ExperimentDecision(variation=#{variation}, reason=#{reason}, config=#{config})"
40
+ end
41
+
42
+ end
43
+
44
+ class FeatureFlagDecision
45
+
46
+ # @return [boolean]
47
+ attr_accessor :is_on
48
+
49
+ # @return [String]
50
+ attr_accessor :reason
51
+
52
+ # @return [ParameterConfig]
53
+ attr_reader :config
54
+
55
+ # @param is_on [boolean]
56
+ # @param reason [String]
57
+ # @param config [ParameterConfig]
58
+ def initialize(is_on, reason, config)
59
+ @is_on = is_on
60
+ @reason = reason
61
+ @config = config
62
+ end
63
+
64
+ # @return [boolean]
65
+ def on?
66
+ @is_on
67
+ end
68
+
69
+ # @param key [String]
70
+ # @param default_value [Object, nil]
71
+ # @return [Object, nil]
72
+ def get(key, default_value = nil)
73
+ config.get(key, default_value)
74
+ end
75
+
76
+ def ==(other)
77
+ other.is_a?(FeatureFlagDecision) &&
78
+ is_on == other.is_on &&
79
+ reason == other.reason &&
80
+ config == other.config
81
+ end
82
+
83
+ def to_s
84
+ "FeatureFlagDecision(is_on=#{is_on}, reason=#{reason}, config=#{config})"
85
+ end
86
+ end
87
+
88
+ class RemoteConfigDecision
89
+
90
+ # @return [Object, nil]
91
+ attr_reader :value
92
+
93
+ # @return [String]
94
+ attr_reader :reason
95
+
96
+ # @param value [Object, nil]
97
+ # @param reason [String]
98
+ def initialize(value, reason)
99
+ @value = value
100
+ @reason = reason
101
+ end
102
+
103
+ def ==(other)
104
+ other.is_a?(RemoteConfigDecision) &&
105
+ value == other.value &&
106
+ reason == other.reason
107
+ end
108
+
109
+ def to_s
110
+ "RemoteConfigDecision(value=#{value}, reason=#{reason})"
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/properties/properties_builder'
4
+
5
+ module Hackle
6
+ class Event
7
+ # @return [String]
8
+ attr_reader :key
9
+
10
+ # @return [Float, nil]
11
+ attr_reader :value
12
+
13
+ # @return [Hash{String => Object}]
14
+ attr_reader :properties
15
+
16
+ # @param key [String]
17
+ # @param value [Numeric, nil]
18
+ # @param properties [Hash{String => Object}]
19
+ def initialize(key:, value:, properties:)
20
+ @key = key
21
+ @value = value
22
+ @properties = properties
23
+ end
24
+
25
+ # @return [boolean]
26
+ def valid?
27
+ error_or_nil.nil?
28
+ end
29
+
30
+ # @return [String, nil]
31
+ def error_or_nil
32
+ return "Invalid event key: #{key} (expected: not empty string)" unless ValueType.not_empty_string?(key)
33
+ return "Invalid event value: #{value} (expected: number)" if !value.nil? && !ValueType.number?(value)
34
+ return "Invalid event properties: #{properties} (expected: Hash)" if !properties.nil? && !properties.is_a?(Hash)
35
+
36
+ nil
37
+ end
38
+
39
+ def ==(other)
40
+ other.is_a?(Event) && other.key == key && other.value == value && other.properties == properties
41
+ end
42
+
43
+ def to_s
44
+ "Hackle::Event(key: #{key}, value: #{value}, properties: #{properties})"
45
+ end
46
+
47
+ # @param key [String]
48
+ # @return [Hackle::Event::Builder]
49
+ def self.builder(key)
50
+ Builder.new(key)
51
+ end
52
+
53
+ class Builder
54
+ # @param key [String]
55
+ def initialize(key)
56
+ @key = key
57
+ @value = nil
58
+ @properties = PropertiesBuilder.new
59
+ end
60
+
61
+ # @param value [Float, nil]
62
+ # @return [Hackle::Event::Builder]
63
+ def value(value)
64
+ @value = value
65
+ self
66
+ end
67
+
68
+ # @param key [String]
69
+ # @param value [Object, nil]
70
+ # @return [Hackle::Event::Builder]
71
+ def property(key, value)
72
+ @properties.add(key, value)
73
+ self
74
+ end
75
+
76
+ # @param properties [Hash{String => Object}]
77
+ # @return [Hackle::Event::Builder]
78
+ def properties(properties)
79
+ @properties.add_all(properties)
80
+ self
81
+ end
82
+
83
+ # @return [Hackle::Event]
84
+ def build
85
+ Event.new(key: @key, value: @value, properties: @properties.build)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ module Clock
5
+ # @return [Integer]
6
+ def current_millis; end
7
+
8
+ # @return [Integer]
9
+ def tick; end
10
+ end
11
+
12
+ class SystemClock
13
+ include Clock
14
+
15
+ @instance = new
16
+
17
+ # @return [SystemClock]
18
+ def self.instance
19
+ @instance
20
+ end
21
+
22
+ def current_millis
23
+ (Time.now.to_f * 1000).to_i
24
+ end
25
+
26
+ def tick
27
+ (Time.now.to_f * 1000 * 1000 * 1000).to_i
28
+ end
29
+ end
30
+
31
+ class FixedClock
32
+ include Clock
33
+
34
+ # @param time [Integer]
35
+ def initialize(time)
36
+ @time = time
37
+ end
38
+
39
+ def current_millis
40
+ @time
41
+ end
42
+
43
+ def tick
44
+ @time
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+ require 'hackle/internal/concurrent/schedule/timer_scheduler'
5
+
6
+ module Hackle
7
+ class Executors
8
+ # @param pool_size [Integer]
9
+ # @param queue_capacity [Integer]
10
+ # @return [Concurrent::ThreadPoolExecutor]
11
+ def self.thread_pool(pool_size:, queue_capacity:)
12
+ Concurrent::ThreadPoolExecutor.new(min_threads: pool_size, max_threads: pool_size, max_queue: queue_capacity)
13
+ end
14
+
15
+ # @return [TimerScheduler]
16
+ def self.scheduler
17
+ TimerScheduler.new
18
+ end
19
+ end
20
+ end