splitclient-rb 6.2.0.pre.rc2-java → 6.3.0-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +11 -0
- data/CHANGES.txt +18 -1
- data/Detailed-README.md +1 -1
- data/NEWS +16 -2
- data/lib/splitclient-rb/clients/localhost_split_client.rb +126 -34
- data/lib/splitclient-rb/clients/split_client.rb +142 -104
- data/lib/splitclient-rb/engine/api/client.rb +2 -2
- data/lib/splitclient-rb/engine/parser/evaluator.rb +12 -7
- data/lib/splitclient-rb/localhost_split_factory.rb +1 -1
- data/lib/splitclient-rb/localhost_utils.rb +24 -1
- data/lib/splitclient-rb/managers/localhost_split_manager.rb +21 -6
- data/lib/splitclient-rb/managers/split_manager.rb +2 -1
- data/lib/splitclient-rb/split_config.rb +1 -1
- data/lib/splitclient-rb/split_factory_builder.rb +14 -2
- data/lib/splitclient-rb/validators.rb +28 -28
- data/lib/splitclient-rb/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ab1fb5ff175b557b4ebf69f1fcac911de9834eb
|
4
|
+
data.tar.gz: 7278faa59eb5e7f42f03c49163f239b2b74182a4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6c4b30dd8be0afb4ad141bc48eb2f62d7cf0cbc6252fc7d35fd6d45ecc7449b376308f56bad7130882e9f77423bfb21fae9a36556ec9c7c54a279a5e8d92723
|
7
|
+
data.tar.gz: b3aea4b50de5c338698290218538f966f1ecf06ca8c50d43e1cfcb3cc3451959b15d4acec87e59f6d328397210b5c135aa03632b75fb95e1e1557d2b1c1f2e7e
|
data/.travis.yml
ADDED
data/CHANGES.txt
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
6.3.0 (Apr 30, 2019)
|
2
|
+
|
3
|
+
- Added Dynamic Configurations support through two new methods that mimick the regular ones, changing the type of what is returned.
|
4
|
+
- get_treatment_with_config: Same as get_treatment but returning the treatment with it's config.
|
5
|
+
- get_treatments_with_config: Same as get_treatments, but instead of a map of string it returns a map of treatments with config.
|
6
|
+
- Added configs to SplitViews returned by the manager module.
|
7
|
+
- Updated localhost mode. Now besides supporting the old text files with `.split` extension (to be deprecated soon), we support YAML (.yaml/.yml) files where you can
|
8
|
+
define configurations for your treatments and also whitelisted keys. Read more in our docs!
|
9
|
+
|
10
|
+
6.2.0 (Mar 7th, 2019)
|
11
|
+
- Reworked SplitClient#destroy to ensure events, impressions and metrics are sent to Split backend when called
|
12
|
+
- Ensured destroy is called when keyboard interrupts are sent to the application
|
13
|
+
- Changed SDK blocker (and block_until_ready) to have no effect in consumer mode
|
14
|
+
- Added support for applications tied to Faraday < 0.13 and net-http-persistent 3 using a patched Faraday adapter
|
15
|
+
- Added documentation for input validation in detailed readme
|
16
|
+
- Changed SplitConfig#default_features_refresh_rate value to 5 seconds
|
17
|
+
|
1
18
|
6.1.0 (Feb 8th, 2019)
|
2
19
|
- Review input validation for client API methods. Better control and logging over nil, empty, numeric, and NaN parameters
|
3
20
|
- Added logging when block_until_ready is not set or api key is not provided
|
@@ -12,10 +29,10 @@
|
|
12
29
|
- Fix an issue in events and impressions API calls log messages caused by a wrong variable name introduced in 6.0.0
|
13
30
|
|
14
31
|
6.0.0 (December 17th, 2018)
|
32
|
+
- BREAKING CHANGE: Change format used to store impressions in repositories to reduce the number of Redis operations. It requires an update of the Split Synchronizer to >2.0.0 if you're using Redis mode.
|
15
33
|
- Change `sender` and `store` classes to reuse Faraday connections, preventing issues with net-http-persistent 3.0
|
16
34
|
- Remove producer mode and make `memory + standalone` and `Redis + consumer` the only valid SDK modes. This is a breaking change
|
17
35
|
- Fix `evaluator` bucket calculation when traffic allocation is set to 1%
|
18
|
-
- Change format used to store impressions in repositories to reduce the number of Redis operations
|
19
36
|
- Add cache wrapper to `segments_repository` and `splits_repository` to reduce the number of Redis operations
|
20
37
|
- Add `cache_ttl` and `max_cache_size` options to setup the memory cache wrapper when using redis
|
21
38
|
|
data/Detailed-README.md
CHANGED
@@ -287,7 +287,7 @@ The following values can be customized:
|
|
287
287
|
|
288
288
|
**features_refresh_rate** : The SDK polls Split servers for changes to feature Splits every X seconds, where X is this property's value.
|
289
289
|
|
290
|
-
*default value* = `
|
290
|
+
*default value* = `5`
|
291
291
|
|
292
292
|
**segments_refresh_rate** : The SDK polls Split servers for changes to segments every X seconds, where X is this property's value.
|
293
293
|
|
data/NEWS
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
6.3.0 (Apr 30, 2019)
|
2
|
+
|
3
|
+
- Added Dynamic Configurations support through two new methods that mimick the regular ones, changing the type of what is returned.
|
4
|
+
- get_treatment_with_config: Same as get_treatment but returning the treatment with it's config.
|
5
|
+
- get_treatments_with_config: Same as get_treatments, but instead of a map of string it returns a map of treatments with config.
|
6
|
+
- Added configs to SplitViews returned by the manager module.
|
7
|
+
- Updated localhost mode. Now besides supporting the old text files with `.split` extension (to be deprecated soon), we support YAML (.yaml/.yml) files where you can
|
8
|
+
define configurations for your treatments and also whitelisted keys. Read more in our docs!
|
9
|
+
|
10
|
+
6.2.0
|
11
|
+
Ensure SDK flushes information to Split servers on client destroy
|
12
|
+
Fix for compatibility issue between Faraday < 0.13 and net-http-persistent 3
|
13
|
+
Change default features refresh rate interval to 5 seconds
|
14
|
+
|
1
15
|
6.1.0
|
2
16
|
|
3
17
|
Review input validation for client API methods: get_treatment, get_treatments, track, manager. Add input validation to block_until_ready, client startup, and destroy
|
@@ -10,8 +24,8 @@ Fix an issue in events and impressions API calls log messages introduced in 6.0.
|
|
10
24
|
|
11
25
|
6.0.0
|
12
26
|
|
13
|
-
Remove producer mode, make memory adapter mandatory in standalone mode, and Redis adapter mandatory in consumer mode.
|
14
|
-
Reduce the total number of Redis operations of the SDK by changing the impressions storage format and adding a memory cache for splits and segments.
|
27
|
+
BREAKING CHANGE: Remove producer mode, make memory adapter mandatory in standalone mode, and Redis adapter mandatory in consumer mode.
|
28
|
+
BREAKING CHANGE: Reduce the total number of Redis operations of the SDK by changing the impressions storage format and adding a memory cache for splits and segments. It requires to have Split Synchronizer > v2.0.0 running if you are using Redis.
|
15
29
|
SDK is now compatible with net-http-persistent 3.0.
|
16
30
|
|
17
31
|
5.1.2
|
@@ -27,31 +27,56 @@ module SplitIoClient
|
|
27
27
|
'ruby-'+SplitIoClient::VERSION
|
28
28
|
end
|
29
29
|
|
30
|
+
#
|
31
|
+
# obtains the treatments and configs for a given set of features
|
32
|
+
#
|
33
|
+
# @param key [string] evaluation key, only used with yaml split files
|
34
|
+
# @param split_names [array] name of the features being validated
|
35
|
+
# @param attributes [hash] kept for consistency with actual SDK client. Omitted in calls
|
36
|
+
#
|
37
|
+
# @return [hash] map of treatments (split_name, treatment)
|
38
|
+
def get_treatments_with_config(key, split_names, attributes = nil)
|
39
|
+
get_localhost_treatments(key, split_names, attributes, 'get_treatments_with_config')
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# obtains the treatments for a given set of features
|
44
|
+
#
|
45
|
+
# @param key [string] evaluation key, only used with yaml split files
|
46
|
+
# @param split_names [array] name of the features being validated
|
47
|
+
# @param attributes [hash] kept for consistency with actual SDK client. Omitted in calls
|
48
|
+
#
|
49
|
+
# @return [hash] map of treatments (split_name, treatment_name)
|
30
50
|
def get_treatments(key, split_names, attributes = nil)
|
31
|
-
|
32
|
-
|
33
|
-
|
51
|
+
treatments = get_localhost_treatments(key, split_names, attributes)
|
52
|
+
return treatments if treatments.nil?
|
53
|
+
keys = treatments.keys
|
54
|
+
treats = treatments.map { |_,t| t[:treatment] }
|
55
|
+
Hash[keys.zip(treats)]
|
34
56
|
end
|
35
57
|
|
36
58
|
#
|
37
59
|
# obtains the treatment for a given feature
|
38
60
|
#
|
39
|
-
# @param
|
40
|
-
# @param
|
61
|
+
# @param key [string] evaluation key, only used with yaml split files
|
62
|
+
# @param split_name [string] name of the feature that is being validated
|
63
|
+
# @param attributes [hash] kept for consistency with actual SDK client. Omitted in calls
|
41
64
|
#
|
42
|
-
# @return [
|
43
|
-
def get_treatment(
|
44
|
-
|
45
|
-
|
46
|
-
return SplitIoClient::Engine::Models::Treatment::CONTROL
|
47
|
-
end
|
48
|
-
|
49
|
-
unless feature
|
50
|
-
SplitIoClient.configuration.logger.warn('feature was null for id: ' + id)
|
51
|
-
return SplitIoClient::Engine::Models::Treatment::CONTROL
|
52
|
-
end
|
65
|
+
# @return [string] corresponding treatment
|
66
|
+
def get_treatment(key, split_name, attributes = nil)
|
67
|
+
get_localhost_treatment(key, split_name, attributes)[:treatment]
|
68
|
+
end
|
53
69
|
|
54
|
-
|
70
|
+
#
|
71
|
+
# obtains the treatment and config for a given feature
|
72
|
+
#
|
73
|
+
# @param key [string] evaluation key, only used with yaml split files
|
74
|
+
# @param split_name [string] name of the feature that is being validated
|
75
|
+
# @param attributes [hash] kept for consistency with actual SDK client. Omitted in calls
|
76
|
+
#
|
77
|
+
# @return [hash] corresponding treatment and config
|
78
|
+
def get_treatment_with_config(key, split_name, attributes = nil)
|
79
|
+
get_localhost_treatment(key, split_name, attributes, 'get_treatment_with_config')
|
55
80
|
end
|
56
81
|
|
57
82
|
def track
|
@@ -59,17 +84,6 @@ module SplitIoClient
|
|
59
84
|
|
60
85
|
private
|
61
86
|
|
62
|
-
#
|
63
|
-
# auxiliary method to get the treatments avoding exceptions
|
64
|
-
#
|
65
|
-
# @param id [string] user id
|
66
|
-
# @param feature [string] name of the feature that is being validated
|
67
|
-
#
|
68
|
-
# @return [Treatment] tretment constant value
|
69
|
-
def get_treatment_without_exception_handling(id, feature, attributes = nil)
|
70
|
-
get_treatment(id, feature, attributes)
|
71
|
-
end
|
72
|
-
|
73
87
|
#
|
74
88
|
# method to check if the sdk is running in localhost mode based on api key
|
75
89
|
#
|
@@ -78,14 +92,92 @@ module SplitIoClient
|
|
78
92
|
true
|
79
93
|
end
|
80
94
|
|
95
|
+
# @param key [string] evaluation key, only used with yaml split files
|
96
|
+
# @param split_name [string] name of the feature that is being validated
|
81
97
|
#
|
82
|
-
#
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
98
|
+
# @return [Hash] corresponding treatment and config, control otherwise
|
99
|
+
def get_localhost_treatment(key, split_name, attributes, calling_method = 'get_treatment')
|
100
|
+
control_treatment = { label: Engine::Models::Label::EXCEPTION, treatment: SplitIoClient::Engine::Models::Treatment::CONTROL, config: nil }
|
101
|
+
parsed_control_treatment = parsed_treatment(control_treatment)
|
102
|
+
|
103
|
+
bucketing_key, matching_key = keys_from_key(key)
|
104
|
+
return parsed_control_treatment unless SplitIoClient::Validators.valid_get_treatment_parameters(calling_method, key, split_name, matching_key, bucketing_key, attributes)
|
105
|
+
|
106
|
+
sanitized_split_name = split_name.to_s.strip
|
87
107
|
|
88
|
-
|
108
|
+
if split_name.to_s != sanitized_split_name
|
109
|
+
SplitIoClient.configuration.logger.warn("get_treatment: split_name #{split_name} has extra whitespace, trimming")
|
110
|
+
split_name = sanitized_split_name
|
111
|
+
end
|
112
|
+
|
113
|
+
treatment = @localhost_mode_features.select { |h| h[:feature] == split_name && has_key(h[:keys], key) }.last
|
114
|
+
|
115
|
+
if treatment.nil?
|
116
|
+
treatment = @localhost_mode_features.select { |h| h[:feature] == split_name && h[:keys] == nil }.last
|
117
|
+
end
|
118
|
+
|
119
|
+
if treatment && treatment[:treatment]
|
120
|
+
{
|
121
|
+
treatment: treatment[:treatment],
|
122
|
+
config: treatment[:config]
|
123
|
+
}
|
124
|
+
else
|
125
|
+
parsed_control_treatment
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def get_localhost_treatments(key, split_names, attributes = nil, calling_method = 'get_treatments')
|
130
|
+
return nil unless SplitIoClient::Validators.valid_get_treatments_parameters(calling_method, split_names)
|
131
|
+
|
132
|
+
sanitized_split_names = sanitize_split_names(calling_method, split_names)
|
133
|
+
|
134
|
+
if sanitized_split_names.empty?
|
135
|
+
SplitIoClient.configuration.logger.error("#{calling_method}: split_names must be a non-empty Array")
|
136
|
+
return {}
|
137
|
+
end
|
138
|
+
|
139
|
+
split_names.each_with_object({}) do |split_name, memo|
|
140
|
+
memo.merge!(split_name => get_treatment_with_config(key, split_name, attributes))
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def sanitize_split_names(calling_method, split_names)
|
145
|
+
split_names.compact.uniq.select do |split_name|
|
146
|
+
if (split_name.is_a?(String) || split_name.is_a?(Symbol)) && !split_name.empty?
|
147
|
+
true
|
148
|
+
elsif split_name.is_a?(String) && split_name.empty?
|
149
|
+
SplitIoClient.configuration.logger.warn("#{calling_method}: you passed an empty split_name, split_name must be a non-empty String or a Symbol")
|
150
|
+
false
|
151
|
+
else
|
152
|
+
SplitIoClient.configuration.logger.warn("#{calling_method}: you passed an invalid split_name, split_name must be a non-empty String or a Symbol")
|
153
|
+
false
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def parsed_treatment(treatment_data)
|
159
|
+
{
|
160
|
+
treatment: treatment_data[:treatment],
|
161
|
+
config: treatment_data[:config]
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
def has_key(keys, key)
|
166
|
+
case keys
|
167
|
+
when Array then keys.include? key
|
168
|
+
when String then keys == key
|
169
|
+
else
|
170
|
+
false
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def keys_from_key(key)
|
175
|
+
case key
|
176
|
+
when Hash
|
177
|
+
key.values_at(:bucketing_key, :matching_key).map { |k| k.nil? ? nil : k }
|
178
|
+
else
|
179
|
+
[nil, key].map { |k| k.nil? ? nil : k }
|
180
|
+
end
|
89
181
|
end
|
90
182
|
end
|
91
183
|
end
|
@@ -18,109 +18,35 @@ module SplitIoClient
|
|
18
18
|
@adapter = adapter
|
19
19
|
end
|
20
20
|
|
21
|
-
def get_treatments(key, split_names, attributes = {})
|
22
|
-
return nil unless SplitIoClient::Validators.valid_get_treatments_parameters(split_names)
|
23
|
-
|
24
|
-
sanitized_split_names = sanitize_split_names(split_names)
|
25
|
-
|
26
|
-
if sanitized_split_names.empty?
|
27
|
-
SplitIoClient.configuration.logger.error('get_treatments: split_names must be a non-empty Array')
|
28
|
-
return {}
|
29
|
-
end
|
30
|
-
|
31
|
-
bucketing_key, matching_key = keys_from_key(key)
|
32
|
-
bucketing_key = bucketing_key ? bucketing_key.to_s : nil
|
33
|
-
matching_key = matching_key ? matching_key.to_s : nil
|
34
|
-
|
35
|
-
evaluator = Engine::Parser::Evaluator.new(@segments_repository, @splits_repository, true)
|
36
|
-
start = Time.now
|
37
|
-
treatments_labels_change_numbers =
|
38
|
-
@splits_repository.get_splits(sanitized_split_names).each_with_object({}) do |(name, data), memo|
|
39
|
-
memo.merge!(name => get_treatment(key, name, attributes, data, false, true, evaluator))
|
40
|
-
end
|
41
|
-
latency = (Time.now - start) * 1000.0
|
42
|
-
# Measure
|
43
|
-
@adapter.metrics.time('sdk.get_treatments', latency)
|
44
|
-
|
45
|
-
unless SplitIoClient.configuration.disable_impressions
|
46
|
-
time = (Time.now.to_f * 1000.0).to_i
|
47
|
-
@impressions_repository.add_bulk(
|
48
|
-
matching_key, bucketing_key, treatments_labels_change_numbers, time
|
49
|
-
)
|
50
|
-
|
51
|
-
route_impressions(sanitized_split_names, matching_key, bucketing_key, time, treatments_labels_change_numbers, attributes)
|
52
|
-
end
|
53
|
-
|
54
|
-
split_names_keys = treatments_labels_change_numbers.keys
|
55
|
-
treatments = treatments_labels_change_numbers.values.map { |v| v[:treatment] }
|
56
|
-
|
57
|
-
Hash[split_names_keys.zip(treatments)]
|
58
|
-
end
|
59
|
-
|
60
|
-
#
|
61
|
-
# obtains the treatment for a given feature
|
62
|
-
#
|
63
|
-
# @param key [String/Hash] user id or hash with matching_key/bucketing_key
|
64
|
-
# @param split_name [String/Array] name of the feature that is being validated or array of them
|
65
|
-
# @param attributes [Hash] attributes to pass to the treatment class
|
66
|
-
# @param split_data [Hash] split data, when provided this method doesn't fetch splits_repository for the data
|
67
|
-
# @param store_impressions [Boolean] impressions aren't stored if this flag is false
|
68
|
-
# @param multiple [Hash] internal flag to signal if method is called by get_treatments
|
69
|
-
# @param evaluator [Evaluator] Evaluator class instance, used to cache treatments
|
70
|
-
#
|
71
|
-
# @return [String/Hash] Treatment as String or Hash of treatments in case of array of features
|
72
21
|
def get_treatment(
|
73
22
|
key, split_name, attributes = {}, split_data = nil, store_impressions = true,
|
74
23
|
multiple = false, evaluator = nil
|
75
24
|
)
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
return parsed_control_treatment unless valid_client && SplitIoClient::Validators.valid_get_treatment_parameters(key, split_name, matching_key, bucketing_key, attributes)
|
83
|
-
|
84
|
-
bucketing_key = bucketing_key ? bucketing_key.to_s : nil
|
85
|
-
matching_key = matching_key.to_s
|
86
|
-
sanitized_split_name = split_name.to_s.strip
|
87
|
-
|
88
|
-
if split_name.to_s != sanitized_split_name
|
89
|
-
SplitIoClient.configuration.logger.warn("get_treatment: split_name #{split_name} has extra whitespace, trimming")
|
90
|
-
split_name = sanitized_split_name
|
25
|
+
treatment = treatment(key, split_name, attributes, split_data, store_impressions, multiple, evaluator)
|
26
|
+
if multiple
|
27
|
+
treatment.tap { |t| t.delete(:config) }
|
28
|
+
else
|
29
|
+
treatment[:treatment]
|
91
30
|
end
|
31
|
+
end
|
92
32
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
if split.nil?
|
101
|
-
SplitIoClient.configuration.logger.warn("split_name: #{split_name} does not exist. Returning CONTROL")
|
102
|
-
return parsed_control_treatment
|
103
|
-
end
|
104
|
-
|
105
|
-
treatment_data =
|
106
|
-
evaluator.call(
|
107
|
-
{ bucketing_key: bucketing_key, matching_key: matching_key }, split, attributes
|
108
|
-
)
|
109
|
-
|
110
|
-
latency = (Time.now - start) * 1000.0
|
111
|
-
store_impression(split_name, matching_key, bucketing_key, treatment_data, store_impressions, attributes)
|
112
|
-
|
113
|
-
# Measure
|
114
|
-
@adapter.metrics.time('sdk.get_treatment', latency) unless multiple
|
115
|
-
rescue StandardError => error
|
116
|
-
SplitIoClient.configuration.log_found_exception(__method__.to_s, error)
|
117
|
-
|
118
|
-
store_impression(split_name, matching_key, bucketing_key, control_treatment, store_impressions, attributes)
|
33
|
+
def get_treatment_with_config(
|
34
|
+
key, split_name, attributes = {}, split_data = nil, store_impressions = true,
|
35
|
+
multiple = false, evaluator = nil
|
36
|
+
)
|
37
|
+
treatment(key, split_name, attributes, split_data, store_impressions, multiple, evaluator, 'get_treatment_with_config')
|
38
|
+
end
|
119
39
|
|
120
|
-
|
121
|
-
|
40
|
+
def get_treatments(key, split_names, attributes = {})
|
41
|
+
treatments = treatments(key, split_names, attributes)
|
42
|
+
return treatments if treatments.nil?
|
43
|
+
keys = treatments.keys
|
44
|
+
treats = treatments.map { |_,t| t[:treatment] }
|
45
|
+
Hash[keys.zip(treats)]
|
46
|
+
end
|
122
47
|
|
123
|
-
|
48
|
+
def get_treatments_with_config(key, split_names, attributes = {})
|
49
|
+
treatments(key, split_names, attributes,'get_treatments_with_config')
|
124
50
|
end
|
125
51
|
|
126
52
|
def destroy
|
@@ -198,8 +124,8 @@ module SplitIoClient
|
|
198
124
|
end
|
199
125
|
|
200
126
|
def keys_from_key(key)
|
201
|
-
case key
|
202
|
-
when
|
127
|
+
case key
|
128
|
+
when Hash
|
203
129
|
key.values_at(:bucketing_key, :matching_key).map { |k| k.nil? ? nil : k }
|
204
130
|
else
|
205
131
|
[nil, key].map { |k| k.nil? ? nil : k }
|
@@ -208,25 +134,29 @@ module SplitIoClient
|
|
208
134
|
|
209
135
|
def parsed_treatment(multiple, treatment_data)
|
210
136
|
if multiple
|
137
|
+
{
|
138
|
+
treatment: treatment_data[:treatment],
|
139
|
+
label: treatment_data[:label],
|
140
|
+
change_number: treatment_data[:change_number],
|
141
|
+
config: treatment_data[:config]
|
142
|
+
}
|
143
|
+
else
|
211
144
|
{
|
212
145
|
treatment: treatment_data[:treatment],
|
213
|
-
|
214
|
-
change_number: treatment_data[:change_number]
|
146
|
+
config: treatment_data[:config]
|
215
147
|
}
|
216
|
-
else
|
217
|
-
treatment_data[:treatment]
|
218
148
|
end
|
219
149
|
end
|
220
150
|
|
221
|
-
def sanitize_split_names(split_names)
|
151
|
+
def sanitize_split_names(calling_method, split_names)
|
222
152
|
split_names.compact.uniq.select do |split_name|
|
223
153
|
if (split_name.is_a?(String) || split_name.is_a?(Symbol)) && !split_name.empty?
|
224
154
|
true
|
225
155
|
elsif split_name.is_a?(String) && split_name.empty?
|
226
|
-
SplitIoClient.configuration.logger.warn(
|
156
|
+
SplitIoClient.configuration.logger.warn("#{calling_method}: you passed an empty split_name, split_name must be a non-empty String or a Symbol")
|
227
157
|
false
|
228
158
|
else
|
229
|
-
SplitIoClient.configuration.logger.warn(
|
159
|
+
SplitIoClient.configuration.logger.warn("#{calling_method}: you passed an invalid split_name, split_name must be a non-empty String or a Symbol")
|
230
160
|
false
|
231
161
|
end
|
232
162
|
end
|
@@ -241,5 +171,113 @@ module SplitIoClient
|
|
241
171
|
end
|
242
172
|
SplitIoClient.configuration.valid_mode
|
243
173
|
end
|
174
|
+
|
175
|
+
def treatments(key, split_names, attributes = {}, calling_method = 'get_treatments')
|
176
|
+
return nil unless SplitIoClient::Validators.valid_get_treatments_parameters(calling_method, split_names)
|
177
|
+
|
178
|
+
sanitized_split_names = sanitize_split_names(calling_method, split_names)
|
179
|
+
|
180
|
+
if sanitized_split_names.empty?
|
181
|
+
SplitIoClient.configuration.logger.error("#{calling_method}: split_names must be a non-empty Array")
|
182
|
+
return {}
|
183
|
+
end
|
184
|
+
|
185
|
+
bucketing_key, matching_key = keys_from_key(key)
|
186
|
+
bucketing_key = bucketing_key ? bucketing_key.to_s : nil
|
187
|
+
matching_key = matching_key ? matching_key.to_s : nil
|
188
|
+
|
189
|
+
evaluator = Engine::Parser::Evaluator.new(@segments_repository, @splits_repository, true)
|
190
|
+
start = Time.now
|
191
|
+
treatments_labels_change_numbers =
|
192
|
+
@splits_repository.get_splits(sanitized_split_names).each_with_object({}) do |(name, data), memo|
|
193
|
+
memo.merge!(name => treatment(key, name, attributes, data, false, true, evaluator))
|
194
|
+
end
|
195
|
+
latency = (Time.now - start) * 1000.0
|
196
|
+
# Measure
|
197
|
+
@adapter.metrics.time('sdk.' + calling_method, latency)
|
198
|
+
|
199
|
+
unless SplitIoClient.configuration.disable_impressions
|
200
|
+
time = (Time.now.to_f * 1000.0).to_i
|
201
|
+
@impressions_repository.add_bulk(
|
202
|
+
matching_key, bucketing_key, treatments_labels_change_numbers, time
|
203
|
+
)
|
204
|
+
|
205
|
+
route_impressions(sanitized_split_names, matching_key, bucketing_key, time, treatments_labels_change_numbers, attributes)
|
206
|
+
end
|
207
|
+
|
208
|
+
split_names_keys = treatments_labels_change_numbers.keys
|
209
|
+
treatments = treatments_labels_change_numbers.values.map do |v|
|
210
|
+
{
|
211
|
+
treatment: v[:treatment],
|
212
|
+
config: v[:config]
|
213
|
+
}
|
214
|
+
end
|
215
|
+
Hash[split_names_keys.zip(treatments)]
|
216
|
+
end
|
217
|
+
|
218
|
+
#
|
219
|
+
# obtains the treatment for a given feature
|
220
|
+
#
|
221
|
+
# @param key [String/Hash] user id or hash with matching_key/bucketing_key
|
222
|
+
# @param split_name [String/Array] name of the feature that is being validated or array of them
|
223
|
+
# @param attributes [Hash] attributes to pass to the treatment class
|
224
|
+
# @param split_data [Hash] split data, when provided this method doesn't fetch splits_repository for the data
|
225
|
+
# @param store_impressions [Boolean] impressions aren't stored if this flag is false
|
226
|
+
# @param multiple [Hash] internal flag to signal if method is called by get_treatments
|
227
|
+
# @param evaluator [Evaluator] Evaluator class instance, used to cache treatments
|
228
|
+
#
|
229
|
+
# @return [String/Hash] Treatment as String or Hash of treatments in case of array of features
|
230
|
+
def treatment(
|
231
|
+
key, split_name, attributes = {}, split_data = nil, store_impressions = true,
|
232
|
+
multiple = false, evaluator = nil, calling_method = 'get_treatment'
|
233
|
+
)
|
234
|
+
control_treatment = { label: Engine::Models::Label::EXCEPTION, treatment: SplitIoClient::Engine::Models::Treatment::CONTROL, config: nil }
|
235
|
+
parsed_control_treatment = parsed_treatment(multiple, control_treatment)
|
236
|
+
|
237
|
+
bucketing_key, matching_key = keys_from_key(key)
|
238
|
+
|
239
|
+
return parsed_control_treatment unless valid_client && SplitIoClient::Validators.valid_get_treatment_parameters(calling_method, key, split_name, matching_key, bucketing_key, attributes)
|
240
|
+
|
241
|
+
bucketing_key = bucketing_key ? bucketing_key.to_s : nil
|
242
|
+
matching_key = matching_key.to_s
|
243
|
+
sanitized_split_name = split_name.to_s.strip
|
244
|
+
|
245
|
+
if split_name.to_s != sanitized_split_name
|
246
|
+
SplitIoClient.configuration.logger.warn("#{calling_method}: split_name #{split_name} has extra whitespace, trimming")
|
247
|
+
split_name = sanitized_split_name
|
248
|
+
end
|
249
|
+
|
250
|
+
evaluator ||= Engine::Parser::Evaluator.new(@segments_repository, @splits_repository)
|
251
|
+
|
252
|
+
begin
|
253
|
+
start = Time.now
|
254
|
+
|
255
|
+
split = multiple ? split_data : @splits_repository.get_split(split_name)
|
256
|
+
|
257
|
+
if split.nil?
|
258
|
+
SplitIoClient.configuration.logger.warn("split_name: #{split_name} does not exist. Returning CONTROL")
|
259
|
+
return parsed_control_treatment
|
260
|
+
end
|
261
|
+
|
262
|
+
treatment_data =
|
263
|
+
evaluator.call(
|
264
|
+
{ bucketing_key: bucketing_key, matching_key: matching_key }, split, attributes
|
265
|
+
)
|
266
|
+
|
267
|
+
latency = (Time.now - start) * 1000.0
|
268
|
+
store_impression(split_name, matching_key, bucketing_key, treatment_data, store_impressions, attributes)
|
269
|
+
|
270
|
+
# Measure
|
271
|
+
@adapter.metrics.time('sdk.' + calling_method, latency) unless multiple
|
272
|
+
rescue StandardError => error
|
273
|
+
SplitIoClient.configuration.log_found_exception(__method__.to_s, error)
|
274
|
+
|
275
|
+
store_impression(split_name, matching_key, bucketing_key, control_treatment, store_impressions, attributes)
|
276
|
+
|
277
|
+
return parsed_control_treatment
|
278
|
+
end
|
279
|
+
|
280
|
+
parsed_treatment(multiple, treatment_data)
|
281
|
+
end
|
244
282
|
end
|
245
283
|
end
|
@@ -18,7 +18,7 @@ module SplitIoClient
|
|
18
18
|
end
|
19
19
|
rescue StandardError => e
|
20
20
|
SplitIoClient.configuration.logger.warn("#{e}\nURL:#{url}\nparams:#{params}")
|
21
|
-
raise 'Split SDK failed to connect to backend to retrieve information'
|
21
|
+
raise e, 'Split SDK failed to connect to backend to retrieve information', e.backtrace
|
22
22
|
end
|
23
23
|
|
24
24
|
def post_api(url, api_key, data, headers = {}, params = {})
|
@@ -37,7 +37,7 @@ module SplitIoClient
|
|
37
37
|
end
|
38
38
|
rescue StandardError => e
|
39
39
|
SplitIoClient.configuration.logger.warn("#{e}\nURL:#{url}\ndata:#{data}\nparams:#{params}")
|
40
|
-
raise 'Split SDK failed to connect to backend to post information'
|
40
|
+
raise e, 'Split SDK failed to connect to backend to post information', e.backtrace
|
41
41
|
end
|
42
42
|
|
43
43
|
private
|
@@ -26,7 +26,7 @@ module SplitIoClient
|
|
26
26
|
match(split, keys, attributes)
|
27
27
|
end
|
28
28
|
else
|
29
|
-
treatment_hash(Models::Label::KILLED, split[:defaultTreatment], split[:changeNumber])
|
29
|
+
treatment_hash(Models::Label::KILLED, split[:defaultTreatment], split[:changeNumber], split_configurations(split[:defaultTreatment], split))
|
30
30
|
end
|
31
31
|
|
32
32
|
@cache[digest] = treatment if cache_result
|
@@ -36,6 +36,11 @@ module SplitIoClient
|
|
36
36
|
|
37
37
|
private
|
38
38
|
|
39
|
+
def split_configurations(treatment, split)
|
40
|
+
return nil if split[:configurations].nil?
|
41
|
+
split[:configurations][treatment.to_sym]
|
42
|
+
end
|
43
|
+
|
39
44
|
def match(split, keys, attributes)
|
40
45
|
in_rollout = false
|
41
46
|
key = keys[:bucketing_key] ? keys[:bucketing_key] : keys[:matching_key]
|
@@ -52,7 +57,7 @@ module SplitIoClient
|
|
52
57
|
bucket = splitter.bucket(splitter.count_hash(key, split[:trafficAllocationSeed].to_i, legacy_algo))
|
53
58
|
|
54
59
|
if bucket > split[:trafficAllocation]
|
55
|
-
return treatment_hash(Models::Label::NOT_IN_SPLIT, split[:defaultTreatment], split[:changeNumber])
|
60
|
+
return treatment_hash(Models::Label::NOT_IN_SPLIT, split[:defaultTreatment], split[:changeNumber], split_configurations(split[:defaultTreatment], split))
|
56
61
|
end
|
57
62
|
end
|
58
63
|
|
@@ -71,13 +76,13 @@ module SplitIoClient
|
|
71
76
|
result = splitter.get_treatment(key, split[:seed], condition.partitions, split[:algo])
|
72
77
|
|
73
78
|
if result.nil?
|
74
|
-
return treatment_hash(Models::Label::NO_RULE_MATCHED, split[:defaultTreatment], split[:changeNumber])
|
79
|
+
return treatment_hash(Models::Label::NO_RULE_MATCHED, split[:defaultTreatment], split[:changeNumber], split_configurations(split[:defaultTreatment], split))
|
75
80
|
else
|
76
|
-
return treatment_hash(c[:label], result, split[:changeNumber])
|
81
|
+
return treatment_hash(c[:label], result, split[:changeNumber],split_configurations(result, split))
|
77
82
|
end
|
78
83
|
end
|
79
84
|
|
80
|
-
treatment_hash(Models::Label::NO_RULE_MATCHED, split[:defaultTreatment], split[:changeNumber])
|
85
|
+
treatment_hash(Models::Label::NO_RULE_MATCHED, split[:defaultTreatment], split[:changeNumber], split_configurations(split[:defaultTreatment], split))
|
81
86
|
end
|
82
87
|
|
83
88
|
def matcher_type(condition)
|
@@ -102,8 +107,8 @@ module SplitIoClient
|
|
102
107
|
end
|
103
108
|
end
|
104
109
|
|
105
|
-
def treatment_hash(label, treatment, change_number = nil)
|
106
|
-
{ label: label, treatment: treatment, change_number: change_number }
|
110
|
+
def treatment_hash(label, treatment, change_number = nil, configurations = nil)
|
111
|
+
{ label: label, treatment: treatment, change_number: change_number, config: configurations }
|
107
112
|
end
|
108
113
|
|
109
114
|
def matcher_instance(type, condition, matcher)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module SplitIoClient
|
2
2
|
module LocalhostUtils
|
3
|
+
|
4
|
+
require 'yaml'
|
3
5
|
#
|
4
6
|
# method to set localhost mode features by reading the given .splits
|
5
7
|
#
|
@@ -24,12 +26,33 @@ module SplitIoClient
|
|
24
26
|
end
|
25
27
|
|
26
28
|
def store_features(splits_file)
|
29
|
+
yaml_extensions = [".yml", ".yaml"]
|
30
|
+
if yaml_extensions.include? File.extname(splits_file)
|
31
|
+
store_yaml_features(splits_file)
|
32
|
+
else
|
33
|
+
store_plain_text_features(splits_file)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def store_plain_text_features(splits_file)
|
27
40
|
File.open(splits_file).each do |line|
|
28
41
|
feature, treatment = line.strip.split(' ')
|
29
42
|
|
30
43
|
next if line.start_with?('#') || line.strip.empty?
|
31
44
|
|
32
|
-
@localhost_mode_features << { feature: feature, treatment: treatment }
|
45
|
+
@localhost_mode_features << { feature: feature, treatment: treatment, key: nil, config: nil }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def store_yaml_features(splits_file)
|
50
|
+
YAML.load(File.read(splits_file)).each do |feature|
|
51
|
+
feat_symbolized_keys = feature[feature.keys.first].inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
52
|
+
|
53
|
+
feat_symbolized_keys[:config] = feat_symbolized_keys[:config].to_json
|
54
|
+
|
55
|
+
@localhost_mode_features << { feature: feature.keys.first }.merge(feat_symbolized_keys)
|
33
56
|
end
|
34
57
|
end
|
35
58
|
end
|
@@ -21,15 +21,32 @@ module SplitIoClient
|
|
21
21
|
#
|
22
22
|
# @returns a split view
|
23
23
|
def split(split_name)
|
24
|
-
@localhost_mode_features.
|
24
|
+
features = @localhost_mode_features.find_all { |feat| feat[:feature] == split_name }
|
25
|
+
|
26
|
+
return nil if features.nil?
|
27
|
+
|
28
|
+
treatments = features.map { |feat| feat[:treatment] }
|
29
|
+
|
30
|
+
configs = Hash[ features.map { |feat| [ feat[:treatment].to_sym, feat[:config] ] } ]
|
31
|
+
|
32
|
+
{
|
33
|
+
change_number: nil,
|
34
|
+
killed: false,
|
35
|
+
name: split_name,
|
36
|
+
traffic_type: nil,
|
37
|
+
treatments: treatments,
|
38
|
+
configs: configs
|
39
|
+
}
|
25
40
|
end
|
26
41
|
|
27
42
|
#
|
28
43
|
# method to get the split list from the client
|
29
44
|
#
|
30
|
-
# @returns
|
45
|
+
# @returns Array of split view
|
31
46
|
def splits
|
32
|
-
|
47
|
+
split_names.map do |split_name|
|
48
|
+
split(split_name)
|
49
|
+
end
|
33
50
|
end
|
34
51
|
|
35
52
|
#
|
@@ -37,9 +54,7 @@ module SplitIoClient
|
|
37
54
|
#
|
38
55
|
# @returns [object] array of split names (String)
|
39
56
|
def split_names
|
40
|
-
@localhost_mode_features.
|
41
|
-
memo << split[:feature]
|
42
|
-
end
|
57
|
+
@localhost_mode_features.map{ |feat| feat[:feature]}.uniq
|
43
58
|
end
|
44
59
|
end
|
45
60
|
end
|
@@ -75,7 +75,8 @@ module SplitIoClient
|
|
75
75
|
traffic_type_name: split[:trafficTypeName],
|
76
76
|
killed: split[:killed],
|
77
77
|
treatments: treatments,
|
78
|
-
change_number: split[:changeNumber]
|
78
|
+
change_number: split[:changeNumber],
|
79
|
+
configs: split[:configurations] || {}
|
79
80
|
}
|
80
81
|
end
|
81
82
|
end
|
@@ -5,12 +5,24 @@ module SplitIoClient
|
|
5
5
|
def self.build(api_key, config = {})
|
6
6
|
case api_key
|
7
7
|
when 'localhost'
|
8
|
-
|
8
|
+
SplitIoClient.configure( { logger: config[:logger] } )
|
9
9
|
|
10
|
-
LocalhostSplitFactory.new(
|
10
|
+
LocalhostSplitFactory.new(split_file(config[:split_file]), config[:reload_rate])
|
11
11
|
else
|
12
12
|
SplitFactory.new(api_key, config)
|
13
13
|
end
|
14
14
|
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def self.split_file(split_file_path)
|
19
|
+
return split_file_path unless split_file_path.nil?
|
20
|
+
|
21
|
+
SplitIoClient.configuration.logger.warn('Localhost mode: .split mocks ' \
|
22
|
+
'will be deprecated soon in favor of YAML files, which provide more ' \
|
23
|
+
'targeting power. Take a look in our documentation.')
|
24
|
+
|
25
|
+
File.join(Dir.home, '.split')
|
26
|
+
end
|
15
27
|
end
|
16
28
|
end
|
@@ -4,16 +4,16 @@ module SplitIoClient
|
|
4
4
|
module Validators
|
5
5
|
extend self
|
6
6
|
|
7
|
-
def valid_get_treatment_parameters(key, split_name, matching_key, bucketing_key, attributes)
|
8
|
-
valid_key?(key) &&
|
9
|
-
valid_split_name?(split_name) &&
|
10
|
-
valid_matching_key?(matching_key) &&
|
11
|
-
valid_bucketing_key?(key, bucketing_key) &&
|
12
|
-
valid_attributes?(attributes)
|
7
|
+
def valid_get_treatment_parameters(method, key, split_name, matching_key, bucketing_key, attributes)
|
8
|
+
valid_key?(method, key) &&
|
9
|
+
valid_split_name?(method, split_name) &&
|
10
|
+
valid_matching_key?(method, matching_key) &&
|
11
|
+
valid_bucketing_key?(method, key, bucketing_key) &&
|
12
|
+
valid_attributes?(method, attributes)
|
13
13
|
end
|
14
14
|
|
15
|
-
def valid_get_treatments_parameters(split_names)
|
16
|
-
valid_split_names?(split_names)
|
15
|
+
def valid_get_treatments_parameters(method, split_names)
|
16
|
+
valid_split_names?(method, split_names)
|
17
17
|
end
|
18
18
|
|
19
19
|
def valid_track_parameters(key, traffic_type_name, event_type, value)
|
@@ -24,7 +24,7 @@ module SplitIoClient
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def valid_split_parameters(split_name)
|
27
|
-
valid_split_name?(
|
27
|
+
valid_split_name?(:split, split_name)
|
28
28
|
end
|
29
29
|
|
30
30
|
def valid_matcher_arguments(args)
|
@@ -68,7 +68,7 @@ module SplitIoClient
|
|
68
68
|
SplitIoClient.configuration.logger.error("#{method}: #{key} is too long - must be #{SplitIoClient.configuration.max_key_size} characters or less")
|
69
69
|
end
|
70
70
|
|
71
|
-
def valid_split_name?(
|
71
|
+
def valid_split_name?(method, split_name)
|
72
72
|
if split_name.nil?
|
73
73
|
log_nil(:split_name, method)
|
74
74
|
return false
|
@@ -87,62 +87,62 @@ module SplitIoClient
|
|
87
87
|
true
|
88
88
|
end
|
89
89
|
|
90
|
-
def valid_key?(key)
|
90
|
+
def valid_key?(method, key)
|
91
91
|
if key.nil?
|
92
|
-
log_nil(:key,
|
92
|
+
log_nil(:key, method)
|
93
93
|
return false
|
94
94
|
end
|
95
95
|
|
96
96
|
true
|
97
97
|
end
|
98
98
|
|
99
|
-
def valid_matching_key?(matching_key)
|
99
|
+
def valid_matching_key?(method, matching_key)
|
100
100
|
if matching_key.nil?
|
101
|
-
log_nil(:matching_key,
|
101
|
+
log_nil(:matching_key, method)
|
102
102
|
return false
|
103
103
|
end
|
104
104
|
|
105
105
|
unless number_or_string?(matching_key)
|
106
|
-
log_invalid_type(:matching_key,
|
106
|
+
log_invalid_type(:matching_key, method)
|
107
107
|
return false
|
108
108
|
end
|
109
109
|
|
110
110
|
if empty_string?(matching_key)
|
111
|
-
log_empty_string(:matching_key,
|
111
|
+
log_empty_string(:matching_key, method)
|
112
112
|
return false
|
113
113
|
end
|
114
114
|
|
115
|
-
log_convert_numeric(:matching_key,
|
115
|
+
log_convert_numeric(:matching_key, method, matching_key) if matching_key.is_a? Numeric
|
116
116
|
|
117
117
|
if matching_key.size > SplitIoClient.configuration.max_key_size
|
118
|
-
log_key_too_long(:matching_key,
|
118
|
+
log_key_too_long(:matching_key, method)
|
119
119
|
return false
|
120
120
|
end
|
121
121
|
|
122
122
|
true
|
123
123
|
end
|
124
124
|
|
125
|
-
def valid_bucketing_key?(key, bucketing_key)
|
125
|
+
def valid_bucketing_key?(method, key, bucketing_key)
|
126
126
|
if key.is_a? Hash
|
127
127
|
if bucketing_key.nil?
|
128
|
-
log_nil(:bucketing_key,
|
128
|
+
log_nil(:bucketing_key, method)
|
129
129
|
return false
|
130
130
|
end
|
131
131
|
|
132
132
|
unless number_or_string?(bucketing_key)
|
133
|
-
log_invalid_type(:bucketing_key,
|
133
|
+
log_invalid_type(:bucketing_key, method)
|
134
134
|
return false
|
135
135
|
end
|
136
136
|
|
137
137
|
if empty_string?(bucketing_key)
|
138
|
-
log_empty_string(:bucketing_key,
|
138
|
+
log_empty_string(:bucketing_key, method)
|
139
139
|
return false
|
140
140
|
end
|
141
141
|
|
142
|
-
log_convert_numeric(:bucketing_key,
|
142
|
+
log_convert_numeric(:bucketing_key, method, bucketing_key) if bucketing_key.is_a? Numeric
|
143
143
|
|
144
144
|
if bucketing_key.size > SplitIoClient.configuration.max_key_size
|
145
|
-
log_key_too_long(:bucketing_key,
|
145
|
+
log_key_too_long(:bucketing_key, method)
|
146
146
|
return false
|
147
147
|
end
|
148
148
|
end
|
@@ -150,18 +150,18 @@ module SplitIoClient
|
|
150
150
|
true
|
151
151
|
end
|
152
152
|
|
153
|
-
def valid_split_names?(split_names)
|
153
|
+
def valid_split_names?(method, split_names)
|
154
154
|
unless !split_names.nil? && split_names.is_a?(Array)
|
155
|
-
SplitIoClient.configuration.logger.error(
|
155
|
+
SplitIoClient.configuration.logger.error("#{method}: split_names must be a non-empty Array")
|
156
156
|
return false
|
157
157
|
end
|
158
158
|
|
159
159
|
true
|
160
160
|
end
|
161
161
|
|
162
|
-
def valid_attributes?(attributes)
|
162
|
+
def valid_attributes?(method, attributes)
|
163
163
|
unless attributes.nil? || attributes.is_a?(Hash)
|
164
|
-
SplitIoClient.configuration.logger.error(
|
164
|
+
SplitIoClient.configuration.logger.error("#{method}: attributes must be of type Hash")
|
165
165
|
return false
|
166
166
|
end
|
167
167
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: splitclient-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 6.
|
4
|
+
version: 6.3.0
|
5
5
|
platform: java
|
6
6
|
authors:
|
7
7
|
- Split Software
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-04-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
@@ -286,6 +286,7 @@ files:
|
|
286
286
|
- ".gitignore"
|
287
287
|
- ".rubocop.yml"
|
288
288
|
- ".simplecov"
|
289
|
+
- ".travis.yml"
|
289
290
|
- Appraisals
|
290
291
|
- CHANGES.txt
|
291
292
|
- Detailed-README.md
|
@@ -398,9 +399,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
398
399
|
version: '0'
|
399
400
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
400
401
|
requirements:
|
401
|
-
- - "
|
402
|
+
- - ">="
|
402
403
|
- !ruby/object:Gem::Version
|
403
|
-
version:
|
404
|
+
version: '0'
|
404
405
|
requirements: []
|
405
406
|
rubyforge_project:
|
406
407
|
rubygems_version: 2.6.14
|