diff --git a/Gemfile.lock b/Gemfile.lock index c4ac2b6..b844b11 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - absmartly-sdk (1.2.1) + absmartly-sdk (1.2.2) base64 (~> 0.2) faraday (~> 2.0) faraday-retry (~> 2.0) diff --git a/lib/a_b_smartly.rb b/lib/a_b_smartly.rb index 60474db..6328eda 100644 --- a/lib/a_b_smartly.rb +++ b/lib/a_b_smartly.rb @@ -1,51 +1,92 @@ # frozen_string_literal: true require "time" -require "singleton" -require "forwardable" require_relative "context" require_relative "audience_matcher" -require_relative "a_b_smartly_config" -require_relative "absmartly/version" +require_relative "default_context_data_provider" +require_relative "default_context_event_handler" +require_relative "default_variable_parser" +require_relative "default_audience_deserializer" +require_relative "scheduled_thread_pool_executor" class ABSmartly - extend Forwardable + attr_accessor :context_data_provider, :context_event_handler, + :variable_parser, :scheduler, :context_event_logger, + :audience_deserializer, :client - attr_reader :config - - def_delegators :@config, :context_data_provider, :context_event_handler, :variable_parser, :context_event_logger, - :audience_deserializer, :client - - def_delegators :@config, :endpoint, :api_key, :application, :environment + def self.configure_client(&block) + @@init_http = block + end def self.create(config) - new(config) + ABSmartly.new(config) end def initialize(config) - config.validate! + @@init_http = nil + @context_data_provider = config.context_data_provider + @context_event_handler = config.context_event_handler + @context_event_logger = config.context_event_logger + @variable_parser = config.variable_parser + @audience_deserializer = config.audience_deserializer + @scheduler = config.scheduler + + if @context_data_provider.nil? || @context_event_handler.nil? + @client = config.client + raise ArgumentError.new("Missing Client instance configuration") if @client.nil? + + if @context_data_provider.nil? + @context_data_provider = DefaultContextDataProvider.new(@client) + end + + if @context_event_handler.nil? + @context_event_handler = DefaultContextEventHandler.new(@client) + end + end - @config = config + if @variable_parser.nil? + @variable_parser = DefaultVariableParser.new + end + + if @audience_deserializer.nil? + @audience_deserializer = DefaultAudienceDeserializer.new + end + if @scheduler.nil? + @scheduler = ScheduledThreadPoolExecutor.new(1) + end end - def create_context(context_config) - Context.create(get_utc_format, context_config, context_data, - context_data_provider, context_event_handler, context_event_logger, variable_parser, - AudienceMatcher.new(audience_deserializer)) + def create_context(config) + validate_params(config) + Context.create(get_utc_format, config, @context_data_provider.context_data, + @context_data_provider, @context_event_handler, @context_event_logger, @variable_parser, + AudienceMatcher.new(@audience_deserializer)) end - def create_context_with(context_config, data) - Context.create(get_utc_format, context_config, data, - context_data_provider, context_event_handler, context_event_logger, variable_parser, - AudienceMatcher.new(audience_deserializer)) + def create_context_with(config, data) + Context.create(get_utc_format, config, data, + @context_data_provider, @context_event_handler, @context_event_logger, @variable_parser, + AudienceMatcher.new(@audience_deserializer)) end def context_data - context_data_provider.context_data + @context_data_provider.context_data end private def get_utc_format Time.now.utc.iso8601(3) end + + def validate_params(params) + params.units.each do |key, value| + unless value.is_a?(String) || value.is_a?(Numeric) + raise ArgumentError.new("Unit '#{key}' UID is of unsupported type '#{value.class}'. UID must be one of ['string', 'number']") + end + + if value.to_s.size.zero? + raise ArgumentError.new("Unit '#{key}' UID length must be >= 1") + end + end + end end diff --git a/lib/a_b_smartly_config.rb b/lib/a_b_smartly_config.rb index 581659f..42a4034 100644 --- a/lib/a_b_smartly_config.rb +++ b/lib/a_b_smartly_config.rb @@ -1,65 +1,49 @@ # frozen_string_literal: true -require "forwardable" - -require_relative "client" -require_relative "client_config" -require_relative "default_context_data_provider" -require_relative "default_context_event_handler" -require_relative "default_variable_parser" -require_relative "default_audience_deserializer" - class ABSmartlyConfig - extend Forwardable - - attr_accessor :scheduler - - attr_writer :context_data_provider, :context_event_handler, :audience_deserializer, :variable_parser, :client - - attr_reader :client_config, :context_event_logger - - def_delegators :@client_config, :endpoint, :api_key, :application, :environment - def_delegators :@client_config, :connect_timeout, :connection_request_timeout, :retry_interval, :max_retries - + attr_accessor :context_data_provider, :context_event_handler, + :variable_parser, :scheduler, :context_event_logger, + :client, :audience_deserializer def self.create - new + ABSmartlyConfig.new end - def initialize - @client_config = ClientConfig.new + def context_data_provider=(context_data_provider) + @context_data_provider = context_data_provider + self end - def validate! - raise ArgumentError.new("event logger not configured") if context_event_logger.nil? - raise ArgumentError.new("failed to initialize client") if client.nil? - raise ArgumentError.new("failed to initialize context_data_provider") if context_data_provider.nil? + def context_event_handler=(context_event_handler) + @context_event_handler = context_event_handler + self end - def context_event_logger=(context_event_logger) - if context_event_logger.is_a?(Proc) - @context_event_logger = ContextEventLoggerCallback.new(context_event_logger) - else - @context_event_logger = context_event_logger - end + def context_data_provide + @context_event_handler end - def variable_parser - @variable_parser ||= DefaultVariableParser.new + def variable_parser=(variable_parser) + @variable_parser = variable_parser + self end - def audience_deserializer - @audience_deserializer ||= DefaultAudienceDeserializer.new + def scheduler=(scheduler) + @scheduler = scheduler + self end - def context_data_provider - @context_data_provider ||= DefaultContextDataProvider.new(client) + def context_event_logger=(context_event_logger) + @context_event_logger = context_event_logger + self end - def context_event_handler - @context_event_handler ||= DefaultContextEventHandler.new(client) + def audience_deserializer=(audience_deserializer) + @audience_deserializer = audience_deserializer + self end - def client - @client ||= Client.new(client_config) + def client=(client) + @client = client + self end end diff --git a/lib/absmartly.rb b/lib/absmartly.rb index 1fe3680..c2febb2 100644 --- a/lib/absmartly.rb +++ b/lib/absmartly.rb @@ -8,16 +8,16 @@ require_relative "context_config" module Absmartly + @@init_config = nil + class Error < StandardError end class << self - MUTEX = Thread::Mutex.new + attr_accessor :endpoint, :api_key, :application, :environment def configure_client - yield sdk_config - - sdk_config.validate! + yield self end def create @@ -40,15 +40,24 @@ def context_data sdk.context_data end - private_constant :MUTEX - private + def client_config + @client_config = ClientConfig.create + @client_config.endpoint = @endpoint + @client_config.api_key = @api_key + @client_config.application = @application + @client_config.environment = @environment + @client_config + end + def sdk_config - MUTEX.synchronize { @sdk_config ||= ABSmartlyConfig.create } + @sdk_config = ABSmartlyConfig.create + @sdk_config.client = Client.create(client_config) + @sdk_config end def sdk - MUTEX.synchronize { @sdk ||= create } + @sdk ||= create end end end diff --git a/lib/absmartly/version.rb b/lib/absmartly/version.rb index d2c762f..56b13dc 100644 --- a/lib/absmartly/version.rb +++ b/lib/absmartly/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Absmartly - VERSION = "1.2.1" + VERSION = "1.2.2" end diff --git a/lib/client.rb b/lib/client.rb index 79723b1..ea6ed7c 100644 --- a/lib/client.rb +++ b/lib/client.rb @@ -1,49 +1,80 @@ # frozen_string_literal: true -require "forwardable" require_relative "default_http_client" require_relative "default_http_client_config" require_relative "default_context_data_deserializer" require_relative "default_context_event_serializer" class Client - extend Forwardable + attr_accessor :url, :query, :headers, :http_client, :executor, :deserializer, :serializer + attr_reader :data_future, :promise, :exception - attr_accessor :http_client - attr_reader :config, :data_future, :promise, :exception - - def_delegators :@config, :url, :query, :headers, :deserializer, :serializer - def_delegator :@http_client, :close - def_delegator :@promise, :success? - - def self.create(config = nil, http_client = nil) - new(config, http_client) + def self.create(config, http_client = nil) + Client.new(config, http_client || DefaultHttpClient.create(DefaultHttpClientConfig.create)) end def initialize(config = nil, http_client = nil) - @config = config || ClientConfig.new - @config.validate! + endpoint = config.endpoint + raise ArgumentError.new("Missing Endpoint configuration") if endpoint.nil? || endpoint.empty? + + api_key = config.api_key + raise ArgumentError.new("Missing APIKey configuration") if api_key.nil? || api_key.empty? + + application = config.application + raise ArgumentError.new("Missing Application configuration") if application.nil? || application.empty? + + environment = config.environment + raise ArgumentError.new("Missing Environment configuration") if environment.nil? || environment.empty? + + @url = "#{endpoint}/context" + @http_client = http_client + @deserializer = config.context_data_deserializer + @serializer = config.context_event_serializer + @executor = config.executor - @http_client = http_client || DefaultHttpClient.create(@config.http_client_config) + @deserializer = DefaultContextDataDeserializer.new if @deserializer.nil? + @serializer = DefaultContextEventSerializer.new if @serializer.nil? + + @headers = { + "Content-Type": "application/json", + "X-API-Key": api_key, + "X-Application": application, + "X-Environment": environment, + "X-Application-Version": "0", + "X-Agent": "absmartly-ruby-sdk" + } + + @query = { + "application": application, + "environment": environment + } end def context_data - @promise = http_client.get(config.url, config.query, config.headers) + @promise = @http_client.get(@url, @query, @headers) unless @promise.success? @exception = Exception.new(@promise.body) return self end content = (@promise.body || {}).to_s - @data_future = deserializer.deserialize(content, 0, content.size) + @data_future = @deserializer.deserialize(content, 0, content.size) self end def publish(event) - content = serializer.serialize(event) - response = http_client.put(config.url, nil, config.headers, content) + content = @serializer.serialize(event) + response = @http_client.put(@url, nil, @headers, content) return Exception.new(response.body) unless response.success? response end + + def close + @http_client.close + end + + def success? + @promise.success? + end end diff --git a/lib/client_config.rb b/lib/client_config.rb index 0b6437c..7434da5 100644 --- a/lib/client_config.rb +++ b/lib/client_config.rb @@ -1,33 +1,21 @@ # frozen_string_literal: true -require "forwardable" -require_relative "default_context_data_deserializer" -require_relative "default_context_event_serializer" -require_relative "default_http_client_config" - class ClientConfig - extend Forwardable - - attr_accessor :endpoint, :api_key, :environment, :application - - attr_reader :http_client_config - - attr_writer :context_data_deserializer, :context_event_serializer - - def_delegators :@http_client_config, :connect_timeout, :connection_request_timeout, :retry_interval, :max_retries + attr_accessor :endpoint, :api_key, :environment, :application, :deserializer, + :serializer, :executor - def self.create(endpoint: nil, environment: nil, application: nil, api_key: nil) - new(endpoint: endpoint, environment: environment, application: application, api_key: api_key) + def self.create + ClientConfig.new end def self.create_from_properties(properties, prefix) properties = properties.transform_keys(&:to_sym) - create( - endpoint: properties["#{prefix}endpoint".to_sym], - environment: properties["#{prefix}environment".to_sym], - application: properties["#{prefix}application".to_sym], - api_key: properties["#{prefix}apikey".to_sym] - ) + client_config = create + client_config.endpoint = properties["#{prefix}endpoint".to_sym] + client_config.environment = properties["#{prefix}environment".to_sym] + client_config.application = properties["#{prefix}application".to_sym] + client_config.api_key = properties["#{prefix}apikey".to_sym] + client_config end def initialize(endpoint: nil, environment: nil, application: nil, api_key: nil) @@ -35,60 +23,21 @@ def initialize(endpoint: nil, environment: nil, application: nil, api_key: nil) @environment = environment @application = application @api_key = api_key - - @http_client_config = DefaultHttpClientConfig.new end def context_data_deserializer - @context_data_deserializer ||= DefaultContextDataDeserializer.new - end - - def context_event_serializer - @context_event_serializer ||= DefaultContextEventSerializer.new - end - - def deserializer=(deserializer) - @context_data_deserializer = deserializer - end - - def serializer=(serializer) - @context_event_serializer = serializer - end - - def deserializer - context_data_deserializer + @deserializer end - def serializer - context_event_serializer + def context_data_deserializer=(deserializer) + @deserializer = deserializer end - def url - @url ||= "#{endpoint}/context" - end - - def headers - @headers ||= { - "Content-Type": "application/json", - "X-API-Key": api_key, - "X-Application": application, - "X-Environment": environment, - "X-Application-Version": "0", - "X-Agent": "absmartly-ruby-sdk" - } - end - - def query - @query ||= { - "application": application, - "environment": environment - } + def context_event_serializer + @serializer end - def validate! - raise ArgumentError.new("Missing Endpoint configuration") if endpoint.nil? || endpoint.empty? - raise ArgumentError.new("Missing APIKey configuration") if api_key.nil? || api_key.empty? - raise ArgumentError.new("Missing Application configuration") if application.nil? || application.empty? - raise ArgumentError.new("Missing Environment configuration") if environment.nil? || environment.empty? + def context_event_serializer=(serializer) + @serializer = serializer end end diff --git a/lib/context.rb b/lib/context.rb index 397abdb..964065d 100644 --- a/lib/context.rb +++ b/lib/context.rb @@ -10,7 +10,7 @@ require_relative "json/goal_achievement" class Context - attr_reader :pending_count + attr_reader :data, :pending_count def self.create(clock, config, data_future, data_provider, event_handler, event_logger, variable_parser, audience_matcher) @@ -114,10 +114,6 @@ def custom_assignment(experiment_name) def set_unit(unit_type, uid) check_not_closed? - unless uid.is_a?(String) || uid.is_a?(Numeric) - raise IllegalStateException.new("Unit '#{unit_type}' UID is of unsupported type '#{uid.class}'. UID must be one of ['string', 'number']") - end - previous = @units[unit_type.to_sym] if !previous.nil? && previous != uid raise IllegalStateException.new("Unit '#{unit_type}' already set.") @@ -545,6 +541,7 @@ def assign_data(data) @experimentCustomFieldValues[custom_field_value.name] = value end + end end diff --git a/lib/context_config.rb b/lib/context_config.rb index 0cb7fe7..9c036c6 100644 --- a/lib/context_config.rb +++ b/lib/context_config.rb @@ -69,8 +69,11 @@ def custom_assignment(experiment_name) @custom_assignments[experiment_name.to_sym] end + def set_event_logger(event_logger) @event_logger = event_logger self end + + attr_reader :event_logger end diff --git a/lib/default_http_client.rb b/lib/default_http_client.rb index ca777a3..eea0895 100644 --- a/lib/default_http_client.rb +++ b/lib/default_http_client.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "faraday" -require "faraday/retry" +require 'faraday/retry' require "uri" require_relative "http_client" diff --git a/spec/a_b_smartly_spec.rb b/spec/a_b_smartly_spec.rb index 634001a..23a8ca9 100644 --- a/spec/a_b_smartly_spec.rb +++ b/spec/a_b_smartly_spec.rb @@ -17,7 +17,6 @@ it ".create" do config = ABSmartlyConfig.create config.client = client - config.context_event_logger = ContextEventLogger.new absmartly = described_class.create(config) expect(absmartly).not_to be_nil end @@ -26,7 +25,7 @@ expect { config = ABSmartlyConfig.create ABSmartly.create(config) - }.to raise_error(ArgumentError, "event logger not configured") + }.to raise_error(ArgumentError, "Missing Client instance configuration") end it ".create_context" do diff --git a/spec/context_spec.rb b/spec/context_spec.rb index 268076c..c5a7d91 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -736,7 +736,7 @@ def faraday_response(content) expect(context.custom_field_value("exp_test_ab", "country")).to eq("US,PT,ES,DE,FR") expect(context.custom_field_type("exp_test_ab", "country")).to eq("string") - data = { "123": 1, "456": 0 } + data = {"123": 1, "456": 0} expect(context.custom_field_value("exp_test_ab", "overrides")).to eq(data) expect(context.custom_field_type("exp_test_ab", "overrides")).to eq("json") @@ -757,6 +757,7 @@ def faraday_response(content) expect(context.custom_field_type("exp_test_no_custom_fields", "languages")).to be_nil expect(context.custom_field_value("exp_test_no_custom_fields", "languages")).to be_nil + end it "peek_treatmentReturnsOverrideVariant" do @@ -1148,7 +1149,7 @@ class MockContextEventLoggerProxy < ContextEventLogger def initialize @called = 0 @events = [] - @logger = Logger.new(IO::NULL) + @logger = Logger.new(STDOUT) end def handle_event(event, data) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3b55452..4b08390 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,7 +2,6 @@ require "absmartly" require "helpers" -require "ostruct" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure