hackle-ruby-sdk 0.1.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 +191 -79
  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/internal/evaluation/bucketer/bucketer.rb +46 -0
  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/internal/model/event_type.rb +19 -0
  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 -32
  71. metadata +123 -51
  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/bucketer.rb +0 -30
  80. data/lib/hackle/decision/decider.rb +0 -54
  81. data/lib/hackle/events/event.rb +0 -33
  82. data/lib/hackle/events/event_dispatcher.rb +0 -89
  83. data/lib/hackle/events/event_processor.rb +0 -115
  84. data/lib/hackle/http/http.rb +0 -37
  85. data/lib/hackle/models/bucket.rb +0 -15
  86. data/lib/hackle/models/event_type.rb +0 -14
  87. data/lib/hackle/models/experiment.rb +0 -36
  88. data/lib/hackle/models/slot.rb +0 -15
  89. data/lib/hackle/models/variation.rb +0 -11
  90. data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
  91. data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -44
  92. data/lib/hackle/workspaces/workspace.rb +0 -87
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7ece6c3cc0981ab69f50731472d68c3f4903ccf30a5ffd5fb069210e076cc55
4
- data.tar.gz: f9f19bb60c2a4d34afe50203cc4637d92939e9c1a94e8ff86f1334e38d60a515
3
+ metadata.gz: 572cfbd5a794b4516be482e888dffdbfbf98485ea566ceef44187b8555f754d8
4
+ data.tar.gz: 80c1c73b16728ee50eab4a613c0810f42883db15154a0b697d796e003a222274
5
5
  SHA512:
6
- metadata.gz: e63f1afec1b82a27376f69db48721be968d72b7f959df243155d952159e615aa629dc1e98c473d72155769b7fca567c712e37c11efbbf3ccdee48726e1d76108
7
- data.tar.gz: f89a85d084ebb33b2c788674e4fb4869bae3c5d9952126faa75374a4d07b7db382af3a93b7f3c52acbf64525bdc3e0bd652d11ab64db0637f98f8b0a593541fb
6
+ metadata.gz: 9f68e02af4c482fba3d10f8598ffc832240524791ad66e29db644996d128236e594c79602ec37e040ba800e6a77f3bab8eb226e5f336aa852c0b9e60eedbc723
7
+ data.tar.gz: 11eaa326223fd384bc0c0261c1ea3ef6ab7fbff6885570cf62ce3c124cdd0104d63f6525420c5a976c2e14cac932320f07e7aef80f1e0b751be6d1702e11b26d
data/lib/hackle/client.rb CHANGED
@@ -1,114 +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/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_type'
14
- require 'hackle/models/experiment'
15
- require 'hackle/models/slot'
16
- require 'hackle/models/variation'
17
-
18
- require 'hackle/workspaces/http_workspace_fetcher'
19
- require 'hackle/workspaces/polling_workspace_fetcher'
20
- 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'
21
20
 
22
21
  module Hackle
23
-
24
22
  #
25
- # A client for Hackle API.
23
+ # The entry point of Hackle SDKs.
26
24
  #
27
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 = {}
28
37
 
29
38
  #
30
- # Initializes a Hackle client.
39
+ # Instantiates a Hackle client.
31
40
  #
32
- # @param config [Config]
33
- # @param workspace_fetcher [PollingWorkspaceFetcher]
34
- # @param event_processor [EventProcessor]
35
- # @param decider [Decider]
41
+ # @param sdk_key [String]
42
+ # @param config [Hackle::Config]
43
+ # @return [Hackle::Client]
36
44
  #
37
- def initialize(config:, workspace_fetcher:, event_processor:, decider:)
38
- @logger = config.logger
39
- @workspace_fetcher = workspace_fetcher
40
- @event_processor = event_processor
41
- @decider = decider
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
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
42
94
  end
43
95
 
44
96
  #
45
97
  # Decide the variation to expose to the user for experiment.
46
98
  #
47
- # This method return the control variation 'A' if:
48
- # - The experiment key is invalid
49
- # - The experiment has not started yet
50
- # - The user is not allocated to the experiment
51
- # - The decided variation has been dropped
52
- #
53
- # @param experiment_key [Integer] The unique key of the experiment.
54
- # @param user_id [String] The identifier of your customer. (e.g. user_email, account_id, decide_id, etc.)
55
- # @param default_variation [String] The default variation of the experiment.
56
- #
57
- # @return [String] The decided variation for the user, or default variation
58
- #
59
- def variation(experiment_key:, user_id:, default_variation: 'A')
60
-
61
- return default_variation if experiment_key.nil?
62
- return default_variation if user_id.nil?
63
-
64
- workspace = @workspace_fetcher.fetch
65
- return default_variation if workspace.nil?
66
-
67
- experiment = workspace.get_experiment(experiment_key: experiment_key)
68
- return default_variation if experiment.nil?
69
-
70
- decision = @decider.decide(experiment: experiment, user_id: user_id)
71
- case decision
72
- when Decision::NotAllocated
73
- default_variation
74
- when Decision::ForcedAllocated
75
- decision.variation_key
76
- when Decision::NaturalAllocated
77
- exposure_event = Event::Exposure.new(user_id: user_id, experiment: experiment, variation: decision.variation)
78
- @event_processor.process(event: exposure_event)
79
- decision.variation.key
80
- else
81
- default_variation
99
+ # @param experiment_key [Integer]
100
+ # @param user [Hackle::User]
101
+ #
102
+ # @return [String] the decided variation for the user
103
+ #
104
+ def variation(experiment_key, user)
105
+ variation_detail(experiment_key, user).variation
106
+ end
107
+
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
122
+
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
128
+
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)
133
+ end
134
+
135
+ #
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
+
148
+ #
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)
161
+ end
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)
82
167
  end
168
+
169
+ @core.feature_flag(feature_key, hackle_user)
170
+ rescue => e
171
+ Log.get.error { "Unexpected error while deciding feature flag[#{feature_key}]: #{e.inspect}]" }
172
+ FeatureFlagDecision.new(false, DecisionReason::EXCEPTION, ParameterConfig.empty)
83
173
  end
84
174
 
85
175
  #
86
- # Records the events performed by the user.
176
+ # Returns a instance of Hackle::RemoteConfig.
177
+ #
178
+ # @param user [Hackle::User] the user requesting the remote config.
87
179
  #
88
- # @param event_key [String] The unique key of the events.
89
- # @param user_id [String] The identifier of user that performed the vent.
90
- # @param value [Float] Additional numeric value of the events (e.g. purchase_amount, api_latency, etc.)
180
+ # @return [Hackle::RemoteConfig]
91
181
  #
92
- def track(event_key:, user_id:, value: nil)
182
+ def remote_config(user)
183
+ RemoteConfig.new(user: user, user_resolver: @user_resolver, core: @core)
184
+ end
93
185
 
94
- return if event_key.nil?
95
- return if user_id.nil?
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
96
197
 
97
- workspace = @workspace_fetcher.fetch
98
- return if workspace.nil?
198
+ unless event.valid?
199
+ Log.get.error { "Invalid event: #{event.error_or_nil}" }
200
+ return
201
+ end
99
202
 
100
- event_type = workspace.get_event_type(event_type_key: event_key)
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
101
208
 
102
- track_event = Event::Track.new(user_id: user_id, event_type: event_type, value: value)
103
- @event_processor.process(event: track_event)
209
+ @core.track(event, hackle_user)
210
+ rescue => e
211
+ Log.get.error { "Unexpected error while tracking event: #{e.inspect}]" }
104
212
  end
105
213
 
106
214
  #
107
215
  # Shutdown the background task and release the resources used for the background task.
216
+ # This should only be called when the application shutdown.
108
217
  #
109
218
  def close
110
- @workspace_fetcher.stop!
111
- @event_processor.stop!
219
+ @core.close
220
+ end
221
+
222
+ def __resume
223
+ @core.resume
112
224
  end
113
225
  end
114
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