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
@@ -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