optimizely-sdk 3.3.2 → 3.7.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 +5 -5
- data/lib/optimizely.rb +248 -46
- data/lib/optimizely/audience.rb +22 -13
- data/lib/optimizely/bucketer.rb +3 -8
- data/lib/optimizely/config/datafile_project_config.rb +19 -3
- data/lib/optimizely/config/proxy_config.rb +34 -0
- data/lib/optimizely/config_manager/async_scheduler.rb +6 -2
- data/lib/optimizely/config_manager/http_project_config_manager.rb +45 -25
- data/lib/optimizely/config_manager/static_project_config_manager.rb +6 -2
- data/lib/optimizely/custom_attribute_condition_evaluator.rb +133 -37
- data/lib/optimizely/decision_service.rb +31 -29
- data/lib/optimizely/event/batch_event_processor.rb +47 -39
- data/lib/optimizely/event/entity/decision.rb +6 -4
- data/lib/optimizely/event/entity/impression_event.rb +4 -2
- data/lib/optimizely/event/event_factory.rb +4 -3
- data/lib/optimizely/event/user_event_factory.rb +4 -3
- data/lib/optimizely/event_dispatcher.rb +8 -14
- data/lib/optimizely/exceptions.rb +17 -9
- data/lib/optimizely/helpers/constants.rb +18 -5
- data/lib/optimizely/helpers/http_utils.rb +64 -0
- data/lib/optimizely/helpers/variable_type.rb +8 -1
- data/lib/optimizely/optimizely_config.rb +117 -0
- data/lib/optimizely/optimizely_factory.rb +54 -5
- data/lib/optimizely/project_config.rb +5 -1
- data/lib/optimizely/semantic_version.rb +166 -0
- data/lib/optimizely/version.rb +1 -1
- metadata +7 -18
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2016-
|
4
|
+
# Copyright 2016-2020, Optimizely and contributors
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -81,14 +81,6 @@ module Optimizely
|
|
81
81
|
end
|
82
82
|
end
|
83
83
|
|
84
|
-
class InvalidDatafileError < Error
|
85
|
-
# Raised when a public method fails due to an invalid datafile
|
86
|
-
|
87
|
-
def initialize(aborted_method)
|
88
|
-
super("Provided datafile is in an invalid format. Aborting #{aborted_method}.")
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
84
|
class InvalidDatafileVersionError < Error
|
93
85
|
# Raised when a datafile with an unsupported version is provided
|
94
86
|
|
@@ -128,4 +120,20 @@ module Optimizely
|
|
128
120
|
super("Optimizely instance is not valid. Failing '#{aborted_method}'.")
|
129
121
|
end
|
130
122
|
end
|
123
|
+
|
124
|
+
class InvalidAttributeType < Error
|
125
|
+
# Raised when an attribute is not provided in expected type.
|
126
|
+
|
127
|
+
def initialize(msg = 'Provided attribute value is not in the expected data type.')
|
128
|
+
super
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class InvalidSemanticVersion < Error
|
133
|
+
# Raised when an invalid value is provided as semantic version.
|
134
|
+
|
135
|
+
def initialize(msg = 'Provided semantic version is invalid.')
|
136
|
+
super
|
137
|
+
end
|
138
|
+
end
|
131
139
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2016-
|
4
|
+
# Copyright 2016-2020, Optimizely and contributors
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -304,7 +304,8 @@ module Optimizely
|
|
304
304
|
'BOOLEAN' => 'boolean',
|
305
305
|
'DOUBLE' => 'double',
|
306
306
|
'INTEGER' => 'integer',
|
307
|
-
'STRING' => 'string'
|
307
|
+
'STRING' => 'string',
|
308
|
+
'JSON' => 'json'
|
308
309
|
}.freeze
|
309
310
|
|
310
311
|
INPUT_VARIABLES = {
|
@@ -334,11 +335,11 @@ module Optimizely
|
|
334
335
|
|
335
336
|
AUDIENCE_EVALUATION_LOGS = {
|
336
337
|
'AUDIENCE_EVALUATION_RESULT' => "Audience '%s' evaluated to %s.",
|
337
|
-
'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for experiment '%s' collectively evaluated to %s.",
|
338
338
|
'EVALUATING_AUDIENCE' => "Starting to evaluate audience '%s' with conditions: %s.",
|
339
|
-
'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for experiment '%s': %s.",
|
340
339
|
'INFINITE_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because the number value ' \
|
341
340
|
"for user attribute '%s' is not in the range [-2^53, +2^53].",
|
341
|
+
'INVALID_SEMANTIC_VERSION' => 'Audience condition %s evaluated as UNKNOWN because an invalid semantic version ' \
|
342
|
+
"was passed for user attribute '%s'.",
|
342
343
|
'MISSING_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated as UNKNOWN because no value ' \
|
343
344
|
"was passed for user attribute '%s'.",
|
344
345
|
'NULL_ATTRIBUTE_VALUE' => 'Audience condition %s evaluated to UNKNOWN because a nil value was passed ' \
|
@@ -353,15 +354,27 @@ module Optimizely
|
|
353
354
|
'to upgrade to a newer release of the Optimizely SDK.'
|
354
355
|
}.freeze
|
355
356
|
|
357
|
+
EXPERIMENT_AUDIENCE_EVALUATION_LOGS = {
|
358
|
+
'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for experiment '%s' collectively evaluated to %s.",
|
359
|
+
'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for experiment '%s': %s."
|
360
|
+
}.merge(AUDIENCE_EVALUATION_LOGS).freeze
|
361
|
+
|
362
|
+
ROLLOUT_AUDIENCE_EVALUATION_LOGS = {
|
363
|
+
'AUDIENCE_EVALUATION_RESULT_COMBINED' => "Audiences for rule '%s' collectively evaluated to %s.",
|
364
|
+
'EVALUATING_AUDIENCES_COMBINED' => "Evaluating audiences for rule '%s': %s."
|
365
|
+
}.merge(AUDIENCE_EVALUATION_LOGS).freeze
|
366
|
+
|
356
367
|
DECISION_NOTIFICATION_TYPES = {
|
357
368
|
'AB_TEST' => 'ab-test',
|
358
369
|
'FEATURE' => 'feature',
|
359
370
|
'FEATURE_TEST' => 'feature-test',
|
360
|
-
'FEATURE_VARIABLE' => 'feature-variable'
|
371
|
+
'FEATURE_VARIABLE' => 'feature-variable',
|
372
|
+
'ALL_FEATURE_VARIABLES' => 'all-feature-variables'
|
361
373
|
}.freeze
|
362
374
|
|
363
375
|
CONFIG_MANAGER = {
|
364
376
|
'DATAFILE_URL_TEMPLATE' => 'https://cdn.optimizely.com/datafiles/%s.json',
|
377
|
+
'AUTHENTICATED_DATAFILE_URL_TEMPLATE' => 'https://config.optimizely.com/datafiles/auth/%s.json',
|
365
378
|
# Default time in seconds to block the 'config' method call until 'config' instance has been initialized.
|
366
379
|
'DEFAULT_BLOCKING_TIMEOUT' => 15,
|
367
380
|
# Default config update interval of 5 minutes
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright 2020, Optimizely and contributors
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'net/http'
|
20
|
+
|
21
|
+
module Optimizely
|
22
|
+
module Helpers
|
23
|
+
module HttpUtils
|
24
|
+
module_function
|
25
|
+
|
26
|
+
def make_request(url, http_method, request_body = nil, headers = {}, read_timeout = nil, proxy_config = nil)
|
27
|
+
# makes http/https GET/POST request and returns response
|
28
|
+
#
|
29
|
+
uri = URI.parse(url)
|
30
|
+
|
31
|
+
if http_method == :get
|
32
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
33
|
+
elsif http_method == :post
|
34
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
35
|
+
request.body = request_body if request_body
|
36
|
+
else
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# set headers
|
41
|
+
headers&.each do |key, val|
|
42
|
+
request[key] = val
|
43
|
+
end
|
44
|
+
|
45
|
+
# do not try to make request with proxy unless we have at least a host
|
46
|
+
http_class = if proxy_config&.host
|
47
|
+
Net::HTTP::Proxy(
|
48
|
+
proxy_config.host,
|
49
|
+
proxy_config.port,
|
50
|
+
proxy_config.username,
|
51
|
+
proxy_config.password
|
52
|
+
)
|
53
|
+
else
|
54
|
+
Net::HTTP
|
55
|
+
end
|
56
|
+
|
57
|
+
http = http_class.new(uri.host, uri.port)
|
58
|
+
http.read_timeout = read_timeout if read_timeout
|
59
|
+
http.use_ssl = uri.scheme == 'https'
|
60
|
+
http.request(request)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright 2017, Optimizely and contributors
|
4
|
+
# Copyright 2017, 2020, Optimizely and contributors
|
5
5
|
#
|
6
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
7
|
# you may not use this file except in compliance with the License.
|
@@ -48,6 +48,13 @@ module Optimizely
|
|
48
48
|
logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type "\
|
49
49
|
"'#{variable_type}': #{e.message}.")
|
50
50
|
end
|
51
|
+
when 'json'
|
52
|
+
begin
|
53
|
+
return_value = JSON.parse(value)
|
54
|
+
rescue => e
|
55
|
+
logger.log(Logger::ERROR, "Unable to cast variable value '#{value}' to type "\
|
56
|
+
"'#{variable_type}': #{e.message}.")
|
57
|
+
end
|
51
58
|
else
|
52
59
|
# default case is string
|
53
60
|
return_value = value
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019-2020, Optimizely and contributors
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
module Optimizely
|
19
|
+
class OptimizelyConfig
|
20
|
+
def initialize(project_config)
|
21
|
+
@project_config = project_config
|
22
|
+
end
|
23
|
+
|
24
|
+
def config
|
25
|
+
experiments_map_object = experiments_map
|
26
|
+
features_map = get_features_map(experiments_map_object)
|
27
|
+
{
|
28
|
+
'datafile' => @project_config.datafile,
|
29
|
+
'experimentsMap' => experiments_map_object,
|
30
|
+
'featuresMap' => features_map,
|
31
|
+
'revision' => @project_config.revision
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def experiments_map
|
38
|
+
feature_variables_map = @project_config.feature_flags.reduce({}) do |result_map, feature|
|
39
|
+
result_map.update(feature['id'] => feature['variables'])
|
40
|
+
end
|
41
|
+
@project_config.experiments.reduce({}) do |experiments_map, experiment|
|
42
|
+
experiments_map.update(
|
43
|
+
experiment['key'] => {
|
44
|
+
'id' => experiment['id'],
|
45
|
+
'key' => experiment['key'],
|
46
|
+
'variationsMap' => experiment['variations'].reduce({}) do |variations_map, variation|
|
47
|
+
variation_object = {
|
48
|
+
'id' => variation['id'],
|
49
|
+
'key' => variation['key'],
|
50
|
+
'variablesMap' => get_merged_variables_map(variation, experiment['id'], feature_variables_map)
|
51
|
+
}
|
52
|
+
variation_object['featureEnabled'] = variation['featureEnabled'] if @project_config.feature_experiment?(experiment['id'])
|
53
|
+
variations_map.update(variation['key'] => variation_object)
|
54
|
+
end
|
55
|
+
}
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Merges feature key and type from feature variables to variation variables.
|
61
|
+
def get_merged_variables_map(variation, experiment_id, feature_variables_map)
|
62
|
+
feature_ids = @project_config.experiment_feature_map[experiment_id]
|
63
|
+
return {} unless feature_ids
|
64
|
+
|
65
|
+
experiment_feature_variables = feature_variables_map[feature_ids[0]]
|
66
|
+
# temporary variation variables map to get values to merge.
|
67
|
+
temp_variables_id_map = {}
|
68
|
+
if variation['variables']
|
69
|
+
temp_variables_id_map = variation['variables'].reduce({}) do |variables_map, variable|
|
70
|
+
variables_map.update(
|
71
|
+
variable['id'] => {
|
72
|
+
'id' => variable['id'],
|
73
|
+
'value' => variable['value']
|
74
|
+
}
|
75
|
+
)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
experiment_feature_variables.reduce({}) do |variables_map, feature_variable|
|
79
|
+
variation_variable = temp_variables_id_map[feature_variable['id']]
|
80
|
+
variable_value = variation['featureEnabled'] && variation_variable ? variation_variable['value'] : feature_variable['defaultValue']
|
81
|
+
variables_map.update(
|
82
|
+
feature_variable['key'] => {
|
83
|
+
'id' => feature_variable['id'],
|
84
|
+
'key' => feature_variable['key'],
|
85
|
+
'type' => feature_variable['type'],
|
86
|
+
'value' => variable_value
|
87
|
+
}
|
88
|
+
)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_features_map(all_experiments_map)
|
93
|
+
@project_config.feature_flags.reduce({}) do |features_map, feature|
|
94
|
+
features_map.update(
|
95
|
+
feature['key'] => {
|
96
|
+
'id' => feature['id'],
|
97
|
+
'key' => feature['key'],
|
98
|
+
'experimentsMap' => feature['experimentIds'].reduce({}) do |experiments_map, experiment_id|
|
99
|
+
experiment_key = @project_config.experiment_id_map[experiment_id]['key']
|
100
|
+
experiments_map.update(experiment_key => all_experiments_map[experiment_key])
|
101
|
+
end,
|
102
|
+
'variablesMap' => feature['variables'].reduce({}) do |variables, variable|
|
103
|
+
variables.update(
|
104
|
+
variable['key'] => {
|
105
|
+
'id' => variable['id'],
|
106
|
+
'key' => variable['key'],
|
107
|
+
'type' => variable['type'],
|
108
|
+
'value' => variable['defaultValue']
|
109
|
+
}
|
110
|
+
)
|
111
|
+
end
|
112
|
+
}
|
113
|
+
)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -17,16 +17,18 @@
|
|
17
17
|
#
|
18
18
|
|
19
19
|
require 'optimizely'
|
20
|
+
require 'optimizely/error_handler'
|
20
21
|
require 'optimizely/event_dispatcher'
|
21
22
|
require 'optimizely/event/batch_event_processor'
|
23
|
+
require 'optimizely/logger'
|
24
|
+
require 'optimizely/notification_center'
|
25
|
+
|
22
26
|
module Optimizely
|
23
27
|
class OptimizelyFactory
|
24
|
-
attr_reader :max_event_batch_size, :max_event_flush_interval
|
25
|
-
|
26
28
|
# Convenience method for setting the maximum number of events contained within a batch.
|
27
29
|
# @param batch_size Integer - Sets size of EventQueue.
|
28
30
|
# @param logger - Optional LoggerInterface Provides a log method to log messages.
|
29
|
-
def self.max_event_batch_size(batch_size, logger)
|
31
|
+
def self.max_event_batch_size(batch_size, logger = NoOpLogger.new)
|
30
32
|
unless batch_size.is_a? Integer
|
31
33
|
logger.log(
|
32
34
|
Logger::ERROR,
|
@@ -48,7 +50,7 @@ module Optimizely
|
|
48
50
|
# Convenience method for setting the maximum time interval in milliseconds between event dispatches.
|
49
51
|
# @param flush_interval Numeric - Time interval between event dispatches.
|
50
52
|
# @param logger - Optional LoggerInterface Provides a log method to log messages.
|
51
|
-
def self.max_event_flush_interval(flush_interval, logger)
|
53
|
+
def self.max_event_flush_interval(flush_interval, logger = NoOpLogger.new)
|
52
54
|
unless flush_interval.is_a? Numeric
|
53
55
|
logger.log(
|
54
56
|
Logger::ERROR,
|
@@ -67,12 +69,42 @@ module Optimizely
|
|
67
69
|
@max_event_flush_interval = flush_interval
|
68
70
|
end
|
69
71
|
|
72
|
+
# Convenience method for setting frequency at which datafile has to be polled and ProjectConfig updated.
|
73
|
+
#
|
74
|
+
# @param polling_interval Numeric - Time in seconds after which to update datafile.
|
75
|
+
def self.polling_interval(polling_interval)
|
76
|
+
@polling_interval = polling_interval
|
77
|
+
end
|
78
|
+
|
79
|
+
# Convenience method for setting timeout to block the config call until config has been initialized.
|
80
|
+
#
|
81
|
+
# @param blocking_timeout Numeric - Time in seconds.
|
82
|
+
def self.blocking_timeout(blocking_timeout)
|
83
|
+
@blocking_timeout = blocking_timeout
|
84
|
+
end
|
85
|
+
|
70
86
|
# Returns a new optimizely instance.
|
71
87
|
#
|
72
88
|
# @params sdk_key - Required String uniquely identifying the fallback datafile corresponding to project.
|
73
89
|
# @param fallback datafile - Optional JSON string datafile.
|
74
90
|
def self.default_instance(sdk_key, datafile = nil)
|
75
|
-
|
91
|
+
error_handler = NoOpErrorHandler.new
|
92
|
+
logger = NoOpLogger.new
|
93
|
+
notification_center = NotificationCenter.new(logger, error_handler)
|
94
|
+
|
95
|
+
config_manager = Optimizely::HTTPProjectConfigManager.new(
|
96
|
+
sdk_key: sdk_key,
|
97
|
+
polling_interval: @polling_interval,
|
98
|
+
blocking_timeout: @blocking_timeout,
|
99
|
+
datafile: datafile,
|
100
|
+
logger: logger,
|
101
|
+
error_handler: error_handler,
|
102
|
+
notification_center: notification_center
|
103
|
+
)
|
104
|
+
|
105
|
+
Optimizely::Project.new(
|
106
|
+
datafile, nil, logger, error_handler, nil, nil, sdk_key, config_manager, notification_center
|
107
|
+
)
|
76
108
|
end
|
77
109
|
|
78
110
|
# Returns a new optimizely instance.
|
@@ -108,10 +140,27 @@ module Optimizely
|
|
108
140
|
config_manager = nil,
|
109
141
|
notification_center = nil
|
110
142
|
)
|
143
|
+
|
144
|
+
error_handler ||= NoOpErrorHandler.new
|
145
|
+
logger ||= NoOpLogger.new
|
146
|
+
notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(logger, error_handler)
|
147
|
+
|
111
148
|
event_processor = BatchEventProcessor.new(
|
112
149
|
event_dispatcher: event_dispatcher || EventDispatcher.new,
|
113
150
|
batch_size: @max_event_batch_size,
|
114
151
|
flush_interval: @max_event_flush_interval,
|
152
|
+
logger: logger,
|
153
|
+
notification_center: notification_center
|
154
|
+
)
|
155
|
+
|
156
|
+
config_manager ||= Optimizely::HTTPProjectConfigManager.new(
|
157
|
+
sdk_key: sdk_key,
|
158
|
+
polling_interval: @polling_interval,
|
159
|
+
blocking_timeout: @blocking_timeout,
|
160
|
+
datafile: datafile,
|
161
|
+
logger: logger,
|
162
|
+
error_handler: error_handler,
|
163
|
+
skip_json_validation: skip_json_validation,
|
115
164
|
notification_center: notification_center
|
116
165
|
)
|
117
166
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright 2016-
|
3
|
+
# Copyright 2016-2020, Optimizely and contributors
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
6
|
# you may not use this file except in compliance with the License.
|
@@ -20,6 +20,8 @@ module Optimizely
|
|
20
20
|
# ProjectConfig is an interface capturing the experiment, variation and feature definitions.
|
21
21
|
# The default implementation of ProjectConfig can be found in DatafileProjectConfig.
|
22
22
|
|
23
|
+
def datafile; end
|
24
|
+
|
23
25
|
def account_id; end
|
24
26
|
|
25
27
|
def attributes; end
|
@@ -44,6 +46,8 @@ module Optimizely
|
|
44
46
|
|
45
47
|
def revision; end
|
46
48
|
|
49
|
+
def send_flag_decisions; end
|
50
|
+
|
47
51
|
def rollouts; end
|
48
52
|
|
49
53
|
def experiment_running?(experiment); end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright 2020, Optimizely and contributors
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require_relative 'exceptions'
|
20
|
+
|
21
|
+
module Optimizely
|
22
|
+
module SemanticVersion
|
23
|
+
# Semantic Version Operators
|
24
|
+
SEMVER_PRE_RELEASE = '-'
|
25
|
+
SEMVER_BUILD = '+'
|
26
|
+
|
27
|
+
module_function
|
28
|
+
|
29
|
+
def pre_release?(target)
|
30
|
+
# Method to check if the given version is a prerelease
|
31
|
+
#
|
32
|
+
# target - String representing semantic version
|
33
|
+
#
|
34
|
+
# Returns true if the given version is a prerelease
|
35
|
+
# false if it doesn't
|
36
|
+
|
37
|
+
raise unless target.is_a? String
|
38
|
+
|
39
|
+
prerelease_index = target.index(SEMVER_PRE_RELEASE)
|
40
|
+
build_index = target.index(SEMVER_BUILD)
|
41
|
+
|
42
|
+
return false if prerelease_index.nil?
|
43
|
+
return true if build_index.nil?
|
44
|
+
|
45
|
+
# when both operators are present prerelease should precede the build operator
|
46
|
+
prerelease_index < build_index
|
47
|
+
end
|
48
|
+
|
49
|
+
def build?(target)
|
50
|
+
# Method to check if the given version is a build
|
51
|
+
#
|
52
|
+
# target - String representing semantic version
|
53
|
+
#
|
54
|
+
# Returns true if the given version is a build
|
55
|
+
# false if it doesn't
|
56
|
+
|
57
|
+
raise unless target.is_a? String
|
58
|
+
|
59
|
+
prerelease_index = target.index(SEMVER_PRE_RELEASE)
|
60
|
+
build_index = target.index(SEMVER_BUILD)
|
61
|
+
|
62
|
+
return false if build_index.nil?
|
63
|
+
return true if prerelease_index.nil?
|
64
|
+
|
65
|
+
# when both operators are present build should precede the prerelease operator
|
66
|
+
build_index < prerelease_index
|
67
|
+
end
|
68
|
+
|
69
|
+
def split_semantic_version(target)
|
70
|
+
# Method to split the given version.
|
71
|
+
#
|
72
|
+
# target - String representing semantic version
|
73
|
+
#
|
74
|
+
# Returns List The array of version split into smaller parts i.e major, minor, patch etc,
|
75
|
+
# Exception if the given version is invalid.
|
76
|
+
|
77
|
+
target_prefix = target
|
78
|
+
target_suffix = ''
|
79
|
+
target_parts = []
|
80
|
+
|
81
|
+
raise InvalidSemanticVersion if target.include? ' '
|
82
|
+
|
83
|
+
if pre_release?(target)
|
84
|
+
target_parts = target.split(SEMVER_PRE_RELEASE, 2)
|
85
|
+
elsif build? target
|
86
|
+
target_parts = target.split(SEMVER_BUILD, 2)
|
87
|
+
end
|
88
|
+
|
89
|
+
unless target_parts.empty?
|
90
|
+
target_prefix = target_parts[0].to_s
|
91
|
+
target_suffix = target_parts[1..-1]
|
92
|
+
end
|
93
|
+
|
94
|
+
# expect a version string of the form x.y.z
|
95
|
+
dot_count = target_prefix.count('.')
|
96
|
+
raise InvalidSemanticVersion if dot_count > 2
|
97
|
+
|
98
|
+
target_version_parts = target_prefix.split('.')
|
99
|
+
raise InvalidSemanticVersion if target_version_parts.length != dot_count + 1
|
100
|
+
|
101
|
+
target_version_parts.each do |part|
|
102
|
+
raise InvalidSemanticVersion unless Helpers::Validator.string_numeric? part
|
103
|
+
end
|
104
|
+
|
105
|
+
target_version_parts.concat(target_suffix) if target_suffix.is_a?(Array)
|
106
|
+
|
107
|
+
target_version_parts
|
108
|
+
end
|
109
|
+
|
110
|
+
def compare_user_version_with_target_version(target_version, user_version)
|
111
|
+
# Compares target and user versions
|
112
|
+
#
|
113
|
+
# target_version - String representing target version
|
114
|
+
# user_version - String representing user version
|
115
|
+
|
116
|
+
# Returns boolean 0 if user version is equal to target version,
|
117
|
+
# 1 if user version is greater than target version,
|
118
|
+
# -1 if user version is less than target version.
|
119
|
+
|
120
|
+
raise InvalidAttributeType unless target_version.is_a? String
|
121
|
+
raise InvalidAttributeType unless user_version.is_a? String
|
122
|
+
|
123
|
+
is_target_version_prerelease = pre_release?(target_version)
|
124
|
+
is_user_version_prerelease = pre_release?(user_version)
|
125
|
+
|
126
|
+
target_version_parts = split_semantic_version(target_version)
|
127
|
+
user_version_parts = split_semantic_version(user_version)
|
128
|
+
user_version_parts_len = user_version_parts.length if user_version_parts
|
129
|
+
|
130
|
+
# Up to the precision of targetedVersion, expect version to match exactly.
|
131
|
+
target_version_parts.each_with_index do |_item, idx|
|
132
|
+
if user_version_parts_len <= idx
|
133
|
+
# even if they are equal at this point. if the target is a prerelease
|
134
|
+
# then user version must be greater than the pre release.
|
135
|
+
return 1 if is_target_version_prerelease
|
136
|
+
|
137
|
+
return -1
|
138
|
+
|
139
|
+
elsif !Helpers::Validator.string_numeric? user_version_parts[idx]
|
140
|
+
# compare strings
|
141
|
+
if user_version_parts[idx] < target_version_parts[idx]
|
142
|
+
return 1 if is_target_version_prerelease && !is_user_version_prerelease
|
143
|
+
|
144
|
+
return -1
|
145
|
+
|
146
|
+
elsif user_version_parts[idx] > target_version_parts[idx]
|
147
|
+
return -1 if is_user_version_prerelease && !is_target_version_prerelease
|
148
|
+
|
149
|
+
return 1
|
150
|
+
end
|
151
|
+
|
152
|
+
else
|
153
|
+
user_version_part = user_version_parts[idx].to_i
|
154
|
+
target_version_part = target_version_parts[idx].to_i
|
155
|
+
|
156
|
+
return 1 if user_version_part > target_version_part
|
157
|
+
return -1 if user_version_part < target_version_part
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
return -1 if is_user_version_prerelease && !is_target_version_prerelease
|
162
|
+
|
163
|
+
0
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|