hackle-ruby-sdk 1.0.0 → 2.0.0

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