kameleoon-client-ruby 2.0.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kameleoon/client.rb +277 -260
  3. data/lib/kameleoon/configuration/feature_flag.rb +13 -24
  4. data/lib/kameleoon/configuration/rule.rb +17 -5
  5. data/lib/kameleoon/configuration/settings.rb +20 -0
  6. data/lib/kameleoon/cookie.rb +3 -3
  7. data/lib/kameleoon/data/browser.rb +33 -0
  8. data/lib/kameleoon/data/conversion.rb +26 -0
  9. data/lib/kameleoon/data/custom_data.rb +53 -0
  10. data/lib/kameleoon/data/data.rb +35 -0
  11. data/lib/kameleoon/data/device.rb +26 -0
  12. data/lib/kameleoon/data/page_view.rb +31 -0
  13. data/lib/kameleoon/data/user_agent.rb +14 -0
  14. data/lib/kameleoon/hybrid/manager.rb +60 -0
  15. data/lib/kameleoon/network/activity_event.rb +31 -0
  16. data/lib/kameleoon/network/experiment_event.rb +35 -0
  17. data/lib/kameleoon/network/uri_helper.rb +36 -0
  18. data/lib/kameleoon/network/url_provider.rb +71 -0
  19. data/lib/kameleoon/real_time/real_time_configuration_service.rb +98 -0
  20. data/lib/kameleoon/real_time/real_time_event.rb +22 -0
  21. data/lib/kameleoon/real_time/sse_client.rb +111 -0
  22. data/lib/kameleoon/real_time/sse_message.rb +23 -0
  23. data/lib/kameleoon/real_time/sse_request.rb +59 -0
  24. data/lib/kameleoon/request.rb +5 -19
  25. data/lib/kameleoon/storage/cache.rb +84 -0
  26. data/lib/kameleoon/storage/cache_factory.rb +23 -0
  27. data/lib/kameleoon/targeting/condition.rb +41 -12
  28. data/lib/kameleoon/targeting/condition_factory.rb +35 -12
  29. data/lib/kameleoon/targeting/conditions/browser_condition.rb +71 -0
  30. data/lib/kameleoon/targeting/conditions/conversion_condition.rb +21 -0
  31. data/lib/kameleoon/targeting/conditions/custom_datum.rb +64 -34
  32. data/lib/kameleoon/targeting/conditions/device_condition.rb +21 -0
  33. data/lib/kameleoon/targeting/conditions/exclusive_experiment.rb +0 -12
  34. data/lib/kameleoon/targeting/conditions/page_title_condition.rb +21 -0
  35. data/lib/kameleoon/targeting/conditions/page_url_condition.rb +21 -0
  36. data/lib/kameleoon/targeting/conditions/sdk_language_condition.rb +65 -0
  37. data/lib/kameleoon/targeting/conditions/string_value_condition.rb +40 -0
  38. data/lib/kameleoon/targeting/conditions/target_experiment.rb +5 -9
  39. data/lib/kameleoon/targeting/conditions/unknown_condition.rb +15 -0
  40. data/lib/kameleoon/targeting/conditions/visitor_code_condition.rb +16 -0
  41. data/lib/kameleoon/targeting/models.rb +0 -24
  42. data/lib/kameleoon/utils.rb +1 -1
  43. data/lib/kameleoon/version.rb +28 -1
  44. metadata +45 -4
  45. data/lib/kameleoon/configuration/feature_flag_v2.rb +0 -30
  46. data/lib/kameleoon/data.rb +0 -169
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Kameleoon Real Time Configuration Service
4
+
5
+ require 'json'
6
+ require 'kameleoon/real_time/real_time_event'
7
+ require 'kameleoon/real_time/sse_request'
8
+ require 'kameleoon/real_time/sse_client'
9
+
10
+ module Kameleoon
11
+ module RealTime
12
+ CONFIGURATION_UPDATE_EVENT = 'configuration-update-event'
13
+
14
+ ##
15
+ # RealTimeConfigurationService is used for fetching updates of configuration
16
+ # (experiments and feature flags) in real time.
17
+ class RealTimeConfigurationService
18
+ ##
19
+ # Parametrized initializer.
20
+ #
21
+ # @param url [String]
22
+ # @param update_handler [Callable[Kameleoon::RealTime::RealTimeEvent] | NilClass] Handler which
23
+ # is synchronously called for gotten RealTimeEvent objects.
24
+ # @param log_func [Callable[String] | NilClass] Callable object which synchronously called to log.
25
+ def initialize(url, update_handler, log_func, sse_request_source = nil)
26
+ @url = url
27
+ @update_handler = update_handler
28
+ @need_close = false
29
+ @headers = {
30
+ 'Accept': 'text/event-stream',
31
+ 'Cache-Control': 'no-cache',
32
+ 'Connection': 'Keep-Alive'
33
+ }
34
+ @log_func = log_func
35
+ @sse_request_source = sse_request_source
36
+ @sse_thread = nil
37
+ @sse_client = nil
38
+ create_sse_client
39
+ end
40
+
41
+ ##
42
+ # Closes the connection to the server.
43
+ def close
44
+ return if @need_close
45
+
46
+ @log_func&.call('Real-time configuration service is shutting down')
47
+ @need_close = true
48
+ return if @sse_thread.nil?
49
+
50
+ @sse_thread.kill
51
+ @sse_thread = nil
52
+ @sse_client.call_close_handler
53
+ end
54
+
55
+ private
56
+
57
+ def create_sse_client
58
+ @sse_thread = Thread.new { run_sse_client } unless @need_close
59
+ end
60
+
61
+ def init_sse_client
62
+ message_handler = proc do |message|
63
+ @log_func&.call("Got SSE event: #{message.event}")
64
+ if message.event == CONFIGURATION_UPDATE_EVENT
65
+ event_dict = JSON.parse(message.data)
66
+ @update_handler&.call(RealTimeEvent.new(event_dict))
67
+ end
68
+ end
69
+ sse_request = make_sse_request
70
+ @sse_client = SseClient.new(sse_request, message_handler)
71
+ @log_func&.call('Created SSE client')
72
+ end
73
+
74
+ def make_sse_request
75
+ open_handler = proc { @log_func&.call('SSE connection open') }
76
+ close_handler = proc { @log_func&.call('SSE connection closed') }
77
+ unexpected_status_code_handler =
78
+ proc { |resp| @log_func&.call("Unexpected status code of SSE response: #{resp.code}") }
79
+ if @sse_request_source.nil?
80
+ return SseRequest.new(@url, @headers, open_handler, close_handler, unexpected_status_code_handler)
81
+ end
82
+
83
+ @sse_request_source.call(@url, @headers, open_handler, close_handler, unexpected_status_code_handler)
84
+ end
85
+
86
+ def run_sse_client
87
+ until @need_close
88
+ init_sse_client
89
+ begin
90
+ @sse_client.start
91
+ rescue StandardError => e
92
+ @log_func&.call("Error occurred within SSE client: #{e}")
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Kameleoon Real Time Event
4
+
5
+ module Kameleoon
6
+ module RealTime
7
+ ##
8
+ # RealTimeEvent contains information about timestamp when configuration was updated.
9
+ # Timestamp parameter is used to fetch the latest configuration.
10
+ class RealTimeEvent
11
+ attr_reader :time_stamp
12
+
13
+ ##
14
+ # Parametrized initializer. Gets the new time stamp from a JSON message.
15
+ #
16
+ # @param event_dict [Hash] Hash of SSE message data.
17
+ def initialize(event_dict)
18
+ @time_stamp = event_dict['ts']
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/real_time/sse_message'
4
+
5
+ module Kameleoon
6
+ module RealTime
7
+ ##
8
+ # SseClient is used to interpret SSE event stream.
9
+ class SseClient
10
+ ##
11
+ # Parametrized initializer.
12
+ #
13
+ # @param sse_request [Kameleoon::RealTime::SseRequest] Used to access SSE event stream.
14
+ # @param message_handler [Callable[Kameleoon::RealTime::SseMessage] | NilClass] Callable object which
15
+ # is synchronously called for received SSE messages.
16
+ def initialize(sse_request, message_handler)
17
+ @sse_request = sse_request
18
+ @message_handler = message_handler
19
+
20
+ @cr_prev = nil
21
+ @buffer = nil
22
+ @data_buffer = nil
23
+ @event = nil
24
+ @id = nil
25
+ @reconnection_time = nil
26
+
27
+ @sse_request.resp_char_handler = method(:process_char)
28
+ end
29
+
30
+ ##
31
+ # Starts SSE connection and stay in the loop until close.
32
+ def start
33
+ @cr_prev = false
34
+ @data_buffer = []
35
+ @event = nil
36
+ @id = nil
37
+ @buffer = []
38
+ @sse_request.start
39
+ end
40
+
41
+ ##
42
+ # Calls @sse_request@close_handler if it is not nil.
43
+ def call_close_handler
44
+ @sse_request.call_close_handler
45
+ end
46
+
47
+ private
48
+
49
+ def process_char(ch)
50
+ if @cr_prev && (ch == "\n")
51
+ @cr_prev = false
52
+ return
53
+ end
54
+ @cr_prev = ch == "\r"
55
+ if @cr_prev || (ch == "\n")
56
+ line = @buffer.join
57
+ @buffer.clear
58
+ if line.empty?
59
+ dispatch_event
60
+ else
61
+ handle_line(line)
62
+ end
63
+ return
64
+ end
65
+ @buffer << ch
66
+ end
67
+
68
+ def handle_line(line)
69
+ field, value = parse_line(line)
70
+ return if field.nil?
71
+
72
+ case field
73
+ when 'event'
74
+ @event = value
75
+ when 'data'
76
+ @data_buffer << value
77
+ when 'id'
78
+ @id = value
79
+ when 'retry'
80
+ @reconnection_time = value.to_i # This does not affect anything
81
+ end
82
+ end
83
+
84
+ def parse_line(line)
85
+ colon_index = line.index(':')
86
+ return line, nil if colon_index.nil?
87
+ return nil, nil if colon_index.zero?
88
+
89
+ field = line[0, colon_index]
90
+ return field, '' if colon_index + 1 == line.length
91
+
92
+ value = if line[colon_index + 1] == ' '
93
+ line[colon_index + 2..]
94
+ else
95
+ line[colon_index + 1..]
96
+ end
97
+ [field, value]
98
+ end
99
+
100
+ def dispatch_event
101
+ return unless !@event.nil? || !@data_buffer.empty? # Ignoring empty events
102
+
103
+ data = @data_buffer.join("\n")
104
+ message = SseMessage.new(@event, @id, data)
105
+ @event = nil
106
+ @data_buffer.clear
107
+ @message_handler&.call(message)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module RealTime
5
+ ##
6
+ # SseMessage is a container for a received SSE message.
7
+ class SseMessage
8
+ attr_reader :event, :id, :data
9
+
10
+ ##
11
+ # Parametrized initializer.
12
+ #
13
+ # @param event [String]
14
+ # @param id [String]
15
+ # @param data [String]
16
+ def initialize(event, id, data)
17
+ @event = event
18
+ @id = id
19
+ @data = data
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'restclient'
4
+
5
+ module Kameleoon
6
+ module RealTime
7
+ ##
8
+ # SseRequest is used keep SSE connection and read from its stream.
9
+ class SseRequest
10
+ attr_writer :resp_char_handler
11
+
12
+ ##
13
+ # Parametrized initializer.
14
+ #
15
+ # @param url [String]
16
+ # @param headers [Hash]
17
+ # @param open_handler [Callable | NilClass] Handler which is synchronously called when SSE connection is open.
18
+ # @param close_handler [Callable | NilClass] Handler which is synchronously called when SSE connection is closed.
19
+ # @param unexpected_status_code_handler [Callable[Net::HTTPResponse] | NilClass] Handler whick
20
+ # is synchronously called for responses with not 200 status code.
21
+ def initialize(url, headers, open_handler, close_handler, unexpected_status_code_handler)
22
+ @url = url
23
+ @headers = headers
24
+ @open_handler = open_handler
25
+ @close_handler = close_handler
26
+ @unexpected_status_code_handler = unexpected_status_code_handler
27
+
28
+ @resp_char_handler = nil
29
+ end
30
+
31
+ ##
32
+ # Starts SSE connection and stay in the loop until close.
33
+ def start
34
+ RestClient::Request.execute(method: :get, url: @url, headers: @headers, read_timeout: nil,
35
+ block_response: method(:handle_resp))
36
+ end
37
+
38
+ ##
39
+ # Calls @close_handler if it is not nil.
40
+ def call_close_handler
41
+ @close_handler&.call
42
+ end
43
+
44
+ private
45
+
46
+ def handle_resp(resp)
47
+ @open_handler&.call
48
+ if resp.code == '200'
49
+ resp.read_body do |chunk|
50
+ chunk.each_char { |ch| @resp_char_handler&.call(ch) }
51
+ end
52
+ else
53
+ @unexpected_status_code_handler&.call(resp)
54
+ end
55
+ call_close_handler
56
+ end
57
+ end
58
+ end
59
+ end
@@ -7,27 +7,24 @@ module Kameleoon
7
7
  module Request
8
8
  protected
9
9
 
10
- API_URL = 'https://api.kameleoon.com'.freeze
11
- CLIENT_CONFIG_URL = 'https://client-config.kameleoon.com'.freeze
12
-
13
10
  module Method
14
11
  GET = 'get'.freeze
15
12
  POST = 'post'.freeze
16
13
  end
17
14
 
18
- def get(request_options, url = API_URL, connexion_options = {})
15
+ def get(request_options, url, connexion_options = {})
19
16
  request(Method::GET, request_options, url, connexion_options)
20
17
  end
21
18
 
22
- def post(request_options, url = API_URL, connexion_options = {})
19
+ def post(request_options, url, connexion_options = {})
23
20
  request(Method::POST, request_options, url, connexion_options)
24
21
  end
25
22
 
26
- def get_sync(url = API_URL, connexion_options = {})
23
+ def get_sync(url, connexion_options = {})
27
24
  request_sync(Method::GET, url, connexion_options, {})
28
25
  end
29
26
 
30
- def post_sync(request_options, url = API_URL, connexion_options = {})
27
+ def post_sync(request_options, url, connexion_options = {})
31
28
  request_sync(Method::POST, url, connexion_options, request_options)
32
29
  end
33
30
 
@@ -35,7 +32,6 @@ module Kameleoon
35
32
 
36
33
  def request(method, request_options, url, connexion_options)
37
34
  connexion_options[:tls] = { verify_peer: false }
38
- add_user_agent(request_options)
39
35
  case method
40
36
  when Method::POST then
41
37
  return EventMachine::HttpRequest.new(url, connexion_options).apost request_options
@@ -49,7 +45,6 @@ module Kameleoon
49
45
 
50
46
  def request_sync(method, url, connexion_options, request_options)
51
47
  request_options = {} if request_options.nil?
52
- add_user_agent(request_options)
53
48
  case method
54
49
  when Method::GET then
55
50
  uri = URI(url)
@@ -60,7 +55,7 @@ module Kameleoon
60
55
  uri = URI.parse(url)
61
56
  http = Net::HTTP.new(uri.host, uri.port)
62
57
  http.use_ssl = true
63
- request = Net::HTTP::Post.new(request_options[:path], initheader = request_options[:head])
58
+ request = Net::HTTP::Post.new(uri, initheader = request_options[:head])
64
59
  request.body = request_options[:body]
65
60
  response = http.request(request)
66
61
  response
@@ -77,14 +72,5 @@ module Kameleoon
77
72
  def successful_sync?(response)
78
73
  !response.nil? && response != false && response.is_a?(Net::HTTPSuccess)
79
74
  end
80
-
81
- def add_user_agent(request_options)
82
- sdk_version = "sdk/ruby/#{Kameleoon::VERSION}"
83
- if request_options[:head].nil?
84
- request_options[:head] = { 'Kameleoon-Client' => sdk_version }
85
- else
86
- request_options[:head].store('Kameleoon-Client', sdk_version)
87
- end
88
- end
89
75
  end
90
76
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+
5
+ module Kameleoon
6
+ module Storage
7
+ # Will be useful for Ruby 3.0
8
+ # Abstract Cache class (interface)
9
+ class Cache
10
+ def get(_key)
11
+ raise 'Abstract method `read` called'
12
+ end
13
+
14
+ def set(_key, _value)
15
+ raise 'Abstract method `write` called'
16
+ end
17
+
18
+ def active_items
19
+ raise 'Abstract method `active_items` called'
20
+ end
21
+ end
22
+
23
+ # Implementation of Cache with auto cleaning feature
24
+ class CacheImpl < Cache
25
+ def initialize(expiration_time, cleaning_interval)
26
+ super()
27
+ @mutex = Mutex.new
28
+ @expiration_time = expiration_time
29
+ @cleaning_interval = cleaning_interval
30
+ @cache = {}
31
+ end
32
+
33
+ def set(key, value)
34
+ @mutex.synchronize do
35
+ start_cleaner_timer if @cleaning_interval.positive? && @cleaner_timer.nil?
36
+ @cache[key] = { value: value, expired: Time.now + @expiration_time }
37
+ end
38
+ end
39
+
40
+ def get(key)
41
+ entry = @cache[key]
42
+ return entry[:value] unless entry.nil? || expired?(entry)
43
+
44
+ @mutex.synchronize { @cache.delete(key) }
45
+ nil
46
+ end
47
+
48
+ def active_items
49
+ active_items = {}
50
+ remove_expired_entries
51
+ @mutex.synchronize do
52
+ @cache.each_pair { |key, entry| active_items[key] = entry[:value] }
53
+ end
54
+ active_items
55
+ end
56
+
57
+ private
58
+
59
+ def start_cleaner_timer
60
+ @cleaner_timer = Concurrent::TimerTask.new(execution_interval: @cleaning_interval) do
61
+ remove_expired_entries
62
+ end
63
+ @cleaner_timer.execute
64
+ end
65
+
66
+ def stop_cleaner_timer
67
+ @cleaner_timer&.shutdown
68
+ @cleaner_timer = nil
69
+ end
70
+
71
+ def remove_expired_entries
72
+ time = Time.now
73
+ @mutex.synchronize do
74
+ @cache.delete_if { |_, entry| expired?(entry, time) }
75
+ stop_cleaner_timer if @cache.empty?
76
+ end
77
+ end
78
+
79
+ def expired?(entry, time = Time.now)
80
+ entry[:expired] <= time
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+ require_relative 'cache'
5
+
6
+ module Kameleoon
7
+ module Storage
8
+ # Will be useful for Ruby 3.0
9
+ # Abstract CacheFactory class (interface)
10
+ class CacheFactory
11
+ def create(_experiration_time, _cleaning_time)
12
+ raise 'Abstract method `create` called'
13
+ end
14
+ end
15
+
16
+ # Implementation of CacheFactory with auto cleaning feature
17
+ class CacheFactoryImpl < CacheFactory
18
+ def create(expiration_time, cleaning_interval)
19
+ CacheImpl.new(expiration_time, cleaning_interval)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,28 +1,57 @@
1
1
  # frozen_string_literal: true
2
+ require 'kameleoon/exceptions'
2
3
 
3
4
  module Kameleoon
4
5
  # @api private
5
6
  module Targeting
7
+ module ConditionType
8
+ CUSTOM_DATUM = 'CUSTOM_DATUM'
9
+ TARGET_EXPERIMENT = 'TARGET_EXPERIMENT'
10
+ EXCLUSIVE_EXPERIMENT = 'EXCLUSIVE_EXPERIMENT'
11
+ PAGE_URL = 'PAGE_URL'
12
+ PAGE_TITLE = 'PAGE_TITLE'
13
+ VISITOR_CODE = 'VISITOR_CODE'
14
+ DEVICE_TYPE = 'DEVICE_TYPE'
15
+ BROWSER = 'BROWSER'
16
+ SDK_LANGUAGE = 'SDK_LANGUAGE'
17
+ CONVERSIONS = 'CONVERSIONS'
18
+ end
19
+
20
+ module Operator
21
+ UNDEFINED = 'UNDEFINED'
22
+ CONTAINS = 'CONTAINS'
23
+ EXACT = 'EXACT'
24
+ REGULAR_EXPRESSION = 'REGULAR_EXPRESSION'
25
+ LOWER = 'LOWER'
26
+ EQUAL = 'EQUAL'
27
+ GREATER = 'GREATER'
28
+ IS_TRUE = 'TRUE'
29
+ IS_FALSE = 'FALSE'
30
+ AMONG_VALUES = 'AMONG_VALUES'
31
+ ANY = 'ANY'
32
+ UNKNOWN = 'UNKNOWN'
33
+ end
34
+
35
+ # Base class for all targeting conditions
6
36
  class Condition
7
- attr_accessor :type, :include
37
+ attr_reader :type, :include
8
38
 
9
39
  def initialize(json_condition)
10
- if json_condition['targetingType'].nil?
11
- raise Exception::NotFoundError.new('targetingType'), 'targetingType missed'
12
- end
40
+ raise Exception::NotFound.new('targetingType'), 'targetingType missed' if json_condition['targetingType'].nil?
13
41
 
14
42
  @type = json_condition['targetingType']
43
+ @include = json_condition['isInclude'].nil? ? true : json_condition['isInclude']
44
+ end
15
45
 
16
- if json_condition['include'].nil? && json_condition['isInclude'].nil?
17
- raise Exception::NotFoundError.new('include / isInclude missed'), 'include / isInclude missed'
18
- end
19
-
20
- @include = json_condition['include'] || json_condition['isInclude']
46
+ def check(_data)
47
+ raise 'Abstract method `check` call'
21
48
  end
22
49
 
23
- def check(conditions)
24
- raise "Todo: Implement check method in condition"
50
+ protected
51
+
52
+ def get_last_targeting_data(list_data, data_type)
53
+ list_data.select { |data| data.instance == data_type }.last
25
54
  end
26
55
  end
27
56
  end
28
- end
57
+ end
@@ -1,22 +1,45 @@
1
- require 'kameleoon/targeting/conditions/custom_datum'
2
- require 'kameleoon/targeting/conditions/target_experiment'
3
- require 'kameleoon/targeting/conditions/exclusive_experiment'
1
+ require_relative 'conditions/custom_datum'
2
+ require_relative 'conditions/target_experiment'
3
+ require_relative 'conditions/exclusive_experiment'
4
+ require_relative 'conditions/page_title_condition'
5
+ require_relative 'conditions/page_url_condition'
6
+ require_relative 'conditions/visitor_code_condition'
7
+ require_relative 'conditions/device_condition'
8
+ require_relative 'conditions/conversion_condition'
9
+ require_relative 'conditions/browser_condition'
10
+ require_relative 'conditions/sdk_language_condition'
11
+ require_relative 'conditions/unknown_condition'
4
12
 
5
13
  module Kameleoon
6
- #@api private
14
+ # @api private
7
15
  module Targeting
16
+ # Module for create different targeting conditions
8
17
  module ConditionFactory
9
18
  def get_condition(condition_json)
10
- condition = nil
11
19
  case condition_json['targetingType']
12
- when ConditionType::CUSTOM_DATUM.to_s
13
- condition = CustomDatum.new(condition_json)
14
- when ConditionType::TARGET_EXPERIMENT.to_s
15
- condition = TargetExperiment.new(condition_json)
16
- when ConditionType::EXCLUSIVE_EXPERIMENT.to_s
17
- condition = ExclusiveExperiment.new(condition_json)
20
+ when ConditionType::CUSTOM_DATUM
21
+ CustomDatum.new(condition_json)
22
+ when ConditionType::TARGET_EXPERIMENT
23
+ TargetExperiment.new(condition_json)
24
+ when ConditionType::EXCLUSIVE_EXPERIMENT
25
+ ExclusiveExperiment.new(condition_json)
26
+ when ConditionType::PAGE_URL
27
+ PageUrlCondition.new(condition_json)
28
+ when ConditionType::PAGE_TITLE
29
+ PageTitleCondition.new(condition_json)
30
+ when ConditionType::VISITOR_CODE
31
+ VisitorCodeCondition.new(condition_json)
32
+ when ConditionType::DEVICE_TYPE
33
+ DeviceCondition.new(condition_json)
34
+ when ConditionType::CONVERSIONS
35
+ ConversionCondition.new(condition_json)
36
+ when ConditionType::BROWSER
37
+ BrowserCondition.new(condition_json)
38
+ when ConditionType::SDK_LANGUAGE
39
+ SdkLanguageCondition.new(condition_json)
40
+ else
41
+ UnknownCondition.new(condition_json)
18
42
  end
19
- condition
20
43
  end
21
44
  end
22
45
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/data/browser'
4
+ require 'kameleoon/version'
5
+
6
+ module Kameleoon
7
+ # @api private
8
+ module Targeting
9
+
10
+ CHROME = 'CHROME'
11
+ INTERNET_EXPLORER = 'IE'
12
+ FIREFOX = 'FIREFOX'
13
+ SAFARI = 'SAFARI'
14
+ OPERA = 'OPERA'
15
+
16
+ # BrowserCondition is a condition for checking targeting with browser's type and version
17
+ class BrowserCondition < Condition
18
+ def initialize(json_condition)
19
+ super(json_condition)
20
+ @browser_type = browser_index_from_name(json_condition['browser'])
21
+ @version = json_condition['version']
22
+ @version_match_type = json_condition['versionMatchType']
23
+ end
24
+
25
+ def check(list_data)
26
+ browser = get_last_targeting_data(list_data, Kameleoon::DataType::BROWSER)
27
+ browser && check_targeting(browser)
28
+ end
29
+
30
+ private
31
+
32
+ def check_targeting(browser)
33
+ return false if @browser_type != browser.type
34
+
35
+ return true if @version.nil?
36
+
37
+ version_number = SdkVersion.get_float_version(@version)
38
+ return false if version_number.nan?
39
+
40
+ case @version_match_type
41
+ when Operator::EQUAL
42
+ browser.version == version_number
43
+ when Operator::GREATER
44
+ browser.version > version_number
45
+ when Operator::LOWER
46
+ browser.version < version_number
47
+ else
48
+ puts "Unexpected comparing operation for Browser condition: #{@version_match_type}"
49
+ false
50
+ end
51
+ end
52
+
53
+ def browser_index_from_name(name)
54
+ case name
55
+ when CHROME
56
+ BrowserType::CHROME
57
+ when INTERNET_EXPLORER
58
+ BrowserType::INTERNET_EXPLORER
59
+ when FIREFOX
60
+ BrowserType::FIREFOX
61
+ when SAFARI
62
+ BrowserType::SAFARI
63
+ when OPERA
64
+ BrowserType::OPERA
65
+ else
66
+ BrowserType::OTHER
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end