hackle-ruby-sdk 1.0.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 +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,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/model/target'
4
+
5
+ module Hackle
6
+ class TargetingType
7
+
8
+ # @!attribute [r] supported_key_types
9
+ # @return [Array<TargetKeyType>]
10
+ attr_reader :supported_key_types
11
+
12
+ # @param supported_key_types [Array<TargetKeyType>]
13
+ def initialize(supported_key_types)
14
+ # @type [Array<TargetKeyType>]
15
+ @supported_key_types = supported_key_types
16
+ end
17
+
18
+ # @param key_type [TargetKeyType]
19
+ def supports?(key_type)
20
+ @supported_key_types.include?(key_type)
21
+ end
22
+
23
+ IDENTIFIER = new(
24
+ [
25
+ TargetKeyType::SEGMENT
26
+ ]
27
+ )
28
+ PROPERTY = new(
29
+ [
30
+ TargetKeyType::SEGMENT,
31
+ TargetKeyType::USER_PROPERTY,
32
+ TargetKeyType::HACKLE_PROPERTY,
33
+ TargetKeyType::AB_TEST,
34
+ TargetKeyType::FEATURE_FLAG
35
+ ]
36
+ )
37
+ SEGMENT = new(
38
+ [
39
+ TargetKeyType::USER_ID,
40
+ TargetKeyType::USER_PROPERTY,
41
+ TargetKeyType::HACKLE_PROPERTY
42
+ ]
43
+ )
44
+ end
45
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ class ValueType
5
+ # @!attribute [r] name
6
+ # @return [String]
7
+ attr_reader :name
8
+
9
+ # @param name [String]
10
+ def initialize(name)
11
+ @name = name
12
+ end
13
+
14
+ def to_s
15
+ name
16
+ end
17
+
18
+ NULL = new('NULL')
19
+ UNKNOWN = new('UNKNOWN')
20
+ STRING = new('STRING')
21
+ NUMBER = new('NUMBER')
22
+ BOOLEAN = new('BOOLEAN')
23
+ VERSION = new('VERSION')
24
+ JSON = new('JSON')
25
+
26
+ @types = {
27
+ 'NULL' => NULL,
28
+ 'UNKNOWN' => UNKNOWN,
29
+ 'STRING' => STRING,
30
+ 'NUMBER' => NUMBER,
31
+ 'BOOLEAN' => BOOLEAN,
32
+ 'VERSION' => VERSION,
33
+ 'JSON' => JSON
34
+ }.freeze
35
+
36
+ # @param name [String]
37
+ # @return [ValueType, nil]
38
+ def self.from_or_nil(name)
39
+ @types[name.upcase]
40
+ end
41
+
42
+ # @return [Array<ValueType>]
43
+ def self.values
44
+ @types.values
45
+ end
46
+
47
+ class << self
48
+ def string?(value)
49
+ return false if value.nil?
50
+
51
+ value.is_a?(String)
52
+ end
53
+
54
+ def empty_string?(value)
55
+ string?(value) && value.empty?
56
+ end
57
+
58
+ def not_empty_string?(value)
59
+ string?(value) && !value.empty?
60
+ end
61
+
62
+ def number?(value)
63
+ return false if value.nil?
64
+
65
+ value.is_a?(Numeric)
66
+ end
67
+
68
+ def boolean?(value)
69
+ return false if value.nil?
70
+
71
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ class Variation
5
+
6
+ # @!attribute [r] id
7
+ # @return [Integer]
8
+ # @!attribute [r] key
9
+ # @return [String]
10
+ # @!attribute [r] is_dropped
11
+ # @return [boolean]
12
+ # @!attribute [r] parameter_configuration_id
13
+ # @return [Integer]
14
+ attr_reader :id, :key, :is_dropped, :parameter_configuration_id
15
+
16
+ # @param id [Integer]
17
+ # @param key [String]
18
+ # @param is_dropped [boolean]
19
+ # @param parameter_configuration_id [Integer, nil]
20
+ def initialize(id:, key:, is_dropped:, parameter_configuration_id:)
21
+ @id = id
22
+ @key = key
23
+ @is_dropped = is_dropped
24
+ @parameter_configuration_id = parameter_configuration_id
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ class Version
5
+ include Comparable
6
+
7
+ # @!attribute [r] core_version
8
+ # @return [CoreVersion]
9
+ # @!attribute [r] prerelease
10
+ # @return [MetadataVersion]
11
+ # @!attribute [r] build
12
+ # @return [MetadataVersion]
13
+ attr_reader :core_version, :prerelease, :build
14
+
15
+ # @param major [Integer]
16
+ # @param minor [Integer]
17
+ # @param patch [Integer]
18
+ # @param prerelease [MetadataVersion]
19
+ # @param build [MetadataVersion]
20
+
21
+ def initialize(core_version:, prerelease:, build:)
22
+ @core_version = core_version
23
+ @prerelease = prerelease
24
+ @build = build
25
+ end
26
+
27
+ VERSION_REGEX = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+(\S+))?$/.freeze
28
+
29
+ # @param version [Object]
30
+ # @return [Version, nil]
31
+ def self.parse_or_nil(value)
32
+ return nil unless value.is_a?(String)
33
+
34
+ match = VERSION_REGEX.match(value)
35
+ return nil unless match
36
+
37
+ core_version = CoreVersion.new(major: match[1].to_i, minor: (match[2] || 0).to_i, patch: (match[3] || 0).to_i)
38
+ prerelease = MetadataVersion.parse(match[4])
39
+ build = MetadataVersion.parse(match[5])
40
+ new(core_version: core_version, prerelease: prerelease, build: build)
41
+ end
42
+
43
+ def <=>(other)
44
+ core_comparison = core_version <=> other.core_version
45
+ return core_comparison unless core_comparison.zero?
46
+
47
+ prerelease <=> other.prerelease
48
+ end
49
+
50
+ def ==(other)
51
+ other.is_a?(Version) && (self <=> other).zero?
52
+ end
53
+
54
+ def to_s
55
+ str = core_version.to_s
56
+ str += "-#{prerelease}" unless prerelease.empty?
57
+ str += "+#{build}" unless build.empty?
58
+ str
59
+ end
60
+ end
61
+
62
+ class CoreVersion
63
+ include Comparable
64
+
65
+ # @!attribute [r] major
66
+ # @return [Integer]
67
+ # @!attribute [r] minor
68
+ # @return [Integer]
69
+ # @!attribute [r] patch
70
+ # @return [Integer]
71
+ attr_reader :major, :minor, :patch
72
+
73
+ # @param major [Integer]
74
+ # @param minor [Integer]
75
+ # @param patch [Integer]
76
+ def initialize(major:, minor:, patch:)
77
+ @major = major
78
+ @minor = minor
79
+ @patch = patch
80
+ end
81
+
82
+ def <=>(other)
83
+ return nil unless other.is_a?(CoreVersion)
84
+
85
+ [major, minor, patch] <=> [other.major, other.minor, other.patch]
86
+ end
87
+
88
+ def ==(other)
89
+ other.is_a?(CoreVersion) && (major == other.major && minor == other.minor && patch == other.patch)
90
+ end
91
+
92
+ def to_s
93
+ "#{major}.#{minor}.#{patch}"
94
+ end
95
+ end
96
+
97
+ class MetadataVersion
98
+ include Comparable
99
+
100
+ # @!attribute [r] identifiers
101
+ # @return [Array<String>]
102
+ attr_reader :identifiers
103
+
104
+ def initialize(identifiers)
105
+ @identifiers = identifiers
106
+ end
107
+
108
+ def self.parse(value)
109
+ return new([]) unless value
110
+
111
+ new(value.split('.'))
112
+ end
113
+
114
+ def ==(other)
115
+ other.is_a?(MetadataVersion) && identifiers == other.identifiers
116
+ end
117
+
118
+ def <=>(other)
119
+ return 0 if empty? && other.empty?
120
+ return -1 if empty?
121
+ return 1 if other.empty?
122
+
123
+ compare_identifiers(other)
124
+ end
125
+
126
+ def compare_identifiers(other)
127
+ min_length = [identifiers.length, other.identifiers.length].min
128
+ min_length.times do |i|
129
+ result = compare_single_identifier(identifiers[i], other.identifiers[i])
130
+ return result if result != 0
131
+ end
132
+ identifiers.length <=> other.identifiers.length
133
+ end
134
+
135
+ def compare_single_identifier(id1, id2)
136
+ num1 = id1.to_i
137
+ num2 = id2.to_i
138
+ if id1.to_i.to_s == id1 && id2.to_i.to_s == id2
139
+ num1 <=> num2
140
+ else
141
+ id1 <=> id2
142
+ end
143
+ end
144
+
145
+ def to_s
146
+ identifiers.join('.')
147
+ end
148
+
149
+ def empty?
150
+ identifiers.empty?
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/logger/logger'
4
+
5
+ module Hackle
6
+ class PropertiesBuilder
7
+ def initialize
8
+ # @type [Hash{String => Object}]
9
+ @properties = {}
10
+ end
11
+
12
+ # @param key [String]
13
+ # @param value [Object, nil]
14
+ # @return [Hackle::PropertiesBuilder]
15
+ def add(key, value)
16
+ return self if @properties.length >= MAX_PROPERTIES_COUNT
17
+
18
+ unless valid_key?(key)
19
+ Log.get.warn { "Invalid property key: #{key} (expected: string[1..128])" }
20
+ return self
21
+ end
22
+
23
+ sanitized_value = sanitize_value_or_nil(key, value)
24
+ @properties[key] = sanitized_value unless sanitized_value.nil?
25
+ self
26
+ end
27
+
28
+ # @param properties [Hash{String => Object}]
29
+ # @return [Hackle::PropertiesBuilder]
30
+ def add_all(properties)
31
+ if properties.nil? || !properties.is_a?(Hash)
32
+ Log.get.warn { "Invalid properties: #{properties} (expected: Hash{String => Object})" }
33
+ return self
34
+ end
35
+
36
+ properties.each { |key, value| add(key, value) }
37
+ self
38
+ end
39
+
40
+ # @return [Hash{String (frozen)->Object}]
41
+ def build
42
+ @properties.dup
43
+ end
44
+
45
+ private
46
+
47
+ SYSTEM_PROPERTY_KEY_PREFIX = '$'
48
+ MAX_PROPERTIES_COUNT = 128
49
+ MAX_PROPERTY_KEY_LENGTH = 128
50
+ MAX_PROPERTY_VALUE_LENGTH = 1024
51
+
52
+ # @param key [String]
53
+ # @param value [Object, nil]
54
+ # @return [Object, nil]
55
+ def sanitize_value_or_nil(key, value)
56
+ return nil if value.nil?
57
+ return value.filter { |it| valid_element?(it) } if value.is_a?(Array)
58
+ return value if valid_value?(value)
59
+ return value if key.start_with?(SYSTEM_PROPERTY_KEY_PREFIX)
60
+
61
+ nil
62
+ end
63
+
64
+ # @param key [String]
65
+ # @return [boolean]
66
+ def valid_key?(key)
67
+ return false if key.nil?
68
+ return false unless key.is_a?(String)
69
+ return false if key.empty?
70
+ return false if key.length > MAX_PROPERTY_KEY_LENGTH
71
+
72
+ true
73
+ end
74
+
75
+ # @param value [Object]
76
+ # @return [boolean]
77
+ def valid_value?(value)
78
+ case value
79
+ when String
80
+ value.length <= MAX_PROPERTY_VALUE_LENGTH
81
+ when Numeric, TrueClass, FalseClass
82
+ true
83
+ else
84
+ false
85
+ end
86
+ end
87
+
88
+ # @param element [Object]
89
+ # @return [boolean]
90
+ def valid_element?(element)
91
+ case element
92
+ when String
93
+ element.length <= MAX_PROPERTY_VALUE_LENGTH
94
+ when Numeric
95
+ true
96
+ else
97
+ false
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/identifiers/identifier_builder'
4
+ require 'hackle/internal/properties/properties_builder'
5
+
6
+ module Hackle
7
+ class HackleUser
8
+
9
+ # @return [Hash{String => String}]
10
+ attr_reader :identifiers
11
+
12
+ # @return [Hash{String => Object}]
13
+ attr_reader :properties
14
+
15
+ # @param identifiers [Hash{String => String}]
16
+ # @param properties [Hash{String => Object}]
17
+ def initialize(identifiers:, properties:)
18
+ @identifiers = identifiers
19
+ @properties = properties
20
+ end
21
+
22
+ def ==(other)
23
+ other.is_a?(Hackle::HackleUser) && identifiers == other.identifiers && properties == other.properties
24
+ end
25
+
26
+ def self.builder
27
+ Builder.new
28
+ end
29
+
30
+ class Builder
31
+ def initialize
32
+ @identifiers = IdentifiersBuilder.new
33
+ @properties = PropertiesBuilder.new
34
+ end
35
+
36
+ # @param type [String]
37
+ # @param value [String, nil]
38
+ # @return [Builder]
39
+ def identifier(type, value)
40
+ @identifiers.add(type, value)
41
+ self
42
+ end
43
+
44
+ # @param identifiers [Hash{String => String}]
45
+ # @return [Builder]
46
+ def identifiers(identifiers)
47
+ @identifiers.add_all(identifiers)
48
+ self
49
+ end
50
+
51
+ # @param key [String]
52
+ # @param value [Object, nil]
53
+ # @return [Builder]
54
+ def property(key, value)
55
+ @properties.add(key, value)
56
+ self
57
+ end
58
+
59
+ # @param properties [Hash{String => Object}]
60
+ # @return [Builder]
61
+ def properties(properties)
62
+ @properties.add_all(properties)
63
+ self
64
+ end
65
+
66
+ def build
67
+ HackleUser.new(
68
+ identifiers: @identifiers.build,
69
+ properties: @properties.build
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/user'
4
+ require 'hackle/internal/user/hackle_user'
5
+
6
+ module Hackle
7
+ class HackleUserResolver
8
+ # @param user [User]
9
+ # @return [HackleUser]
10
+ def resolve_or_nil(user)
11
+ return nil if user.nil?
12
+ return nil unless user.is_a?(User)
13
+
14
+ builder = HackleUser.builder
15
+ builder.identifiers(user.identifiers)
16
+ builder.identifier('$id', user.id) unless user.id.nil?
17
+ builder.identifier('$deviceId', user.device_id) unless user.device_id.nil?
18
+ builder.identifier('$userId', user.user_id) unless user.user_id.nil?
19
+ builder.properties(user.properties)
20
+ hackle_user = builder.build
21
+
22
+ return nil if hackle_user.identifiers.empty?
23
+
24
+ hackle_user
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'hackle/internal/http/http'
5
+ require 'hackle/internal/workspace/workspace'
6
+
7
+ module Hackle
8
+ class HttpWorkspaceFetcher
9
+ # @param http_client [HttpClient]
10
+ # @param sdk [Sdk]
11
+ def initialize(http_client:, sdk:)
12
+
13
+ # @type [String]
14
+ @url = "/api/v2/workspaces/#{sdk.key}/config"
15
+
16
+ # @type [HttpClient]
17
+ @http_client = http_client
18
+
19
+ # @type [String, nil]
20
+ @last_modified = nil
21
+ end
22
+
23
+ # @return [Hackle::Workspace, nil]
24
+ def fetch_if_modified
25
+ request = create_request
26
+ response = @http_client.execute(request)
27
+ handle_response(response)
28
+ end
29
+
30
+ private
31
+
32
+ # @return [Net::HTTPRequest]
33
+ def create_request
34
+ request = Net::HTTP::Get.new(@url)
35
+ request['If-Modified-Since'] = @last_modified unless @last_modified.nil?
36
+ request
37
+ end
38
+
39
+ # @param response [Net::HTTPResponse]
40
+ # @return [Workspace, nil]
41
+ def handle_response(response)
42
+ return nil if HTTP.not_modified?(response)
43
+ raise "http status code: #{response.code}" unless HTTP.successful?(response)
44
+
45
+ @last_modified = response.header['Last-Modified']
46
+ response_body = JSON.parse(response.body, symbolize_names: true)
47
+ Workspace.from_hash(response_body)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/internal/logger/logger'
4
+ require 'hackle/internal/workspace/workspace_fetcher'
5
+
6
+ module Hackle
7
+ class PollingWorkspaceFetcher
8
+ include WorkspaceFetcher
9
+ # @param http_workspace_fetcher [HttpWorkspaceFetcher]
10
+ # @param scheduler [Scheduler]
11
+ # @param polling_interval_seconds [Float]
12
+ def initialize(http_workspace_fetcher:, scheduler:, polling_interval_seconds:)
13
+ # @type [HttpWorkspaceFetcher]
14
+ @http_workspace_fetcher = http_workspace_fetcher
15
+
16
+ # @type [Scheduler]
17
+ @scheduler = scheduler
18
+
19
+ # @type [Float]
20
+ @polling_interval_seconds = polling_interval_seconds
21
+
22
+ # @type [ScheduledJob, nil]
23
+ @polling_job = nil
24
+
25
+ # @type [Workspace, nil]
26
+ @workspace = nil
27
+ end
28
+
29
+ # @return [Workspace, nil]
30
+ def fetch
31
+ @workspace
32
+ end
33
+
34
+ def start
35
+ return unless @polling_job.nil?
36
+
37
+ poll
38
+ @polling_job = @scheduler.schedule_periodically(@polling_interval_seconds, -> { poll })
39
+ end
40
+
41
+ def stop
42
+ @polling_job&.cancel
43
+ @polling_job = nil
44
+ end
45
+
46
+ def resume
47
+ @polling_job&.cancel
48
+ @polling_job = @scheduler.schedule_periodically(@polling_interval_seconds, -> { poll })
49
+ end
50
+
51
+ private
52
+
53
+ def poll
54
+ workspace = @http_workspace_fetcher.fetch_if_modified
55
+ return if workspace.nil?
56
+
57
+ @workspace = workspace
58
+ rescue => e
59
+ Log.get.error { "Failed to poll Workspace: #{e.inspect}" }
60
+ end
61
+ end
62
+ end