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.
- checksums.yaml +4 -4
- data/lib/hackle/client.rb +186 -87
- data/lib/hackle/config.rb +59 -17
- data/lib/hackle/decision.rb +113 -0
- data/lib/hackle/event.rb +89 -0
- data/lib/hackle/internal/clock/clock.rb +47 -0
- data/lib/hackle/internal/concurrent/executors.rb +20 -0
- data/lib/hackle/internal/concurrent/schedule/scheduler.rb +12 -0
- data/lib/hackle/internal/concurrent/schedule/timer_scheduler.rb +30 -0
- data/lib/hackle/internal/config/parameter_config.rb +50 -0
- data/lib/hackle/internal/core/hackle_core.rb +182 -0
- data/lib/hackle/{decision → internal/evaluation/bucketer}/bucketer.rb +17 -15
- data/lib/hackle/internal/evaluation/evaluator/contextual/contextual_evaluator.rb +29 -0
- data/lib/hackle/internal/evaluation/evaluator/delegating/delegating_evaluator.rb +26 -0
- data/lib/hackle/internal/evaluation/evaluator/evaluator.rb +117 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory.rb +67 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluator.rb +172 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_flow_evaluator.rb +241 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_resolver.rb +166 -0
- data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_determiner.rb +48 -0
- data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_evaluator.rb +174 -0
- data/lib/hackle/internal/evaluation/flow/evaluation_flow.rb +49 -0
- data/lib/hackle/internal/evaluation/flow/flow_evaluator.rb +11 -0
- data/lib/hackle/internal/evaluation/match/condition/condition_matcher.rb +11 -0
- data/lib/hackle/internal/evaluation/match/condition/condition_matcher_factory.rb +53 -0
- data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_condition_matcher.rb +29 -0
- data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_evaluator_matcher.rb +135 -0
- data/lib/hackle/internal/evaluation/match/condition/segment/segment_condition_matcher.rb +67 -0
- data/lib/hackle/internal/evaluation/match/condition/user/user_condition_matcher.rb +44 -0
- data/lib/hackle/internal/evaluation/match/operator/operator_matcher.rb +185 -0
- data/lib/hackle/internal/evaluation/match/operator/operator_matcher_factory.rb +31 -0
- data/lib/hackle/internal/evaluation/match/target/target_matcher.rb +31 -0
- data/lib/hackle/internal/evaluation/match/value/value_matcher.rb +96 -0
- data/lib/hackle/internal/evaluation/match/value/value_matcher_factory.rb +28 -0
- data/lib/hackle/internal/evaluation/match/value/value_operator_matcher.rb +59 -0
- data/lib/hackle/internal/event/user_event.rb +187 -0
- data/lib/hackle/internal/event/user_event_dispatcher.rb +156 -0
- data/lib/hackle/internal/event/user_event_factory.rb +58 -0
- data/lib/hackle/internal/event/user_event_processor.rb +181 -0
- data/lib/hackle/internal/http/http.rb +28 -0
- data/lib/hackle/internal/http/http_client.rb +48 -0
- data/lib/hackle/internal/identifiers/identifier_builder.rb +67 -0
- data/lib/hackle/internal/logger/logger.rb +31 -0
- data/lib/hackle/internal/model/action.rb +57 -0
- data/lib/hackle/internal/model/bucket.rb +58 -0
- data/lib/hackle/internal/model/container.rb +47 -0
- data/lib/hackle/internal/model/decision_reason.rb +31 -0
- data/lib/hackle/{models → internal/model}/event_type.rb +5 -8
- data/lib/hackle/internal/model/experiment.rb +194 -0
- data/lib/hackle/internal/model/parameter_configuration.rb +19 -0
- data/lib/hackle/internal/model/remote_config_parameter.rb +76 -0
- data/lib/hackle/internal/model/sdk.rb +23 -0
- data/lib/hackle/internal/model/segment.rb +61 -0
- data/lib/hackle/internal/model/target.rb +203 -0
- data/lib/hackle/internal/model/target_rule.rb +19 -0
- data/lib/hackle/internal/model/targeting.rb +45 -0
- data/lib/hackle/internal/model/value_type.rb +75 -0
- data/lib/hackle/internal/model/variation.rb +27 -0
- data/lib/hackle/internal/model/version.rb +153 -0
- data/lib/hackle/internal/properties/properties_builder.rb +101 -0
- data/lib/hackle/internal/user/hackle_user.rb +74 -0
- data/lib/hackle/internal/user/hackle_user_resolver.rb +27 -0
- data/lib/hackle/internal/workspace/http_workspace_fetcher.rb +50 -0
- data/lib/hackle/internal/workspace/polling_workspace_fetcher.rb +62 -0
- data/lib/hackle/internal/workspace/workspace.rb +353 -0
- data/lib/hackle/internal/workspace/workspace_fetcher.rb +18 -0
- data/lib/hackle/remote_config.rb +55 -0
- data/lib/hackle/user.rb +124 -0
- data/lib/hackle/version.rb +1 -11
- data/lib/hackle.rb +4 -69
- metadata +123 -53
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.travis.yml +0 -7
- data/Gemfile +0 -6
- data/README.md +0 -33
- data/Rakefile +0 -6
- data/hackle-ruby-sdk.gemspec +0 -29
- data/lib/hackle/decision/decider.rb +0 -69
- data/lib/hackle/events/event_dispatcher.rb +0 -96
- data/lib/hackle/events/event_processor.rb +0 -126
- data/lib/hackle/events/user_event.rb +0 -61
- data/lib/hackle/http/http.rb +0 -37
- data/lib/hackle/models/bucket.rb +0 -26
- data/lib/hackle/models/event.rb +0 -26
- data/lib/hackle/models/experiment.rb +0 -69
- data/lib/hackle/models/slot.rb +0 -22
- data/lib/hackle/models/user.rb +0 -24
- data/lib/hackle/models/variation.rb +0 -21
- data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
- data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -47
- 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
|