rox-rollout 0.1.3
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 +7 -0
- data/.circleci/config.yml +45 -0
- data/.gitignore +8 -0
- data/Gemfile +6 -0
- data/LICENSE +50 -0
- data/README_DEVELOP.md +19 -0
- data/Rakefile +16 -0
- data/_archive/.document +5 -0
- data/_archive/.rspec +1 -0
- data/_archive/Gemfile +15 -0
- data/_archive/Gemfile.lock +87 -0
- data/_archive/README.md +32 -0
- data/_archive/README.rdoc +19 -0
- data/_archive/Rakefile +50 -0
- data/_archive/lib/expr_function_definition.rb +52 -0
- data/_archive/lib/function_definition.rb +48 -0
- data/_archive/lib/function_token.rb +12 -0
- data/_archive/lib/object_extends.rb +12 -0
- data/_archive/lib/ruby_interpreter.rb +292 -0
- data/_archive/lib/stack.rb +48 -0
- data/_archive/lib/string_extends.rb +14 -0
- data/_archive/spec/ruby_interpreter_spec.rb +203 -0
- data/_archive/spec/spec_helper.rb +30 -0
- data/_archive/spec/stack_spec.rb +77 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/e2e/container.rb +38 -0
- data/e2e/custom_props.rb +55 -0
- data/e2e/rox_e2e_test.rb +159 -0
- data/e2e/test_vars.rb +24 -0
- data/lib/rox.rb +5 -0
- data/lib/rox/core/client/buid.rb +82 -0
- data/lib/rox/core/client/device_properties.rb +45 -0
- data/lib/rox/core/client/internal_flags.rb +20 -0
- data/lib/rox/core/client/sdk_settings.rb +5 -0
- data/lib/rox/core/configuration/configuration.rb +5 -0
- data/lib/rox/core/configuration/configuration_fetched_args.rb +23 -0
- data/lib/rox/core/configuration/configuration_fetched_invoker.rb +37 -0
- data/lib/rox/core/configuration/configuration_parser.rb +85 -0
- data/lib/rox/core/configuration/fetcher_error.rb +13 -0
- data/lib/rox/core/configuration/fetcher_status.rb +10 -0
- data/lib/rox/core/configuration/models/experiment_model.rb +5 -0
- data/lib/rox/core/configuration/models/target_group_model.rb +5 -0
- data/lib/rox/core/consts/build.rb +8 -0
- data/lib/rox/core/consts/environment.rb +42 -0
- data/lib/rox/core/consts/property_type.rb +29 -0
- data/lib/rox/core/context/merged_context.rb +16 -0
- data/lib/rox/core/core.rb +131 -0
- data/lib/rox/core/entities/flag.rb +26 -0
- data/lib/rox/core/entities/flag_setter.rb +39 -0
- data/lib/rox/core/entities/variant.rb +56 -0
- data/lib/rox/core/impression/impression_args.rb +5 -0
- data/lib/rox/core/impression/impression_invoker.rb +41 -0
- data/lib/rox/core/impression/models/experiment.rb +14 -0
- data/lib/rox/core/impression/models/reporting_value.rb +5 -0
- data/lib/rox/core/logging/logging.rb +17 -0
- data/lib/rox/core/logging/no_op_logger.rb +11 -0
- data/lib/rox/core/network/configuration_fetch_result.rb +5 -0
- data/lib/rox/core/network/configuration_fetcher.rb +38 -0
- data/lib/rox/core/network/configuration_fetcher_base.rb +25 -0
- data/lib/rox/core/network/configuration_fetcher_roxy.rb +29 -0
- data/lib/rox/core/network/configuration_source.rb +9 -0
- data/lib/rox/core/network/request.rb +46 -0
- data/lib/rox/core/network/request_configuration_builder.rb +48 -0
- data/lib/rox/core/network/request_data.rb +5 -0
- data/lib/rox/core/network/response.rb +16 -0
- data/lib/rox/core/properties/custom_property.rb +18 -0
- data/lib/rox/core/properties/custom_property_type.rb +18 -0
- data/lib/rox/core/properties/device_property.rb +11 -0
- data/lib/rox/core/register/registerer.rb +35 -0
- data/lib/rox/core/reporting/error_reporter.rb +152 -0
- data/lib/rox/core/repositories/custom_property_repository.rb +61 -0
- data/lib/rox/core/repositories/experiment_repository.rb +21 -0
- data/lib/rox/core/repositories/flag_repository.rb +49 -0
- data/lib/rox/core/repositories/roxx/experiments_extensions.rb +82 -0
- data/lib/rox/core/repositories/roxx/properties_extensions.rb +26 -0
- data/lib/rox/core/repositories/target_group_repository.rb +17 -0
- data/lib/rox/core/roxx/core_stack.rb +22 -0
- data/lib/rox/core/roxx/evaluation_result.rb +28 -0
- data/lib/rox/core/roxx/node.rb +11 -0
- data/lib/rox/core/roxx/parser.rb +143 -0
- data/lib/rox/core/roxx/regular_expression_extensions.rb +33 -0
- data/lib/rox/core/roxx/string_tokenizer.rb +68 -0
- data/lib/rox/core/roxx/symbols.rb +14 -0
- data/lib/rox/core/roxx/token_type.rb +30 -0
- data/lib/rox/core/roxx/tokenized_expression.rb +119 -0
- data/lib/rox/core/roxx/value_compare_extensions.rb +137 -0
- data/lib/rox/core/security/signature_verifier.rb +18 -0
- data/lib/rox/core/utils/periodic_task.rb +12 -0
- data/lib/rox/core/utils/type_utils.rb +9 -0
- data/lib/rox/server/client/sdk_settings.rb +5 -0
- data/lib/rox/server/client/server_properties.rb +20 -0
- data/lib/rox/server/flags/rox_flag.rb +19 -0
- data/lib/rox/server/flags/rox_variant.rb +8 -0
- data/lib/rox/server/logging/server_logger.rb +35 -0
- data/lib/rox/server/rox_options.rb +27 -0
- data/lib/rox/server/rox_server.rb +83 -0
- data/lib/rox/version.rb +3 -0
- data/rox.gemspec +29 -0
- metadata +184 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'rox/core/entities/variant'
|
|
2
|
+
|
|
3
|
+
module Rox
|
|
4
|
+
module Core
|
|
5
|
+
class Flag < Variant
|
|
6
|
+
FLAG_TRUE_VALUE = 'true'.freeze
|
|
7
|
+
FLAG_FALSE_VALUE = 'false'.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(default_value = false)
|
|
10
|
+
super(default_value ? Flag::FLAG_TRUE_VALUE : Flag::FLAG_FALSE_VALUE, [Flag::FLAG_FALSE_VALUE, Flag::FLAG_TRUE_VALUE])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def enabled?(context)
|
|
14
|
+
value(context) == Flag::FLAG_TRUE_VALUE
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def enabled(context)
|
|
18
|
+
yield if enabled?(context)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def disabled(context)
|
|
22
|
+
yield unless enabled?(context)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Rox
|
|
2
|
+
module Core
|
|
3
|
+
class FlagSetter
|
|
4
|
+
def initialize(flag_repository, parser, experiment_repository, impression_invoker)
|
|
5
|
+
@flag_repository = flag_repository
|
|
6
|
+
@parser = parser
|
|
7
|
+
@experiment_repository = experiment_repository
|
|
8
|
+
@impression_invoker = impression_invoker
|
|
9
|
+
|
|
10
|
+
@flag_repository.register_flag_added_handler do |variant|
|
|
11
|
+
exp = @experiment_repository.experiment_by_flag(variant.name)
|
|
12
|
+
set_flag_data(variant, exp)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def set_experiments
|
|
17
|
+
flags_with_condition = []
|
|
18
|
+
|
|
19
|
+
@experiment_repository.all_experiments.each do |exp|
|
|
20
|
+
exp.flags.each do |flag_name|
|
|
21
|
+
flag = @flag_repository.flag(flag_name)
|
|
22
|
+
unless flag.nil?
|
|
23
|
+
set_flag_data(flag, exp)
|
|
24
|
+
flags_with_condition << flag_name
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@flag_repository.all_flags.each do |flag|
|
|
30
|
+
set_flag_data(flag) unless flags_with_condition.include?(flag.name)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def set_flag_data(variant, experiment = nil)
|
|
35
|
+
variant.set_for_evaluation(@parser, experiment, @impression_invoker)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require 'rox/core/context/merged_context'
|
|
2
|
+
require 'rox/core/impression/models/experiment'
|
|
3
|
+
require 'rox/core/impression/models/reporting_value'
|
|
4
|
+
|
|
5
|
+
module Rox
|
|
6
|
+
module Core
|
|
7
|
+
class Variant
|
|
8
|
+
attr_accessor :default_value, :options, :name, :context, :condition, :parser, :impression_invoker, :client_experiment
|
|
9
|
+
|
|
10
|
+
def initialize(default_value, options)
|
|
11
|
+
@default_value = default_value
|
|
12
|
+
@options = options.clone
|
|
13
|
+
@options << default_value unless options.include?(default_value)
|
|
14
|
+
|
|
15
|
+
@condition = nil
|
|
16
|
+
@parser = nil
|
|
17
|
+
@context = nil
|
|
18
|
+
@impression_invoker = nil
|
|
19
|
+
@client_experiment = nil
|
|
20
|
+
@name = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def set_for_evaluation(parser, experiment, impression_invoker)
|
|
24
|
+
if experiment.nil?
|
|
25
|
+
@client_experiment = nil
|
|
26
|
+
@condition = ''
|
|
27
|
+
else
|
|
28
|
+
@client_experiment = Experiment.new(experiment)
|
|
29
|
+
@condition = experiment.condition
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@parser = parser
|
|
33
|
+
@impression_invoker = impression_invoker
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def value(context = nil)
|
|
37
|
+
return_value = @default_value
|
|
38
|
+
merged_context = MergedContext.new(@context, context)
|
|
39
|
+
|
|
40
|
+
if !@parser.nil? && !@condition.nil? && !@condition.empty?
|
|
41
|
+
evaluation_result = @parser.evaluate_expression(@condition, merged_context)
|
|
42
|
+
unless evaluation_result.nil?
|
|
43
|
+
value = evaluation_result.string_value
|
|
44
|
+
if !value.nil? && !value.empty?
|
|
45
|
+
return_value = value if @options.include?(value)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@impression_invoker.invoke(ReportingValue.new(@name, return_value), @client_experiment, merged_context) if @impression_invoker != nil
|
|
51
|
+
|
|
52
|
+
return_value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'rox/core/impression/impression_args'
|
|
2
|
+
|
|
3
|
+
module Rox
|
|
4
|
+
module Core
|
|
5
|
+
class ImpressionInvoker
|
|
6
|
+
def initialize(internal_flags, custom_property_repository, device_properties, analytics_client, is_roxy)
|
|
7
|
+
@internal_flags = internal_flags
|
|
8
|
+
@custom_property_repository = custom_property_repository
|
|
9
|
+
@device_properties = device_properties
|
|
10
|
+
@analytics_client = analytics_client
|
|
11
|
+
@is_roxy = is_roxy
|
|
12
|
+
|
|
13
|
+
@impression_handlers = []
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def invoke(reporting_value, client_experiment, context)
|
|
18
|
+
# TODO: Implement analytics logic
|
|
19
|
+
|
|
20
|
+
raise_impression_event(ImpressionArgs.new(reporting_value, client_experiment, context))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def register_impression_handler(&block)
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
@impression_handlers << block
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def raise_impression_event(args)
|
|
30
|
+
handlers = []
|
|
31
|
+
@mutex.synchronize do
|
|
32
|
+
handlers = @impression_handlers.clone
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
handlers.each do |handler|
|
|
36
|
+
handler.call(args)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Rox
|
|
2
|
+
module Core
|
|
3
|
+
class Experiment
|
|
4
|
+
attr_accessor :name, :identifier, :is_archived, :labels
|
|
5
|
+
|
|
6
|
+
def initialize(experiment)
|
|
7
|
+
@name = experiment.name
|
|
8
|
+
@identifier = experiment.id
|
|
9
|
+
@is_archived = experiment.is_archived
|
|
10
|
+
@labels = experiment.labels
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'rox/core/network/configuration_source'
|
|
2
|
+
require 'rox/core/network/configuration_fetch_result'
|
|
3
|
+
require 'rox/core/network/configuration_fetcher_base'
|
|
4
|
+
|
|
5
|
+
module Rox
|
|
6
|
+
module Core
|
|
7
|
+
class ConfigurationFetcher < ConfigurationFetcherBase
|
|
8
|
+
def fetch
|
|
9
|
+
source = ConfigurationSource::CDN
|
|
10
|
+
begin
|
|
11
|
+
fetch_result = fetch_from_cdn
|
|
12
|
+
return ConfigurationFetchResult.new(fetch_result.text, source) if fetch_result.success_status_code?
|
|
13
|
+
|
|
14
|
+
if [403, 404].include?(fetch_result.status_code)
|
|
15
|
+
write_fetch_error_to_log_and_invoke_fetch_handler(source, fetch_result, false, ConfigurationSource::API)
|
|
16
|
+
source = ConfigurationSource::API
|
|
17
|
+
fetch_result = fetch_from_api
|
|
18
|
+
return ConfigurationFetchResult.new(fetch_result.text, source) if fetch_result.success_status_code?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
write_fetch_error_to_log_and_invoke_fetch_handler(source, fetch_result)
|
|
22
|
+
rescue StandardError => ex
|
|
23
|
+
write_fetch_exception_to_log_and_invoke_fetch_handler(source, ex)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def fetch_from_cdn
|
|
30
|
+
@request.send_get(@request_configuration_builder.build_for_cdn)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch_from_api
|
|
34
|
+
@request.send_get(@request_configuration_builder.build_for_api)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'rox/core/configuration/fetcher_error'
|
|
2
|
+
require 'rox/core/logging/logging'
|
|
3
|
+
|
|
4
|
+
module Rox
|
|
5
|
+
module Core
|
|
6
|
+
class ConfigurationFetcherBase
|
|
7
|
+
def initialize(request_configuration_builder, request, configuration_fetched_invoker)
|
|
8
|
+
@request_configuration_builder = request_configuration_builder
|
|
9
|
+
@request = request
|
|
10
|
+
@configuration_fetched_invoker = configuration_fetched_invoker
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def write_fetch_error_to_log_and_invoke_fetch_handler(source, response, raise_configuration_handler = true, next_source = nil)
|
|
14
|
+
retry_msg = next_source.nil? ? '' : "Trying from #{next_source}. "
|
|
15
|
+
Logging.logger.debug("Failed to fetch from #{source}. #{retry_msg}http error code: #{response.status_code}")
|
|
16
|
+
@configuration_fetched_invoker.invoke_error(FetcherError::NETWORK_ERROR) if raise_configuration_handler
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def write_fetch_exception_to_log_and_invoke_fetch_handler(source, ex)
|
|
20
|
+
Logging.logger.error("Failed to fetch configuration. Source: #{source}. Ex: #{ex}")
|
|
21
|
+
@configuration_fetched_invoker.invoke_error(FetcherError::NETWORK_ERROR)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'rox/core/network/configuration_source'
|
|
2
|
+
require 'rox/core/network/configuration_fetch_result'
|
|
3
|
+
require 'rox/core/network/configuration_fetcher_base'
|
|
4
|
+
|
|
5
|
+
module Rox
|
|
6
|
+
module Core
|
|
7
|
+
class ConfigurationFetcherRoxy < ConfigurationFetcherBase
|
|
8
|
+
def fetch
|
|
9
|
+
source = ConfigurationSource::ROXY
|
|
10
|
+
begin
|
|
11
|
+
fetch_roxy = fetch_from_roxy
|
|
12
|
+
if fetch_roxy.success_status_code?
|
|
13
|
+
return ConfigurationFetchResult.new(fetch_roxy.text, source)
|
|
14
|
+
else
|
|
15
|
+
write_fetch_error_to_log_and_invoke_fetch_handler(source, fetch_roxy)
|
|
16
|
+
end
|
|
17
|
+
rescue StandardError => ex
|
|
18
|
+
write_fetch_exception_to_log_and_invoke_fetch_handler(source, ex)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fetch_from_roxy
|
|
25
|
+
@request.send_get(@request_configuration_builder.build_for_roxy)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'rox/core/network/response'
|
|
4
|
+
|
|
5
|
+
module Rox
|
|
6
|
+
module Core
|
|
7
|
+
class Request
|
|
8
|
+
def send_get(request_data)
|
|
9
|
+
uri = URI(request_data.url)
|
|
10
|
+
uri.query = URI.encode_www_form(request_data.query_params)
|
|
11
|
+
req = Net::HTTP::Get.new(uri)
|
|
12
|
+
|
|
13
|
+
send(req, uri, nil)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def send_post(url, payload)
|
|
17
|
+
uri = URI(url)
|
|
18
|
+
req = Net::HTTP::Post.new(uri)
|
|
19
|
+
|
|
20
|
+
send(req, uri, payload)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def send(request, uri, payload)
|
|
24
|
+
request['Accept-Encoding'] = 'gzip'
|
|
25
|
+
|
|
26
|
+
unless payload.nil?
|
|
27
|
+
request['Content-Type'] = 'application/json'
|
|
28
|
+
request.body = JSON.dump(payload)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
resp = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == 'https') do |http|
|
|
32
|
+
http.request(request)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
resp_body = resp.body
|
|
36
|
+
if resp['Content-Encoding'].eql?('gzip')
|
|
37
|
+
sio = StringIO.new(resp.body)
|
|
38
|
+
gz = Zlib::GzipReader.new(sio)
|
|
39
|
+
resp_body = gz.read
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Response.new(resp.code.to_i, resp_body)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'rox/core/network/request_data'
|
|
2
|
+
require 'rox/core/consts/environment'
|
|
3
|
+
require 'rox/core/consts/property_type'
|
|
4
|
+
|
|
5
|
+
module Rox
|
|
6
|
+
module Core
|
|
7
|
+
class RequestConfigurationBuilder
|
|
8
|
+
def initialize(sdk_settings, buid, device_properties, roxy_url)
|
|
9
|
+
@sdk_settings = sdk_settings
|
|
10
|
+
@buid = buid
|
|
11
|
+
@device_properties = device_properties
|
|
12
|
+
@roxy_url = roxy_url
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_for_roxy
|
|
16
|
+
roxy_endpoint = URI.join(@roxy_url, Rox::Core::Environment.roxy_internal_path).to_s
|
|
17
|
+
build_request_with_full_params(roxy_endpoint)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build_for_cdn
|
|
21
|
+
RequestData.new("#{Rox::Core::Environment.cdn_path}/#{@buid.value}",
|
|
22
|
+
Rox::Core::PropertyType::DISTINCT_ID.name => @device_properties.distinct_id)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def build_for_api
|
|
26
|
+
build_request_with_full_params(Rox::Core::Environment.api_path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_request_with_full_params(url)
|
|
30
|
+
query_params = {}
|
|
31
|
+
|
|
32
|
+
@buid.query_string_parts.each do |key, value|
|
|
33
|
+
query_params[key] = value unless query_params.include?(key)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@device_properties.all_properties.each do |key, value|
|
|
37
|
+
query_params[key] = value unless query_params.include?(key)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
cdn_data = build_for_cdn
|
|
41
|
+
query_params[Rox::Core::PropertyType::CACHE_MISS_URL.name] = cdn_data.url
|
|
42
|
+
query_params['devModeSecret'] = @sdk_settings.dev_mode_secret
|
|
43
|
+
|
|
44
|
+
RequestData.new(url, query_params)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|