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
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hackle
4
-
5
- class UserEvent
6
-
7
- # @!attribute [r] timestamp
8
- # @return [Integer]
9
- # @!attribute [r] user
10
- # @return [User]
11
- attr_reader :timestamp, :user
12
-
13
- # @param user [User]
14
- def initialize(user:)
15
- @timestamp = UserEvent.generate_timestamp
16
- @user = user
17
- end
18
-
19
- class Exposure < UserEvent
20
-
21
- # @!attribute [r] experiment
22
- # @return [Experiment]
23
- # @!attribute [r] variation
24
- # @return [Variation]
25
- attr_reader :experiment, :variation
26
-
27
- # @param user [User]
28
- # @param experiment [Experiment]
29
- # @param variation [Variation]
30
- def initialize(user:, experiment:, variation:)
31
- super(user: user)
32
- @experiment = experiment
33
- @variation = variation
34
- end
35
- end
36
-
37
-
38
- class Track < UserEvent
39
-
40
- # @!attribute [r] event_type
41
- # @return [EventType]
42
- # @!attribute [r] event
43
- # @return [Event]
44
- attr_reader :event_type, :event
45
-
46
- # @param user [User]
47
- # @param event_type [EventType]
48
- # @param event [Event]
49
- def initialize(user:, event_type:, event:)
50
- super(user: user)
51
- @event_type = event_type
52
- @event = event
53
- end
54
- end
55
-
56
- # @return [Integer]
57
- def self.generate_timestamp
58
- (Time.now.to_f * 1000).to_i
59
- end
60
- end
61
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'net/http'
4
-
5
- module Hackle
6
- class UnexpectedResponseError < StandardError
7
- end
8
-
9
- class HTTP
10
- def self.client(base_uri:)
11
- uri = URI.parse(base_uri)
12
- client = Net::HTTP.new(uri.host, uri.port)
13
- client.use_ssl = uri.scheme == 'https'
14
- client.open_timeout = 5
15
- client.read_timeout = 10
16
- client
17
- end
18
-
19
- def self.sdk_headers(sdk_info:)
20
- {
21
- 'X-HACKLE-SDK-KEY' => sdk_info.key,
22
- 'X-HACKLE-SDK-NAME' => sdk_info.name,
23
- 'X-HACKLE-SDK-VERSION' => sdk_info.version
24
- }
25
- end
26
-
27
- def self.successful?(status_code:)
28
- status_code >= 200 && status_code < 300
29
- end
30
-
31
- def self.check_successful(status_code:)
32
- unless successful?(status_code: status_code)
33
- raise UnexpectedResponseError, "HTTP status code #{status_code}"
34
- end
35
- end
36
- end
37
- end
@@ -1,26 +0,0 @@
1
- module Hackle
2
-
3
- class Bucket
4
-
5
- # @!attribute [r] seed
6
- # @return [Integer]
7
- # @!attribute [r] slot_size
8
- # @return [Integer]
9
- attr_reader :seed, :slot_size
10
-
11
- # @param seed [Integer]
12
- # @param slot_size [Integer]
13
- # @param slots [Array]
14
- def initialize(seed:, slot_size:, slots:)
15
- @seed = seed
16
- @slot_size = slot_size
17
- @slots = slots
18
- end
19
-
20
- # @param slot_number [Integer]
21
- # @return [Slot, nil]
22
- def get_slot(slot_number:)
23
- @slots.find { |slot| slot.contains?(slot_number: slot_number) }
24
- end
25
- end
26
- end
@@ -1,26 +0,0 @@
1
- module Hackle
2
- class Event
3
-
4
- # @!attribute [r] key
5
- # @return [String]
6
- # @!attribute [r] value
7
- # @return [Float, nil]
8
- # @!attribute [r] properties
9
- # @return [Hash]
10
- attr_reader :key, :value, :properties
11
-
12
-
13
- # @param key [String]
14
- # @param value [Float, nil]
15
- # @param properties [Hash{Symbol => String, Number, boolean}]
16
- def initialize(key:, value:, properties:)
17
- @key = key
18
- @value = value
19
- @properties = properties
20
- end
21
-
22
- def valid?
23
- !key.nil? && key.is_a?(String)
24
- end
25
- end
26
- end
@@ -1,69 +0,0 @@
1
- module Hackle
2
- class Experiment
3
-
4
- # @!attribute [r] id
5
- # @return [Integer]
6
- # @!attribute [r] key
7
- # @return [Integer]
8
- attr_reader :id, :key
9
-
10
- # @param id [Integer]
11
- # @param key [Integer]
12
- def initialize(id:, key:)
13
- @id = id
14
- @key = key
15
- end
16
-
17
- class Running < Experiment
18
-
19
- # @!attribute [r] bucket
20
- # @return [Bucket]
21
- attr_reader :bucket
22
-
23
- # @param id [Integer]
24
- # @param key [Integer]
25
- # @param bucket [Bucket]
26
- # @param variations [Hash{String => Variation}]
27
- # @param overrides [Hash{String => Integer}]
28
- def initialize(id:, key:, bucket:, variations:, overrides:)
29
- super(id: id, key: key)
30
- @bucket = bucket
31
-
32
- # @type [Hash{String => Variation}]
33
- @variations = variations
34
-
35
- # @type [Hash{String => Integer}]
36
- @overrides = overrides
37
- end
38
-
39
- # @param variation_id [Integer]
40
- # @return [Variation, nil]
41
- def get_variation(variation_id:)
42
- @variations[variation_id]
43
- end
44
-
45
- # @param user [User]
46
- # @return [Variation, nil]
47
- def get_overridden_variation(user:)
48
- overridden_variation_id = @overrides[user.id]
49
- return nil if overridden_variation_id.nil?
50
- get_variation(variation_id: overridden_variation_id)
51
- end
52
- end
53
-
54
- class Completed < Experiment
55
-
56
- # @!attribute [r] winner_variation_key
57
- # @return [String]
58
- attr_reader :winner_variation_key
59
-
60
- # @param id [Integer]
61
- # @param key [Integer]
62
- # @param winner_variation_key [String]
63
- def initialize(id:, key:, winner_variation_key:)
64
- super(id: id, key: key)
65
- @winner_variation_key = winner_variation_key
66
- end
67
- end
68
- end
69
- end
@@ -1,22 +0,0 @@
1
- module Hackle
2
- class Slot
3
- # @!attribute variation_id
4
- # @return [Integer]
5
- attr_reader :variation_id
6
-
7
- # @param start_inclusive [Integer]
8
- # @param end_exclusive [Integer]
9
- # @param variation_id [Integer]
10
- def initialize(start_inclusive:, end_exclusive:, variation_id:)
11
- @start_inclusive = start_inclusive
12
- @end_exclusive = end_exclusive
13
- @variation_id = variation_id
14
- end
15
-
16
- # @param slot_number [Integer]
17
- # @return [boolean]
18
- def contains?(slot_number:)
19
- @start_inclusive <= slot_number && slot_number < @end_exclusive
20
- end
21
- end
22
- end
@@ -1,24 +0,0 @@
1
- module Hackle
2
-
3
- class User
4
-
5
- # @!attribute [r] id
6
- # @return [String]
7
- # @!attribute [r] properties
8
- # @return [Hash]
9
- attr_reader :id, :properties
10
-
11
- #
12
- # @param id [String]
13
- # @param properties [Hash]
14
- #
15
- def initialize(id:, properties:)
16
- @id = id
17
- @properties = properties
18
- end
19
-
20
- def valid?
21
- !id.nil? && id.is_a?(String)
22
- end
23
- end
24
- end
@@ -1,21 +0,0 @@
1
- module Hackle
2
- class Variation
3
-
4
- # @!attribute id
5
- # @return [Integer]
6
- # @!attribute key
7
- # @return [String]
8
- # @!attribute dropped
9
- # @return [boolean]
10
- attr_reader :id, :key, :dropped
11
-
12
- # @param id [Integer]
13
- # @param key [String]
14
- # @param dropped [boolean]
15
- def initialize(id:, key:, dropped:)
16
- @id = id
17
- @key = key
18
- @dropped = dropped
19
- end
20
- end
21
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module Hackle
6
- class HttpWorkspaceFetcher
7
-
8
- def initialize(config:, sdk_info:)
9
- @client = HTTP.client(base_uri: config.base_uri)
10
- @headers = HTTP.sdk_headers(sdk_info: sdk_info)
11
- end
12
-
13
- def fetch
14
- request = Net::HTTP::Get.new('/api/v1/workspaces', @headers)
15
- response = @client.request(request)
16
-
17
- status_code = response.code.to_i
18
- HTTP.check_successful(status_code: status_code)
19
-
20
- response_body = JSON.parse(response.body, symbolize_names: true)
21
- Workspace.create(data: response_body)
22
- end
23
- end
24
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'concurrent'
4
-
5
- module Hackle
6
- class PollingWorkspaceFetcher
7
-
8
- DEFAULT_POLLING_INTERVAL = 10
9
-
10
- def initialize(config:, http_fetcher:)
11
- @logger = config.logger
12
- @http_fetcher = http_fetcher
13
- @current_workspace = Concurrent::AtomicReference.new
14
- @task = Concurrent::TimerTask.new(execution_interval: DEFAULT_POLLING_INTERVAL) { poll }
15
- @running = false
16
- end
17
-
18
- # @return [Workspace, nil]
19
- def fetch
20
- @current_workspace.get
21
- end
22
-
23
- def start!
24
- return if @running
25
-
26
- poll
27
- @task.execute
28
- @running = true
29
- end
30
-
31
- def stop!
32
- return unless @running
33
-
34
- @logger.info { 'Shutting down Hackle workspace_fetcher' }
35
-
36
- @task.shutdown
37
- @running = false
38
- end
39
-
40
- def poll
41
- workspace = @http_fetcher.fetch
42
- @current_workspace.set(workspace)
43
- rescue => e
44
- @logger.error { "Failed to poll Workspace: #{e.inspect}" }
45
- end
46
- end
47
- end
@@ -1,100 +0,0 @@
1
- module Hackle
2
- class Workspace
3
-
4
- # @param experiments [Hash{Integer => Experiment}]
5
- # @param event_types [Hash{String => EventType}]
6
- def initialize(experiments:, event_types:)
7
-
8
- # @type [Hash{Integer => Experiment}]
9
- @experiments = experiments
10
-
11
- # @type [Hash{String => EventType}]
12
- @event_types = event_types
13
- end
14
-
15
- # @param experiment_key [Integer]
16
- #
17
- # @return [Experiment, nil]
18
- def get_experiment(experiment_key:)
19
- @experiments[experiment_key]
20
- end
21
-
22
- # @param event_type_key [String]
23
- #
24
- # @return [EventType]
25
- def get_event_type(event_type_key:)
26
- event_type = @event_types[event_type_key]
27
-
28
- if event_type.nil?
29
- EventType.undefined(key: event_type_key)
30
- else
31
- event_type
32
- end
33
- end
34
-
35
- class << self
36
- def create(data:)
37
- buckets = Hash[data[:buckets].map { |b| [b[:id], bucket(b)] }]
38
- running_experiments = Hash[data[:experiments].map { |re| [re[:key], running_experiment(re, buckets)] }]
39
- completed_experiment = Hash[data[:completedExperiments].map { |ce| [ce[:experimentKey], completed_experiment(ce)] }]
40
- event_types = Hash[data[:events].map { |e| [e[:key], event_type(e)] }]
41
- experiments = running_experiments.merge(completed_experiment)
42
- Workspace.new(
43
- experiments: experiments,
44
- event_types: event_types
45
- )
46
- end
47
-
48
- private
49
-
50
- def running_experiment(data, buckets)
51
- Experiment::Running.new(
52
- id: data[:id],
53
- key: data[:key],
54
- bucket: buckets[data[:bucketId]],
55
- variations: Hash[data[:variations].map { |v| [v[:id], variation(v)] }],
56
- overrides: Hash[data[:execution][:userOverrides].map { |u| [u[:userId], u[:variationId]] }]
57
- )
58
- end
59
-
60
- def completed_experiment(data)
61
- Experiment::Completed.new(
62
- id: data[:experimentId],
63
- key: data[:experimentKey],
64
- winner_variation_key: data[:winnerVariationKey]
65
- )
66
- end
67
-
68
- def variation(data)
69
- Variation.new(
70
- id: data[:id],
71
- key: data[:key],
72
- dropped: data[:status] == 'DROPPED'
73
- )
74
- end
75
-
76
- def bucket(data)
77
- Bucket.new(
78
- seed: data[:seed],
79
- slot_size: data[:slotSize],
80
- slots: data[:slots].map { |s| slot(s) }
81
- )
82
- end
83
-
84
- def slot(data)
85
- Slot.new(
86
- start_inclusive: data[:startInclusive],
87
- end_exclusive: data[:endExclusive],
88
- variation_id: data[:variationId]
89
- )
90
- end
91
-
92
- def event_type(data)
93
- EventType.new(
94
- id: data[:id],
95
- key: data[:key]
96
- )
97
- end
98
- end
99
- end
100
- end