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