kameleoon-client-ruby 2.0.0 → 2.1.1

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.
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