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
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'hackle/internal/http/http'
5
+
6
+ module Hackle
7
+ class UserEventDispatcher
8
+ # @param http_client [HttpClient]
9
+ # @param executor [ThreadPoolExecutor]
10
+ # @param serializer [UserEventSerializer]
11
+ def initialize(http_client:, executor:, serializer:)
12
+ # @type [HttpClient]
13
+ @http_client = http_client
14
+ # @type [ThreadPoolExecutor]
15
+ @executor = executor
16
+ @serializer = serializer
17
+ @url = '/api/v2/events'
18
+ end
19
+
20
+ # @param http_client [HttpClient]
21
+ # @param executor [ThreadPoolExecutor]
22
+ def self.create(http_client:, executor:)
23
+ UserEventDispatcher.new(
24
+ http_client: http_client,
25
+ executor: executor,
26
+ serializer: UserEventSerializer.new
27
+ )
28
+ end
29
+
30
+ # @param events [Array<UserEvent>]
31
+ def dispatch(events)
32
+ payload = @serializer.serialize(events)
33
+ begin
34
+ @executor.post { dispatch_internal(payload) }
35
+ rescue => e
36
+ Log.get.error { "Unexpected error while posting events: #{e.inspect}" }
37
+ end
38
+ end
39
+
40
+ def shutdown
41
+ @executor.shutdown
42
+ return if @executor.wait_for_termination(10)
43
+
44
+ Log.get.warn { 'Failed to dispatch previously submitted events' }
45
+ end
46
+
47
+ private
48
+
49
+ # @param payload [Hash]
50
+ def dispatch_internal(payload)
51
+ request = create_request(payload)
52
+ response = @http_client.execute(request)
53
+ handle_response(response)
54
+ rescue => e
55
+ Log.get.error { "Failed to dispatch events: #{e.inspect}" }
56
+ end
57
+
58
+ # @param payload [Hash]
59
+ # @return [Net::HTTPRequest]
60
+ def create_request(payload)
61
+ request = Net::HTTP::Post.new(@url)
62
+ request.content_type = 'application/json'
63
+ request.body = payload.to_json
64
+ request
65
+ end
66
+
67
+ # @param response [Net::HTTPResponse]
68
+ def handle_response(response)
69
+ raise "http status code: #{response.code}" unless HTTP.successful? response
70
+ end
71
+ end
72
+
73
+ class UserEventSerializer
74
+ # @param events [Array<UserEvent>]
75
+ # @return [Hash]
76
+ def serialize(events)
77
+ exposure_events = []
78
+ track_events = []
79
+ remote_config_events = []
80
+
81
+ events.each do |event|
82
+ exposure_events << exposure_event(event) if event.is_a? ExposureEvent
83
+ track_events << track_event(event) if event.is_a? TrackEvent
84
+ remote_config_events << remote_config_event(event) if event.is_a? RemoteConfigEvent
85
+ end
86
+ {
87
+ exposureEvents: exposure_events,
88
+ trackEvents: track_events,
89
+ remoteConfigEvents: remote_config_events
90
+ }
91
+ end
92
+
93
+ # @param event [ExposureEvent]
94
+ # @return [Hash]
95
+ def exposure_event(event)
96
+ {
97
+ insertId: event.insert_id,
98
+ timestamp: event.timestamp,
99
+
100
+ userId: event.user.identifiers['$id'],
101
+ identifiers: event.user.identifiers,
102
+ userProperties: event.user.properties,
103
+ hackleProperties: {},
104
+
105
+ experimentId: event.experiment.id,
106
+ experimentKey: event.experiment.key,
107
+ experimentType: event.experiment.type.name,
108
+ experimentVersion: event.experiment.version,
109
+ variationId: event.variation_id,
110
+ variationKey: event.variation_key,
111
+ decisionReason: event.decision_reason,
112
+ properties: event.properties
113
+ }
114
+ end
115
+
116
+ # @param event [TrackEvent]
117
+ # @return [Hash]
118
+ def track_event(event)
119
+ {
120
+ insertId: event.insert_id,
121
+ timestamp: event.timestamp,
122
+
123
+ userId: event.user.identifiers['$id'],
124
+ identifiers: event.user.identifiers,
125
+ userProperties: event.user.properties,
126
+ hackleProperties: {},
127
+
128
+ eventTypeId: event.event_type.id,
129
+ eventTypeKey: event.event_type.key,
130
+ value: event.event.value,
131
+ properties: event.event.properties
132
+ }
133
+ end
134
+
135
+ # @param event [RemoteConfigEvent]
136
+ # @return [Hash]
137
+ def remote_config_event(event)
138
+ {
139
+ insertId: event.insert_id,
140
+ timestamp: event.timestamp,
141
+
142
+ userId: event.user.identifiers['$id'],
143
+ identifiers: event.user.identifiers,
144
+ userProperties: event.user.properties,
145
+ hackleProperties: {},
146
+
147
+ parameterId: event.parameter.id,
148
+ parameterKey: event.parameter.key,
149
+ parameterType: event.parameter.type.name,
150
+ valueId: event.value_id,
151
+ decisionReason: event.decision_reason,
152
+ properties: event.properties
153
+ }
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/evaluation/evaluator/experiment/experiment_evaluator'
4
+
5
+ module Hackle
6
+ class UserEventFactory
7
+ # @param clock [Clock]
8
+ def initialize(clock:)
9
+ # @type [Clock]
10
+ @clock = clock
11
+ end
12
+
13
+ # @param request [EvaluatorRequest]
14
+ # @param evaluation [EvaluatorEvaluation]
15
+ # @return [Array<UserEvent>]
16
+ def create(request, evaluation)
17
+ timestamp = @clock.current_millis
18
+ events = []
19
+
20
+ root_event = create_internal(request, evaluation, timestamp, PropertiesBuilder.new)
21
+ events << root_event unless root_event.nil?
22
+
23
+ evaluation.target_evaluations.each do |target_evaluation|
24
+ properties_builder = PropertiesBuilder.new
25
+ properties_builder.add('$targetingRootType', request.key.type)
26
+ properties_builder.add('$targetingRootId', request.key.id)
27
+ target_event = create_internal(request, target_evaluation, timestamp, properties_builder)
28
+ events << target_event unless target_event.nil?
29
+ end
30
+
31
+ events
32
+ end
33
+
34
+ private
35
+
36
+ # @param request [EvaluatorRequest]
37
+ # @param evaluation [EvaluatorEvaluation]
38
+ # @param timestamp [Integer]
39
+ # @param properties_builder [PropertiesBuilder]
40
+ # @return [UserEvent, nil]
41
+ def create_internal(request, evaluation, timestamp, properties_builder)
42
+ e = evaluation
43
+ case e
44
+ when ExperimentEvaluation
45
+ properties_builder.add('$parameterConfigurationId', e.config&.id)
46
+ properties_builder.add('$experiment_version', e.experiment.version)
47
+ properties_builder.add('$execution_version', e.experiment.execution_version)
48
+ UserEvent.exposure(e, properties_builder.build, request.user, timestamp)
49
+ when RemoteConfigEvaluation
50
+ properties_builder.add_all(e.properties)
51
+ UserEvent.remote_config(e, properties_builder.build, request.user, timestamp)
52
+ else
53
+ Log.get.error { "unsupported evaluator evaluation: #{e.class}" }
54
+ nil
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/logger/logger'
4
+
5
+ module Hackle
6
+ module UserEventProcessor
7
+ # @param event [UserEvent]
8
+ def process(event) end
9
+
10
+ def start
11
+ end
12
+
13
+ def stop
14
+ end
15
+
16
+ def resume
17
+ end
18
+ end
19
+
20
+ # noinspection RubyTooManyInstanceVariablesInspection
21
+ class DefaultUserEventProcessor
22
+ include UserEventProcessor
23
+ # @param queue [SizedQueue]
24
+ # @param event_dispatcher [UserEventDispatcher]
25
+ # @param event_dispatch_size [Integer]
26
+ # @param flush_scheduler [Scheduler]
27
+ # @param flush_interval_seconds [Float]
28
+ # @param shutdown_timeout_seconds [Float]
29
+ def initialize(
30
+ queue:,
31
+ event_dispatcher:,
32
+ event_dispatch_size:,
33
+ flush_scheduler:,
34
+ flush_interval_seconds:,
35
+ shutdown_timeout_seconds:
36
+ )
37
+ # @type [SizedQueue]
38
+ @queue = queue
39
+
40
+ # @type [UserEventDispatcher]
41
+ @event_dispatcher = event_dispatcher
42
+
43
+ # @type [Integer]
44
+ @event_dispatch_size = event_dispatch_size
45
+
46
+ # @type [Scheduler]
47
+ @flush_scheduler = flush_scheduler
48
+
49
+ # @type [Float]
50
+ @flush_interval_seconds = flush_interval_seconds
51
+
52
+ # @type [Float]
53
+ @shutdown_timeout_seconds = shutdown_timeout_seconds
54
+
55
+ # @type [ScheduledJob, nil]
56
+ @flushing_job = nil
57
+
58
+ # @type [Thread, nil]
59
+ @consuming_task = nil
60
+
61
+ # @type [boolean]
62
+ @is_started = false
63
+
64
+ # @type [Array<UserEvent>]
65
+ @current_batch = []
66
+ end
67
+
68
+ # @param event [UserEvent]
69
+ def process(event)
70
+ produce(message: Message::Event.new(event))
71
+ end
72
+
73
+ def start
74
+ if @is_started
75
+ Log.get.info { "#{UserEventProcessor} is already started." }
76
+ return
77
+ end
78
+
79
+ @consuming_task = Thread.new { consuming }
80
+ @flushing_job = @flush_scheduler.schedule_periodically(@flush_interval_seconds, -> { flush })
81
+ @is_started = true
82
+ Log.get.info { "#{UserEventProcessor} started. Flush event every #{@flush_interval_seconds} seconds." }
83
+ end
84
+
85
+ def stop
86
+ return unless @is_started
87
+
88
+ Log.get.info { "Shutting down #{UserEventProcessor}" }
89
+
90
+ @flushing_job&.cancel
91
+
92
+ produce(message: Message::Shutdown.new, non_block: false)
93
+ @consuming_task&.join(@shutdown_timeout_seconds)
94
+
95
+ @event_dispatcher.shutdown
96
+
97
+ @is_started = false
98
+ end
99
+
100
+ def resume
101
+ @consuming_task = Thread.new { consuming } if @consuming_task.nil? || !@consuming_task.alive?
102
+
103
+ @flushing_job&.cancel
104
+ @flushing_job = @flush_scheduler.schedule_periodically(@flush_interval_seconds, -> { flush })
105
+
106
+ @is_started = true
107
+ end
108
+
109
+ private
110
+
111
+ # @param message [Message]
112
+ # @param non_block [boolean]
113
+ def produce(message:, non_block: true)
114
+ @queue.push(message, non_block)
115
+ rescue ThreadError
116
+ Log.get.warn { 'Events are produced faster than can be consumed. Some events will be dropped.' }
117
+ end
118
+
119
+ def flush
120
+ produce(message: Message::Flush.new)
121
+ end
122
+
123
+ def consuming
124
+ loop do
125
+ message = @queue.pop
126
+ case message
127
+ when Message::Event
128
+ consume_event(message.event)
129
+ when Message::Flush
130
+ dispatch_events
131
+ when Message::Shutdown
132
+ break
133
+ else
134
+ Log.get.error { "Unsupported message type: #{message.class}" }
135
+ end
136
+ rescue => e
137
+ Log.get.error { "Unexpected error in event processor: #{e.inspect}" }
138
+ end
139
+ rescue => e
140
+ Log.get.error { "Unexpected error in event processor: #{e.inspect}" }
141
+ ensure
142
+ dispatch_events
143
+ end
144
+
145
+ # @param event [UserEvent]
146
+ def consume_event(event)
147
+ @current_batch << event
148
+ dispatch_events if @current_batch.size >= @event_dispatch_size
149
+ end
150
+
151
+ def dispatch_events
152
+ return if @current_batch.empty?
153
+
154
+ begin
155
+ @event_dispatcher.dispatch(@current_batch)
156
+ @current_batch = []
157
+ rescue => e
158
+ Log.get.error { "Failed to dispatch events: #{e.inspect}" }
159
+ end
160
+ end
161
+
162
+ class Message
163
+ class Event < Message
164
+ # @return [UserEvent]
165
+ attr_reader :event
166
+
167
+ # @param event [UserEvent]
168
+ def initialize(event)
169
+ super()
170
+ @event = event
171
+ end
172
+ end
173
+
174
+ class Flush < Message
175
+ end
176
+
177
+ class Shutdown < Message
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module Hackle
6
+ module HTTP
7
+
8
+ def self.client(base_url:)
9
+ uri = URI.parse(base_url)
10
+ # noinspection RubyMismatchedArgumentType
11
+ client = Net::HTTP.new(uri.host, uri.port)
12
+ client.use_ssl = uri.scheme == 'https'
13
+ client.open_timeout = 5
14
+ client.read_timeout = 10
15
+ client
16
+ end
17
+
18
+ # @param response [Net::HTTPResponse]
19
+ def self.successful?(response)
20
+ response.code.start_with?('2')
21
+ end
22
+
23
+ # @param response [Net::HTTPResponse]
24
+ def self.not_modified?(response)
25
+ response.code == '304'
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/http/http'
4
+ require 'hackle/internal/clock/clock'
5
+
6
+ module Hackle
7
+ class HttpClient
8
+
9
+ # @param http [Net::HTTP]
10
+ # @param sdk [Sdk]
11
+ # @param clock [Clock]
12
+ def initialize(http:, sdk:, clock:)
13
+ # @type [Net::HTTP]
14
+ @http = http
15
+ @sdk = sdk
16
+ @clock = clock
17
+ end
18
+
19
+ # @param base_url [String]
20
+ # @param sdk [Sdk]
21
+ # @param clock [Clock]
22
+ # @return [HttpClient]
23
+ def self.create(base_url:, sdk:, clock: SystemClock.instance)
24
+ HttpClient.new(
25
+ http: HTTP.client(base_url: base_url),
26
+ sdk: sdk,
27
+ clock: clock
28
+ )
29
+ end
30
+
31
+ # @param request [Net::HTTPRequest]
32
+ # @return [Net::HTTPResponse]
33
+ def execute(request)
34
+ decorate(request)
35
+ @http.request(request)
36
+ end
37
+
38
+ private
39
+
40
+ # @param request [Net::HTTPRequest]
41
+ def decorate(request)
42
+ request['X-HACKLE-SDK-KEY'] = @sdk.key
43
+ request['X-HACKLE-SDK-NAME'] = @sdk.name
44
+ request['X-HACKLE-SDK-VERSION'] = @sdk.version
45
+ request['X-HACKLE-SDK-TIME'] = @clock.current_millis.to_s
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/logger/logger'
4
+
5
+ module Hackle
6
+ class IdentifiersBuilder
7
+ def initialize
8
+ # @type [Hash{String => String}]
9
+ @identifiers = {}
10
+ end
11
+
12
+ # @param identifier_type [String]
13
+ # @param identifier_value [String, nil]
14
+ # @return [Hackle::IdentifiersBuilder]
15
+ def add(identifier_type, identifier_value)
16
+ value = IdentifiersBuilder.sanitize_value_or_nil(identifier_value)
17
+
18
+ if valid_type?(identifier_type) && !value.nil?
19
+ @identifiers[identifier_type] = value
20
+ else
21
+ Log.get.warn { "Invalid user identifier [type=#{identifier_type}] value=#{identifier_value}]" }
22
+ end
23
+
24
+ self
25
+ end
26
+
27
+ # @param identifiers [Hash]
28
+ # @return [Hackle::IdentifiersBuilder]
29
+ def add_all(identifiers)
30
+ identifiers.each do |identifier_type, identifier_value|
31
+ add(identifier_type, identifier_value)
32
+ end
33
+ self
34
+ end
35
+
36
+ # @return [Hash{String => String}]
37
+ def build
38
+ @identifiers.dup
39
+ end
40
+
41
+ def self.sanitize_value_or_nil(identifier_value)
42
+ return nil if identifier_value.nil?
43
+
44
+ if identifier_value.is_a?(String) && !identifier_value.empty? && identifier_value.length <= MAX_IDENTIFIER_VALUE_LENGTH
45
+ return identifier_value
46
+ end
47
+
48
+ return identifier_value.to_s if identifier_value.is_a?(Numeric)
49
+
50
+ nil
51
+ end
52
+
53
+ private
54
+
55
+ MAX_IDENTIFIER_TYPE_LENGTH = 128
56
+ MAX_IDENTIFIER_VALUE_LENGTH = 512
57
+
58
+ def valid_type?(identifier_type)
59
+ return false if identifier_type.nil?
60
+ return false unless identifier_type.is_a?(String)
61
+ return false if identifier_type.empty?
62
+ return false if identifier_type.length > MAX_IDENTIFIER_TYPE_LENGTH
63
+
64
+ true
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Hackle
6
+
7
+ class Log
8
+
9
+ attr_accessor :logger
10
+
11
+ def initialize
12
+ @logger = Logger.new($stdout)
13
+ end
14
+
15
+ @instance = new
16
+
17
+ # @return [Log]
18
+ def self.instance
19
+ @instance
20
+ end
21
+
22
+ def self.init(logger)
23
+ instance.logger = logger
24
+ end
25
+
26
+ # @return [Logger]
27
+ def self.get
28
+ Log.instance.logger
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ class Action
5
+
6
+ # @!attribute [r] type
7
+ # @return [ActionType]
8
+ # @!attribute [r] variation_id
9
+ # @return [Integer, nil]
10
+ # @!attribute [r] bucket_id
11
+ # @return [Integer, nil]
12
+ attr_reader :type, :variation_id, :bucket_id
13
+
14
+ # @param type [ActionType]
15
+ # @param variation_id [Integer, nil]
16
+ # @param bucket_id [Integer, nil]
17
+ def initialize(type:, variation_id: nil, bucket_id: nil)
18
+ @type = type
19
+ @variation_id = variation_id
20
+ @bucket_id = bucket_id
21
+ end
22
+ end
23
+
24
+ class ActionType
25
+ # @!attribute [r] name
26
+ # @return [String]
27
+ attr_reader :name
28
+
29
+ # @param name [String]
30
+ def initialize(name)
31
+ @name = name
32
+ end
33
+
34
+ def to_s
35
+ name
36
+ end
37
+
38
+ VARIATION = new('VARIATION')
39
+ BUCKET = new('BUCKET')
40
+
41
+ @types = {
42
+ 'VARIATION' => VARIATION,
43
+ 'BUCKET' => BUCKET
44
+ }.freeze
45
+
46
+ # @param name [String]
47
+ # @return [ActionType, nil]
48
+ def self.from_or_nil(name)
49
+ @types[name.upcase]
50
+ end
51
+
52
+ # @return [Array<ActionType>]
53
+ def self.values
54
+ @types.values
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+
5
+ class Bucket
6
+
7
+ # @!attribute [r] id
8
+ # @return [Integer]
9
+ # @!attribute [r] seed
10
+ # @return [Integer]
11
+ # @!attribute [r] slot_size
12
+ # @return [Integer]
13
+ # @!attribute [r] slots
14
+ # @return [Array<Slot>]
15
+ attr_accessor :id, :seed, :slot_size, :slots
16
+
17
+ # @param id [Integer]
18
+ # @param seed [Integer]
19
+ # @param slot_size [Integer]
20
+ # @param slots [Array<Slot>]
21
+ def initialize(id:, seed:, slot_size:, slots:)
22
+ @id = id
23
+ @seed = seed
24
+ @slot_size = slot_size
25
+ @slots = slots
26
+ end
27
+
28
+ # @param slot_number [Integer]
29
+ # @return [Slot, nil]
30
+ def get_slot_or_nil(slot_number)
31
+ slots.each do |slot|
32
+ return slot if slot.contains?(slot_number)
33
+ end
34
+ nil
35
+ end
36
+ end
37
+
38
+ class Slot
39
+ # @!attribute [r] variation_id
40
+ # @return [Integer]
41
+ attr_reader :variation_id
42
+
43
+ # @param start_inclusive [Integer]
44
+ # @param end_exclusive [Integer]
45
+ # @param variation_id [Integer]
46
+ def initialize(start_inclusive:, end_exclusive:, variation_id:)
47
+ @start_inclusive = start_inclusive
48
+ @end_exclusive = end_exclusive
49
+ @variation_id = variation_id
50
+ end
51
+
52
+ # @param slot_number [Integer]
53
+ # @return [boolean]
54
+ def contains?(slot_number)
55
+ @start_inclusive <= slot_number && slot_number < @end_exclusive
56
+ end
57
+ end
58
+ end