splitclient-rb 5.1.1.pre.rc2-java → 5.1.2-java
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/.gitignore +4 -1
- data/CHANGES.txt +10 -0
- data/NEWS +8 -0
- data/lib/splitclient-rb.rb +2 -2
- data/lib/splitclient-rb/cache/repositories/splits_repository.rb +1 -1
- data/lib/splitclient-rb/clients/split_client.rb +57 -50
- data/lib/splitclient-rb/engine/models/label.rb +0 -1
- data/lib/splitclient-rb/exceptions.rb +7 -0
- data/lib/splitclient-rb/managers/split_manager.rb +1 -2
- data/lib/splitclient-rb/validators.rb +185 -0
- data/lib/splitclient-rb/version.rb +1 -1
- metadata +6 -6
- data/lib/splitclient-rb/exceptions/impressions_shutdown_exception.rb +0 -4
- data/lib/splitclient-rb/exceptions/sdk_blocker_timeout_expired_exception.rb +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 300bd19c760f74a20aa21b3c2f1d17f1652c0eb4
|
4
|
+
data.tar.gz: b6a959b96fc3f7bfb89dfcafb46b44933e26bc42
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dda2b8ba1238ec2059dd8e414442417211a87aeb49d656e5103786512e31fb11e854a2f3658b5e0ce19e073f015658d166c34965d6a96a6bec9bfb53aec9936a
|
7
|
+
data.tar.gz: 393b9f1280a80e22ac3d7e75a2e0b91bfa370d3667cf6ba88e9ae9c3376643ce69b0c3d3bd4303ed0aead943e3e1ab7e73883b3f2d52a18e0159ab852d98bceb
|
data/.gitignore
CHANGED
data/CHANGES.txt
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
5.1.2 (October 26th, 2018)
|
2
|
+
- Add input validation for client API methods
|
3
|
+
|
4
|
+
5.1.1 (October 4th, 2018)
|
5
|
+
- Change get_treatments so that it sends a single latency metric
|
6
|
+
- Removed unused call to Redis#scan when adding latencies
|
7
|
+
- Removed Redis calls on initialization when SDK is set to consumer mode
|
8
|
+
- Change split_config approach so that every property has an accessor
|
9
|
+
- Removed @config parameter on most initializers
|
10
|
+
|
1
11
|
5.1.0 (September 10th, 2018)
|
2
12
|
- Change `get_api` to return only a Faraday response.
|
3
13
|
- Add `SplitLogger` to clean up logging code and reduce the complexity in several methods.
|
data/NEWS
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
5.1.2
|
2
|
+
|
3
|
+
Add input validation for client API methods: get_treatment, get_treatments, track, manager
|
4
|
+
|
5
|
+
5.1.1
|
6
|
+
|
7
|
+
Reduces the number of calls to Redis when calling #client.get_treatments using such cache adapter.
|
8
|
+
|
1
9
|
5.1.0
|
2
10
|
|
3
11
|
Prevent unhandled exceptions from raising when API get calls fail on Segments and Treatments.
|
data/lib/splitclient-rb.rb
CHANGED
@@ -2,8 +2,7 @@ require 'forwardable'
|
|
2
2
|
|
3
3
|
require 'splitclient-rb/version'
|
4
4
|
|
5
|
-
require 'splitclient-rb/exceptions
|
6
|
-
require 'splitclient-rb/exceptions/sdk_blocker_timeout_expired_exception'
|
5
|
+
require 'splitclient-rb/exceptions'
|
7
6
|
require 'splitclient-rb/cache/routers/impression_router'
|
8
7
|
require 'splitclient-rb/cache/adapters/memory_adapters/map_adapter'
|
9
8
|
require 'splitclient-rb/cache/adapters/memory_adapters/queue_adapter'
|
@@ -80,6 +79,7 @@ require 'splitclient-rb/engine/models/split'
|
|
80
79
|
require 'splitclient-rb/engine/models/label'
|
81
80
|
require 'splitclient-rb/engine/models/treatment'
|
82
81
|
require 'splitclient-rb/utilitites'
|
82
|
+
require 'splitclient-rb/validators'
|
83
83
|
|
84
84
|
# C extension
|
85
85
|
require 'murmurhash/murmurhash_mri'
|
@@ -26,7 +26,7 @@ module SplitIoClient
|
|
26
26
|
|
27
27
|
def get_splits(names)
|
28
28
|
splits = {}
|
29
|
-
split_names = names.
|
29
|
+
split_names = names.map { |name| namespace_key(".split.#{name}") }
|
30
30
|
splits.merge!(
|
31
31
|
@adapter
|
32
32
|
.multiple_strings(split_names)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
module SplitIoClient
|
2
|
+
|
2
3
|
class SplitClient
|
3
4
|
#
|
4
5
|
# Creates a new split client instance that connects to split.io API.
|
@@ -17,11 +18,24 @@ module SplitIoClient
|
|
17
18
|
end
|
18
19
|
|
19
20
|
def get_treatments(key, split_names, attributes = {})
|
21
|
+
|
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.warn('get_treatments: split_names is an empty array or has null values')
|
28
|
+
return {}
|
29
|
+
end
|
30
|
+
|
20
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
|
+
|
21
35
|
evaluator = Engine::Parser::Evaluator.new(@segments_repository, @splits_repository, true)
|
22
36
|
start = Time.now
|
23
37
|
treatments_labels_change_numbers =
|
24
|
-
@splits_repository.get_splits(
|
38
|
+
@splits_repository.get_splits(sanitized_split_names).each_with_object({}) do |(name, data), memo|
|
25
39
|
memo.merge!(name => get_treatment(key, name, attributes, data, false, true, evaluator))
|
26
40
|
end
|
27
41
|
latency = (Time.now - start) * 1000.0
|
@@ -34,13 +48,13 @@ module SplitIoClient
|
|
34
48
|
matching_key, bucketing_key, treatments_labels_change_numbers, time
|
35
49
|
)
|
36
50
|
|
37
|
-
route_impressions(
|
51
|
+
route_impressions(sanitized_split_names, matching_key, bucketing_key, time, treatments_labels_change_numbers, attributes)
|
38
52
|
end
|
39
53
|
|
40
|
-
|
54
|
+
split_names_keys = treatments_labels_change_numbers.keys
|
41
55
|
treatments = treatments_labels_change_numbers.values.map { |v| v[:treatment] }
|
42
56
|
|
43
|
-
Hash[
|
57
|
+
Hash[split_names_keys.zip(treatments)]
|
44
58
|
end
|
45
59
|
|
46
60
|
#
|
@@ -59,68 +73,43 @@ module SplitIoClient
|
|
59
73
|
key, split_name, attributes = {}, split_data = nil, store_impressions = true,
|
60
74
|
multiple = false, evaluator = nil
|
61
75
|
)
|
62
|
-
|
63
|
-
|
64
|
-
evaluator ||= Engine::Parser::Evaluator.new(@segments_repository, @splits_repository)
|
76
|
+
control_treatment = { label: Engine::Models::Label::EXCEPTION, treatment: SplitIoClient::Engine::Models::Treatment::CONTROL }
|
77
|
+
parsed_control_treatment = parsed_treatment(multiple, control_treatment)
|
65
78
|
|
66
|
-
|
67
|
-
SplitIoClient.configuration.logger.warn('matching_key was null for split_name: ' + split_name.to_s)
|
68
|
-
return parsed_treatment(multiple, treatment_data)
|
69
|
-
end
|
79
|
+
bucketing_key, matching_key = keys_from_key(key)
|
70
80
|
|
71
|
-
|
72
|
-
SplitIoClient.configuration.logger.warn('split_name was null for key: ' + key)
|
73
|
-
return parsed_treatment(multiple, treatment_data)
|
74
|
-
end
|
81
|
+
return parsed_control_treatment unless SplitIoClient::Validators.valid_get_treatment_parameters(key, split_name, matching_key, bucketing_key)
|
75
82
|
|
76
|
-
|
83
|
+
bucketing_key = bucketing_key ? bucketing_key.to_s : nil
|
84
|
+
matching_key = matching_key.to_s
|
85
|
+
evaluator ||= Engine::Parser::Evaluator.new(@segments_repository, @splits_repository)
|
77
86
|
|
78
87
|
begin
|
88
|
+
start = Time.now
|
89
|
+
|
79
90
|
split = multiple ? split_data : @splits_repository.get_split(split_name)
|
80
91
|
|
81
92
|
if split.nil?
|
82
|
-
SplitIoClient.configuration.logger.
|
83
|
-
return
|
84
|
-
else
|
85
|
-
treatment_data =
|
86
|
-
evaluator.call(
|
87
|
-
{ bucketing_key: bucketing_key, matching_key: matching_key }, split, attributes
|
88
|
-
)
|
93
|
+
SplitIoClient.configuration.logger.warn("split_name: #{split_name} does not exist. Returning CONTROL")
|
94
|
+
return parsed_control_treatment
|
89
95
|
end
|
90
|
-
rescue StandardError => error
|
91
|
-
SplitIoClient.configuration.log_found_exception(__method__.to_s, error)
|
92
96
|
|
93
|
-
|
94
|
-
|
95
|
-
{
|
96
|
-
treatment: SplitIoClient::Engine::Models::Treatment::CONTROL,
|
97
|
-
label: SplitIoClient::Engine::Models::Label::EXCEPTION
|
98
|
-
},
|
99
|
-
store_impressions, attributes
|
97
|
+
treatment_data =
|
98
|
+
evaluator.call(
|
99
|
+
{ bucketing_key: bucketing_key, matching_key: matching_key }, split, attributes
|
100
100
|
)
|
101
101
|
|
102
|
-
return parsed_treatment(multiple, treatment_data)
|
103
|
-
end
|
104
|
-
|
105
|
-
begin
|
106
102
|
latency = (Time.now - start) * 1000.0
|
107
|
-
|
103
|
+
store_impression(split_name, matching_key, bucketing_key, treatment_data, store_impressions, attributes)
|
108
104
|
|
109
105
|
# Measure
|
110
106
|
@adapter.metrics.time('sdk.get_treatment', latency) unless multiple
|
111
107
|
rescue StandardError => error
|
112
108
|
SplitIoClient.configuration.log_found_exception(__method__.to_s, error)
|
113
109
|
|
114
|
-
store_impression(
|
115
|
-
split_name, matching_key, bucketing_key,
|
116
|
-
{
|
117
|
-
treatment: SplitIoClient::Engine::Models::Treatment::CONTROL,
|
118
|
-
label: SplitIoClient::Engine::Models::Label::EXCEPTION
|
119
|
-
},
|
120
|
-
store_impressions, attributes
|
121
|
-
)
|
110
|
+
store_impression(split_name, matching_key, bucketing_key, control_treatment, store_impressions, attributes)
|
122
111
|
|
123
|
-
return
|
112
|
+
return parsed_control_treatment
|
124
113
|
end
|
125
114
|
|
126
115
|
parsed_treatment(multiple, treatment_data)
|
@@ -188,16 +177,23 @@ module SplitIoClient
|
|
188
177
|
@impression_router ||= SplitIoClient::ImpressionRouter.new
|
189
178
|
end
|
190
179
|
|
191
|
-
def track(key,
|
192
|
-
|
180
|
+
def track(key, traffic_type_name, event_type, value = nil)
|
181
|
+
return false unless SplitIoClient::Validators.valid_track_parameters(key, traffic_type_name, event_type, value)
|
182
|
+
begin
|
183
|
+
@events_repository.add(key.to_s, traffic_type_name, event_type.to_s, (Time.now.to_f * 1000).to_i, value)
|
184
|
+
true
|
185
|
+
rescue StandardError => error
|
186
|
+
SplitIoClient.configuration.log_found_exception(__method__.to_s, error)
|
187
|
+
false
|
188
|
+
end
|
193
189
|
end
|
194
190
|
|
195
191
|
def keys_from_key(key)
|
196
192
|
case key.class.to_s
|
197
193
|
when 'Hash'
|
198
|
-
key.values_at(:bucketing_key, :matching_key).map { |k| k.nil? ? nil : k
|
194
|
+
key.values_at(:bucketing_key, :matching_key).map { |k| k.nil? ? nil : k }
|
199
195
|
else
|
200
|
-
[nil, key].map { |k| k.nil? ? nil : k
|
196
|
+
[nil, key].map { |k| k.nil? ? nil : k }
|
201
197
|
end
|
202
198
|
end
|
203
199
|
|
@@ -212,5 +208,16 @@ module SplitIoClient
|
|
212
208
|
treatment_data[:treatment]
|
213
209
|
end
|
214
210
|
end
|
211
|
+
|
212
|
+
def sanitize_split_names(split_names)
|
213
|
+
split_names.compact.uniq.select do |split_name|
|
214
|
+
if split_name.is_a?(String) && !split_name.empty?
|
215
|
+
true
|
216
|
+
else
|
217
|
+
SplitIoClient.configuration.logger.warn('get_treatments: split_name has to be a non empty string')
|
218
|
+
false
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
215
222
|
end
|
216
223
|
end
|
@@ -43,7 +43,7 @@ module SplitIoClient
|
|
43
43
|
#
|
44
44
|
# @returns a split view
|
45
45
|
def split(split_name)
|
46
|
-
return unless @splits_repository
|
46
|
+
return unless @splits_repository && SplitIoClient::Validators.valid_split_parameters(split_name)
|
47
47
|
|
48
48
|
split = @splits_repository.get_split(split_name)
|
49
49
|
|
@@ -63,7 +63,6 @@ module SplitIoClient
|
|
63
63
|
treatments = []
|
64
64
|
end
|
65
65
|
|
66
|
-
|
67
66
|
{
|
68
67
|
name: name,
|
69
68
|
traffic_type_name: split[:trafficTypeName],
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module SplitIoClient
|
2
|
+
module Validators
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def valid_get_treatment_parameters(key, split_name, matching_key, bucketing_key)
|
6
|
+
valid_key?(key) && valid_split_name?(split_name) && valid_matching_key?(matching_key) && valid_bucketing_key?(bucketing_key)
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid_get_treatments_parameters(split_names)
|
10
|
+
valid_split_names?(split_names)
|
11
|
+
end
|
12
|
+
|
13
|
+
def valid_track_parameters(key, traffic_type_name, event_type, value)
|
14
|
+
valid_track_key?(key) && valid_traffic_type_name?(traffic_type_name) && valid_event_type?(event_type) && valid_value?(value)
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid_split_parameters(split_name)
|
18
|
+
valid_split_name?(split_name, :split)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def string?(value)
|
24
|
+
value.is_a?(String) || value.is_a?(Symbol)
|
25
|
+
end
|
26
|
+
|
27
|
+
def number_or_string?(value)
|
28
|
+
value.is_a?(Numeric) || string?(value)
|
29
|
+
end
|
30
|
+
|
31
|
+
def log_nil(key, method)
|
32
|
+
SplitIoClient.configuration.logger.error("#{method}: #{key} cannot be nil")
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_string(key, method)
|
36
|
+
SplitIoClient.configuration.logger.error("#{method}: #{key} must be a String or a Symbol")
|
37
|
+
end
|
38
|
+
|
39
|
+
def log_number_or_string(key, method)
|
40
|
+
SplitIoClient.configuration.logger.error("#{method}: #{key} must be a String")
|
41
|
+
end
|
42
|
+
|
43
|
+
def log_convert_numeric(key, method)
|
44
|
+
SplitIoClient.configuration.logger.warn("#{method}: #{key} is not of type String, converting to String")
|
45
|
+
end
|
46
|
+
|
47
|
+
def valid_split_name?(split_name, method=:get_treatment)
|
48
|
+
if split_name.nil?
|
49
|
+
log_nil(:split_name, method)
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
|
53
|
+
unless string?(split_name)
|
54
|
+
log_string(:split_name, method)
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
|
58
|
+
return true
|
59
|
+
end
|
60
|
+
|
61
|
+
def valid_key?(key)
|
62
|
+
if key.nil?
|
63
|
+
log_nil(:key, :get_treatment)
|
64
|
+
return false
|
65
|
+
end
|
66
|
+
|
67
|
+
return true
|
68
|
+
end
|
69
|
+
|
70
|
+
def valid_matching_key?(matching_key)
|
71
|
+
if matching_key.nil?
|
72
|
+
log_nil(:matching_key, :get_treatment)
|
73
|
+
return false
|
74
|
+
end
|
75
|
+
|
76
|
+
unless number_or_string?(matching_key)
|
77
|
+
log_number_or_string(:matching_key, :get_treatment)
|
78
|
+
return false
|
79
|
+
end
|
80
|
+
|
81
|
+
if matching_key.is_a? Numeric
|
82
|
+
log_convert_numeric(:matching_key, :get_treatment)
|
83
|
+
end
|
84
|
+
|
85
|
+
return true
|
86
|
+
end
|
87
|
+
|
88
|
+
def valid_bucketing_key?(bucketing_key)
|
89
|
+
if bucketing_key.nil?
|
90
|
+
SplitIoClient.configuration.logger.warn('get_treatment: key object should have bucketing_key set')
|
91
|
+
return true
|
92
|
+
end
|
93
|
+
|
94
|
+
unless number_or_string?(bucketing_key)
|
95
|
+
log_number_or_string(:bucketing_key, :get_treatment)
|
96
|
+
return false
|
97
|
+
end
|
98
|
+
|
99
|
+
if bucketing_key.is_a? Numeric
|
100
|
+
log_convert_numeric(:bucketing_key, :get_treatment)
|
101
|
+
end
|
102
|
+
|
103
|
+
return true
|
104
|
+
end
|
105
|
+
|
106
|
+
def valid_split_names?(split_names)
|
107
|
+
if split_names.nil?
|
108
|
+
log_nil(:split_names, :get_treatments)
|
109
|
+
return false
|
110
|
+
end
|
111
|
+
|
112
|
+
unless split_names.is_a? Array
|
113
|
+
SplitIoClient.configuration.logger.warn('get_treatments: split_names must be an Array')
|
114
|
+
return false
|
115
|
+
end
|
116
|
+
|
117
|
+
return true
|
118
|
+
end
|
119
|
+
|
120
|
+
def valid_track_key?(key)
|
121
|
+
if key.nil?
|
122
|
+
log_nil(:key, :track)
|
123
|
+
return false
|
124
|
+
end
|
125
|
+
|
126
|
+
unless number_or_string?(key)
|
127
|
+
log_number_or_string(:key, :track)
|
128
|
+
return false
|
129
|
+
end
|
130
|
+
|
131
|
+
if key.is_a? Numeric
|
132
|
+
log_convert_numeric(:key, :track)
|
133
|
+
end
|
134
|
+
|
135
|
+
return true
|
136
|
+
end
|
137
|
+
|
138
|
+
def valid_event_type?(event_type)
|
139
|
+
if event_type.nil?
|
140
|
+
log_nil(:event_type, :track)
|
141
|
+
return false
|
142
|
+
end
|
143
|
+
|
144
|
+
unless string?(event_type)
|
145
|
+
log_string(:event_type, :track)
|
146
|
+
return false
|
147
|
+
end
|
148
|
+
|
149
|
+
if (event_type.to_s =~ /[a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}/).nil?
|
150
|
+
SplitIoClient.configuration.logger.error('track: event_type must adhere to [a-zA-Z0-9][-_\.a-zA-Z0-9]{0,62}')
|
151
|
+
return false
|
152
|
+
end
|
153
|
+
|
154
|
+
return true
|
155
|
+
end
|
156
|
+
|
157
|
+
def valid_traffic_type_name?(traffic_type_name)
|
158
|
+
if traffic_type_name.nil?
|
159
|
+
log_nil(:traffic_type_name, :track)
|
160
|
+
return false
|
161
|
+
end
|
162
|
+
|
163
|
+
unless string?(traffic_type_name)
|
164
|
+
log_string(:traffic_type_name, :track)
|
165
|
+
return false
|
166
|
+
end
|
167
|
+
|
168
|
+
if traffic_type_name.empty?
|
169
|
+
SplitIoClient.configuration.logger.error('track: traffic_type_name must not be an empty String')
|
170
|
+
return false
|
171
|
+
end
|
172
|
+
|
173
|
+
return true
|
174
|
+
end
|
175
|
+
|
176
|
+
def valid_value?(value)
|
177
|
+
unless value.is_a?(Numeric) || value.nil?
|
178
|
+
SplitIoClient.configuration.logger.error('track: value must be a number')
|
179
|
+
return false
|
180
|
+
end
|
181
|
+
|
182
|
+
return true
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
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: 5.1.
|
4
|
+
version: 5.1.2
|
5
5
|
platform: java
|
6
6
|
authors:
|
7
7
|
- Split Software
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-10-
|
11
|
+
date: 2018-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
@@ -321,8 +321,7 @@ files:
|
|
321
321
|
- lib/splitclient-rb/engine/parser/evaluator.rb
|
322
322
|
- lib/splitclient-rb/engine/parser/partition.rb
|
323
323
|
- lib/splitclient-rb/engine/parser/split_adapter.rb
|
324
|
-
- lib/splitclient-rb/exceptions
|
325
|
-
- lib/splitclient-rb/exceptions/sdk_blocker_timeout_expired_exception.rb
|
324
|
+
- lib/splitclient-rb/exceptions.rb
|
326
325
|
- lib/splitclient-rb/localhost_split_factory.rb
|
327
326
|
- lib/splitclient-rb/localhost_utils.rb
|
328
327
|
- lib/splitclient-rb/managers/localhost_split_manager.rb
|
@@ -332,6 +331,7 @@ files:
|
|
332
331
|
- lib/splitclient-rb/split_factory_builder.rb
|
333
332
|
- lib/splitclient-rb/split_logger.rb
|
334
333
|
- lib/splitclient-rb/utilitites.rb
|
334
|
+
- lib/splitclient-rb/validators.rb
|
335
335
|
- lib/splitclient-rb/version.rb
|
336
336
|
- splitclient-rb.gemspec
|
337
337
|
- splitio.yml.example
|
@@ -353,9 +353,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
353
353
|
version: '0'
|
354
354
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
355
355
|
requirements:
|
356
|
-
- - "
|
356
|
+
- - ">="
|
357
357
|
- !ruby/object:Gem::Version
|
358
|
-
version:
|
358
|
+
version: '0'
|
359
359
|
requirements: []
|
360
360
|
rubyforge_project:
|
361
361
|
rubygems_version: 2.6.14
|