hackle-ruby-sdk 0.1.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 +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