diff --git a/.gitignore b/.gitignore index 2cee92b..adfdcfc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ .rspec_status .byebug_history -.gemspec \ No newline at end of file +.gemspec +vendor/bundle diff --git a/Gemfile.lock b/Gemfile.lock index 22334b1..0fbad35 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - absmartly-sdk (1.3.0) + absmartly-sdk (2.0.0) base64 (~> 0.2) faraday (~> 2.0) faraday-net_http_persistent (~> 2.0) diff --git a/REFACTOR_PLAN.md b/REFACTOR_PLAN.md new file mode 100644 index 0000000..94c3742 --- /dev/null +++ b/REFACTOR_PLAN.md @@ -0,0 +1,250 @@ +# Refactoring Plan: Namespace all classes under `Absmartly` module + +## Problem + +All 55+ classes in the `absmartly-sdk` gem are defined at the **top level** with generic names like `Client`, `Context`, `Unit`, `Attribute`, etc. This pollutes the global Ruby namespace and causes collisions with consuming applications (e.g., `Client` conflicts with `module Client` in host apps using Zeitwerk autoloading). + +This violates the standard Ruby gem convention: all code should be wrapped under a namespace module matching the gem name. + +## Goal + +Wrap all top-level classes under the existing `module Absmartly` and restructure the file layout to follow the `lib//` convention. + +## Namespace choice + +Use the existing `module Absmartly` (already defined in `lib/absmartly.rb` and `lib/absmartly/version.rb`). Do **not** use `ABSmartly` — that is a separate class that will become `Absmartly::ABSmartly`. + +## Scope + +- **59 lib files** to wrap/move +- **32 spec files** to update references +- **1 example file** to update references + +--- + +## Phase 1 — Move files into `lib/absmartly/` and wrap in `module Absmartly` + +The standard convention is that directory structure mirrors module nesting. All files (except the entry point `lib/absmartly.rb` and the existing `lib/absmartly/version.rb`) should move into `lib/absmartly/`. + +### File moves + +``` +# Before # After +lib/a_b_smartly.rb → lib/absmartly/a_b_smartly.rb +lib/a_b_smartly_config.rb → lib/absmartly/a_b_smartly_config.rb +lib/audience_deserializer.rb → lib/absmartly/audience_deserializer.rb +lib/audience_matcher.rb → lib/absmartly/audience_matcher.rb +lib/client.rb → lib/absmartly/client.rb +lib/client_config.rb → lib/absmartly/client_config.rb +lib/context.rb → lib/absmartly/context.rb +lib/context_config.rb → lib/absmartly/context_config.rb +lib/context_data_deserializer.rb → lib/absmartly/context_data_deserializer.rb +lib/context_data_provider.rb → lib/absmartly/context_data_provider.rb +lib/context_event_handler.rb → lib/absmartly/context_event_handler.rb +lib/context_event_logger.rb → lib/absmartly/context_event_logger.rb +lib/context_event_logger_callback.rb → lib/absmartly/context_event_logger_callback.rb +lib/context_event_serializer.rb → lib/absmartly/context_event_serializer.rb +lib/default_audience_deserializer.rb → lib/absmartly/default_audience_deserializer.rb +lib/default_context_data_deserializer.rb → lib/absmartly/default_context_data_deserializer.rb +lib/default_context_data_provider.rb → lib/absmartly/default_context_data_provider.rb +lib/default_context_event_handler.rb → lib/absmartly/default_context_event_handler.rb +lib/default_context_event_serializer.rb → lib/absmartly/default_context_event_serializer.rb +lib/default_http_client.rb → lib/absmartly/default_http_client.rb +lib/default_http_client_config.rb → lib/absmartly/default_http_client_config.rb +lib/default_variable_parser.rb → lib/absmartly/default_variable_parser.rb +lib/hashing.rb → lib/absmartly/hashing.rb +lib/http_client.rb → lib/absmartly/http_client.rb +lib/scheduled_executor_service.rb → lib/absmartly/scheduled_executor_service.rb +lib/scheduled_thread_pool_executor.rb → lib/absmartly/scheduled_thread_pool_executor.rb +lib/string.rb → lib/absmartly/string.rb (see Phase 2 note) +lib/variable_parser.rb → lib/absmartly/variable_parser.rb +lib/variant_assigner.rb → lib/absmartly/variant_assigner.rb + +lib/json/attribute.rb → lib/absmartly/json/attribute.rb +lib/json/context_data.rb → lib/absmartly/json/context_data.rb +lib/json/custom_field_value.rb → lib/absmartly/json/custom_field_value.rb +lib/json/experiment.rb → lib/absmartly/json/experiment.rb +lib/json/experiment_application.rb → lib/absmartly/json/experiment_application.rb +lib/json/experiment_variant.rb → lib/absmartly/json/experiment_variant.rb +lib/json/exposure.rb → lib/absmartly/json/exposure.rb +lib/json/goal_achievement.rb → lib/absmartly/json/goal_achievement.rb +lib/json/publish_event.rb → lib/absmartly/json/publish_event.rb +lib/json/unit.rb → lib/absmartly/json/unit.rb + +lib/json_expr/evaluator.rb → lib/absmartly/json_expr/evaluator.rb +lib/json_expr/expr_evaluator.rb → lib/absmartly/json_expr/expr_evaluator.rb +lib/json_expr/json_expr.rb → lib/absmartly/json_expr/json_expr.rb +lib/json_expr/operator.rb → lib/absmartly/json_expr/operator.rb + +lib/json_expr/operators/and_combinator.rb → lib/absmartly/json_expr/operators/and_combinator.rb +lib/json_expr/operators/binary_operator.rb → lib/absmartly/json_expr/operators/binary_operator.rb +lib/json_expr/operators/boolean_combinator.rb → lib/absmartly/json_expr/operators/boolean_combinator.rb +lib/json_expr/operators/equals_operator.rb → lib/absmartly/json_expr/operators/equals_operator.rb +lib/json_expr/operators/greater_than_operator.rb → lib/absmartly/json_expr/operators/greater_than_operator.rb +lib/json_expr/operators/greater_than_or_equal_operator.rb → lib/absmartly/json_expr/operators/greater_than_or_equal_operator.rb +lib/json_expr/operators/in_operator.rb → lib/absmartly/json_expr/operators/in_operator.rb +lib/json_expr/operators/less_than_operator.rb → lib/absmartly/json_expr/operators/less_than_operator.rb +lib/json_expr/operators/less_than_or_equal_operator.rb → lib/absmartly/json_expr/operators/less_than_or_equal_operator.rb +lib/json_expr/operators/match_operator.rb → lib/absmartly/json_expr/operators/match_operator.rb +lib/json_expr/operators/nil_operator.rb → lib/absmartly/json_expr/operators/nil_operator.rb +lib/json_expr/operators/not_operator.rb → lib/absmartly/json_expr/operators/not_operator.rb +lib/json_expr/operators/or_combinator.rb → lib/absmartly/json_expr/operators/or_combinator.rb +lib/json_expr/operators/unary_operator.rb → lib/absmartly/json_expr/operators/unary_operator.rb +lib/json_expr/operators/value_operator.rb → lib/absmartly/json_expr/operators/value_operator.rb +lib/json_expr/operators/var_operator.rb → lib/absmartly/json_expr/operators/var_operator.rb +``` + +### Code change per file + +For every moved file, wrap the class/module definition inside `module Absmartly`: + +```ruby +# Before (lib/client.rb) +class Client + ... +end + +# After (lib/absmartly/client.rb) +module Absmartly + class Client + ... + end +end +``` + +Inheritance declarations (e.g., `class DefaultHttpClient < HttpClient`) resolve automatically since both classes are now inside the same module. + +### Entry point update (`lib/absmartly.rb`) + +Update all `require_relative` paths: + +```ruby +# Before +require_relative "a_b_smartly" +require_relative "client" +require_relative "client_config" + +# After +require_relative "absmartly/a_b_smartly" +require_relative "absmartly/client" +require_relative "absmartly/client_config" +``` + +Internal `require_relative` calls between files within `lib/absmartly/` stay the same (they're at the same depth relative to each other). + +### Move rationale + +- **Convention**: Every well-structured gem puts code under `lib//`. Developers expect this layout. +- **Avoids shadowing**: The current `lib/json/` directory shadows Ruby's stdlib `json`. Moving to `lib/absmartly/json/` eliminates that risk. +- **Clear ownership**: Makes it unambiguous which files belong to this gem. + +--- + +## Phase 2 — Do NOT namespace monkey-patches + +`lib/string.rb` monkey-patches Ruby's core `String` class (adds a `compare_to` method). This is intentional and cannot be namespaced. Move the file to `lib/absmartly/string.rb` but do **not** wrap `class String` in `module Absmartly`. + +Same for the `Array` monkey-patch inside `lib/json_expr/expr_evaluator.rb` — leave it as-is. + +--- + +## Phase 3 — Update cross-references within `lib/` + +After wrapping, verify these specific cases: + +1. **Inheritance declarations** — e.g., `class DefaultHttpClient < HttpClient` will resolve correctly when both are inside `module Absmartly`. No changes needed. + +2. **`require_relative` statements in `lib/absmartly.rb`** — Update paths from `require_relative "client"` to `require_relative "absmartly/client"`. + +3. **`require_relative` statements between `lib/absmartly/` files** — These should remain unchanged since relative paths between sibling files are the same. + +4. **Explicit class references** — Search all `lib/` files for bare class references like `Client.new(...)`, `ContextConfig.new`, `ABSmartly.new(...)`. Inside the `module Absmartly` wrapper they resolve correctly. Verify carefully. + +5. **`lib/absmartly.rb` main entry point** — This file already defines `module Absmartly` and references classes like `ABSmartly`, `ABSmartlyConfig`, `Client`, `ClientConfig`, `ContextConfig`. After the refactor, these references are **inside** the module and resolve correctly. Verify carefully. + +6. **`lib/json_expr/json_expr.rb` uses `require` (not `require_relative`)** for operators — update these paths if needed after the move. + +--- + +## Phase 4 — Update all specs + +For every spec file, update class references to use fully qualified namespaced names: + +```ruby +# Before +RSpec.describe Client do + subject { Client.new(...) } +end + +# After +RSpec.describe Absmartly::Client do + subject { Absmartly::Client.new(...) } +end +``` + +### Spec files to update (30 files) + +- `spec/a_b_smartly_config_spec.rb` — `ABSmartlyConfig` → `Absmartly::ABSmartlyConfig` +- `spec/a_b_smartly_spec.rb` — `ABSmartly` → `Absmartly::ABSmartly` +- `spec/absmartly_spec.rb` — already uses `Absmartly`, but check internal references +- `spec/audience_matcher_spec.rb` — `AudienceMatcher` → `Absmartly::AudienceMatcher` +- `spec/client_config_spec.rb` — `ClientConfig` → `Absmartly::ClientConfig` +- `spec/client_spec.rb` — `Client` → `Absmartly::Client` +- `spec/context_config_spec.rb` — `ContextConfig` → `Absmartly::ContextConfig` +- `spec/context_spec.rb` — `Context` → `Absmartly::Context` (also update inner class refs like `Assignment`, `ExperimentVariables`, etc.) +- `spec/default_audience_deserializer_spec.rb` — update refs +- `spec/default_context_data_deserializer_spec.rb` — update refs +- `spec/default_http_client_config_spec.rb` — update refs +- `spec/default_http_client_spec.rb` — update refs +- `spec/default_variable_parser_spec.rb` — update refs +- `spec/hashing_spec.rb` — update refs +- `spec/variant_assigner_spec.rb` — update refs +- `spec/json_expr/expr_evaluator_spec.rb` — update refs +- `spec/json_expr/json_expr_spec.rb` — update refs +- All 13 `spec/json_expr/operators/*_spec.rb` — update refs + +--- + +## Phase 5 — Update `example/example.rb` + +The example file uses `Absmartly.configure_client`, `Absmartly.create_context`, etc. These methods are defined on `module Absmartly` in `lib/absmartly.rb` and should continue to work. + +Check for direct references to unnamespaced classes like `ContextConfig.new` and update to `Absmartly::ContextConfig.new`. + +--- + +## Phase 6 — Backward compatibility aliases (optional) + +If backward compatibility is needed (minor/patch release), add a deprecation shim in `lib/absmartly.rb`: + +```ruby +# Backward compatibility — deprecated, will be removed in next major version +Client = Absmartly::Client unless defined?(Client) +Context = Absmartly::Context unless defined?(Context) +# ... etc for all previously top-level classes +``` + +This is **optional** and can be skipped if this is a major version bump. + +--- + +## Phase 7 — Version bump + +Update `lib/absmartly/version.rb` from `1.3.0` to `2.0.0` (this is a breaking change in the public API). + +--- + +## Phase 8 — Verify + +1. Run `bundle exec rspec` — all specs must pass +2. Add a spec that checks `Object.const_defined?(:Client)` returns `false` to verify no top-level constants are leaked +3. Test integration with a consuming application (e.g., cw-mailer) to confirm the namespace conflict is resolved + +--- + +## Known issues to flag (not in scope for this refactor) + +- `ScheduledThreadPoolExecutor < AudienceDeserializer` — suspicious inheritance, likely a copy-paste bug +- `String#compare_to` monkey-patch — should ideally be a refinement or static method +- `Array` monkey-patch in `expr_evaluator.rb` — same concern +- `lib/json/` directory name previously shadowed Ruby's stdlib `json` (resolved by the move to `lib/absmartly/json/`) diff --git a/lib/a_b_smartly.rb b/lib/a_b_smartly.rb deleted file mode 100644 index 2ac57c8..0000000 --- a/lib/a_b_smartly.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require "time" -require_relative "context" -require_relative "audience_matcher" -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 - attr_accessor :context_data_provider, :context_event_handler, - :variable_parser, :scheduler, :context_event_logger, - :audience_deserializer, :client - - def self.create(config) - ABSmartly.new(config) - end - - def initialize(config) - @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 - - 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(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(config, data) - validate_params(config) - 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 - 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 deleted file mode 100644 index 9e04760..0000000 --- a/lib/a_b_smartly_config.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -class ABSmartlyConfig - attr_accessor :context_data_provider, :context_event_handler, - :variable_parser, :scheduler, :context_event_logger, - :client, :audience_deserializer - def self.create - ABSmartlyConfig.new - end - - def context_data_provider=(context_data_provider) - @context_data_provider = context_data_provider - self - end - - def context_event_handler=(context_event_handler) - @context_event_handler = context_event_handler - self - end - - def variable_parser=(variable_parser) - @variable_parser = variable_parser - self - end - - def scheduler=(scheduler) - @scheduler = scheduler - self - end - - def context_event_logger=(context_event_logger) - @context_event_logger = context_event_logger - self - end - - def audience_deserializer=(audience_deserializer) - @audience_deserializer = audience_deserializer - self - end - - def client=(client) - @client = client - self - end -end diff --git a/lib/absmartly.rb b/lib/absmartly.rb index 2b37374..bb4aa89 100644 --- a/lib/absmartly.rb +++ b/lib/absmartly.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true require_relative "absmartly/version" -require_relative "a_b_smartly" -require_relative "a_b_smartly_config" -require_relative "client" -require_relative "client_config" -require_relative "context_config" +require_relative "absmartly/a_b_smartly" +require_relative "absmartly/a_b_smartly_config" +require_relative "absmartly/client" +require_relative "absmartly/client_config" +require_relative "absmartly/context_config" module Absmartly class Error < StandardError diff --git a/lib/absmartly/a_b_smartly.rb b/lib/absmartly/a_b_smartly.rb new file mode 100644 index 0000000..5b58a69 --- /dev/null +++ b/lib/absmartly/a_b_smartly.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require "time" +require_relative "context" +require_relative "audience_matcher" +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" + +module Absmartly + class ABSmartly + attr_accessor :context_data_provider, :context_event_handler, + :variable_parser, :scheduler, :context_event_logger, + :audience_deserializer, :client + + def self.create(config) + ABSmartly.new(config) + end + + def initialize(config) + @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 + + 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(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(config, data) + validate_params(config) + 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 + 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 +end diff --git a/lib/absmartly/a_b_smartly_config.rb b/lib/absmartly/a_b_smartly_config.rb new file mode 100644 index 0000000..9b1729c --- /dev/null +++ b/lib/absmartly/a_b_smartly_config.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Absmartly + class ABSmartlyConfig + attr_accessor :context_data_provider, :context_event_handler, + :variable_parser, :scheduler, :context_event_logger, + :client, :audience_deserializer + + def self.create + ABSmartlyConfig.new + end + + def context_data_provider=(context_data_provider) + @context_data_provider = context_data_provider + self + end + + def context_event_handler=(context_event_handler) + @context_event_handler = context_event_handler + self + end + + def variable_parser=(variable_parser) + @variable_parser = variable_parser + self + end + + def scheduler=(scheduler) + @scheduler = scheduler + self + end + + def context_event_logger=(context_event_logger) + @context_event_logger = context_event_logger + self + end + + def audience_deserializer=(audience_deserializer) + @audience_deserializer = audience_deserializer + self + end + + def client=(client) + @client = client + self + end + end +end diff --git a/lib/absmartly/audience_deserializer.rb b/lib/absmartly/audience_deserializer.rb new file mode 100644 index 0000000..04c4df2 --- /dev/null +++ b/lib/absmartly/audience_deserializer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Absmartly + class AudienceDeserializer + # @interface method + def deserialize(bytes, offset, length) + raise NotImplementedError.new("You must implement deserialize method.") + end + end +end diff --git a/lib/absmartly/audience_matcher.rb b/lib/absmartly/audience_matcher.rb new file mode 100644 index 0000000..db1fb54 --- /dev/null +++ b/lib/absmartly/audience_matcher.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "json" +require_relative "json_expr/json_expr" + +module Absmartly + class AudienceMatcher + attr_accessor :deserializer, :json_expr + + def initialize(deserializer) + @deserializer = deserializer + @json_expr = JsonExpr.new + end + + class Result + attr_accessor :result + + def initialize(result) + @result = result + end + + def get + @result + end + end + + def evaluate(audience, attributes) + audience_map = JSON.parse(audience, symbolize_names: true) + + unless audience_map.nil? + filter = audience_map[:filter] + if filter.is_a?(Hash) || filter.is_a?(Array) + Result.new(@json_expr.evaluate_boolean_expr(filter, attributes)) + end + end + rescue + nil + end + end +end diff --git a/lib/absmartly/client.rb b/lib/absmartly/client.rb new file mode 100644 index 0000000..3fbf4bd --- /dev/null +++ b/lib/absmartly/client.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require_relative "default_http_client" +require_relative "default_context_data_deserializer" +require_relative "default_context_event_serializer" + +module Absmartly + class Client + attr_accessor :url, :query, :headers, :http_client, :executor, :deserializer, :serializer + attr_reader :data_future, :promise, :exception + + def self.create(config, http_client = nil) + Client.new(config, http_client || DefaultHttpClient.create(config.http_client_config)) + end + + def initialize(config, http_client = nil) + 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 + + @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(@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) + self + end + + def publish(event) + 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? || false + end + end +end diff --git a/lib/absmartly/client_config.rb b/lib/absmartly/client_config.rb new file mode 100644 index 0000000..a3e2136 --- /dev/null +++ b/lib/absmartly/client_config.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative "default_http_client_config" + +module Absmartly + class ClientConfig + attr_accessor :endpoint, :api_key, :environment, :application, :deserializer, + :serializer, :executor, :connect_timeout, :connection_request_timeout, + :retry_interval, :max_retries + + def self.create + ClientConfig.new + end + + def self.create_from_properties(properties, prefix) + properties = properties.transform_keys(&: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) + @endpoint = endpoint + @environment = environment + @application = application + @api_key = api_key + end + + def context_data_deserializer + @deserializer + end + + def context_data_deserializer=(deserializer) + @deserializer = deserializer + end + + def context_event_serializer + @serializer + end + + def context_event_serializer=(serializer) + @serializer = serializer + end + + def http_client_config + http_config = DefaultHttpClientConfig.create + http_config.connect_timeout = @connect_timeout unless @connect_timeout.nil? + http_config.connection_request_timeout = @connection_request_timeout unless @connection_request_timeout.nil? + http_config.retry_interval = @retry_interval unless @retry_interval.nil? + http_config.max_retries = @max_retries unless @max_retries.nil? + http_config + end + end +end diff --git a/lib/absmartly/context.rb b/lib/absmartly/context.rb new file mode 100644 index 0000000..fef2655 --- /dev/null +++ b/lib/absmartly/context.rb @@ -0,0 +1,661 @@ +# frozen_string_literal: true + +require_relative "hashing" +require_relative "variant_assigner" +require_relative "context_event_logger" +require_relative "json/unit" +require_relative "json/attribute" +require_relative "json/exposure" +require_relative "json/publish_event" +require_relative "json/goal_achievement" + +module Absmartly + class Context + attr_reader :data, :pending_count + + def self.create(clock, config, data_future, data_provider, + event_handler, event_logger, variable_parser, audience_matcher) + Context.new(clock, config, data_future, data_provider, + event_handler, event_logger, variable_parser, audience_matcher) + end + + def initialize(clock, config, data_future, data_provider, + event_handler, event_logger, variable_parser, audience_matcher) + @index = [] + @context_custom_fields = {} + @achievements = [] + @assignment_cache = {} + @assignments = {} + @clock = clock.is_a?(String) ? Time.iso8601(clock) : clock + @publish_delay = config.publish_delay + @refresh_interval = config.refresh_interval + @event_handler = event_handler + @event_logger = !config.event_logger.nil? ? config.event_logger : event_logger + @data_provider = data_provider + @variable_parser = variable_parser + @audience_matcher = audience_matcher + @closed = false + + @units = {} + @attributes = [] + @overrides = {} + @cassignments = {} + @assigners = {} + @hashed_units = {} + @pending_count = 0 + @exposures ||= [] + @attrs_seq = 0 + + set_units(config.units) if config.units + set_attributes(config.attributes) if config.attributes + set_overrides(config.overrides) if config.overrides + set_custom_assignments(config.custom_assignments) if config.custom_assignments + if data_future.success? + assign_data(data_future.data_future) + log_event(ContextEventLogger::EVENT_TYPE::READY, data_future.data_future) + else + set_data_failed(data_future.exception) + log_error(data_future.exception) + end + end + + def ready? + !@data.nil? + end + + def failed? + @failed + end + + def closed? + @closed + end + + def experiments + check_ready?(true) + + @data.experiments.map(&:name) + end + + def set_override(experiment_name, variant) + check_not_closed? + + @overrides[experiment_name.to_s.to_sym] = variant + end + + def set_overrides(overrides) + check_not_closed? + + @overrides.merge!(overrides.transform_keys(&:to_sym)) + end + + def override(experiment_name) + check_not_closed? + + @overrides[experiment_name.to_s.to_sym] + end + + def set_custom_assignment(experiment_name, variant) + check_not_closed? + + @cassignments[experiment_name.to_s.to_sym] = variant + end + + def set_custom_assignments(custom_assignments) + check_not_closed? + + @cassignments.merge!(custom_assignments.transform_keys(&:to_sym)) + end + + def custom_assignment(experiment_name) + check_not_closed? + + @cassignments[experiment_name.to_s.to_sym] + end + + def set_unit(unit_type, uid) + check_not_closed? + + previous = @units[unit_type.to_sym] + if !previous.nil? && previous != uid + raise IllegalStateException.new("Unit '#{unit_type}' already set.") + end + + trimmed = uid.to_s.strip + if trimmed.empty? + raise IllegalStateException.new("Unit '#{unit_type}' UID must not be blank.") + end + + @units[unit_type] = trimmed + end + + def set_units(units) + check_not_closed? + + units.each { |key, value| self.set_unit(key, value) } + end + + def set_attribute(name, value) + check_not_closed? + + @attributes.push(Attribute.new(name, value, @clock.to_i)) + @attrs_seq += 1 + end + + def set_attributes(attributes) + check_not_closed? + + attributes.each { |key, value| self.set_attribute(key, value) } + end + + def treatment(experiment_name) + check_ready?(true) + assignment = assignment(experiment_name) + unless assignment.exposed + queue_exposure(assignment) + end + + assignment.variant + end + + def queue_exposure(assignment) + unless assignment.exposed + assignment.exposed = true + + exposure = Exposure.new + exposure.id = assignment.id || 0 + exposure.name = assignment.name + exposure.unit = assignment.unit_type + exposure.variant = assignment.variant + exposure.exposed_at = @clock.to_i + exposure.assigned = assignment.assigned + exposure.eligible = assignment.eligible + exposure.overridden = assignment.overridden + exposure.full_on = assignment.full_on + exposure.custom = assignment.custom + exposure.audience_mismatch = assignment.audience_mismatch + + @pending_count += 1 + @exposures.push(exposure) + log_event(ContextEventLogger::EVENT_TYPE::EXPOSURE, exposure) + end + end + + def peek_treatment(experiment_name) + check_ready?(true) + + assignment(experiment_name).variant + end + + alias peek peek_treatment + + def variable_keys + check_ready?(true) + + hsh = {} + @index_variables.each { |key, value| hsh[key] = value.data.name } + hsh + end + + def variable_value(key, default_value) + check_ready?(true) + + assignment = variable_assignment(key) + unless assignment.nil? || assignment.variables.nil? + queue_exposure(assignment) unless assignment.exposed + return assignment.variables[key.to_s.to_sym] if assignment.variables.key?(key.to_s.to_sym) + end + + default_value + end + + def custom_field_keys + check_ready?(true) + keys = [] + + @data.experiments.each do |experiment| + custom_field_values = experiment.custom_field_values + if custom_field_values != nil + custom_field_values.each do |custom_field| + keys.append(custom_field.name) + end + end + end + + return keys.sort.uniq + end + + def custom_field_value(experimentName, key) + check_ready?(true) + + experiment_custom_fields = @context_custom_fields[experimentName] + + if experiment_custom_fields != nil + field = experiment_custom_fields[key] + if field != nil + return field.value + end + end + + return nil + end + + def custom_field_type(experimentName, key) + check_ready?(true) + + experiment_custom_fields = @context_custom_fields[experimentName] + + if experiment_custom_fields != nil + field = experiment_custom_fields[key] + if field != nil + return field.type + end + end + + return nil + end + + def peek_variable_value(key, default_value) + check_ready?(true) + + assignment = variable_assignment(key) + return assignment.variables[key.to_s.to_sym] if !assignment.nil? && + !assignment.variables.nil? && + assignment.variables.key?(key.to_s.to_sym) + + default_value + end + + def track(goal_name, properties) + check_not_closed? + + achievement = GoalAchievement.new + achievement.achieved_at = @clock.to_i + achievement.name = goal_name + achievement.properties = properties + + @pending_count += 1 + @achievements.push(achievement) + log_event(ContextEventLogger::EVENT_TYPE::GOAL, achievement) + end + + def publish + check_not_closed? + + flush + end + + def refresh + check_not_closed? + + unless @failed + data_future = @data_provider.context_data + if data_future.success? + assign_data(data_future.data_future) + log_event(ContextEventLogger::EVENT_TYPE::REFRESH, data_future.data_future) + else + set_data_failed(data_future.exception) + log_error(data_future.exception) + end + end + end + + def close + unless @closed + if @pending_count > 0 + flush + end + @closed = true + log_event(ContextEventLogger::EVENT_TYPE::CLOSE, nil) + end + end + + def data + check_ready?(true) + + @data + end + + private + def flush + if !@failed + if @pending_count > 0 + exposures = nil + achievements = nil + event_count = @pending_count + + if event_count > 0 + unless @exposures.empty? + exposures = @exposures + @exposures = [] + end + + unless @achievements.empty? + achievements = @achievements + @achievements = [] + end + + @pending_count = 0 + + event = PublishEvent.new + event.hashed = true + event.published_at = @clock.to_i + event.units = @units.map do |key, value| + Unit.new(key.to_s, unit_hash(key, value)) + end + event.exposures = exposures + event.attributes = @attributes unless @attributes.empty? + event.goals = achievements unless achievements.nil? + log_event(ContextEventLogger::EVENT_TYPE::PUBLISH, event) + @event_handler.publish(self, event) + end + end + else + @exposures = [] + @achievements = [] + @pending_count = 0 + @data_failed + end + end + + def check_not_closed? + if @closed + raise IllegalStateException.new("ABSmartly Context is closed") + end + end + + def check_ready?(expect_not_closed) + if !ready? + raise IllegalStateException.new("ABSmartly Context is not yet ready") + elsif expect_not_closed + check_not_closed? + end + end + + def experiment_matches(experiment, assignment) + experiment.id == assignment.id && + experiment.unit_type == assignment.unit_type && + experiment.iteration == assignment.iteration && + experiment.full_on_variant == assignment.full_on_variant && + experiment.traffic_split == assignment.traffic_split + end + + def audience_matches(experiment, assignment) + if !experiment.audience.nil? && experiment.audience.size > 0 + if @attrs_seq > (assignment.attrs_seq || 0) + attrs = @attributes.inject({}) do |hash, attr| + hash[attr.name] = attr.value + hash + end + match = @audience_matcher.evaluate(experiment.audience, attrs) + new_audience_mismatch = match && !match.result + + if new_audience_mismatch != assignment.audience_mismatch + return false + end + + assignment.attrs_seq = @attrs_seq + end + end + true + end + + def assignment(experiment_name) + assignment = @assignment_cache[experiment_name.to_s] + + if !assignment.nil? + custom = @cassignments.transform_keys(&:to_sym)[experiment_name.to_s.to_sym] + override = @overrides.transform_keys(&:to_sym)[experiment_name.to_s.to_sym] + experiment = experiment(experiment_name.to_s) + if !override.nil? + if assignment.overridden && assignment.variant == override + return assignment + end + elsif experiment.nil? + if !assignment.assigned + return assignment + end + elsif custom.nil? || custom == assignment.variant + if experiment_matches(experiment.data, assignment) && audience_matches(experiment.data, assignment) + return assignment + end + end + end + + custom = @cassignments.transform_keys(&:to_sym)[experiment_name.to_s.to_sym] + override = @overrides.transform_keys(&:to_sym)[experiment_name.to_s.to_sym] + experiment = experiment(experiment_name.to_s) + + assignment = Assignment.new + assignment.name = experiment_name + assignment.eligible = true + + if !override.nil? + unless experiment.nil? + assignment.id = experiment.data.id + assignment.unit_type = experiment.data.unit_type + end + + assignment.overridden = true + assignment.variant = override + else + unless experiment.nil? + unit_type = experiment.data.unit_type + + if !experiment.data.audience.nil? && experiment.data.audience.size > 0 + attrs = @attributes.inject({}) do |hash, attr| + hash[attr.name] = attr.value + hash + end + match = @audience_matcher.evaluate(experiment.data.audience, attrs) + if match && !match.result + assignment.audience_mismatch = true + end + end + + if experiment.data.audience_strict && assignment.audience_mismatch + assignment.variant = 0 + elsif experiment.data.full_on_variant == 0 + uid = @units.transform_keys(&:to_sym)[experiment.data.unit_type.to_sym] + unless uid.nil? + assigner = VariantAssigner.new(uid) + eligible = assigner.assign(experiment.data.traffic_split, + experiment.data.traffic_seed_hi, + experiment.data.traffic_seed_lo) == 1 + if eligible + if !custom.nil? + assignment.variant = custom + assignment.custom = true + else + assignment.variant = assigner.assign(experiment.data.split, + experiment.data.seed_hi, + experiment.data.seed_lo) + end + else + assignment.eligible = false + assignment.variant = 0 + end + assignment.assigned = true + end + else + assignment.assigned = true + assignment.variant = experiment.data.full_on_variant + assignment.full_on = true + end + + assignment.unit_type = unit_type + assignment.id = experiment.data.id + assignment.iteration = experiment.data.iteration + assignment.traffic_split = experiment.data.traffic_split + assignment.full_on_variant = experiment.data.full_on_variant + assignment.attrs_seq = @attrs_seq + end + end + + if !experiment.nil? && assignment.variant >= 0 && assignment.variant < experiment.data.variants.length + assignment.variables = experiment.variables[assignment.variant] || {} + end + + @assignment_cache[experiment_name.to_s] = assignment + assignment + end + + def variable_assignment(key) + experiment = variable_experiment(key) + + assignment(experiment.data.name) unless experiment.nil? + end + + def experiment(experiment) + @index.transform_keys(&:to_sym)[experiment.to_s.to_sym] + end + + def variable_experiment(key) + @index_variables.transform_keys(&:to_sym)[key.to_s.to_sym] + end + + def unit_hash(unit_type, unit_uid) + @hashed_units[unit_type] = Hashing.hash_unit(unit_uid) + end + + def variant_assigner(unit_type, unit_hash) + @assigners[unit_type] ||= VariantAssigner.new(unit_hash) + end + + def assign_data(data) + @data = data + @index = {} + @index_variables = {} + + if data && !data.experiments.nil? && !data.experiments.empty? + data.experiments.each do |experiment| + @experimentCustomFieldValues = {} + + experiment_variables = ExperimentVariables.new + experiment_variables.data = experiment + experiment_variables.variables ||= [] + experiment.variants.each do |variant| + if !variant.config.nil? && !variant.config.empty? + variables = @variable_parser.parse(self, experiment.name, variant.name, + variant.config) + variables.keys.each { |key| @index_variables[key] = experiment_variables } + experiment_variables.variables.push(variables) + else + experiment_variables.variables.push({}) + end + end + + if !experiment.custom_field_values.nil? + experiment.custom_field_values.each do |custom_field_value| + value = ContextCustomFieldValues.new + value.type = custom_field_value.type + + if !custom_field_value.value.nil? + custom_value = custom_field_value.value + + if custom_field_value.type.start_with?("json") + value.value = @variable_parser.parse(self, experiment.name, custom_field_value.name, custom_value) + + elsif custom_field_value.type.start_with?("boolean") + value.value = custom_value == "true" + + elsif custom_field_value.type.start_with?("number") + value.value = custom_value.to_i + + else + value.value = custom_field_value.value + end + + @experimentCustomFieldValues[custom_field_value.name] = value + + end + + end + end + + @index[experiment.name] = experiment_variables + @context_custom_fields[experiment.name] = @experimentCustomFieldValues + end + end + end + + def set_data_failed(exception) + @data_failed = exception + @index = {} + @index_variables = {} + @data = ContextData.new + @failed = true + end + + def log_event(event, data) + unless @event_logger.nil? + @event_logger.handle_event(event, data) + end + end + + def log_error(error) + unless @event_logger.nil? + @event_logger.handle_event(ContextEventLogger::EVENT_TYPE::ERROR, error.message) + end + end + + attr_accessor :clock, + :publish_delay, + :event_handler, + :event_logger, + :data_provider, + :variable_parser, + :audience_matcher, + :units, + :failed, + :data_lock, + :index, + :index_variables, + :context_lock, + :hashed_units, + :assigners, + :assignment_cache, + :event_lock, + :exposures, + :achievements, + :attributes, + :overrides, + :cassignments, + :closed, + :refreshing, + :ready_future, + :refresh_future + attr_writer :data, :pending_count + end + + class Assignment + attr_accessor :id, :iteration, :full_on_variant, :name, :unit_type, + :traffic_split, :variant, :assigned, :overridden, :eligible, + :full_on, :custom, :audience_mismatch, :variables, :exposed, :attrs_seq + + def initialize + @variant = 0 + @iteration = 0 + @full_on_variant = 0 + @overridden = false + @assigned = false + @exposed = false + @eligible = true + @full_on = false + @custom = false + @audience_mismatch = false + @attrs_seq = 0 + end + end + + class ExperimentVariables + attr_accessor :data, :variables + end + + class ContextCustomFieldValues + attr_accessor :type, :value + end + + class IllegalStateException < StandardError + end +end diff --git a/lib/absmartly/context_config.rb b/lib/absmartly/context_config.rb new file mode 100644 index 0000000..f266eef --- /dev/null +++ b/lib/absmartly/context_config.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Absmartly + class ContextConfig + attr_accessor :units, :attributes, :custom_assignments, :overrides, + :event_logger, :publish_delay, :refresh_interval + + def self.create + ContextConfig.new + end + + def initialize + @units = {} + @attributes = {} + @overrides = {} + @custom_assignments = {} + end + + def set_unit(unit_type, uid) + @units[unit_type.to_sym] = uid + self + end + + def set_units(units) + units.map { |k, v| set_unit(k, v) } + end + + def unit(unit_type) + @units[unit_type.to_sym] + end + + def set_attributes(attributes) + @attributes.merge!(attributes.transform_keys(&:to_sym)) + self + end + + def set_attribute(name, value) + @attributes[name.to_sym] = value + self + end + + def attribute(name) + @attributes[name.to_sym] + end + + def set_overrides(overrides) + @overrides.merge!(overrides.transform_keys(&:to_sym)) + end + + def set_override(experiment_name, variant) + @overrides[experiment_name.to_sym] = variant + self + end + + def override(experiment_name) + @overrides[experiment_name.to_sym] + end + + def set_custom_assignment(experiment_name, variant) + @custom_assignments[experiment_name.to_sym] = variant + self + end + + def set_custom_assignments(customAssignments) + @custom_assignments.merge!(customAssignments.transform_keys(&:to_sym)) + self + end + + 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 +end diff --git a/lib/absmartly/context_data_deserializer.rb b/lib/absmartly/context_data_deserializer.rb new file mode 100644 index 0000000..6813780 --- /dev/null +++ b/lib/absmartly/context_data_deserializer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Absmartly + class ContextDataDeserializer + # @interface method + def deserialize(bytes, offset, length) + raise NotImplementedError.new("You must implement deserialize method.") + end + end +end diff --git a/lib/absmartly/context_data_provider.rb b/lib/absmartly/context_data_provider.rb new file mode 100644 index 0000000..bb6acac --- /dev/null +++ b/lib/absmartly/context_data_provider.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Absmartly + class ContextDataProvider + # @interface method + def context_data + raise NotImplementedError.new("You must implement context_data method.") + end + end +end diff --git a/lib/absmartly/context_event_handler.rb b/lib/absmartly/context_event_handler.rb new file mode 100644 index 0000000..5c10cd6 --- /dev/null +++ b/lib/absmartly/context_event_handler.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Absmartly + class ContextEventHandler + # @interface method + def publish(context, event) + raise NotImplementedError.new("You must implement publish method.") + end + end +end diff --git a/lib/absmartly/context_event_logger.rb b/lib/absmartly/context_event_logger.rb new file mode 100644 index 0000000..b104920 --- /dev/null +++ b/lib/absmartly/context_event_logger.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Absmartly + class ContextEventLogger + module EVENT_TYPE + ERROR = "error" + READY = "ready" + REFRESH = "refresh" + PUBLISH = "publish" + EXPOSURE = "exposure" + GOAL = "goal" + CLOSE = "close" + end + + def handle_event(event, data) + raise NotImplementedError.new("You must implement handle_event method.") + end + end +end diff --git a/lib/absmartly/context_event_logger_callback.rb b/lib/absmartly/context_event_logger_callback.rb new file mode 100644 index 0000000..c2eabde --- /dev/null +++ b/lib/absmartly/context_event_logger_callback.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Absmartly + class ContextEventLoggerCallback < ContextEventLogger + attr_accessor :callable + + def initialize(callable) + @callable = callable + end + + def handle_event(event, data) + @callable.call(event, data) if @callable.present? + end + end +end diff --git a/lib/absmartly/context_event_serializer.rb b/lib/absmartly/context_event_serializer.rb new file mode 100644 index 0000000..0125d08 --- /dev/null +++ b/lib/absmartly/context_event_serializer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Absmartly + class ContextEventSerializer + # @interface method + def serialize(publish_event) + raise NotImplementedError.new("You must implement serialize method.") + end + end +end diff --git a/lib/absmartly/default_audience_deserializer.rb b/lib/absmartly/default_audience_deserializer.rb new file mode 100644 index 0000000..8a31cde --- /dev/null +++ b/lib/absmartly/default_audience_deserializer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "audience_deserializer" + +module Absmartly + class DefaultAudienceDeserializer < AudienceDeserializer + attr_accessor :log, :reader + + def deserialize(bytes, offset, length) + JSON.parse(bytes[offset..length], symbolize_names: true) + rescue JSON::ParserError + nil + end + end +end diff --git a/lib/absmartly/default_context_data_deserializer.rb b/lib/absmartly/default_context_data_deserializer.rb new file mode 100644 index 0000000..a5e3dc8 --- /dev/null +++ b/lib/absmartly/default_context_data_deserializer.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "json" +require_relative "context_data_deserializer" +require_relative "json/context_data" + +module Absmartly + class DefaultContextDataDeserializer < ContextDataDeserializer + attr_accessor :log, :reader + + def deserialize(bytes, offset, length) + parse = JSON.parse(bytes[offset..length], symbolize_names: true) + @reader = ContextData.new(parse[:experiments]) + rescue JSON::ParserError + nil + end + end +end diff --git a/lib/absmartly/default_context_data_provider.rb b/lib/absmartly/default_context_data_provider.rb new file mode 100644 index 0000000..dfa3db1 --- /dev/null +++ b/lib/absmartly/default_context_data_provider.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "context_data_provider" + +module Absmartly + class DefaultContextDataProvider < ContextDataProvider + attr_accessor :client + + def initialize(client) + @client = client + end + + def context_data + @client.context_data + end + end +end diff --git a/lib/absmartly/default_context_event_handler.rb b/lib/absmartly/default_context_event_handler.rb new file mode 100644 index 0000000..cc0f6c4 --- /dev/null +++ b/lib/absmartly/default_context_event_handler.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative "context_event_handler" + +module Absmartly + class DefaultContextEventHandler < ContextEventHandler + attr_accessor :client + + def initialize(client) + @client = client + end + + def publish(context, event) + @client.publish(event) + end + end +end diff --git a/lib/absmartly/default_context_event_serializer.rb b/lib/absmartly/default_context_event_serializer.rb new file mode 100644 index 0000000..6c6f222 --- /dev/null +++ b/lib/absmartly/default_context_event_serializer.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require_relative "context_event_serializer" + +module Absmartly + class DefaultContextEventSerializer < ContextEventSerializer + def serialize(event) + units = event.units.nil? ? [] : event.units.map do |unit| + { + type: unit.type, + uid: unit.uid, + } + end + req = {} + unless units.empty? + req = { + publishedAt: event.published_at, + units: units, + hashed: event.hashed + } + end + + req[:goals] = event.goals.map do |x| + { + name: x.name, + achievedAt: x.achieved_at, + properties: x.properties, + } + end unless event.goals.nil? + + req[:exposures] = event.exposures.select { |x| !x.id.nil? }.map do |x| + { + id: x.id, + name: x.name, + unit: x.unit, + exposedAt: x.exposed_at.to_i, + variant: x.variant, + assigned: x.assigned, + eligible: x.eligible, + overridden: x.overridden, + fullOn: x.full_on, + custom: x.custom, + audienceMismatch: x.audience_mismatch + } + end unless event.exposures.nil? + + req[:attributes] = event.attributes.map do |x| + { + name: x.name, + value: x.value, + setAt: x.set_at, + } + end unless event.attributes.nil? + + return nil if req.empty? + + req.to_json + end + end +end diff --git a/lib/absmartly/default_http_client.rb b/lib/absmartly/default_http_client.rb new file mode 100644 index 0000000..0439608 --- /dev/null +++ b/lib/absmartly/default_http_client.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "faraday" +require "faraday/net_http_persistent" +require "faraday/retry" +require "uri" +require_relative "http_client" + +module Absmartly + class DefaultHttpClient < HttpClient + attr_accessor :config, :session + + def self.create(config) + DefaultHttpClient.new(config) + end + + def initialize(config) + @config = config + @session = Faraday.new("") do |f| + f.request :retry, + max: config.max_retries, + interval: config.retry_interval, + interval_randomness: 0.5, + backoff_factor: 2 + f.options.timeout = config.connect_timeout + f.options.open_timeout = config.connection_request_timeout + f.adapter :net_http_persistent, pool_size: config.pool_size do |http| + http.idle_timeout = config.pool_idle_timeout + end + end + end + + def get(url, query, headers) + @session.get(url, query, headers) + end + + def put(url, query, headers, body) + @session.put(add_tracking(url, query), body, headers) + end + + def post(url, query, headers, body) + @session.post(add_tracking(url, query), body, headers) + end + + def close + @session.close + end + + def self.default_response(status_code, status_message, content_type, content) + env = Faraday::Env.from(status: status_code, body: content || status_message, + response_headers: { "Content-Type" => content_type }) + Faraday::Response.new(env) + end + + private + def add_tracking(url, params) + parsed = URI.parse(url) + query = parsed.query ? CGI.parse(parsed.query) : {} + query = query.merge(params) if params && params.is_a?(Hash) + parsed.query = URI.encode_www_form(query) + str = parsed.to_s + str[-1] == "?" ? str.chop : str + end + end +end diff --git a/lib/absmartly/default_http_client_config.rb b/lib/absmartly/default_http_client_config.rb new file mode 100644 index 0000000..3718051 --- /dev/null +++ b/lib/absmartly/default_http_client_config.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Absmartly + class DefaultHttpClientConfig + attr_accessor :connect_timeout, + :connection_request_timeout, + :retry_interval, + :max_retries, + :pool_size, + :pool_idle_timeout + + def self.create + DefaultHttpClientConfig.new + end + + def initialize + @connect_timeout = 3.0 + @connection_request_timeout = 3.0 + @retry_interval = 0.5 + @max_retries = 5 + @pool_size = 20 + @pool_idle_timeout = 5 + end + end +end diff --git a/lib/absmartly/default_variable_parser.rb b/lib/absmartly/default_variable_parser.rb new file mode 100644 index 0000000..366cabd --- /dev/null +++ b/lib/absmartly/default_variable_parser.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative "variable_parser" + +module Absmartly + class DefaultVariableParser < VariableParser + attr_accessor :reader, :log + + def parse(context, experiment_name, variant_name, config) + JSON.parse(config, symbolize_names: true) + rescue JSON::ParserError + nil + end + end +end diff --git a/lib/absmartly/hashing.rb b/lib/absmartly/hashing.rb new file mode 100644 index 0000000..0e67f0e --- /dev/null +++ b/lib/absmartly/hashing.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "digest" + +module Absmartly + class Hashing + def self.hash_unit(str) + Digest::MD5.base64digest(str.to_s).gsub("==", "").gsub("+", "-").gsub("/", "_") + end + end +end diff --git a/lib/absmartly/http_client.rb b/lib/absmartly/http_client.rb new file mode 100644 index 0000000..fc0dc38 --- /dev/null +++ b/lib/absmartly/http_client.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Absmartly + class HttpClient + # @interface method + def response + raise NotImplementedError.new("You must implement response method.") + end + + def get(url, query, headers) + raise NotImplementedError.new("You must implement get method.") + end + + def post(url, query, headers, body) + raise NotImplementedError.new("You must implement post method.") + end + + def put(url, query, headers, body) + raise NotImplementedError.new("You must implement put method.") + end + end +end diff --git a/lib/absmartly/json/attribute.rb b/lib/absmartly/json/attribute.rb new file mode 100644 index 0000000..bf44312 --- /dev/null +++ b/lib/absmartly/json/attribute.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Absmartly + class Attribute + attr_accessor :name, :value, :set_at + + def initialize(name = nil, value = nil, set_at = nil) + @name = name + @value = value + @set_at = set_at + end + + def ==(o) + return true if self.object_id == o.object_id + return false if o.nil? || self.class != o.class + + @name == o.name && @value == o.value && @set_at == o.set_at + end + + def hash_code + { name: @name, value: @value, set_at: @set_at } + end + + def to_s + "Attribute{" + + "name='#{@name}'" + + ", value=#{@value}" + + ", setAt=#{@set_at}" + + "}" + end + end +end diff --git a/lib/absmartly/json/context_data.rb b/lib/absmartly/json/context_data.rb new file mode 100644 index 0000000..cab0053 --- /dev/null +++ b/lib/absmartly/json/context_data.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative "experiment" + +module Absmartly + class ContextData + attr_accessor :experiments + + def initialize(experiments = []) + @experiments = experiments.map do |experiment| + Experiment.new(experiment) + end unless experiments.nil? + self + end + + def ==(o) + return true if self.object_id == o.object_id + return false if o.nil? || self.class != o.class + + @experiments == o.experiments + end + + def hash_code + { name: @name, config: @config } + end + + def to_s + "ContextData{" + + "experiments='" + @experiments.join + + "}" + end + end +end diff --git a/lib/absmartly/json/custom_field_value.rb b/lib/absmartly/json/custom_field_value.rb new file mode 100644 index 0000000..3f65ca5 --- /dev/null +++ b/lib/absmartly/json/custom_field_value.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Absmartly + class CustomFieldValue + attr_accessor :name, :type, :value + + def initialize(name, value, type) + @name = name + @type = type + @value = value + end + + def ==(o) + return true if self.object_id == o.object_id + return false if o.nil? || self.class != o.class + + that = o + @name == that.name && @type == that.type && @value == that.value + end + + def hash_code + { name: @name, type: @type, value: @value } + end + + def to_s + "CustomFieldValue{" + + "name='#{@name}'" + + ", type='#{@type}'" + + ", value='#{@value}'" + + "}" + end + end +end diff --git a/lib/absmartly/json/experiment.rb b/lib/absmartly/json/experiment.rb new file mode 100644 index 0000000..49f880a --- /dev/null +++ b/lib/absmartly/json/experiment.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require_relative "../string" +require_relative "experiment_application" +require_relative "experiment_variant" +require_relative "custom_field_value" + +module Absmartly + class Experiment + attr_accessor :id, :name, :unit_type, :iteration, :seed_hi, :seed_lo, :split, + :traffic_seed_hi, :traffic_seed_lo, :traffic_split, :full_on_variant, + :applications, :variants, :audience_strict, :audience, :custom_field_values + + def initialize(args = {}) + args.each do |name, value| + if name == :applications + @applications = assign_to_klass(ExperimentApplication, value) + elsif name == :variants + @variants = assign_to_klass(ExperimentVariant, value) + elsif name == :customFieldValues + if value != nil + @custom_field_values = assign_to_klass(CustomFieldValue, value) + end + else + self.instance_variable_set("@#{name.to_s.underscore}", value) + end + end + @audience_strict ||= false + self + end + + def assign_to_klass(klass, arr) + arr.map do |item| + return item if item.is_a?(klass) + + klass.new(*item.values) + end + end + + def ==(o) + return true if self.object_id == o.object_id + return false if o.nil? || self.class != o.class + + that = o + @id == that.id && @iteration == that.iteration && @seed_hi == that.seed_hi && @seed_lo == that.seed_lo && + @traffic_seed_hi == that.traffic_seed_hi && @traffic_seed_lo == that.traffic_seed_lo && + @full_on_variant == that.full_on_variant && @name == that.name && + @unit_type == that.unit_type && @split == that.split && + @traffic_split == that.traffic_split && @applications == that.applications && + @variants == that.variants && @audience_strict == that.audience_strict && + @audience == that.audience && @custom_field_values == that.custom_field_values + end + + def hash_code + { + id: @id, + name: @name, + unit_type: @unit_type, + iteration: @iteration, + seed_hi: @seed_hi, + seed_lo: @seed_lo, + traffic_seed_hi: @traffic_seed_hi, + traffic_seed_lo: @traffic_seed_lo, + full_on_variant: @full_on_variant, + audience_strict: @audience_strict, + audience: @audience, + custom_field_values: @custom_field_values + } + end + + def to_s + "ContextExperiment{" + + "id= #{@id}"+ + ", name='#{@name}'" + + ", unitType='#{@unit_type}'" + + ", iteration=#{@iteration}" + + ", seedHi=#{@seed_hi}" + + ", seedLo=#{@seed_lo}" + + ", split=#{@split.join}" + + ", trafficSeedHi=#{@traffic_seed_hi}" + + ", trafficSeedLo=#{@traffic_seed_lo}" + + ", trafficSplit=#{@traffic_split.join}" + + ", fullOnVariant=#{@full_on_variant}" + + ", applications=#{@applications.join}" + + ", variants=#{@variants.join}" + + ", audienceStrict=#{@audience_strict}" + + ", audience='#{@audience}'" + + ", custom_field_values='#{@custom_field_values}'" + + "}" + end + end +end diff --git a/lib/absmartly/json/experiment_application.rb b/lib/absmartly/json/experiment_application.rb new file mode 100644 index 0000000..ce83eba --- /dev/null +++ b/lib/absmartly/json/experiment_application.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Absmartly + class ExperimentApplication + attr_accessor :name + + def initialize(name = nil) + @name = name + end + + def ==(o) + return true if self.object_id == o.object_id + return false if o.nil? || self.class != o.class + + that = o + @name == that.name + end + + def hash_code + { name: @name } + end + + def to_s + "ExperimentApplication{" + + "name='" + @name + "'" + + "}" + end + end +end diff --git a/lib/absmartly/json/experiment_variant.rb b/lib/absmartly/json/experiment_variant.rb new file mode 100644 index 0000000..7e317f1 --- /dev/null +++ b/lib/absmartly/json/experiment_variant.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Absmartly + class ExperimentVariant + attr_accessor :name, :config + + def initialize(name = nil, config) + @name = name + @config = config + end + + def ==(o) + return true if self.object_id == o.object_id + return false if o.nil? || self.class != o.class + + that = o + @name == that.name && @config == that.config + end + + def hash_code + { name: @name, config: @config } + end + + def to_s + "ExperimentVariant{" + + "name='#{@name}'" + + ", config='#{@config}'" + + "}" + end + end +end diff --git a/lib/absmartly/json/exposure.rb b/lib/absmartly/json/exposure.rb new file mode 100644 index 0000000..8d17dcc --- /dev/null +++ b/lib/absmartly/json/exposure.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Absmartly + class Exposure + attr_accessor :id, :name, :unit, :variant, :exposed_at, :assigned, :eligible, + :overridden, :full_on, :custom, :audience_mismatch + + def initialize(id = nil, name = nil, unit = nil, variant = nil, + exposed_at = nil, assigned = nil, eligible = nil, + overridden = nil, full_on = nil, custom = nil, + audience_mismatch = nil) + @id = id || 0 + @name = name + @unit = unit + @variant = variant + @exposed_at = exposed_at + @assigned = assigned + @eligible = eligible + @overridden = overridden + @full_on = full_on + @custom = custom + @audience_mismatch = audience_mismatch + end + + def ==(o) + return true if self.object_id == o.object_id + return false if o.nil? || self.class != o.class + + @id == o.id && @name == o.name && @unit == o.unit && + @variant == o.variant && @exposed_at == o.exposed_at && + @assigned == o.assigned && @eligible == o.eligible && + @overridden == o.overridden && @full_on == o.full_on && + @custom == o.custom && @audience_mismatch == o.audience_mismatch + end + + def hash_code + { + id: @id, name: @name, unit: @unit, + variant: @variant, exposed_at: @exposed_at, + assigned: @assigned, eligible: @eligible, + overridden: @overridden, full_on: @full_on, + custom: @custom, audience_mismatch: @audience_mismatch + } + end + + def to_s + "Exposure{" + + "id=#{@id}" + + "name='#{@name}'" + + ", unit=#{@unit}" + + ", variant=#{@variant}" + + ", exposed_at=#{@exposed_at}" + + ", assigned=#{@assigned}" + + ", eligible=#{@eligible}" + + ", overridden=#{@overridden}" + + ", full_on=#{@full_on}" + + ", custom=#{@custom}" + + ", audience_mismatch=#{@audience_mismatch}" + + "}" + end + end +end diff --git a/lib/absmartly/json/goal_achievement.rb b/lib/absmartly/json/goal_achievement.rb new file mode 100644 index 0000000..51ca9ce --- /dev/null +++ b/lib/absmartly/json/goal_achievement.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Absmartly + class GoalAchievement + attr_accessor :name, :achieved_at, :properties + + def initialize(name = nil, achieved_at = nil, properties = nil) + @name = name + @achieved_at = achieved_at + @properties = properties + end + + def ==(o) + return true if self.object_id == o.object_id + return false if o.nil? || self.class != o.class + + that = o + @name == that.name && @achieved_at == that.achieved_at && + @properties == that.properties + end + + def hash_code + { name: @name, achieved_at: @achieved_at, properties: @properties } + end + + def to_s + "GoalAchievement{" + + "name='#{@name}'" + + ", achieved_at='#{@achieved_at}'" + + ", properties='#{@properties.inspect}'" + + "}" + end + end +end diff --git a/lib/absmartly/json/publish_event.rb b/lib/absmartly/json/publish_event.rb new file mode 100644 index 0000000..f22fad9 --- /dev/null +++ b/lib/absmartly/json/publish_event.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Absmartly + class PublishEvent + attr_accessor :hashed, :units, :published_at, :exposures, :goals, :attributes + + def initialize + @published_at = 0 + @hashed = false + end + + def ==(o) + return true if self.object_id == o.object_id + return false if o.nil? || self.class != o.class + + that = o + @hashed == that.hashed && @units == that.units && + @published_at == that.published_at && @exposures == that.exposures && + @goals == that.goals && @attributes == that.attributes + end + + def hash_code + { + hashed: @hashed, + units: @units, + published_at: @published_at, + exposures: @exposures, + goals: @goals, + attributes: @attributes + } + end + + def to_s + "PublishEvent{" + + "hashedUnits=#{@hashed}" + + ", units=#{@units.inspect}" + + ", publishedAt=#{@published_at}" + + ", exposures=#{@exposures.inspect}" + + ", goals=#{@goals.inspect}" + + ", attributes=#{@attributes!=nil ? @attributes.join : ""}" + + "}" + end + end +end diff --git a/lib/absmartly/json/unit.rb b/lib/absmartly/json/unit.rb new file mode 100644 index 0000000..2760de9 --- /dev/null +++ b/lib/absmartly/json/unit.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Absmartly + class Unit + attr_accessor :type, :uid + + def initialize(type = nil, uid = nil) + @type = type + @uid = uid + end + + def ==(o) + return true if self.object_id == o.object_id + return false if o.nil? || self.class != o.class + + @type == o.type && @uid == o.uid + end + + def hash_code + { + type: @type, uid: @uid + } + end + + def to_s + "Unit{" + + "type='" + @type + "'" + + ", uid=" + @uid + + "}" + end + end +end diff --git a/lib/absmartly/json_expr/evaluator.rb b/lib/absmartly/json_expr/evaluator.rb new file mode 100644 index 0000000..f117ebb --- /dev/null +++ b/lib/absmartly/json_expr/evaluator.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Absmartly + class Evaluator + def evaluate(expr) + raise NotImplementedError.new("You must implement evaluate method.") + end + + def boolean_convert(_) + raise NotImplementedError.new("You must implement boolean convert method.") + end + + def number_convert(_) + raise NotImplementedError.new("You must implement number convert method.") + end + + def string_convert(_) + raise NotImplementedError.new("You must implement string convert method.") + end + + def extract_var(_) + raise NotImplementedError.new("You must implement extract var method.") + end + + def compare(_, _) + raise NotImplementedError.new("You must implement extract_var method.") + end + end +end diff --git a/lib/absmartly/json_expr/expr_evaluator.rb b/lib/absmartly/json_expr/expr_evaluator.rb new file mode 100644 index 0000000..5f9e7e3 --- /dev/null +++ b/lib/absmartly/json_expr/expr_evaluator.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require_relative "../string" +require_relative "./evaluator" + +module Absmartly + EMPTY_MAP = {} + EMPTY_LIST = [] + + class ExprEvaluator < Evaluator + attr_accessor :operators + attr_accessor :vars + NUMERIC_REGEX = /\A[-+]?[0-9]*\.?[0-9]+\Z/ + + def initialize(operators, vars) + @operators = operators + @vars = vars + end + + def evaluate(expr) + if expr.is_a? Array + return @operators[:and].evaluate(self, expr) + elsif expr.is_a? Hash + expr.transform_keys(&:to_sym).each do |key, value| + if @operators[key] + return @operators[key].evaluate(self, value) + end + end + end + nil + end + + def boolean_convert(x) + if x.is_a?(TrueClass) || x.is_a?(FalseClass) + return x + elsif x.is_a?(Numeric) || !(x.to_s =~ NUMERIC_REGEX).nil? + return !x.to_f.zero? + elsif x.is_a?(String) + return x != "false" && x != "0" && x != "" + end + + !x.nil? + end + + def number_convert(x) + return if x.nil? || x.to_s.empty? + + if x.is_a?(Numeric) || !(x.to_s =~ NUMERIC_REGEX).nil? + return x.to_f + elsif x.is_a?(TrueClass) || x.is_a?(FalseClass) + return x ? 1.0 : 0.0 + end + nil + end + + def string_convert(x) + if x.is_a?(String) + return x + elsif x.is_a?(TrueClass) || x.is_a?(FalseClass) + return x.to_s + elsif x.is_a?(Numeric) || !(x.to_s =~ NUMERIC_REGEX).nil? + return x == x.to_i ? x.to_i.to_s : x.to_s + end + nil + end + + def extract_var(path) + frags = path.split("/") + target = !vars.nil? ? vars : {} + + frags.each do |frag| + list = target + value = nil + if target.is_a?(Array) + value = list[frag.to_i] + elsif target.is_a?(Hash) + value = list[frag].nil? ? list[frag.to_sym] : list[frag] + end + + unless value.nil? + target = value + next + end + + return nil + end + target + end + + def compare(lhs, rhs) + if lhs.nil? + return rhs.nil? ? 0 : nil + elsif rhs.nil? + return nil + end + + if lhs.is_a?(Numeric) + rvalue = number_convert(rhs) + return lhs.to_f.to_s.casecmp(rvalue.to_s) unless rvalue.nil? + elsif lhs.is_a?(String) + rvalue = string_convert(rhs) + return lhs.compare_to(rvalue) unless rvalue.nil? + elsif lhs.is_a?(TrueClass) || lhs.is_a?(FalseClass) + rvalue = boolean_convert(rhs) + return lhs.to_s.casecmp(rvalue.to_s) unless rvalue.nil? + elsif lhs.class == rhs.class && lhs === rhs + return 0 + end + nil + end + end +end + +class Array + def self.wrap(object) + if object.nil? + [] + elsif object.respond_to?(:to_ary) + object.to_ary || [object] + else + [object] + end + end +end diff --git a/lib/absmartly/json_expr/json_expr.rb b/lib/absmartly/json_expr/json_expr.rb new file mode 100644 index 0000000..688424c --- /dev/null +++ b/lib/absmartly/json_expr/json_expr.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require_relative "./expr_evaluator" +require 'absmartly/json_expr/operators/and_combinator' +require 'absmartly/json_expr/operators/binary_operator' +require 'absmartly/json_expr/operators/boolean_combinator' +require 'absmartly/json_expr/operators/equals_operator' +require 'absmartly/json_expr/operators/greater_than_operator' +require 'absmartly/json_expr/operators/greater_than_or_equal_operator' +require 'absmartly/json_expr/operators/in_operator' +require 'absmartly/json_expr/operators/less_than_operator' +require 'absmartly/json_expr/operators/less_than_or_equal_operator' +require 'absmartly/json_expr/operators/match_operator' +require 'absmartly/json_expr/operators/nil_operator' +require 'absmartly/json_expr/operators/not_operator' +require 'absmartly/json_expr/operators/or_combinator' +require 'absmartly/json_expr/operators/unary_operator' +require 'absmartly/json_expr/operators/value_operator' +require 'absmartly/json_expr/operators/var_operator' + +module Absmartly + class JsonExpr + attr_accessor :operators + attr_accessor :vars + + def initialize + @operators = { + "and": AndCombinator.new, + "or": OrCombinator.new, + "value": ValueOperator.new, + "var": VarOperator.new, + "null": NilOperator.new, + "not": NotOperator.new, + "in": InOperator.new, + "match": MatchOperator.new, + "eq": EqualsOperator.new, + "gt": GreaterThanOperator.new, + "gte": GreaterThanOrEqualOperator.new, + "lt": LessThanOperator.new, + "lte": LessThanOrEqualOperator.new + } + end + + def evaluate_boolean_expr(expr, vars) + evaluator = ExprEvaluator.new(operators, vars) + evaluator.boolean_convert(evaluator.evaluate(expr)) + end + + def evaluate_expr(expr, vars) + evaluator = ExprEvaluator.new(operators, vars) + evaluator.evaluate(expr) + end + end +end diff --git a/lib/absmartly/json_expr/operator.rb b/lib/absmartly/json_expr/operator.rb new file mode 100644 index 0000000..5352410 --- /dev/null +++ b/lib/absmartly/json_expr/operator.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Absmartly + module Operator + # @interface method + def evaluate(evaluator, args) + raise NotImplementedError.new("You must implement evaluate method.") + end + end +end diff --git a/lib/absmartly/json_expr/operators/and_combinator.rb b/lib/absmartly/json_expr/operators/and_combinator.rb new file mode 100644 index 0000000..11ee3e3 --- /dev/null +++ b/lib/absmartly/json_expr/operators/and_combinator.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "boolean_combinator" + +module Absmartly + class AndCombinator + include BooleanCombinator + + def combine(evaluator, exprs) + Array.wrap(exprs).each do |expr| + return false unless evaluator.boolean_convert(evaluator.evaluate(expr)) + end + true + end + end +end diff --git a/lib/absmartly/json_expr/operators/binary_operator.rb b/lib/absmartly/json_expr/operators/binary_operator.rb new file mode 100644 index 0000000..c07842e --- /dev/null +++ b/lib/absmartly/json_expr/operators/binary_operator.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Absmartly + module BinaryOperator + def evaluate(evaluator, args) + if args.is_a? Array + args_list = args + lhs = args_list.size > 0 ? evaluator.evaluate(args_list[0]) : nil + unless lhs.nil? + rhs = args_list.size > 1 ? evaluator.evaluate(args_list[1]) : nil + unless rhs.nil? + return binary(evaluator, lhs, rhs) + end + end + end + nil + end + + # @abstract method + def binary(evaluator, lhs, rhs) + raise NotImplementedError.new("You must implement binary method.") + end + end +end diff --git a/lib/absmartly/json_expr/operators/boolean_combinator.rb b/lib/absmartly/json_expr/operators/boolean_combinator.rb new file mode 100644 index 0000000..9db79f9 --- /dev/null +++ b/lib/absmartly/json_expr/operators/boolean_combinator.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Absmartly + module BooleanCombinator + def evaluate(evaluator, args) + if args.is_a? Array + return combine(evaluator, args) + end + nil + end + + # @abstract method + def combine(evaluator, args) + raise NotImplementedError.new("You must implement combine method.") + end + end +end diff --git a/lib/absmartly/json_expr/operators/equals_operator.rb b/lib/absmartly/json_expr/operators/equals_operator.rb new file mode 100644 index 0000000..e7dcab2 --- /dev/null +++ b/lib/absmartly/json_expr/operators/equals_operator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require_relative "binary_operator" + +module Absmartly + class EqualsOperator + include BinaryOperator + + def binary(evaluator, lhs, rhs) + result = evaluator.compare(lhs, rhs) + !result.nil? ? (result == 0) : nil + end + end +end diff --git a/lib/absmartly/json_expr/operators/greater_than_operator.rb b/lib/absmartly/json_expr/operators/greater_than_operator.rb new file mode 100644 index 0000000..abd0a4c --- /dev/null +++ b/lib/absmartly/json_expr/operators/greater_than_operator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require_relative "binary_operator" + +module Absmartly + class GreaterThanOperator + include BinaryOperator + + def binary(evaluator, lhs, rhs) + result = evaluator.compare(lhs, rhs) + !result.nil? ? (result > 0) : nil + end + end +end diff --git a/lib/absmartly/json_expr/operators/greater_than_or_equal_operator.rb b/lib/absmartly/json_expr/operators/greater_than_or_equal_operator.rb new file mode 100644 index 0000000..38467fd --- /dev/null +++ b/lib/absmartly/json_expr/operators/greater_than_or_equal_operator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require_relative "binary_operator" + +module Absmartly + class GreaterThanOrEqualOperator + include BinaryOperator + + def binary(evaluator, lhs, rhs) + result = evaluator.compare(lhs, rhs) + !result.nil? ? (result >= 0) : nil + end + end +end diff --git a/lib/absmartly/json_expr/operators/in_operator.rb b/lib/absmartly/json_expr/operators/in_operator.rb new file mode 100644 index 0000000..19adf26 --- /dev/null +++ b/lib/absmartly/json_expr/operators/in_operator.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require_relative "binary_operator" + +module Absmartly + class InOperator + include BinaryOperator + + def binary(evaluator, haystack, needle) + if haystack.is_a? Array + haystack.each do |item| + return true if evaluator.compare(item, needle) == 0 + end + return false + elsif haystack.is_a? String + needle_string = evaluator.string_convert(needle) + return !needle_string.nil? && haystack.include?(needle_string) + elsif haystack.is_a?(Hash) + needle_string = evaluator.string_convert(needle) + return !needle_string.nil? && haystack.key?(needle_string) + end + nil + end + end +end diff --git a/lib/absmartly/json_expr/operators/less_than_operator.rb b/lib/absmartly/json_expr/operators/less_than_operator.rb new file mode 100644 index 0000000..5377c27 --- /dev/null +++ b/lib/absmartly/json_expr/operators/less_than_operator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require_relative "binary_operator" + +module Absmartly + class LessThanOperator + include BinaryOperator + + def binary(evaluator, lhs, rhs) + result = evaluator.compare(lhs, rhs) + !result.nil? ? (result < 0) : nil + end + end +end diff --git a/lib/absmartly/json_expr/operators/less_than_or_equal_operator.rb b/lib/absmartly/json_expr/operators/less_than_or_equal_operator.rb new file mode 100644 index 0000000..0d4a082 --- /dev/null +++ b/lib/absmartly/json_expr/operators/less_than_or_equal_operator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require_relative "binary_operator" + +module Absmartly + class LessThanOrEqualOperator + include BinaryOperator + + def binary(evaluator, lhs, rhs) + result = evaluator.compare(lhs, rhs) + !result.nil? ? (result <= 0) : nil + end + end +end diff --git a/lib/absmartly/json_expr/operators/match_operator.rb b/lib/absmartly/json_expr/operators/match_operator.rb new file mode 100644 index 0000000..8cb2938 --- /dev/null +++ b/lib/absmartly/json_expr/operators/match_operator.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require_relative "binary_operator" + +module Absmartly + class MatchOperator + include BinaryOperator + + def binary(evaluator, lhs, rhs) + text = evaluator.string_convert(lhs) + unless text.nil? + pattern = evaluator.string_convert(rhs) + unless pattern.nil? + text.match(pattern) + end + end + end + end +end diff --git a/lib/absmartly/json_expr/operators/nil_operator.rb b/lib/absmartly/json_expr/operators/nil_operator.rb new file mode 100644 index 0000000..c7ec47f --- /dev/null +++ b/lib/absmartly/json_expr/operators/nil_operator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "./unary_operator" + +module Absmartly + class NilOperator + include UnaryOperator + + def unary(evaluator, arg) + arg.nil? + end + end +end diff --git a/lib/absmartly/json_expr/operators/not_operator.rb b/lib/absmartly/json_expr/operators/not_operator.rb new file mode 100644 index 0000000..4fd6805 --- /dev/null +++ b/lib/absmartly/json_expr/operators/not_operator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "./unary_operator" + +module Absmartly + class NotOperator + include UnaryOperator + + def unary(evaluator, args) + !evaluator.boolean_convert(args) + end + end +end diff --git a/lib/absmartly/json_expr/operators/or_combinator.rb b/lib/absmartly/json_expr/operators/or_combinator.rb new file mode 100644 index 0000000..b8096d9 --- /dev/null +++ b/lib/absmartly/json_expr/operators/or_combinator.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "boolean_combinator" + +module Absmartly + class OrCombinator + include BooleanCombinator + + def combine(evaluator, exprs) + Array.wrap(exprs).each do |expr| + return true if evaluator.boolean_convert(evaluator.evaluate(expr)) + end + Array.wrap(exprs).empty? + end + end +end diff --git a/lib/absmartly/json_expr/operators/unary_operator.rb b/lib/absmartly/json_expr/operators/unary_operator.rb new file mode 100644 index 0000000..dae7d61 --- /dev/null +++ b/lib/absmartly/json_expr/operators/unary_operator.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Absmartly + module UnaryOperator + def evaluate(evaluator, args) + arg = evaluator.evaluate(args) + unary(evaluator, arg) + end + + # @abstract method + def unary(evaluator, arg) + raise NotImplementedError.new("You must implement unary method.") + end + end +end diff --git a/lib/absmartly/json_expr/operators/value_operator.rb b/lib/absmartly/json_expr/operators/value_operator.rb new file mode 100644 index 0000000..9f08f39 --- /dev/null +++ b/lib/absmartly/json_expr/operators/value_operator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "binary_operator" + +module Absmartly + class ValueOperator + include BinaryOperator + + def evaluate(evaluator, value) + value + end + end +end diff --git a/lib/absmartly/json_expr/operators/var_operator.rb b/lib/absmartly/json_expr/operators/var_operator.rb new file mode 100644 index 0000000..2f7e720 --- /dev/null +++ b/lib/absmartly/json_expr/operators/var_operator.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative "binary_operator" + +module Absmartly + class VarOperator + include BinaryOperator + + def evaluate(evaluator, path) + if path.is_a?(Hash) + path = to_sym(path) + path = path[:path] + end + + path.is_a?(String) ? evaluator.extract_var(path) : nil + end + + private + def to_sym(path) + path.transform_keys(&:to_sym) + end + end +end diff --git a/lib/absmartly/scheduled_executor_service.rb b/lib/absmartly/scheduled_executor_service.rb new file mode 100644 index 0000000..d21739e --- /dev/null +++ b/lib/absmartly/scheduled_executor_service.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Absmartly + class ScheduledExecutorService + # @interface method + def schedule(command, delay, unit) + raise NotImplementedError.new("You must implement schedule method.") + end + end +end diff --git a/lib/absmartly/scheduled_thread_pool_executor.rb b/lib/absmartly/scheduled_thread_pool_executor.rb new file mode 100644 index 0000000..2d276f9 --- /dev/null +++ b/lib/absmartly/scheduled_thread_pool_executor.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "audience_deserializer" + +module Absmartly + class ScheduledThreadPoolExecutor < AudienceDeserializer + attr_accessor :log, :reader + + def initialize(timer = 1) + end + + def deserialize(bytes, offset, length) + @reader.read_value(bytes, offset, length) + end + end +end diff --git a/lib/string.rb b/lib/absmartly/string.rb similarity index 100% rename from lib/string.rb rename to lib/absmartly/string.rb diff --git a/lib/absmartly/variable_parser.rb b/lib/absmartly/variable_parser.rb new file mode 100644 index 0000000..b18a481 --- /dev/null +++ b/lib/absmartly/variable_parser.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Absmartly + class VariableParser + # @interface method + def parse + raise NotImplementedError.new("You must implement parse method.") + end + end +end diff --git a/lib/absmartly/variant_assigner.rb b/lib/absmartly/variant_assigner.rb new file mode 100644 index 0000000..c784b4e --- /dev/null +++ b/lib/absmartly/variant_assigner.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "murmurhash3" +require_relative "./hashing" + +module Absmartly + class VariantAssigner + attr_reader :key + + def initialize(key) + md5 = Hashing.hash_unit(key.to_s) + @key = MurmurHash3::V32.str_hash(md5) + @normalizer = 1.0 / 0xffffffff + end + + def probability(seed_hi, seed_lo) + buffer = Array.new + put_uint32(buffer, seed_lo) + put_uint32(buffer, seed_hi) + put_uint32(buffer, key) + hash = MurmurHash3::V32.str_hash(buffer.pack("C*")) + + prob = (hash & 0xffffffff) * @normalizer + prob + end + + def self.choose_variant(split, prob) + sum = 0 + split.each_with_index do |s, i| + sum += s + return i if prob < sum + end + + split.count - 1 + end + + def assign(split, seed_hi, seed_lo) + prob = probability(seed_hi, seed_lo) + self.class.choose_variant(split, prob) + end + + def put_uint32(buffer, x) + buffer << (x & 0xff) + buffer << ((x >> 8) & 0xff) + buffer << ((x >> 16) & 0xff) + buffer << ((x >> 24) & 0xff) + end + end +end diff --git a/lib/absmartly/version.rb b/lib/absmartly/version.rb index bdfd7b2..0156af4 100644 --- a/lib/absmartly/version.rb +++ b/lib/absmartly/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Absmartly - VERSION = "1.3.0" + VERSION = "2.0.0" end diff --git a/lib/audience_deserializer.rb b/lib/audience_deserializer.rb deleted file mode 100644 index 6b9ad09..0000000 --- a/lib/audience_deserializer.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class AudienceDeserializer - # @interface method - def deserialize(bytes, offset, length) - raise NotImplementedError.new("You must implement deserialize method.") - end -end diff --git a/lib/audience_matcher.rb b/lib/audience_matcher.rb deleted file mode 100644 index 0cf0d40..0000000 --- a/lib/audience_matcher.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require "json" -require_relative "json_expr/json_expr" - -class AudienceMatcher - attr_accessor :deserializer, :json_expr - - def initialize(deserializer) - @deserializer = deserializer - @json_expr = JsonExpr.new - end - - class Result - attr_accessor :result - - def initialize(result) - @result = result - end - - def get - @result - end - end - - def evaluate(audience, attributes) - audience_map = JSON.parse(audience, symbolize_names: true) - - unless audience_map.nil? - filter = audience_map[:filter] - if filter.is_a?(Hash) || filter.is_a?(Array) - Result.new(@json_expr.evaluate_boolean_expr(filter, attributes)) - end - end - rescue - nil - end -end diff --git a/lib/client.rb b/lib/client.rb deleted file mode 100644 index 7c13c60..0000000 --- a/lib/client.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require_relative "default_http_client" -require_relative "default_context_data_deserializer" -require_relative "default_context_event_serializer" - -class Client - attr_accessor :url, :query, :headers, :http_client, :executor, :deserializer, :serializer - attr_reader :data_future, :promise, :exception - - def self.create(config, http_client = nil) - Client.new(config, http_client || DefaultHttpClient.create(config.http_client_config)) - end - - def initialize(config, http_client = nil) - 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 - - @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(@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) - self - end - - def publish(event) - 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? || false - end -end diff --git a/lib/client_config.rb b/lib/client_config.rb deleted file mode 100644 index 104a8a3..0000000 --- a/lib/client_config.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require_relative "default_http_client_config" - -class ClientConfig - attr_accessor :endpoint, :api_key, :environment, :application, :deserializer, - :serializer, :executor, :connect_timeout, :connection_request_timeout, - :retry_interval, :max_retries - - def self.create - ClientConfig.new - end - - def self.create_from_properties(properties, prefix) - properties = properties.transform_keys(&: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) - @endpoint = endpoint - @environment = environment - @application = application - @api_key = api_key - end - - def context_data_deserializer - @deserializer - end - - def context_data_deserializer=(deserializer) - @deserializer = deserializer - end - - def context_event_serializer - @serializer - end - - def context_event_serializer=(serializer) - @serializer = serializer - end - - def http_client_config - http_config = DefaultHttpClientConfig.create - http_config.connect_timeout = @connect_timeout unless @connect_timeout.nil? - http_config.connection_request_timeout = @connection_request_timeout unless @connection_request_timeout.nil? - http_config.retry_interval = @retry_interval unless @retry_interval.nil? - http_config.max_retries = @max_retries unless @max_retries.nil? - http_config - end -end diff --git a/lib/context.rb b/lib/context.rb deleted file mode 100644 index db7bd69..0000000 --- a/lib/context.rb +++ /dev/null @@ -1,659 +0,0 @@ -# frozen_string_literal: true - -require_relative "hashing" -require_relative "variant_assigner" -require_relative "context_event_logger" -require_relative "json/unit" -require_relative "json/attribute" -require_relative "json/exposure" -require_relative "json/publish_event" -require_relative "json/goal_achievement" - -class Context - attr_reader :data, :pending_count - - def self.create(clock, config, data_future, data_provider, - event_handler, event_logger, variable_parser, audience_matcher) - Context.new(clock, config, data_future, data_provider, - event_handler, event_logger, variable_parser, audience_matcher) - end - - def initialize(clock, config, data_future, data_provider, - event_handler, event_logger, variable_parser, audience_matcher) - @index = [] - @context_custom_fields = {} - @achievements = [] - @assignment_cache = {} - @assignments = {} - @clock = clock.is_a?(String) ? Time.iso8601(clock) : clock - @publish_delay = config.publish_delay - @refresh_interval = config.refresh_interval - @event_handler = event_handler - @event_logger = !config.event_logger.nil? ? config.event_logger : event_logger - @data_provider = data_provider - @variable_parser = variable_parser - @audience_matcher = audience_matcher - @closed = false - - @units = {} - @attributes = [] - @overrides = {} - @cassignments = {} - @assigners = {} - @hashed_units = {} - @pending_count = 0 - @exposures ||= [] - @attrs_seq = 0 - - set_units(config.units) if config.units - set_attributes(config.attributes) if config.attributes - set_overrides(config.overrides) if config.overrides - set_custom_assignments(config.custom_assignments) if config.custom_assignments - if data_future.success? - assign_data(data_future.data_future) - log_event(ContextEventLogger::EVENT_TYPE::READY, data_future.data_future) - else - set_data_failed(data_future.exception) - log_error(data_future.exception) - end - end - - def ready? - !@data.nil? - end - - def failed? - @failed - end - - def closed? - @closed - end - - def experiments - check_ready?(true) - - @data.experiments.map(&:name) - end - - def set_override(experiment_name, variant) - check_not_closed? - - @overrides[experiment_name.to_s.to_sym] = variant - end - - def set_overrides(overrides) - check_not_closed? - - @overrides.merge!(overrides.transform_keys(&:to_sym)) - end - - def override(experiment_name) - check_not_closed? - - @overrides[experiment_name.to_s.to_sym] - end - - def set_custom_assignment(experiment_name, variant) - check_not_closed? - - @cassignments[experiment_name.to_s.to_sym] = variant - end - - def set_custom_assignments(custom_assignments) - check_not_closed? - - @cassignments.merge!(custom_assignments.transform_keys(&:to_sym)) - end - - def custom_assignment(experiment_name) - check_not_closed? - - @cassignments[experiment_name.to_s.to_sym] - end - - def set_unit(unit_type, uid) - check_not_closed? - - previous = @units[unit_type.to_sym] - if !previous.nil? && previous != uid - raise IllegalStateException.new("Unit '#{unit_type}' already set.") - end - - trimmed = uid.to_s.strip - if trimmed.empty? - raise IllegalStateException.new("Unit '#{unit_type}' UID must not be blank.") - end - - @units[unit_type] = trimmed - end - - def set_units(units) - check_not_closed? - - units.each { |key, value| self.set_unit(key, value) } - end - - def set_attribute(name, value) - check_not_closed? - - @attributes.push(Attribute.new(name, value, @clock.to_i)) - @attrs_seq += 1 - end - - def set_attributes(attributes) - check_not_closed? - - attributes.each { |key, value| self.set_attribute(key, value) } - end - - def treatment(experiment_name) - check_ready?(true) - assignment = assignment(experiment_name) - unless assignment.exposed - queue_exposure(assignment) - end - - assignment.variant - end - - def queue_exposure(assignment) - unless assignment.exposed - assignment.exposed = true - - exposure = Exposure.new - exposure.id = assignment.id || 0 - exposure.name = assignment.name - exposure.unit = assignment.unit_type - exposure.variant = assignment.variant - exposure.exposed_at = @clock.to_i - exposure.assigned = assignment.assigned - exposure.eligible = assignment.eligible - exposure.overridden = assignment.overridden - exposure.full_on = assignment.full_on - exposure.custom = assignment.custom - exposure.audience_mismatch = assignment.audience_mismatch - - @pending_count += 1 - @exposures.push(exposure) - log_event(ContextEventLogger::EVENT_TYPE::EXPOSURE, exposure) - end - end - - def peek_treatment(experiment_name) - check_ready?(true) - - assignment(experiment_name).variant - end - - alias peek peek_treatment - - def variable_keys - check_ready?(true) - - hsh = {} - @index_variables.each { |key, value| hsh[key] = value.data.name } - hsh - end - - def variable_value(key, default_value) - check_ready?(true) - - assignment = variable_assignment(key) - unless assignment.nil? || assignment.variables.nil? - queue_exposure(assignment) unless assignment.exposed - return assignment.variables[key.to_s.to_sym] if assignment.variables.key?(key.to_s.to_sym) - end - - default_value - end - - def custom_field_keys - check_ready?(true) - keys = [] - - @data.experiments.each do |experiment| - custom_field_values = experiment.custom_field_values - if custom_field_values != nil - custom_field_values.each do |custom_field| - keys.append(custom_field.name) - end - end - end - - return keys.sort.uniq - end - - def custom_field_value(experimentName, key) - check_ready?(true) - - experiment_custom_fields = @context_custom_fields[experimentName] - - if experiment_custom_fields != nil - field = experiment_custom_fields[key] - if field != nil - return field.value - end - end - - return nil - end - - def custom_field_type(experimentName, key) - check_ready?(true) - - experiment_custom_fields = @context_custom_fields[experimentName] - - if experiment_custom_fields != nil - field = experiment_custom_fields[key] - if field != nil - return field.type - end - end - - return nil - end - - def peek_variable_value(key, default_value) - check_ready?(true) - - assignment = variable_assignment(key) - return assignment.variables[key.to_s.to_sym] if !assignment.nil? && - !assignment.variables.nil? && - assignment.variables.key?(key.to_s.to_sym) - - default_value - end - - def track(goal_name, properties) - check_not_closed? - - achievement = GoalAchievement.new - achievement.achieved_at = @clock.to_i - achievement.name = goal_name - achievement.properties = properties - - @pending_count += 1 - @achievements.push(achievement) - log_event(ContextEventLogger::EVENT_TYPE::GOAL, achievement) - end - - def publish - check_not_closed? - - flush - end - - def refresh - check_not_closed? - - unless @failed - data_future = @data_provider.context_data - if data_future.success? - assign_data(data_future.data_future) - log_event(ContextEventLogger::EVENT_TYPE::REFRESH, data_future.data_future) - else - set_data_failed(data_future.exception) - log_error(data_future.exception) - end - end - end - - def close - unless @closed - if @pending_count > 0 - flush - end - @closed = true - log_event(ContextEventLogger::EVENT_TYPE::CLOSE, nil) - end - end - - def data - check_ready?(true) - - @data - end - - private - def flush - if !@failed - if @pending_count > 0 - exposures = nil - achievements = nil - event_count = @pending_count - - if event_count > 0 - unless @exposures.empty? - exposures = @exposures - @exposures = [] - end - - unless @achievements.empty? - achievements = @achievements - @achievements = [] - end - - @pending_count = 0 - - event = PublishEvent.new - event.hashed = true - event.published_at = @clock.to_i - event.units = @units.map do |key, value| - Unit.new(key.to_s, unit_hash(key, value)) - end - event.exposures = exposures - event.attributes = @attributes unless @attributes.empty? - event.goals = achievements unless achievements.nil? - log_event(ContextEventLogger::EVENT_TYPE::PUBLISH, event) - @event_handler.publish(self, event) - end - end - else - @exposures = [] - @achievements = [] - @pending_count = 0 - @data_failed - end - end - - def check_not_closed? - if @closed - raise IllegalStateException.new("ABSmartly Context is closed") - end - end - - def check_ready?(expect_not_closed) - if !ready? - raise IllegalStateException.new("ABSmartly Context is not yet ready") - elsif expect_not_closed - check_not_closed? - end - end - - def experiment_matches(experiment, assignment) - experiment.id == assignment.id && - experiment.unit_type == assignment.unit_type && - experiment.iteration == assignment.iteration && - experiment.full_on_variant == assignment.full_on_variant && - experiment.traffic_split == assignment.traffic_split - end - - def audience_matches(experiment, assignment) - if !experiment.audience.nil? && experiment.audience.size > 0 - if @attrs_seq > (assignment.attrs_seq || 0) - attrs = @attributes.inject({}) do |hash, attr| - hash[attr.name] = attr.value - hash - end - match = @audience_matcher.evaluate(experiment.audience, attrs) - new_audience_mismatch = match && !match.result - - if new_audience_mismatch != assignment.audience_mismatch - return false - end - - assignment.attrs_seq = @attrs_seq - end - end - true - end - - def assignment(experiment_name) - assignment = @assignment_cache[experiment_name.to_s] - - if !assignment.nil? - custom = @cassignments.transform_keys(&:to_sym)[experiment_name.to_s.to_sym] - override = @overrides.transform_keys(&:to_sym)[experiment_name.to_s.to_sym] - experiment = experiment(experiment_name.to_s) - if !override.nil? - if assignment.overridden && assignment.variant == override - return assignment - end - elsif experiment.nil? - if !assignment.assigned - return assignment - end - elsif custom.nil? || custom == assignment.variant - if experiment_matches(experiment.data, assignment) && audience_matches(experiment.data, assignment) - return assignment - end - end - end - - custom = @cassignments.transform_keys(&:to_sym)[experiment_name.to_s.to_sym] - override = @overrides.transform_keys(&:to_sym)[experiment_name.to_s.to_sym] - experiment = experiment(experiment_name.to_s) - - assignment = Assignment.new - assignment.name = experiment_name - assignment.eligible = true - - if !override.nil? - unless experiment.nil? - assignment.id = experiment.data.id - assignment.unit_type = experiment.data.unit_type - end - - assignment.overridden = true - assignment.variant = override - else - unless experiment.nil? - unit_type = experiment.data.unit_type - - if !experiment.data.audience.nil? && experiment.data.audience.size > 0 - attrs = @attributes.inject({}) do |hash, attr| - hash[attr.name] = attr.value - hash - end - match = @audience_matcher.evaluate(experiment.data.audience, attrs) - if match && !match.result - assignment.audience_mismatch = true - end - end - - if experiment.data.audience_strict && assignment.audience_mismatch - assignment.variant = 0 - elsif experiment.data.full_on_variant == 0 - uid = @units.transform_keys(&:to_sym)[experiment.data.unit_type.to_sym] - unless uid.nil? - assigner = VariantAssigner.new(uid) - eligible = assigner.assign(experiment.data.traffic_split, - experiment.data.traffic_seed_hi, - experiment.data.traffic_seed_lo) == 1 - if eligible - if !custom.nil? - assignment.variant = custom - assignment.custom = true - else - assignment.variant = assigner.assign(experiment.data.split, - experiment.data.seed_hi, - experiment.data.seed_lo) - end - else - assignment.eligible = false - assignment.variant = 0 - end - assignment.assigned = true - end - else - assignment.assigned = true - assignment.variant = experiment.data.full_on_variant - assignment.full_on = true - end - - assignment.unit_type = unit_type - assignment.id = experiment.data.id - assignment.iteration = experiment.data.iteration - assignment.traffic_split = experiment.data.traffic_split - assignment.full_on_variant = experiment.data.full_on_variant - assignment.attrs_seq = @attrs_seq - end - end - - if !experiment.nil? && assignment.variant >= 0 && assignment.variant < experiment.data.variants.length - assignment.variables = experiment.variables[assignment.variant] || {} - end - - @assignment_cache[experiment_name.to_s] = assignment - assignment - end - - def variable_assignment(key) - experiment = variable_experiment(key) - - assignment(experiment.data.name) unless experiment.nil? - end - - def experiment(experiment) - @index.transform_keys(&:to_sym)[experiment.to_s.to_sym] - end - - def variable_experiment(key) - @index_variables.transform_keys(&:to_sym)[key.to_s.to_sym] - end - - def unit_hash(unit_type, unit_uid) - @hashed_units[unit_type] = Hashing.hash_unit(unit_uid) - end - - def variant_assigner(unit_type, unit_hash) - @assigners[unit_type] ||= VariantAssigner.new(unit_hash) - end - - def assign_data(data) - @data = data - @index = {} - @index_variables = {} - - if data && !data.experiments.nil? && !data.experiments.empty? - data.experiments.each do |experiment| - @experimentCustomFieldValues = {} - - experiment_variables = ExperimentVariables.new - experiment_variables.data = experiment - experiment_variables.variables ||= [] - experiment.variants.each do |variant| - if !variant.config.nil? && !variant.config.empty? - variables = @variable_parser.parse(self, experiment.name, variant.name, - variant.config) - variables.keys.each { |key| @index_variables[key] = experiment_variables } - experiment_variables.variables.push(variables) - else - experiment_variables.variables.push({}) - end - end - - if !experiment.custom_field_values.nil? - experiment.custom_field_values.each do |custom_field_value| - value = ContextCustomFieldValues.new - value.type = custom_field_value.type - - if !custom_field_value.value.nil? - custom_value = custom_field_value.value - - if custom_field_value.type.start_with?("json") - value.value = @variable_parser.parse(self, experiment.name, custom_field_value.name, custom_value) - - elsif custom_field_value.type.start_with?("boolean") - value.value = custom_value == "true" - - elsif custom_field_value.type.start_with?("number") - value.value = custom_value.to_i - - else - value.value = custom_field_value.value - end - - @experimentCustomFieldValues[custom_field_value.name] = value - - end - - end - end - - @index[experiment.name] = experiment_variables - @context_custom_fields[experiment.name] = @experimentCustomFieldValues - end - end - end - - def set_data_failed(exception) - @data_failed = exception - @index = {} - @index_variables = {} - @data = ContextData.new - @failed = true - end - - def log_event(event, data) - unless @event_logger.nil? - @event_logger.handle_event(event, data) - end - end - - def log_error(error) - unless @event_logger.nil? - @event_logger.handle_event(ContextEventLogger::EVENT_TYPE::ERROR, error.message) - end - end - - attr_accessor :clock, - :publish_delay, - :event_handler, - :event_logger, - :data_provider, - :variable_parser, - :audience_matcher, - :units, - :failed, - :data_lock, - :index, - :index_variables, - :context_lock, - :hashed_units, - :assigners, - :assignment_cache, - :event_lock, - :exposures, - :achievements, - :attributes, - :overrides, - :cassignments, - :closed, - :refreshing, - :ready_future, - :refresh_future - attr_writer :data, :pending_count -end - -class Assignment - attr_accessor :id, :iteration, :full_on_variant, :name, :unit_type, - :traffic_split, :variant, :assigned, :overridden, :eligible, - :full_on, :custom, :audience_mismatch, :variables, :exposed, :attrs_seq - - def initialize - @variant = 0 - @iteration = 0 - @full_on_variant = 0 - @overridden = false - @assigned = false - @exposed = false - @eligible = true - @full_on = false - @custom = false - @audience_mismatch = false - @attrs_seq = 0 - end -end - -class ExperimentVariables - attr_accessor :data, :variables -end - -class ContextCustomFieldValues - attr_accessor :type, :value -end - -class IllegalStateException < StandardError -end diff --git a/lib/context_config.rb b/lib/context_config.rb deleted file mode 100644 index 9c036c6..0000000 --- a/lib/context_config.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -class ContextConfig - attr_accessor :units, :attributes, :custom_assignments, :overrides, - :event_logger, :publish_delay, :refresh_interval - - def self.create - ContextConfig.new - end - - def initialize - @units = {} - @attributes = {} - @overrides = {} - @custom_assignments = {} - end - - def set_unit(unit_type, uid) - @units[unit_type.to_sym] = uid - self - end - - def set_units(units) - units.map { |k, v| set_unit(k, v) } - end - - def unit(unit_type) - @units[unit_type.to_sym] - end - - def set_attributes(attributes) - @attributes.merge!(attributes.transform_keys(&:to_sym)) - self - end - - def set_attribute(name, value) - @attributes[name.to_sym] = value - self - end - - def attribute(name) - @attributes[name.to_sym] - end - - def set_overrides(overrides) - @overrides.merge!(overrides.transform_keys(&:to_sym)) - end - - def set_override(experiment_name, variant) - @overrides[experiment_name.to_sym] = variant - self - end - - def override(experiment_name) - @overrides[experiment_name.to_sym] - end - - def set_custom_assignment(experiment_name, variant) - @custom_assignments[experiment_name.to_sym] = variant - self - end - - def set_custom_assignments(customAssignments) - @custom_assignments.merge!(customAssignments.transform_keys(&:to_sym)) - self - end - - 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/context_data_deserializer.rb b/lib/context_data_deserializer.rb deleted file mode 100644 index 7589f27..0000000 --- a/lib/context_data_deserializer.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class ContextDataDeserializer - # @interface method - def deserialize(bytes, offset, length) - raise NotImplementedError.new("You must implement deserialize method.") - end -end diff --git a/lib/context_data_provider.rb b/lib/context_data_provider.rb deleted file mode 100644 index 9cf2e1e..0000000 --- a/lib/context_data_provider.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class ContextDataProvider - # @interface method - def context_data - raise NotImplementedError.new("You must implement context_data method.") - end -end diff --git a/lib/context_event_handler.rb b/lib/context_event_handler.rb deleted file mode 100644 index 8757196..0000000 --- a/lib/context_event_handler.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class ContextEventHandler - # @interface method - def publish(context, event) - raise NotImplementedError.new("You must implement publish method.") - end -end diff --git a/lib/context_event_logger.rb b/lib/context_event_logger.rb deleted file mode 100644 index 4aab7b1..0000000 --- a/lib/context_event_logger.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class ContextEventLogger - module EVENT_TYPE - ERROR = "error" - READY = "ready" - REFRESH = "refresh" - PUBLISH = "publish" - EXPOSURE = "exposure" - GOAL = "goal" - CLOSE = "close" - end - - def handle_event(event, data) - raise NotImplementedError.new("You must implement handle_event method.") - end -end diff --git a/lib/context_event_logger_callback.rb b/lib/context_event_logger_callback.rb deleted file mode 100644 index b6b76b9..0000000 --- a/lib/context_event_logger_callback.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -class ContextEventLoggerCallback < ContextEventLogger - attr_accessor :callable - - def initialize(callable) - @callable = callable - end - - def handle_event(event, data) - @callable.call(event, data) if @callable.present? - end -end diff --git a/lib/context_event_serializer.rb b/lib/context_event_serializer.rb deleted file mode 100644 index 4d0cd21..0000000 --- a/lib/context_event_serializer.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class ContextEventSerializer - # @interface method - def serialize(publish_event) - raise NotImplementedError.new("You must implement serialize method.") - end -end diff --git a/lib/default_audience_deserializer.rb b/lib/default_audience_deserializer.rb deleted file mode 100644 index 0d4ac12..0000000 --- a/lib/default_audience_deserializer.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require_relative "audience_deserializer" - -class DefaultAudienceDeserializer < AudienceDeserializer - attr_accessor :log, :reader - - def deserialize(bytes, offset, length) - JSON.parse(bytes[offset..length], symbolize_names: true) - rescue JSON::ParserError - nil - end -end diff --git a/lib/default_context_data_deserializer.rb b/lib/default_context_data_deserializer.rb deleted file mode 100644 index 7a5b4a8..0000000 --- a/lib/default_context_data_deserializer.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require "json" -require_relative "context_data_deserializer" -require_relative "json/context_data" - -class DefaultContextDataDeserializer < ContextDataDeserializer - attr_accessor :log, :reader - - def deserialize(bytes, offset, length) - parse = JSON.parse(bytes[offset..length], symbolize_names: true) - @reader = ContextData.new(parse[:experiments]) - rescue JSON::ParserError - nil - end -end diff --git a/lib/default_context_data_provider.rb b/lib/default_context_data_provider.rb deleted file mode 100644 index dff170c..0000000 --- a/lib/default_context_data_provider.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require_relative "context_data_provider" - -class DefaultContextDataProvider < ContextDataProvider - attr_accessor :client - - def initialize(client) - @client = client - end - - def context_data - @client.context_data - end -end diff --git a/lib/default_context_event_handler.rb b/lib/default_context_event_handler.rb deleted file mode 100644 index caecf68..0000000 --- a/lib/default_context_event_handler.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require_relative "context_event_handler" - -class DefaultContextEventHandler < ContextEventHandler - attr_accessor :client - - def initialize(client) - @client = client - end - - def publish(context, event) - @client.publish(event) - end -end diff --git a/lib/default_context_event_serializer.rb b/lib/default_context_event_serializer.rb deleted file mode 100644 index e2630ce..0000000 --- a/lib/default_context_event_serializer.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require_relative "context_event_serializer" - -class DefaultContextEventSerializer < ContextEventSerializer - def serialize(event) - units = event.units.nil? ? [] : event.units.map do |unit| - { - type: unit.type, - uid: unit.uid, - } - end - req = {} - unless units.empty? - req = { - publishedAt: event.published_at, - units: units, - hashed: event.hashed - } - end - - req[:goals] = event.goals.map do |x| - { - name: x.name, - achievedAt: x.achieved_at, - properties: x.properties, - } - end unless event.goals.nil? - - req[:exposures] = event.exposures.select { |x| !x.id.nil? }.map do |x| - { - id: x.id, - name: x.name, - unit: x.unit, - exposedAt: x.exposed_at.to_i, - variant: x.variant, - assigned: x.assigned, - eligible: x.eligible, - overridden: x.overridden, - fullOn: x.full_on, - custom: x.custom, - audienceMismatch: x.audience_mismatch - } - end unless event.exposures.nil? - - req[:attributes] = event.attributes.map do |x| - { - name: x.name, - value: x.value, - setAt: x.set_at, - } - end unless event.attributes.nil? - - return nil if req.empty? - - req.to_json - end -end diff --git a/lib/default_http_client.rb b/lib/default_http_client.rb deleted file mode 100644 index 8381cdb..0000000 --- a/lib/default_http_client.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require "faraday" -require "faraday/net_http_persistent" -require "faraday/retry" -require "uri" -require_relative "http_client" - -class DefaultHttpClient < HttpClient - attr_accessor :config, :session - - def self.create(config) - DefaultHttpClient.new(config) - end - - def initialize(config) - @config = config - @session = Faraday.new("") do |f| - f.request :retry, - max: config.max_retries, - interval: config.retry_interval, - interval_randomness: 0.5, - backoff_factor: 2 - f.options.timeout = config.connect_timeout - f.options.open_timeout = config.connection_request_timeout - f.adapter :net_http_persistent, pool_size: config.pool_size do |http| - http.idle_timeout = config.pool_idle_timeout - end - end - end - - # def context_data - # end - - def get(url, query, headers) - @session.get(url, query, headers) - end - - def put(url, query, headers, body) - @session.put(add_tracking(url, query), body, headers) - end - - def post(url, query, headers, body) - @session.post(add_tracking(url, query), body, headers) - end - - def close - @session.close - end - - def self.default_response(status_code, status_message, content_type, content) - env = Faraday::Env.from(status: status_code, body: content || status_message, - response_headers: { "Content-Type" => content_type }) - Faraday::Response.new(env) - end - - private - def add_tracking(url, params) - parsed = URI.parse(url) - query = parsed.query ? CGI.parse(parsed.query) : {} - query = query.merge(params) if params && params.is_a?(Hash) - parsed.query = URI.encode_www_form(query) - str = parsed.to_s - str[-1] == "?" ? str.chop : str - end -end diff --git a/lib/default_http_client_config.rb b/lib/default_http_client_config.rb deleted file mode 100644 index 1cd212f..0000000 --- a/lib/default_http_client_config.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class DefaultHttpClientConfig - attr_accessor :connect_timeout, - :connection_request_timeout, - :retry_interval, - :max_retries, - :pool_size, - :pool_idle_timeout - - def self.create - DefaultHttpClientConfig.new - end - - def initialize - @connect_timeout = 3.0 - @connection_request_timeout = 3.0 - @retry_interval = 0.5 - @max_retries = 5 - @pool_size = 20 - @pool_idle_timeout = 5 - end -end diff --git a/lib/default_variable_parser.rb b/lib/default_variable_parser.rb deleted file mode 100644 index ee3b89f..0000000 --- a/lib/default_variable_parser.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require_relative "variable_parser" - -class DefaultVariableParser < VariableParser - attr_accessor :reader, :log - - def parse(context, experiment_name, variant_name, config) - JSON.parse(config, symbolize_names: true) - rescue JSON::ParserError - nil - end -end diff --git a/lib/hashing.rb b/lib/hashing.rb deleted file mode 100644 index 81de948..0000000 --- a/lib/hashing.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require "digest" - -class Hashing - def self.hash_unit(str) - Digest::MD5.base64digest(str.to_s).gsub("==", "").gsub("+", "-").gsub("/", "_") - end -end diff --git a/lib/http_client.rb b/lib/http_client.rb deleted file mode 100644 index 8cd4130..0000000 --- a/lib/http_client.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class HttpClient - # @interface method - def response - raise NotImplementedError.new("You must implement response method.") - end - - def get(url, query, headers) - raise NotImplementedError.new("You must implement get method.") - end - - def post(url, query, headers, body) - raise NotImplementedError.new("You must implement post method.") - end - - def put(url, query, headers, body) - raise NotImplementedError.new("You must implement put method.") - end -end diff --git a/lib/json/attribute.rb b/lib/json/attribute.rb deleted file mode 100644 index 2cf8ab4..0000000 --- a/lib/json/attribute.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -class Attribute - attr_accessor :name, :value, :set_at - - def initialize(name = nil, value = nil, set_at = nil) - @name = name - @value = value - @set_at = set_at - end - - def ==(o) - return true if self.object_id == o.object_id - return false if o.nil? || self.class != o.class - - @name == o.name && @value == o.value && @set_at == o.set_at - end - - def hash_code - { name: @name, value: @value, set_at: @set_at } - end - - def to_s - "Attribute{" + - "name='#{@name}'" + - ", value=#{@value}" + - ", setAt=#{@set_at}" + - "}" - end -end diff --git a/lib/json/context_data.rb b/lib/json/context_data.rb deleted file mode 100644 index 196188e..0000000 --- a/lib/json/context_data.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require_relative "experiment" - -class ContextData - attr_accessor :experiments - - def initialize(experiments = []) - @experiments = experiments.map do |experiment| - Experiment.new(experiment) - end unless experiments.nil? - self - end - - def ==(o) - return true if self.object_id == o.object_id - return false if o.nil? || self.class != o.class - - @experiments == o.experiments - end - - def hash_code - { name: @name, config: @config } - end - - def to_s - "ContextData{" + - "experiments='" + @experiments.join + - "}" - end -end diff --git a/lib/json/custom_field_value.rb b/lib/json/custom_field_value.rb deleted file mode 100644 index 286dd23..0000000 --- a/lib/json/custom_field_value.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class CustomFieldValue - attr_accessor :name, :type, :value - - def initialize(name, value, type) - @name = name - @type = type - @value = value - end - - def ==(o) - return true if self.object_id == o.object_id - return false if o.nil? || self.class != o.class - - that = o - @name == that.name && @type == that.type && @value == that.value - end - - def hash_code - { name: @name, type: @type, value: @value } - end - - def to_s - "CustomFieldValue{" + - "name='#{@name}'" + - ", type='#{@type}'" + - ", value='#{@value}'" + - "}" - end -end diff --git a/lib/json/experiment.rb b/lib/json/experiment.rb deleted file mode 100644 index 42e70e3..0000000 --- a/lib/json/experiment.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -require_relative "../string" -require_relative "experiment_application" -require_relative "experiment_variant" -require_relative "custom_field_value" - -class Experiment - attr_accessor :id, :name, :unit_type, :iteration, :seed_hi, :seed_lo, :split, - :traffic_seed_hi, :traffic_seed_lo, :traffic_split, :full_on_variant, - :applications, :variants, :audience_strict, :audience, :custom_field_values - - def initialize(args = {}) - args.each do |name, value| - if name == :applications - @applications = assign_to_klass(ExperimentApplication, value) - elsif name == :variants - @variants = assign_to_klass(ExperimentVariant, value) - elsif name == :customFieldValues - if value != nil - @custom_field_values = assign_to_klass(CustomFieldValue, value) - end - else - self.instance_variable_set("@#{name.to_s.underscore}", value) - end - end - @audience_strict ||= false - self - end - - def assign_to_klass(klass, arr) - arr.map do |item| - return item if item.is_a?(klass) - - klass.new(*item.values) - end - end - - def ==(o) - return true if self.object_id == o.object_id - return false if o.nil? || self.class != o.class - - that = o - @id == that.id && @iteration == that.iteration && @seed_hi == that.seed_hi && @seed_lo == that.seed_lo && - @traffic_seed_hi == that.traffic_seed_hi && @traffic_seed_lo == that.traffic_seed_lo && - @full_on_variant == that.full_on_variant && @name == that.name && - @unit_type == that.unit_type && @split == that.split && - @traffic_split == that.traffic_split && @applications == that.applications && - @variants == that.variants && @audience_strict == that.audience_strict && - @audience == that.audience && @custom_field_values == that.custom_field_values - end - - def hash_code - { - id: @id, - name: @name, - unit_type: @unit_type, - iteration: @iteration, - seed_hi: @seed_hi, - seed_lo: @seed_lo, - traffic_seed_hi: @traffic_seed_hi, - traffic_seed_lo: @traffic_seed_lo, - full_on_variant: @full_on_variant, - audience_strict: @audience_strict, - audience: @audience, - custom_field_values: @custom_field_values - } - end - - def to_s - "ContextExperiment{" + - "id= #{@id}"+ - ", name='#{@name}'" + - ", unitType='#{@unit_type}'" + - ", iteration=#{@iteration}" + - ", seedHi=#{@seed_hi}" + - ", seedLo=#{@seed_lo}" + - ", split=#{@split.join}" + - ", trafficSeedHi=#{@traffic_seed_hi}" + - ", trafficSeedLo=#{@traffic_seed_lo}" + - ", trafficSplit=#{@traffic_split.join}" + - ", fullOnVariant=#{@full_on_variant}" + - ", applications=#{@applications.join}" + - ", variants=#{@variants.join}" + - ", audienceStrict=#{@audience_strict}" + - ", audience='#{@audience}'" + - ", custom_field_values='#{@custom_field_values}'" + - "}" - end -end diff --git a/lib/json/experiment_application.rb b/lib/json/experiment_application.rb deleted file mode 100644 index 1fc35e3..0000000 --- a/lib/json/experiment_application.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -class ExperimentApplication - attr_accessor :name - - def initialize(name = nil) - @name = name - end - - def ==(o) - return true if self.object_id == o.object_id - return false if o.nil? || self.class != o.class - - that = o - @name == that.name - end - - def hash_code - { name: @name } - end - - def to_s - "ExperimentApplication{" + - "name='" + @name + "'" + - "}" - end -end diff --git a/lib/json/experiment_variant.rb b/lib/json/experiment_variant.rb deleted file mode 100644 index b833280..0000000 --- a/lib/json/experiment_variant.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -class ExperimentVariant - attr_accessor :name, :config - - def initialize(name = nil, config) - @name = name - @config = config - end - - def ==(o) - return true if self.object_id == o.object_id - return false if o.nil? || self.class != o.class - - that = o - @name == that.name && @config == that.config - end - - def hash_code - { name: @name, config: @config } - end - - def to_s - "ExperimentVariant{" + - "name='#{@name}'" + - ", config='#{@config}'" + - "}" - end -end diff --git a/lib/json/exposure.rb b/lib/json/exposure.rb deleted file mode 100644 index 69eadfe..0000000 --- a/lib/json/exposure.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -class Exposure - attr_accessor :id, :name, :unit, :variant, :exposed_at, :assigned, :eligible, - :overridden, :full_on, :custom, :audience_mismatch - - def initialize(id = nil, name = nil, unit = nil, variant = nil, - exposed_at = nil, assigned = nil, eligible = nil, - overridden = nil, full_on = nil, custom = nil, - audience_mismatch = nil) - @id = id || 0 - @name = name - @unit = unit - @variant = variant - @exposed_at = exposed_at - @assigned = assigned - @eligible = eligible - @overridden = overridden - @full_on = full_on - @custom = custom - @audience_mismatch = audience_mismatch - end - - def ==(o) - return true if self.object_id == o.object_id - return false if o.nil? || self.class != o.class - - @id == o.id && @name == o.name && @unit == o.unit && - @variant == o.variant && @exposed_at == o.exposed_at && - @assigned == o.assigned && @eligible == o.eligible && - @overridden == o.overridden && @full_on == o.full_on && - @custom == o.custom && @audience_mismatch == o.audience_mismatch - end - - def hash_code - { - id: @id, name: @name, unit: @unit, - variant: @variant, exposed_at: @exposed_at, - assigned: @assigned, eligible: @eligible, - overridden: @overridden, full_on: @full_on, - custom: @custom, audience_mismatch: @audience_mismatch - } - end - - def to_s - "Exposure{" + - "id=#{@id}" + - "name='#{@name}'" + - ", unit=#{@unit}" + - ", variant=#{@variant}" + - ", exposed_at=#{@exposed_at}" + - ", assigned=#{@assigned}" + - ", eligible=#{@eligible}" + - ", overridden=#{@overridden}" + - ", full_on=#{@full_on}" + - ", custom=#{@custom}" + - ", audience_mismatch=#{@audience_mismatch}" + - "}" - end -end diff --git a/lib/json/goal_achievement.rb b/lib/json/goal_achievement.rb deleted file mode 100644 index 97a3240..0000000 --- a/lib/json/goal_achievement.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -class GoalAchievement - attr_accessor :name, :achieved_at, :properties - - def initialize(name = nil, achieved_at = nil, properties = nil) - @name = name - @achieved_at = achieved_at - @properties = properties - end - - def ==(o) - return true if self.object_id == o.object_id - return false if o.nil? || self.class != o.class - - that = o - @name == that.name && @achieved_at == that.achieved_at && - @properties == that.properties - end - - def hash_code - { name: @name, achieved_at: @achieved_at, properties: @properties } - end - - def to_s - "GoalAchievement{" + - "name='#{@name}'" + - ", achieved_at='#{@achieved_at}'" + - ", properties='#{@properties.inspect}'" + - "}" - end -end diff --git a/lib/json/publish_event.rb b/lib/json/publish_event.rb deleted file mode 100644 index b765cde..0000000 --- a/lib/json/publish_event.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -class PublishEvent - attr_accessor :hashed, :units, :published_at, :exposures, :goals, :attributes - - def initialize - @published_at = 0 - @hashed = false - end - - def ==(o) - return true if self.object_id == o.object_id - return false if o.nil? || self.class != o.class - - that = o - @hashed == that.hashed && @units == that.units && - @published_at == that.published_at && @exposures == that.exposures && - @goals == that.goals && @attributes == that.attributes - end - - def hash_code - { - hashed: @hashed, - units: @units, - published_at: @published_at, - exposures: @exposures, - goals: @goals, - attributes: @attributes - } - end - - def to_s - "PublishEvent{" + - "hashedUnits=#{@hashed}" + - ", units=#{@units.inspect}" + - ", publishedAt=#{@published_at}" + - ", exposures=#{@exposures.inspect}" + - ", goals=#{@goals.inspect}" + - ", attributes=#{@attributes!=nil ? @attributes.join : ""}" + - "}" - end -end diff --git a/lib/json/unit.rb b/lib/json/unit.rb deleted file mode 100644 index 42afe74..0000000 --- a/lib/json/unit.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -class Unit - attr_accessor :type, :uid - - def initialize(type = nil, uid = nil) - @type = type - @uid = uid - end - - def ==(o) - return true if self.object_id == o.object_id - return false if o.nil? || self.class != o.class - - @type == o.type && @uid == o.uid - end - - def hash_code - { - type: @type, uid: @uid - } - end - - def to_s - "Unit{" + - "type='" + @type + "'" + - ", uid=" + @uid + - "}" - end -end diff --git a/lib/json_expr/evaluator.rb b/lib/json_expr/evaluator.rb deleted file mode 100644 index e50a65e..0000000 --- a/lib/json_expr/evaluator.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -class Evaluator - def evaluate(expr) - raise NotImplementedError.new("You must implement evaluate method.") - end - - def boolean_convert(_) - raise NotImplementedError.new("You must implement boolean convert method.") - end - - def number_convert(_) - raise NotImplementedError.new("You must implement number convert method.") - end - - def string_convert(_) - raise NotImplementedError.new("You must implement string convert method.") - end - - def extract_var(_) - raise NotImplementedError.new("You must implement extract var method.") - end - - def compare(_, _) - raise NotImplementedError.new("You must implement extract_var method.") - end -end diff --git a/lib/json_expr/expr_evaluator.rb b/lib/json_expr/expr_evaluator.rb deleted file mode 100644 index 8e73197..0000000 --- a/lib/json_expr/expr_evaluator.rb +++ /dev/null @@ -1,121 +0,0 @@ -# frozen_string_literal: true - -require_relative "../string" -require_relative "./evaluator" -EMPTY_MAP = {} -EMPTY_LIST = [] - -class ExprEvaluator < Evaluator - attr_accessor :operators - attr_accessor :vars - NUMERIC_REGEX = /\A[-+]?[0-9]*\.?[0-9]+\Z/ - - def initialize(operators, vars) - @operators = operators - @vars = vars - end - - def evaluate(expr) - if expr.is_a? Array - return @operators[:and].evaluate(self, expr) - elsif expr.is_a? Hash - expr.transform_keys(&:to_sym).each do |key, value| - if @operators[key] - return @operators[key].evaluate(self, value) - end - end - end - nil - end - - def boolean_convert(x) - if x.is_a?(TrueClass) || x.is_a?(FalseClass) - return x - elsif x.is_a?(Numeric) || !(x.to_s =~ NUMERIC_REGEX).nil? - return !x.to_f.zero? - elsif x.is_a?(String) - return x != "false" && x != "0" && x != "" - end - - !x.nil? - end - - def number_convert(x) - return if x.nil? || x.to_s.empty? - - if x.is_a?(Numeric) || !(x.to_s =~ NUMERIC_REGEX).nil? - return x.to_f - elsif x.is_a?(TrueClass) || x.is_a?(FalseClass) - return x ? 1.0 : 0.0 - end - nil - end - - def string_convert(x) - if x.is_a?(String) - return x - elsif x.is_a?(TrueClass) || x.is_a?(FalseClass) - return x.to_s - elsif x.is_a?(Numeric) || !(x.to_s =~ NUMERIC_REGEX).nil? - return x == x.to_i ? x.to_i.to_s : x.to_s - end - nil - end - - def extract_var(path) - frags = path.split("/") - target = !vars.nil? ? vars : {} - - frags.each do |frag| - list = target - value = nil - if target.is_a?(Array) - value = list[frag.to_i] - elsif target.is_a?(Hash) - value = list[frag].nil? ? list[frag.to_sym] : list[frag] - end - - unless value.nil? - target = value - next - end - - return nil - end - target - end - - def compare(lhs, rhs) - if lhs.nil? - return rhs.nil? ? 0 : nil - elsif rhs.nil? - return nil - end - - if lhs.is_a?(Numeric) - rvalue = number_convert(rhs) - return lhs.to_f.to_s.casecmp(rvalue.to_s) unless rvalue.nil? - elsif lhs.is_a?(String) - rvalue = string_convert(rhs) - return lhs.compare_to(rvalue) unless rvalue.nil? - elsif lhs.is_a?(TrueClass) || lhs.is_a?(FalseClass) - rvalue = boolean_convert(rhs) - return lhs.to_s.casecmp(rvalue.to_s) unless rvalue.nil? - elsif lhs.class == rhs.class && lhs === rhs - return 0 - end - nil - end -end - -class Array - def self.wrap(object) - if object.nil? - [] - elsif object.respond_to?(:to_ary) - object.to_ary || [object] - else - [object] - end - end -end diff --git a/lib/json_expr/json_expr.rb b/lib/json_expr/json_expr.rb deleted file mode 100644 index 4bc93f9..0000000 --- a/lib/json_expr/json_expr.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -require_relative "./expr_evaluator" -require 'json_expr/operators/and_combinator' -require 'json_expr/operators/binary_operator' -require 'json_expr/operators/boolean_combinator' -require 'json_expr/operators/equals_operator' -require 'json_expr/operators/greater_than_operator' -require 'json_expr/operators/greater_than_or_equal_operator' -require 'json_expr/operators/in_operator' -require 'json_expr/operators/less_than_operator' -require 'json_expr/operators/less_than_or_equal_operator' -require 'json_expr/operators/match_operator' -require 'json_expr/operators/nil_operator' -require 'json_expr/operators/not_operator' -require 'json_expr/operators/or_combinator' -require 'json_expr/operators/unary_operator' -require 'json_expr/operators/value_operator' -require 'json_expr/operators/var_operator' - -class JsonExpr - attr_accessor :operators - attr_accessor :vars - - def initialize - @operators = { - "and": AndCombinator.new, - "or": OrCombinator.new, - "value": ValueOperator.new, - "var": VarOperator.new, - "null": NilOperator.new, - "not": NotOperator.new, - "in": InOperator.new, - "match": MatchOperator.new, - "eq": EqualsOperator.new, - "gt": GreaterThanOperator.new, - "gte": GreaterThanOrEqualOperator.new, - "lt": LessThanOperator.new, - "lte": LessThanOrEqualOperator.new - } - end - - def evaluate_boolean_expr(expr, vars) - evaluator = ExprEvaluator.new(operators, vars) - evaluator.boolean_convert(evaluator.evaluate(expr)) - end - - def evaluate_expr(expr, vars) - evaluator = ExprEvaluator.new(operators, vars) - evaluator.evaluate(expr) - end -end diff --git a/lib/json_expr/operator.rb b/lib/json_expr/operator.rb deleted file mode 100644 index a9a2efd..0000000 --- a/lib/json_expr/operator.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -module Operator - # @interface method - def evaluate(evaluator, args) - raise NotImplementedError.new("You must implement evaluate method.") - end -end diff --git a/lib/json_expr/operators/and_combinator.rb b/lib/json_expr/operators/and_combinator.rb deleted file mode 100644 index f286ec2..0000000 --- a/lib/json_expr/operators/and_combinator.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require_relative "boolean_combinator" - -class AndCombinator - include BooleanCombinator - - def combine(evaluator, exprs) - Array.wrap(exprs).each do |expr| - return false unless evaluator.boolean_convert(evaluator.evaluate(expr)) - end - true - end -end diff --git a/lib/json_expr/operators/binary_operator.rb b/lib/json_expr/operators/binary_operator.rb deleted file mode 100644 index be2bf98..0000000 --- a/lib/json_expr/operators/binary_operator.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module BinaryOperator - def evaluate(evaluator, args) - if args.is_a? Array - args_list = args - lhs = args_list.size > 0 ? evaluator.evaluate(args_list[0]) : nil - unless lhs.nil? - rhs = args_list.size > 1 ? evaluator.evaluate(args_list[1]) : nil - unless rhs.nil? - return binary(evaluator, lhs, rhs) - end - end - end - nil - end - - # @abstract method - def binary(evaluator, lhs, rhs) - raise NotImplementedError.new("You must implement binary method.") - end -end diff --git a/lib/json_expr/operators/boolean_combinator.rb b/lib/json_expr/operators/boolean_combinator.rb deleted file mode 100644 index 3dd1521..0000000 --- a/lib/json_expr/operators/boolean_combinator.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module BooleanCombinator - def evaluate(evaluator, args) - if args.is_a? Array - return combine(evaluator, args) - end - nil - end - - # @abstract method - def combine(evaluator, args) - raise NotImplementedError.new("You must implement combine method.") - end -end diff --git a/lib/json_expr/operators/equals_operator.rb b/lib/json_expr/operators/equals_operator.rb deleted file mode 100644 index 743fc43..0000000 --- a/lib/json_expr/operators/equals_operator.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require_relative "binary_operator" - -class EqualsOperator - include BinaryOperator - - def binary(evaluator, lhs, rhs) - result = evaluator.compare(lhs, rhs) - !result.nil? ? (result == 0) : nil - end -end diff --git a/lib/json_expr/operators/greater_than_operator.rb b/lib/json_expr/operators/greater_than_operator.rb deleted file mode 100644 index 4214472..0000000 --- a/lib/json_expr/operators/greater_than_operator.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require_relative "binary_operator" - -class GreaterThanOperator - include BinaryOperator - - def binary(evaluator, lhs, rhs) - result = evaluator.compare(lhs, rhs) - !result.nil? ? (result > 0) : nil - end -end diff --git a/lib/json_expr/operators/greater_than_or_equal_operator.rb b/lib/json_expr/operators/greater_than_or_equal_operator.rb deleted file mode 100644 index 31e97b8..0000000 --- a/lib/json_expr/operators/greater_than_or_equal_operator.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require_relative "binary_operator" - -class GreaterThanOrEqualOperator - include BinaryOperator - - def binary(evaluator, lhs, rhs) - result = evaluator.compare(lhs, rhs) - !result.nil? ? (result >= 0) : nil - end -end diff --git a/lib/json_expr/operators/in_operator.rb b/lib/json_expr/operators/in_operator.rb deleted file mode 100644 index 9a3ab4a..0000000 --- a/lib/json_expr/operators/in_operator.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require_relative "binary_operator" - -class InOperator - include BinaryOperator - - def binary(evaluator, haystack, needle) - if haystack.is_a? Array - haystack.each do |item| - return true if evaluator.compare(item, needle) == 0 - end - return false - elsif haystack.is_a? String - needle_string = evaluator.string_convert(needle) - return !needle_string.nil? && haystack.include?(needle_string) - elsif haystack.is_a?(Hash) - needle_string = evaluator.string_convert(needle) - return !needle_string.nil? && haystack.key?(needle_string) - end - nil - end -end diff --git a/lib/json_expr/operators/less_than_operator.rb b/lib/json_expr/operators/less_than_operator.rb deleted file mode 100644 index 3c02112..0000000 --- a/lib/json_expr/operators/less_than_operator.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require_relative "binary_operator" - -class LessThanOperator - include BinaryOperator - - def binary(evaluator, lhs, rhs) - result = evaluator.compare(lhs, rhs) - !result.nil? ? (result < 0) : nil - end -end diff --git a/lib/json_expr/operators/less_than_or_equal_operator.rb b/lib/json_expr/operators/less_than_or_equal_operator.rb deleted file mode 100644 index 4264505..0000000 --- a/lib/json_expr/operators/less_than_or_equal_operator.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -require_relative "binary_operator" - -class LessThanOrEqualOperator - include BinaryOperator - - def binary(evaluator, lhs, rhs) - result = evaluator.compare(lhs, rhs) - !result.nil? ? (result <= 0) : nil - end -end diff --git a/lib/json_expr/operators/match_operator.rb b/lib/json_expr/operators/match_operator.rb deleted file mode 100644 index 3d450fc..0000000 --- a/lib/json_expr/operators/match_operator.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require_relative "binary_operator" - -class MatchOperator - include BinaryOperator - - def binary(evaluator, lhs, rhs) - text = evaluator.string_convert(lhs) - unless text.nil? - pattern = evaluator.string_convert(rhs) - unless pattern.nil? - text.match(pattern) - end - end - end -end diff --git a/lib/json_expr/operators/nil_operator.rb b/lib/json_expr/operators/nil_operator.rb deleted file mode 100644 index 0e6fcc8..0000000 --- a/lib/json_expr/operators/nil_operator.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require_relative "./unary_operator" - -class NilOperator - include UnaryOperator - - def unary(evaluator, arg) - arg.nil? - end -end diff --git a/lib/json_expr/operators/not_operator.rb b/lib/json_expr/operators/not_operator.rb deleted file mode 100644 index 8f2f4fe..0000000 --- a/lib/json_expr/operators/not_operator.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require_relative "./unary_operator" - -class NotOperator - include UnaryOperator - - def unary(evaluator, args) - !evaluator.boolean_convert(args) - end -end diff --git a/lib/json_expr/operators/or_combinator.rb b/lib/json_expr/operators/or_combinator.rb deleted file mode 100644 index a7102fb..0000000 --- a/lib/json_expr/operators/or_combinator.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require_relative "boolean_combinator" - -class OrCombinator - include BooleanCombinator - - def combine(evaluator, exprs) - Array.wrap(exprs).each do |expr| - return true if evaluator.boolean_convert(evaluator.evaluate(expr)) - end - Array.wrap(exprs).empty? - end -end diff --git a/lib/json_expr/operators/unary_operator.rb b/lib/json_expr/operators/unary_operator.rb deleted file mode 100644 index 3510f7c..0000000 --- a/lib/json_expr/operators/unary_operator.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module UnaryOperator - def evaluate(evaluator, args) - arg = evaluator.evaluate(args) - unary(evaluator, arg) - end - - # @abstract method - def unary(evaluator, arg) - raise NotImplementedError.new("You must implement unnnary method.") - end -end diff --git a/lib/json_expr/operators/value_operator.rb b/lib/json_expr/operators/value_operator.rb deleted file mode 100644 index 3dde96d..0000000 --- a/lib/json_expr/operators/value_operator.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require_relative "binary_operator" - -class ValueOperator - include BinaryOperator - - def evaluate(evaluator, value) - value - end -end diff --git a/lib/json_expr/operators/var_operator.rb b/lib/json_expr/operators/var_operator.rb deleted file mode 100644 index a6251f8..0000000 --- a/lib/json_expr/operators/var_operator.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require_relative "binary_operator" - -class VarOperator - include BinaryOperator - - def evaluate(evaluator, path) - if path.is_a?(Hash) - path = to_sym(path) - path = path[:path] - end - - path.is_a?(String) ? evaluator.extract_var(path) : nil - end - - private - def to_sym(path) - path.transform_keys(&:to_sym) - end -end diff --git a/lib/scheduled_executor_service.rb b/lib/scheduled_executor_service.rb deleted file mode 100644 index 60379eb..0000000 --- a/lib/scheduled_executor_service.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class ScheduledExecutorService - # @interface method - def schedule(command, delay, unit) - raise NotImplementedError.new("You must implement schedule method.") - end -end diff --git a/lib/scheduled_thread_pool_executor.rb b/lib/scheduled_thread_pool_executor.rb deleted file mode 100644 index 52a3578..0000000 --- a/lib/scheduled_thread_pool_executor.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require_relative "audience_deserializer" - -class ScheduledThreadPoolExecutor < AudienceDeserializer - attr_accessor :log, :reader - - def initialize(timer = 1) - end - - def deserialize(bytes, offset, length) - @reader.read_value(bytes, offset, length) - end -end diff --git a/lib/variable_parser.rb b/lib/variable_parser.rb deleted file mode 100644 index b05027d..0000000 --- a/lib/variable_parser.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class VariableParser - # @interface method - def parse - raise NotImplementedError.new("You must implement parse method.") - end -end diff --git a/lib/variant_assigner.rb b/lib/variant_assigner.rb deleted file mode 100644 index f3f4317..0000000 --- a/lib/variant_assigner.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require "murmurhash3" -require_relative "./hashing" - -class VariantAssigner - attr_reader :key - - def initialize(key) - md5 = Hashing.hash_unit(key.to_s) - @key = MurmurHash3::V32.str_hash(md5) - @normalizer = 1.0 / 0xffffffff - end - - def probability(seed_hi, seed_lo) - buffer = Array.new - put_uint32(buffer, seed_lo) - put_uint32(buffer, seed_hi) - put_uint32(buffer, key) - hash = MurmurHash3::V32.str_hash(buffer.pack("C*")) - - prob = (hash & 0xffffffff) * @normalizer - prob - end - - def self.choose_variant(split, prob) - sum = 0 - split.each_with_index do |s, i| - sum += s - return i if prob < sum - end - - split.count - 1 - end - - def assign(split, seed_hi, seed_lo) - prob = probability(seed_hi, seed_lo) - self.class.choose_variant(split, prob) - end - - def put_uint32(buffer, x) - buffer << (x & 0xff) - buffer << ((x >> 8) & 0xff) - buffer << ((x >> 16) & 0xff) - buffer << ((x >> 24) & 0xff) - end -end diff --git a/spec/a_b_smartly_config_spec.rb b/spec/a_b_smartly_config_spec.rb index 0eb58e7..01c1888 100644 --- a/spec/a_b_smartly_config_spec.rb +++ b/spec/a_b_smartly_config_spec.rb @@ -1,55 +1,55 @@ # frozen_string_literal: true -require "context_data_provider" -require "a_b_smartly_config" -require "context_event_handler" -require "variable_parser" -require "context_event_logger" -require "scheduled_executor_service" -require "client" +require "absmartly/context_data_provider" +require "absmartly/a_b_smartly_config" +require "absmartly/context_event_handler" +require "absmartly/variable_parser" +require "absmartly/context_event_logger" +require "absmartly/scheduled_executor_service" +require "absmartly/client" -RSpec.describe ABSmartlyConfig do +RSpec.describe Absmartly::ABSmartlyConfig do it ".context_data_provider" do - provider = ContextDataProvider.new + provider = Absmartly::ContextDataProvider.new config = described_class.create config.context_data_provider = provider expect(provider).to eq(config.context_data_provider) end it ".context_event_handler" do - handler = ContextEventHandler.new + handler = Absmartly::ContextEventHandler.new config = described_class.create config.context_event_handler = handler expect(handler).to eq(config.context_event_handler) end it ".variable_parser" do - variable_parser = VariableParser.new + variable_parser = Absmartly::VariableParser.new config = described_class.create config.variable_parser = variable_parser expect(variable_parser).to eq(config.variable_parser) end it ".scheduler" do - scheduler = VariableParser.new + scheduler = Absmartly::VariableParser.new config = described_class.create config.scheduler = scheduler expect(scheduler).to eq(config.scheduler) end it ".context_event_logger" do - logger = ContextEventLogger.new + logger = Absmartly::ContextEventLogger.new config = described_class.create config.context_event_logger = logger expect(logger).to eq(config.context_event_logger) end it "set all" do - handler = ContextEventHandler.new - provider = ContextDataProvider.new - parser = VariableParser.new - scheduler = ScheduledExecutorService.new - client = instance_double(Client) + handler = Absmartly::ContextEventHandler.new + provider = Absmartly::ContextDataProvider.new + parser = Absmartly::VariableParser.new + scheduler = Absmartly::ScheduledExecutorService.new + client = instance_double(Absmartly::Client) config = described_class.create config.variable_parser = parser config.context_data_provider = provider diff --git a/spec/a_b_smartly_spec.rb b/spec/a_b_smartly_spec.rb index 23a8ca9..46cf1ff 100644 --- a/spec/a_b_smartly_spec.rb +++ b/spec/a_b_smartly_spec.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true -require "a_b_smartly" -require "a_b_smartly_config" -require "context_data_provider" -require "context_event_handler" -require "variable_parser" -require "context_event_logger" -require "scheduled_executor_service" -require "client" -require "context" -require "context_config" +require "absmartly/a_b_smartly" +require "absmartly/a_b_smartly_config" +require "absmartly/context_data_provider" +require "absmartly/context_event_handler" +require "absmartly/variable_parser" +require "absmartly/context_event_logger" +require "absmartly/scheduled_executor_service" +require "absmartly/client" +require "absmartly/context" +require "absmartly/context_config" -RSpec.describe ABSmartly do - let(:client) { instance_double(Client) } +RSpec.describe Absmartly::ABSmartly do + let(:client) { instance_double(Absmartly::Client) } it ".create" do - config = ABSmartlyConfig.create + config = Absmartly::ABSmartlyConfig.create config.client = client absmartly = described_class.create(config) expect(absmartly).not_to be_nil @@ -23,13 +23,13 @@ it ".create throws with invalid config" do expect { - config = ABSmartlyConfig.create - ABSmartly.create(config) + config = Absmartly::ABSmartlyConfig.create + Absmartly::ABSmartly.create(config) }.to raise_error(ArgumentError, "Missing Client instance configuration") end it ".create_context" do - config = ABSmartlyConfig.create + config = Absmartly::ABSmartlyConfig.create config.client = client # data_future = (CompletdawdableFuture) mock( @@ -38,7 +38,7 @@ # DefaultContextDataProvider.class, (mock, context) -> { # when(mock.getContextData()).thenReturn(dataFuture); # })) { - # final ABSmartly absmartly = ABSmartly.create(config); + # final ABSmartly absmartly = Absmartly::ABSmartly.create(config); # assertEquals(1, dataProviderCtor.constructed().size()); # # try (final MockedStatic contextStatic = mockStatic(Context.class)) { @@ -89,13 +89,13 @@ end it "createContextWith" do - # final ABSmartlyConfig config = ABSmartlyConfig.create() + # final Absmartly::ABSmartlyConfig config = Absmartly::ABSmartlyConfig.create() # .setClient(client); # # final ContextData data = new ContextData(); # try (final MockedConstruction dataProviderCtor = mockConstruction( # DefaultContextDataProvider.class)) { - # final ABSmartly absmartly = ABSmartly.create(config); + # final ABSmartly absmartly = Absmartly::ABSmartly.create(config); # assertEquals(1, dataProviderCtor.constructed().size()); # # try (final MockedStatic contextStatic = mockStatic(Context.class)) { @@ -152,11 +152,11 @@ # final ContextDataProvider dataProvider = mock(ContextDataProvider.class); # when(dataProvider.getContextData()).thenReturn(dataFuture); # - # final ABSmartlyConfig config = ABSmartlyConfig.create() + # final Absmartly::ABSmartlyConfig config = Absmartly::ABSmartlyConfig.create() # .setClient(client) # .setContextDataProvider(dataProvider); # - # final ABSmartly absmartly = ABSmartly.create(config); + # final ABSmartly absmartly = Absmartly::ABSmartly.create(config); # # final CompletableFuture contextDataFuture = absmartly.getContextData(); # verify(dataProvider, times(1)).getContextData(); @@ -175,7 +175,7 @@ # final AudienceDeserializer audienceDeserializer = mock(AudienceDeserializer.class); # final VariableParser variableParser = mock(VariableParser.class); # - # final ABSmartlyConfig config = ABSmartlyConfig.create() + # final Absmartly::ABSmartlyConfig config = Absmartly::ABSmartlyConfig.create() # .setClient(client) # .setContextDataProvider(dataProvider) # .setContextEventHandler(eventHandler) @@ -184,7 +184,7 @@ # .setAudienceDeserializer(audienceDeserializer) # .setVariableParser(variableParser); # - # final ABSmartly absmartly = ABSmartly.create(config); + # final ABSmartly absmartly = Absmartly::ABSmartly.create(config); # # try (final MockedStatic contextStatic = mockStatic(Context.class); # final MockedConstruction audienceMatcherCtor = mockConstruction(AudienceMatcher.class, @@ -237,11 +237,11 @@ it "close" do # final ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); # - # final ABSmartlyConfig config = ABSmartlyConfig.create() + # final Absmartly::ABSmartlyConfig config = Absmartly::ABSmartlyConfig.create() # .setClient(client) # .setScheduler(scheduler); # - # final ABSmartly absmartly = ABSmartly.create(config); + # final ABSmartly absmartly = Absmartly::ABSmartly.create(config); # # try (final MockedStatic contextStatic = mockStatic(Context.class)) { # final Context contextMock = mock(Context.class); diff --git a/spec/absmartly_spec.rb b/spec/absmartly_spec.rb index 9f71df9..8ec07e5 100644 --- a/spec/absmartly_spec.rb +++ b/spec/absmartly_spec.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true -require "context" -require "context_config" -require "default_context_data_deserializer" -require "default_variable_parser" -require "default_audience_deserializer" -require "context_data_provider" -require "default_context_data_provider" -require "context_event_handler" -require "context_event_logger" -require "audience_matcher" -require "json/unit" +require "absmartly/context" +require "absmartly/context_config" +require "absmartly/default_context_data_deserializer" +require "absmartly/default_variable_parser" +require "absmartly/default_audience_deserializer" +require "absmartly/context_data_provider" +require "absmartly/default_context_data_provider" +require "absmartly/context_event_handler" +require "absmartly/context_event_logger" +require "absmartly/audience_matcher" +require "absmartly/json/unit" require "logger" -class MockContextEventLoggerProxy < ContextEventLogger +class MockContextEventLoggerProxy < Absmartly::ContextEventLogger attr_accessor :called, :events, :logger def initialize @@ -120,26 +120,26 @@ def clear end let(:publish_units) do [ - Unit.new("session_id", "pAE3a1i5Drs5mKRNq56adA"), - Unit.new("user_id", "JfnnlDI7RTiF9RgfG2JNCw"), - Unit.new("email", "IuqYkNRfEx5yClel4j3NbA") + Absmartly::Unit.new("session_id", "pAE3a1i5Drs5mKRNq56adA"), + Absmartly::Unit.new("user_id", "JfnnlDI7RTiF9RgfG2JNCw"), + Absmartly::Unit.new("email", "IuqYkNRfEx5yClel4j3NbA") ] end let(:clock) { Time.at(1620000000000 / 1000) } let(:clock_in_millis) { clock.to_i } - let(:descr) { DefaultContextDataDeserializer.new } + let(:descr) { Absmartly::DefaultContextDataDeserializer.new } let(:json) { resource("context.json") } let(:data) { descr.deserialize(json, 0, json.length) } let(:data_future) { OpenStruct.new(data_future: nil, success?: true) } - let(:data_provider) { DefaultContextDataProvider.new(client_mock) } + let(:data_provider) { Absmartly::DefaultContextDataProvider.new(client_mock) } let(:data_future_ready) { data_provider.context_data } let(:publish_future) { OpenStruct.new(success?: true) } let(:event_handler) do - ev = instance_double(ContextEventHandler) + ev = instance_double(Absmartly::ContextEventHandler) allow(ev).to receive(:publish).and_return(publish_future) ev end @@ -150,18 +150,18 @@ def clear logger end - let(:variable_parser) { DefaultVariableParser.new } - let(:audience_matcher) { AudienceMatcher.new(DefaultAudienceDeserializer.new) } + let(:variable_parser) { Absmartly::DefaultVariableParser.new } + let(:audience_matcher) { Absmartly::AudienceMatcher.new(Absmartly::DefaultAudienceDeserializer.new) } def client_mock - client = instance_double(Client) + client = instance_double(Absmartly::Client) allow(client).to receive(:context_data).and_return(OpenStruct.new(data_future: data, success?: true)) allow(client).to receive(:publish).and_return(OpenStruct.new(success?: true)) client end def create_ready_context - config = ContextConfig.create + config = Absmartly::ContextConfig.create config.set_units(units) Absmartly.create_context(config) @@ -179,7 +179,7 @@ def create_ready_context context "when configured globally" do before do - allow(Client).to receive(:create).and_return(client_mock) + allow(Absmartly::Client).to receive(:create).and_return(client_mock) Absmartly.configure_client do |config| config.endpoint = "https://test.absmartly.io/v1" @@ -194,7 +194,7 @@ def create_ready_context mock_logger.clear create_ready_context expect(mock_logger).to have_received(:handle_event) - .with(ContextEventLogger::EVENT_TYPE::READY, data).once + .with(Absmartly::ContextEventLogger::EVENT_TYPE::READY, data).once end it "receives EXPOSURE event with correct values when treatment() is called" do @@ -205,7 +205,7 @@ def create_ready_context context.treatment("exp_test_ab") expect(mock_logger).to have_received(:handle_event) - .with(ContextEventLogger::EVENT_TYPE::EXPOSURE, satisfy { |exposure| + .with(Absmartly::ContextEventLogger::EVENT_TYPE::EXPOSURE, satisfy { |exposure| exposure.id == 1 && exposure.name == "exp_test_ab" && exposure.unit == "session_id" && @@ -228,7 +228,7 @@ def create_ready_context context.track("goal1", properties) expect(mock_logger).to have_received(:handle_event) - .with(ContextEventLogger::EVENT_TYPE::GOAL, satisfy { |goal| + .with(Absmartly::ContextEventLogger::EVENT_TYPE::GOAL, satisfy { |goal| goal.name == "goal1" && goal.properties == properties }).once @@ -243,7 +243,7 @@ def create_ready_context context.publish expect(mock_logger).to have_received(:handle_event) - .with(ContextEventLogger::EVENT_TYPE::PUBLISH, instance_of(PublishEvent)).once + .with(Absmartly::ContextEventLogger::EVENT_TYPE::PUBLISH, instance_of(Absmartly::PublishEvent)).once end it "receives CLOSE event when close() is called" do @@ -254,7 +254,7 @@ def create_ready_context context.close expect(mock_logger).to have_received(:handle_event) - .with(ContextEventLogger::EVENT_TYPE::CLOSE, nil).once + .with(Absmartly::ContextEventLogger::EVENT_TYPE::CLOSE, nil).once end end end diff --git a/spec/audience_matcher_spec.rb b/spec/audience_matcher_spec.rb index c8a5f5f..11bcef1 100644 --- a/spec/audience_matcher_spec.rb +++ b/spec/audience_matcher_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "audience_matcher" -require "default_audience_deserializer" +require "absmartly/audience_matcher" +require "absmartly/default_audience_deserializer" -RSpec.describe AudienceMatcher do - let(:matcher) { AudienceMatcher.new(DefaultAudienceDeserializer.new) } +RSpec.describe Absmartly::AudienceMatcher do + let(:matcher) { Absmartly::AudienceMatcher.new(Absmartly::DefaultAudienceDeserializer.new) } it "evaluate returns nil on empty audience" do expect(matcher.evaluate("", nil)).to be_nil diff --git a/spec/client_config_spec.rb b/spec/client_config_spec.rb index 8c8ed2d..c8fc442 100644 --- a/spec/client_config_spec.rb +++ b/spec/client_config_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require "client_config" -require "context_data_deserializer" -require "context_event_serializer" -require "default_http_client_config" +require "absmartly/client_config" +require "absmartly/context_data_deserializer" +require "absmartly/context_event_serializer" +require "absmartly/default_http_client_config" -RSpec.describe ClientConfig do +RSpec.describe Absmartly::ClientConfig do it ".endpoint" do config = described_class.create config.endpoint = "https://test.endpoint.com" @@ -31,22 +31,22 @@ end it ".context_data_deserializer" do - deserializer = instance_double(ContextDataDeserializer) + deserializer = instance_double(Absmartly::ContextDataDeserializer) config = described_class.create config.context_data_deserializer = deserializer expect(config.context_data_deserializer).to eq(deserializer) end it ".context_event_serializer" do - serializer = instance_double(ContextEventSerializer) + serializer = instance_double(Absmartly::ContextEventSerializer) config = described_class.create config.context_event_serializer = serializer expect(config.context_event_serializer).to eq(serializer) end it ".executor" do - deserializer = instance_double(ContextDataDeserializer) - serializer = instance_double(ContextEventSerializer) + deserializer = instance_double(Absmartly::ContextDataDeserializer) + serializer = instance_double(Absmartly::ContextEventSerializer) config = described_class.create config.endpoint = "https://test.endpoint.com" config.api_key = "api-key-test" @@ -70,8 +70,8 @@ "absmartly.application": "website" } - deserializer = instance_double(ContextDataDeserializer) - serializer = instance_double(ContextEventSerializer) + deserializer = instance_double(Absmartly::ContextDataDeserializer) + serializer = instance_double(Absmartly::ContextEventSerializer) config = described_class.create_from_properties(props, "absmartly.") config.context_data_deserializer = deserializer config.context_event_serializer = serializer @@ -115,7 +115,7 @@ config.max_retries = 3 http_config = config.http_client_config - expect(http_config).to be_a(DefaultHttpClientConfig) + expect(http_config).to be_a(Absmartly::DefaultHttpClientConfig) expect(http_config.connect_timeout).to eq(5.0) expect(http_config.connection_request_timeout).to eq(10.0) expect(http_config.retry_interval).to eq(1.0) @@ -126,7 +126,7 @@ config = described_class.create http_config = config.http_client_config - expect(http_config).to be_a(DefaultHttpClientConfig) + expect(http_config).to be_a(Absmartly::DefaultHttpClientConfig) expect(http_config.connect_timeout).to eq(3.0) expect(http_config.connection_request_timeout).to eq(3.0) expect(http_config.retry_interval).to eq(0.5) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index a9b98fb..2aa22d6 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -1,74 +1,74 @@ # frozen_string_literal: true -require "client" -require "client_config" -require "json/context_data" -require "json/publish_event" -require "context_data_deserializer" -require "context_event_serializer" -require "http_client" -require "default_http_client" -require "default_context_data_deserializer" -require "default_context_event_serializer" - -RSpec.describe Client do +require "absmartly/client" +require "absmartly/client_config" +require "absmartly/json/context_data" +require "absmartly/json/publish_event" +require "absmartly/context_data_deserializer" +require "absmartly/context_event_serializer" +require "absmartly/http_client" +require "absmartly/default_http_client" +require "absmartly/default_context_data_deserializer" +require "absmartly/default_context_event_serializer" + +RSpec.describe Absmartly::Client do it "create throws with invalid config" do expect { - config = ClientConfig.create + config = Absmartly::ClientConfig.create config.api_key = "test-api-key" config.application = "website" config.environment = "dev" - Client.create(config) + Absmartly::Client.create(config) }.to raise_error(ArgumentError, "Missing Endpoint configuration") expect { - config = ClientConfig.create + config = Absmartly::ClientConfig.create config.endpoint = "https://localhost/v1" config.application = "website" config.environment = "dev" - Client.create(config) + Absmartly::Client.create(config) }.to raise_error(ArgumentError, "Missing APIKey configuration") expect { - config = ClientConfig.create + config = Absmartly::ClientConfig.create config.endpoint = "https://localhost/v1" config.api_key = "test-api-key" config.environment = "dev" - Client.create(config) + Absmartly::Client.create(config) }.to raise_error(ArgumentError, "Missing Application configuration") expect { - config = ClientConfig.create + config = Absmartly::ClientConfig.create config.endpoint = "https://localhost/v1" config.api_key = "test-api-key" config.application = "website" - Client.create(config) + Absmartly::Client.create(config) }.to raise_error(ArgumentError, "Missing Environment configuration") end it "create with defaults" do - config = ClientConfig.create + config = Absmartly::ClientConfig.create config.endpoint = "https://localhost/v1" config.api_key = "test-api-key" config.application = "website" config.environment = "dev" data_bytes = "{}" - expected = ContextData.new + expected = Absmartly::ContextData.new - event = PublishEvent.new + event = Absmartly::PublishEvent.new publish_bytes = nil - deser_ctor = instance_double(DefaultContextDataDeserializer) + deser_ctor = instance_double(Absmartly::DefaultContextDataDeserializer) allow(deser_ctor).to receive(:deserialize).with(data_bytes, 0, data_bytes.length).and_return(expected) deser_ctor.deserialize(data_bytes, 0, data_bytes.length) - ser_ctor = instance_double(DefaultContextEventSerializer) + ser_ctor = instance_double(Absmartly::DefaultContextEventSerializer) allow(ser_ctor).to receive(:serialize).with(event).and_return(publish_bytes) ser_ctor.serialize(event) - http_client = instance_double(DefaultHttpClient) - allow(DefaultHttpClient).to receive(:create).and_return(http_client) + http_client = instance_double(Absmartly::DefaultHttpClient) + allow(Absmartly::DefaultHttpClient).to receive(:create).and_return(http_client) expected_query = { "application": "website", @@ -90,7 +90,7 @@ allow(http_client).to receive(:put).with("https://localhost/v1/context", nil, expected_headers, publish_bytes).and_return(byte_response(data_bytes)) allow(http_client).to receive(:close) - client = Client.create(config) + client = Absmartly::Client.create(config) client.context_data client.publish(event) @@ -110,15 +110,15 @@ end it "context_data" do - http_client = HttpClient.new - deser = ContextDataDeserializer.new - config = ClientConfig.create + http_client = Absmartly::HttpClient.new + deser = Absmartly::ContextDataDeserializer.new + config = Absmartly::ClientConfig.create config.endpoint = "https://localhost/v1" config.api_key = "test-api-key" config.application = "website" config.environment = "dev" config.context_data_deserializer = deser - client = Client.create(config, http_client) + client = Absmartly::Client.create(config, http_client) data_bytes = "{}" @@ -137,7 +137,7 @@ allow(http_client).to receive(:get).with("https://localhost/v1/context", expected_query, expected_headers).and_return(byte_response(data_bytes)) - expected = ContextData.new + expected = Absmartly::ContextData.new allow(deser).to receive(:deserialize).with(data_bytes, 0, data_bytes.size).and_return(expected) result = client.context_data @@ -146,16 +146,16 @@ end it "context data exceptionally HTTP" do - http_client = instance_double(HttpClient) - deser = instance_double(ContextDataDeserializer) - config = ClientConfig.create + http_client = instance_double(Absmartly::HttpClient) + deser = instance_double(Absmartly::ContextDataDeserializer) + config = Absmartly::ClientConfig.create config.endpoint = "https://localhost/v1" config.api_key = "test-api-key" config.application = "website" config.environment = "dev" config.context_data_deserializer = deser - client = Client.create(config, http_client) + client = Absmartly::Client.create(config, http_client) expected_query = { "application": "website", @@ -172,7 +172,7 @@ allow(deser).to receive(:deserialize).and_return({}) allow(http_client).to receive(:get).with("https://localhost/v1/context", expected_query, expected_headers) - .and_return(DefaultHttpClient.default_response(500, "Internal Server Error", nil, nil)) + .and_return(Absmartly::DefaultHttpClient.default_response(500, "Internal Server Error", nil, nil)) result = client.context_data actual = result.exception @@ -182,16 +182,16 @@ end it "context data exceptionally connection" do - http_client = instance_double(HttpClient) - deser = instance_double(ContextDataDeserializer) - config = ClientConfig.create + http_client = instance_double(Absmartly::HttpClient) + deser = instance_double(Absmartly::ContextDataDeserializer) + config = Absmartly::ClientConfig.create config.endpoint = "https://localhost/v1" config.api_key = "test-api-key" config.application = "website" config.environment = "dev" config.context_data_deserializer = deser - client = Client.create(config, http_client) + client = Absmartly::Client.create(config, http_client) expected_query = { "application": "website", @@ -219,16 +219,16 @@ end it "publish" do - http_client = instance_double(HttpClient) - ser = instance_double(ContextEventSerializer) - config = ClientConfig.create + http_client = instance_double(Absmartly::HttpClient) + ser = instance_double(Absmartly::ContextEventSerializer) + config = Absmartly::ClientConfig.create config.endpoint = "https://localhost/v1" config.api_key = "test-api-key" config.application = "website" config.environment = "dev" config.context_event_serializer = ser - client = Client.create(config, http_client) + client = Absmartly::Client.create(config, http_client) expected_headers = { "Content-Type": "application/json", "X-API-Key": "test-api-key", @@ -238,7 +238,7 @@ "X-Agent": "absmartly-ruby-sdk" } bytes = "0" - event = PublishEvent.new + event = Absmartly::PublishEvent.new allow(ser).to receive(:serialize).with(event).and_return(bytes) allow(http_client).to receive(:put).with("https://localhost/v1/context", nil, expected_headers, bytes) .and_return(byte_response(bytes[0])) @@ -250,15 +250,15 @@ end it "publish Exceptionally HTTP" do - http_client = instance_double(HttpClient) - ser = instance_double(ContextEventSerializer) - config = ClientConfig.create + http_client = instance_double(Absmartly::HttpClient) + ser = instance_double(Absmartly::ContextEventSerializer) + config = Absmartly::ClientConfig.create config.endpoint = "https://localhost/v1" config.api_key = "test-api-key" config.application = "website" config.environment = "dev" config.context_event_serializer = ser - client = Client.create(config, http_client) + client = Absmartly::Client.create(config, http_client) expected_headers = { "Content-Type": "application/json", @@ -268,27 +268,27 @@ "X-Application-Version": "0", "X-Agent": "absmartly-ruby-sdk" } - event = PublishEvent.new + event = Absmartly::PublishEvent.new bytes = "0" allow(ser).to receive(:serialize).with(event).and_return(bytes) allow(http_client).to receive(:put).with("https://localhost/v1/context", nil, expected_headers, bytes) - .and_return(DefaultHttpClient.default_response(500, "Internal Server Error", nil, nil)) + .and_return(Absmartly::DefaultHttpClient.default_response(500, "Internal Server Error", nil, nil)) client.publish(event) expect(http_client).to have_received(:put).once expect(http_client).to have_received(:put).with("https://localhost/v1/context", nil, expected_headers, bytes).once end it "publish Exceptionally Connection" do - http_client = instance_double(HttpClient) - ser = instance_double(ContextEventSerializer) - config = ClientConfig.create + http_client = instance_double(Absmartly::HttpClient) + ser = instance_double(Absmartly::ContextEventSerializer) + config = Absmartly::ClientConfig.create config.endpoint = "https://localhost/v1" config.api_key = "test-api-key" config.application = "website" config.environment = "dev" config.context_event_serializer = ser - client = Client.create(config, http_client) + client = Absmartly::Client.create(config, http_client) expected_headers = { "Content-Type": "application/json", @@ -298,7 +298,7 @@ "X-Application-Version": "0", "X-Agent": "absmartly-ruby-sdk" } - event = PublishEvent.new + event = Absmartly::PublishEvent.new bytes = "0" response_future = failed_response(content: "FAILED") @@ -315,7 +315,7 @@ end def byte_response(bytes) - DefaultHttpClient.default_response( + Absmartly::DefaultHttpClient.default_response( 200, "OK", "application/json; charset=utf8", @@ -323,7 +323,7 @@ def byte_response(bytes) end def failed_response(status_code: 400, status_message: "Bad Request", content: nil) - DefaultHttpClient.default_response( + Absmartly::DefaultHttpClient.default_response( status_code, status_message, "application/json; charset=utf8", diff --git a/spec/context_config_spec.rb b/spec/context_config_spec.rb index ed2e355..3f68b11 100644 --- a/spec/context_config_spec.rb +++ b/spec/context_config_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require "context_config" -require "context_data_deserializer" -require "context_event_serializer" +require "absmartly/context_config" +require "absmartly/context_data_deserializer" +require "absmartly/context_event_serializer" -RSpec.describe ContextConfig do +RSpec.describe Absmartly::ContextConfig do it ".set_unit" do config = described_class.create config.set_unit("session_id", "0ab1e23f4eee") diff --git a/spec/context_spec.rb b/spec/context_spec.rb index 39ee8f8..34a2629 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -1,20 +1,20 @@ # frozen_string_literal: true -require "context" -require "context_config" -require "default_context_data_deserializer" -require "default_variable_parser" -require "default_audience_deserializer" -require "context_data_provider" -require "default_context_data_provider" -require "context_event_handler" -require "context_event_logger" -require "scheduled_executor_service" -require "audience_matcher" -require "json/unit" +require "absmartly/context" +require "absmartly/context_config" +require "absmartly/default_context_data_deserializer" +require "absmartly/default_variable_parser" +require "absmartly/default_audience_deserializer" +require "absmartly/context_data_provider" +require "absmartly/default_context_data_provider" +require "absmartly/context_event_handler" +require "absmartly/context_event_logger" +require "absmartly/scheduled_executor_service" +require "absmartly/audience_matcher" +require "absmartly/json/unit" require "logger" -RSpec.describe Context do +RSpec.describe Absmartly::Context do let(:units) { { session_id: "e791e240fcd3df7d238cfc285f475e8152fcc0ec", @@ -61,15 +61,15 @@ } let(:publish_units) { [ - Unit.new("session_id", "pAE3a1i5Drs5mKRNq56adA"), - Unit.new("user_id", "JfnnlDI7RTiF9RgfG2JNCw"), - Unit.new("email", "IuqYkNRfEx5yClel4j3NbA") + Absmartly::Unit.new("session_id", "pAE3a1i5Drs5mKRNq56adA"), + Absmartly::Unit.new("user_id", "JfnnlDI7RTiF9RgfG2JNCw"), + Absmartly::Unit.new("email", "IuqYkNRfEx5yClel4j3NbA") ] } let(:clock) { Time.at(1620000000000 / 1000) } let(:clock_in_millis) { clock.to_i } - let(:descr) { DefaultContextDataDeserializer.new } + let(:descr) { Absmartly::DefaultContextDataDeserializer.new } let(:json) { resource("context.json") } let(:data) { descr.deserialize(json, 0, json.length) } @@ -84,24 +84,24 @@ let(:data_future) { OpenStruct.new(data_future: nil, success?: true) } - let(:data_provider) { DefaultContextDataProvider.new(client_mock) } + let(:data_provider) { Absmartly::DefaultContextDataProvider.new(client_mock) } let(:data_future_ready) { data_provider.context_data } - let(:failed_data_provider) { DefaultContextDataProvider.new(failed_client_mock) } + let(:failed_data_provider) { Absmartly::DefaultContextDataProvider.new(failed_client_mock) } let(:data_future_failed) { failed_data_provider.context_data } - let(:refresh_data_provider) { DefaultContextDataProvider.new(client_mock(refresh_data)) } + let(:refresh_data_provider) { Absmartly::DefaultContextDataProvider.new(client_mock(refresh_data)) } let(:refresh_data_future_ready) { refresh_data_provider.context_data } - let(:audience_data_provider) { DefaultContextDataProvider.new(client_mock(audience_data)) } + let(:audience_data_provider) { Absmartly::DefaultContextDataProvider.new(client_mock(audience_data)) } let(:audience_data_future_ready) { audience_data_provider.context_data } - let(:audience_strict_data_provider) { DefaultContextDataProvider.new(client_mock(audience_strict_data)) } + let(:audience_strict_data_provider) { Absmartly::DefaultContextDataProvider.new(client_mock(audience_strict_data)) } let(:audience_strict_data_future_ready) { audience_strict_data_provider.context_data } let(:publish_future) { OpenStruct.new(success?: true) } let(:event_handler) do - ev = instance_double(ContextEventHandler) + ev = instance_double(Absmartly::ContextEventHandler) allow(ev).to receive(:publish).and_return(publish_future) ev end @@ -110,52 +110,52 @@ allow(event_logger).to receive(:handle_event).and_call_original event_logger end - let(:variable_parser) { DefaultVariableParser.new } - let(:audience_matcher) { AudienceMatcher.new(DefaultAudienceDeserializer.new) } + let(:variable_parser) { Absmartly::DefaultVariableParser.new } + let(:audience_matcher) { Absmartly::AudienceMatcher.new(Absmartly::DefaultAudienceDeserializer.new) } let(:failure) { Exception.new("FAILED") } let(:failure_future) { OpenStruct.new(exception: failure, success?: false, data_future: nil) } def http_client_mock - http_client = instance_double(DefaultHttpClient) + http_client = instance_double(Absmartly::DefaultHttpClient) allow(http_client).to receive(:get).and_return(faraday_response(refresh_json)) http_client end def client_mock(data_future = nil) - client = instance_double(Client) + client = instance_double(Absmartly::Client) allow(client).to receive(:context_data).and_return(OpenStruct.new(data_future: data_future || data, success?: true)) client end def failed_client_mock - client = instance_double(Client) + client = instance_double(Absmartly::Client) allow(client).to receive(:context_data).and_return(failure_future) client end def create_context(data_future = nil, config: nil, evt_handler: nil, dt_provider: nil) if config.nil? - config = ContextConfig.create + config = Absmartly::ContextConfig.create config.set_units(units) end - Context.create(clock, config, data_future || data_future_ready, dt_provider || data_provider, + Absmartly::Context.create(clock, config, data_future || data_future_ready, dt_provider || data_provider, evt_handler || event_handler, event_logger, variable_parser, audience_matcher) end def create_ready_context(evt_handler: nil) - config = ContextConfig.create + config = Absmartly::ContextConfig.create config.set_units(units) - Context.create(clock, config, data_future_ready, data_provider, + Absmartly::Context.create(clock, config, data_future_ready, data_provider, evt_handler || event_handler, event_logger, variable_parser, audience_matcher) end def create_failed_context - config = ContextConfig.create + config = Absmartly::ContextConfig.create config.set_units(units) - Context.create(clock, config, data_future_failed, failed_data_provider, + Absmartly::Context.create(clock, config, data_future_failed, failed_data_provider, event_handler, event_logger, variable_parser, audience_matcher) end @@ -171,7 +171,7 @@ def faraday_response(content) "exp_test_1": 1 } - config = ContextConfig.create + config = Absmartly::ContextConfig.create config.set_units(units) config.set_overrides(overrides) @@ -184,7 +184,7 @@ def faraday_response(content) "exp_test": 2, "exp_test_1": 1 } - config = ContextConfig.create + config = Absmartly::ContextConfig.create config.set_units(units) config.set_custom_assignments(cassignments) @@ -207,13 +207,13 @@ def faraday_response(content) it "calls event logger when ready" do create_ready_context - expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::READY, data).once + expect(event_logger).to have_received(:handle_event).with(Absmartly::ContextEventLogger::EVENT_TYPE::READY, data).once end it "callsEventLoggerWithException" do create_context(data_future_failed) - expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::ERROR, "FAILED").once + expect(event_logger).to have_received(:handle_event).with(Absmartly::ContextEventLogger::EVENT_TYPE::ERROR, "FAILED").once end it "throwsWhenNotReady" do @@ -224,31 +224,31 @@ def faraday_response(content) not_ready_message = "ABSmartly Context is not yet ready" expect { context.peek_treatment("exp_test_ab") - }.to raise_error(IllegalStateException, not_ready_message) + }.to raise_error(Absmartly::IllegalStateException, not_ready_message) expect { context.treatment("exp_test_ab") - }.to raise_error(IllegalStateException, not_ready_message) + }.to raise_error(Absmartly::IllegalStateException, not_ready_message) expect { context.data - }.to raise_error(IllegalStateException, not_ready_message) + }.to raise_error(Absmartly::IllegalStateException, not_ready_message) expect { context.experiments - }.to raise_error(IllegalStateException, not_ready_message) + }.to raise_error(Absmartly::IllegalStateException, not_ready_message) expect { context.variable_value("banner.border", 17) - }.to raise_error(IllegalStateException, not_ready_message) + }.to raise_error(Absmartly::IllegalStateException, not_ready_message) expect { context.peek_variable_value("banner.border", 17) - }.to raise_error(IllegalStateException, not_ready_message) + }.to raise_error(Absmartly::IllegalStateException, not_ready_message) expect { context.variable_keys - }.to raise_error(IllegalStateException, not_ready_message) + }.to raise_error(Absmartly::IllegalStateException, not_ready_message) end it "throws when closed" do @@ -265,67 +265,67 @@ def faraday_response(content) closed_message = "ABSmartly Context is closed" expect { context.set_attribute("attr1", "value1") - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.set_attributes("attr1": "value1") - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.set_override("exp_test_ab", 2) - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.set_overrides("exp_test_ab": 2) - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.set_unit("test", "test") - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.set_custom_assignment("exp_test_ab", 2) - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.set_custom_assignments("exp_test_ab": 2) - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.peek_treatment("exp_test_ab") - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.treatment("exp_test_ab") - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.track("goal1", nil) - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.publish - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.data - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.experiments - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.variable_value("banner.border", 17) - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.peek_variable_value("banner.border", 17) - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) expect { context.variable_keys - }.to raise_error(IllegalStateException, closed_message) + }.to raise_error(Absmartly::IllegalStateException, closed_message) end it "experiments" do @@ -341,7 +341,7 @@ def faraday_response(content) expect { context.set_unit("db_user_id", "") - }.to raise_error(IllegalStateException, "Unit 'db_user_id' UID must not be blank.") + }.to raise_error(Absmartly::IllegalStateException, "Unit 'db_user_id' UID must not be blank.") end it "set unit throws on already set" do @@ -349,7 +349,7 @@ def faraday_response(content) expect { context.set_unit("session_id", "new_uid") - }.to raise_error(IllegalStateException, + }.to raise_error(Absmartly::IllegalStateException, "Unit 'session_id' already set.") end @@ -441,9 +441,9 @@ def faraday_response(content) context.set_attributes({ attr2: "value2", attr3: 15 }) attrs = context.instance_variable_get(:@attributes) - expect(attrs).to include(Attribute.new("attr1", "value1", clock_in_millis)) - expect(attrs).to include(Attribute.new(:attr2, "value2", clock_in_millis)) - expect(attrs).to include(Attribute.new(:attr3, 15, clock_in_millis)) + expect(attrs).to include(Absmartly::Attribute.new("attr1", "value1", clock_in_millis)) + expect(attrs).to include(Absmartly::Attribute.new(:attr2, "value2", clock_in_millis)) + expect(attrs).to include(Absmartly::Attribute.new(:attr3, 15, clock_in_millis)) end it "set_attributes before ready" do @@ -454,8 +454,8 @@ def faraday_response(content) context.set_attributes({ attr2: "value2" }) attrs = context.instance_variable_get(:@attributes) - expect(attrs).to include(Attribute.new("attr1", "value1", clock_in_millis)) - expect(attrs).to include(Attribute.new(:attr2, "value2", clock_in_millis)) + expect(attrs).to include(Absmartly::Attribute.new("attr1", "value1", clock_in_millis)) + expect(attrs).to include(Absmartly::Attribute.new(:attr2, "value2", clock_in_millis)) end it "set custom assignment" do @@ -621,16 +621,16 @@ def faraday_response(content) allow(event_handler).to receive(:publish).and_return(publish_future) context.publish - expected = PublishEvent.new + expected = Absmartly::PublishEvent.new expected.hashed = true expected.published_at = clock_in_millis expected.units = publish_units expected.attributes = [ - Attribute.new("age", 21, clock_in_millis) + Absmartly::Attribute.new("age", 21, clock_in_millis) ] expected.exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false), + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false), ] context.publish @@ -650,13 +650,13 @@ def faraday_response(content) context.publish - expected = PublishEvent.new + expected = Absmartly::PublishEvent.new expected.hashed = true expected.published_at = clock_in_millis expected.units = publish_units expected.exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, true), + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, true), ] allow(event_handler).to receive(:publish).and_return(publish_future) @@ -678,13 +678,13 @@ def faraday_response(content) context.publish - expected = PublishEvent.new + expected = Absmartly::PublishEvent.new expected.hashed = true expected.published_at = clock_in_millis expected.units = publish_units expected.exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 0, clock_in_millis, false, true, false, false, false, + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 0, clock_in_millis, false, true, false, false, false, true) ] @@ -699,15 +699,15 @@ def faraday_response(content) it "getVariableValueCallsEventLogger" do context = create_ready_context - expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::READY, data).once + expect(event_logger).to have_received(:handle_event).with(Absmartly::ContextEventLogger::EVENT_TYPE::READY, data).once context.variable_value("banner.border", nil) context.variable_value("banner.size", nil) exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false), + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false), ] - expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::EXPOSURE, exposures.first).exactly(exposures.length).time + expect(event_logger).to have_received(:handle_event).with(Absmartly::ContextEventLogger::EVENT_TYPE::EXPOSURE, exposures.first).exactly(exposures.length).time event_logger.clear context.variable_value("banner.border", nil) @@ -835,21 +835,21 @@ def faraday_response(content) expect(context.treatment("not_found")).to eq 0 expect(context.pending_count).to eq(1 + data.experiments.size) - expected = PublishEvent.new + expected = Absmartly::PublishEvent.new expected.hashed = true expected.published_at = clock_in_millis expected.units = publish_units expected.exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false), - Exposure.new(2, "exp_test_abc", "session_id", 2, clock_in_millis, + Absmartly::Exposure.new(2, "exp_test_abc", "session_id", 2, clock_in_millis, true, true, false, false, false, false), - Exposure.new(3, "exp_test_not_eligible", "user_id", 0, clock_in_millis, + Absmartly::Exposure.new(3, "exp_test_not_eligible", "user_id", 0, clock_in_millis, true, false, false, false, false, false), - Exposure.new(4, "exp_test_fullon", "session_id", 2, clock_in_millis, + Absmartly::Exposure.new(4, "exp_test_fullon", "session_id", 2, clock_in_millis, true, true, false, true, false, false), - Exposure.new(0, "not_found", nil, 0, clock_in_millis, + Absmartly::Exposure.new(0, "not_found", nil, 0, clock_in_millis, false, true, false, false, false, false), ] publish_future = nil @@ -875,21 +875,21 @@ def faraday_response(content) expect(context.treatment("not_found")).to eq(3) expect(context.pending_count).to eq(1 + data.experiments.length) - expected = PublishEvent.new + expected = Absmartly::PublishEvent.new expected.hashed = true expected.published_at = clock_in_millis expected.units = publish_units expected.exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 12, clock_in_millis, false, true, true, false, false, + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 12, clock_in_millis, false, true, true, false, false, false), - Exposure.new(2, "exp_test_abc", "session_id", 13, clock_in_millis, false, true, true, false, false, + Absmartly::Exposure.new(2, "exp_test_abc", "session_id", 13, clock_in_millis, false, true, true, false, false, false), - Exposure.new(3, "exp_test_not_eligible", "user_id", 11, clock_in_millis, false, true, true, false, false, + Absmartly::Exposure.new(3, "exp_test_not_eligible", "user_id", 11, clock_in_millis, false, true, true, false, false, false), - Exposure.new(4, "exp_test_fullon", "session_id", 13, clock_in_millis, false, true, true, false, false, + Absmartly::Exposure.new(4, "exp_test_fullon", "session_id", 13, clock_in_millis, false, true, true, false, false, false), - Exposure.new(0, "not_found", nil, 3, clock_in_millis, false, true, true, false, false, false), + Absmartly::Exposure.new(0, "not_found", nil, 3, clock_in_millis, false, true, true, false, false, false), ] context.publish @@ -935,16 +935,16 @@ def faraday_response(content) context.publish - expected = PublishEvent.new + expected = Absmartly::PublishEvent.new expected.hashed = true expected.published_at = clock_in_millis expected.units = publish_units expected.attributes = [ - Attribute.new("age", 21, clock_in_millis), + Absmartly::Attribute.new("age", 21, clock_in_millis), ] expected.exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false), + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false), ] context.publish @@ -963,13 +963,13 @@ def faraday_response(content) context.publish - expected = PublishEvent.new + expected = Absmartly::PublishEvent.new expected.hashed = true expected.published_at = clock_in_millis expected.units = publish_units expected.exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, true), + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, true), ] allow(event_handler).to receive(:publish).and_return(publish_future) @@ -990,13 +990,13 @@ def faraday_response(content) context.publish - expected = PublishEvent.new + expected = Absmartly::PublishEvent.new expected.hashed = true expected.published_at = clock_in_millis expected.units = publish_units expected.exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 0, clock_in_millis, false, true, false, false, false, + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 0, clock_in_millis, false, true, false, false, false, true), ] @@ -1062,13 +1062,13 @@ def faraday_response(content) allow(event_handler).to receive(:publish).and_return(publish_future) context.publish - expected = PublishEvent.new + expected = Absmartly::PublishEvent.new expected.hashed = true expected.published_at = clock_in_millis expected.units = publish_units expected.exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 0, clock_in_millis, false, true, false, false, false, true) + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 0, clock_in_millis, false, true, false, false, false, true) ] expect(event_handler).to have_received(:publish).with(context, expected).once @@ -1080,16 +1080,16 @@ def faraday_response(content) context.publish - expected2 = PublishEvent.new + expected2 = Absmartly::PublishEvent.new expected2.hashed = true expected2.published_at = clock_in_millis expected2.units = publish_units expected2.attributes = [ - Attribute.new("age", 30, clock_in_millis) + Absmartly::Attribute.new("age", 30, clock_in_millis) ] expected2.exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false) + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false) ] expect(event_handler).to have_received(:publish).with(context, expected2).once @@ -1098,18 +1098,18 @@ def faraday_response(content) it "treatmentCallsEventLogger" do event_logger.clear context = create_ready_context - expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::READY, data).once + expect(event_logger).to have_received(:handle_event).with(Absmartly::ContextEventLogger::EVENT_TYPE::READY, data).once context.treatment("exp_test_ab") context.treatment("not_found") exposures = [ - Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false), - Exposure.new(0, "not_found", nil, 0, clock_in_millis, false, true, false, false, false, false), + Absmartly::Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false), + Absmartly::Exposure.new(0, "not_found", nil, 0, clock_in_millis, false, true, false, false, false, false), ] - expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::EXPOSURE, exposures[0]).once - expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::EXPOSURE, exposures[1]).once + expect(event_logger).to have_received(:handle_event).with(Absmartly::ContextEventLogger::EVENT_TYPE::EXPOSURE, exposures[0]).once + expect(event_logger).to have_received(:handle_event).with(Absmartly::ContextEventLogger::EVENT_TYPE::EXPOSURE, exposures[1]).once event_logger.clear context.treatment("exp_test_ab") @@ -1130,17 +1130,17 @@ def faraday_response(content) expect(context.pending_count).to eq(4) - expected = PublishEvent.new + expected = Absmartly::PublishEvent.new expected.hashed = true expected.published_at = clock_in_millis expected.units = publish_units expected.goals = [ - GoalAchievement.new("goal1", clock_in_millis, + Absmartly::GoalAchievement.new("goal1", clock_in_millis, { amount: 125, hours: 245 }), - GoalAchievement.new("goal2", clock_in_millis, { tries: 7 }), - GoalAchievement.new("goal2", clock_in_millis, { tests: 12 }), - GoalAchievement.new("goal3", clock_in_millis, nil), + Absmartly::GoalAchievement.new("goal2", clock_in_millis, { tries: 7 }), + Absmartly::GoalAchievement.new("goal2", clock_in_millis, { tests: 12 }), + Absmartly::GoalAchievement.new("goal3", clock_in_millis, nil), ] context.publish @@ -1172,24 +1172,24 @@ def faraday_response(content) it "publishCallsEventLogger" do event_logger.clear context = create_ready_context - expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::READY, data).once + expect(event_logger).to have_received(:handle_event).with(Absmartly::ContextEventLogger::EVENT_TYPE::READY, data).once context.track("goal1", { amount: 125, hours: 245 }) - expected = PublishEvent.new + expected = Absmartly::PublishEvent.new expected.hashed = true expected.published_at = clock_in_millis expected.units = publish_units expected.goals = [ - GoalAchievement.new("goal1", clock_in_millis, + Absmartly::GoalAchievement.new("goal1", clock_in_millis, { amount: 125, hours: 245 }), ] allow(event_handler).to receive(:publish).and_return(failure_future) context.publish - expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::PUBLISH, expected).once + expect(event_logger).to have_received(:handle_event).with(Absmartly::ContextEventLogger::EVENT_TYPE::PUBLISH, expected).once expect(event_handler).to have_received(:publish).with(context, expected).once end @@ -1202,7 +1202,7 @@ def faraday_response(content) actual = context.publish expect(actual).to eq(failure) - expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::ERROR, "FAILED").once + expect(event_logger).to have_received(:handle_event).with(Absmartly::ContextEventLogger::EVENT_TYPE::ERROR, "FAILED").once end it "publish Does Not Call event handler When Failed" do @@ -1221,7 +1221,7 @@ def faraday_response(content) end it "publishExceptionally" do - ev = instance_double(ContextEventHandler) + ev = instance_double(Absmartly::ContextEventHandler) context = create_ready_context(evt_handler: ev) expect(context.ready?).to be_truthy expect(context.failed?).to be_falsey @@ -1265,7 +1265,7 @@ def faraday_response(content) -class MockContextEventLoggerProxy < ContextEventLogger +class MockContextEventLoggerProxy < Absmartly::ContextEventLogger attr_accessor :called, :events, :logger def initialize diff --git a/spec/default_audience_deserializer_spec.rb b/spec/default_audience_deserializer_spec.rb index db25133..2b2c9be 100644 --- a/spec/default_audience_deserializer_spec.rb +++ b/spec/default_audience_deserializer_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require "default_audience_deserializer" -require "context" -require "client" -require "json/publish_event" +require "absmartly/default_audience_deserializer" +require "absmartly/context" +require "absmartly/client" +require "absmartly/json/publish_event" -RSpec.describe DefaultAudienceDeserializer do +RSpec.describe Absmartly::DefaultAudienceDeserializer do it ".deserialize" do deser = described_class.new audience = "{\"filter\":[{\"gte\":[{\"var\":\"age\"},{\"value\":20}]}]}" diff --git a/spec/default_context_data_deserializer_spec.rb b/spec/default_context_data_deserializer_spec.rb index de605b9..e799c0e 100644 --- a/spec/default_context_data_deserializer_spec.rb +++ b/spec/default_context_data_deserializer_spec.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true -require "default_context_data_deserializer" -require "context" -require "client" -require "json/context_data" -require "json/publish_event" -require "json/experiment" -require "json/experiment_application" -require "json/experiment_variant" +require "absmartly/default_context_data_deserializer" +require "absmartly/context" +require "absmartly/client" +require "absmartly/json/context_data" +require "absmartly/json/publish_event" +require "absmartly/json/experiment" +require "absmartly/json/experiment_application" +require "absmartly/json/experiment_variant" -RSpec.describe DefaultContextDataDeserializer do +RSpec.describe Absmartly::DefaultContextDataDeserializer do it ".deserialize" do string = resource("context.json") deser = described_class.new data = deser.deserialize(string, 0, string.length) - experiment0 = Experiment.new + experiment0 = Absmartly::Experiment.new experiment0.id = 1 experiment0.name = "exp_test_ab" experiment0.unit_type = "session_id" @@ -27,19 +27,19 @@ experiment0.traffic_seed_lo = 455443629 experiment0.traffic_split = [0.0, 1.0] experiment0.full_on_variant = 0 - experiment0.applications = [ExperimentApplication.new("website")] + experiment0.applications = [Absmartly::ExperimentApplication.new("website")] experiment0.variants = [ - ExperimentVariant.new("A", nil), - ExperimentVariant.new("B", "{\"banner.border\":1,\"banner.size\":\"large\"}") + Absmartly::ExperimentVariant.new("A", nil), + Absmartly::ExperimentVariant.new("B", "{\"banner.border\":1,\"banner.size\":\"large\"}") ] experiment0.custom_field_values = [ - CustomFieldValue.new("country", "US,PT,ES,DE,FR", "string"), - CustomFieldValue.new("overrides", "{\"123\":1,\"456\":0}", "json"), + Absmartly::CustomFieldValue.new("country", "US,PT,ES,DE,FR", "string"), + Absmartly::CustomFieldValue.new("overrides", "{\"123\":1,\"456\":0}", "json"), ] experiment0.audience_strict = false experiment0.audience = nil - experiment1 = Experiment.new + experiment1 = Absmartly::Experiment.new experiment1.id = 2 experiment1.name = "exp_test_abc" experiment1.unit_type = "session_id" @@ -51,20 +51,20 @@ experiment1.traffic_seed_lo = 212903484 experiment1.traffic_split = [0.0, 1.0] experiment1.full_on_variant = 0 - experiment1.applications = [ExperimentApplication.new("website")] + experiment1.applications = [Absmartly::ExperimentApplication.new("website")] experiment1.variants = [ - ExperimentVariant.new("A", nil), - ExperimentVariant.new("B", "{\"button.color\":\"blue\"}"), - ExperimentVariant.new("C", "{\"button.color\":\"red\"}") + Absmartly::ExperimentVariant.new("A", nil), + Absmartly::ExperimentVariant.new("B", "{\"button.color\":\"blue\"}"), + Absmartly::ExperimentVariant.new("C", "{\"button.color\":\"red\"}") ] experiment1.custom_field_values = [ - CustomFieldValue.new("country", "US,PT,ES,DE,FR", "string"), - CustomFieldValue.new("languages", "en-US,en-GB,pt-PT,pt-BR,es-ES,es-MX", "string"), + Absmartly::CustomFieldValue.new("country", "US,PT,ES,DE,FR", "string"), + Absmartly::CustomFieldValue.new("languages", "en-US,en-GB,pt-PT,pt-BR,es-ES,es-MX", "string"), ] experiment1.audience_strict = false experiment1.audience = "" - experiment2 = Experiment.new + experiment2 = Absmartly::Experiment.new experiment2.id = 3 experiment2.name = "exp_test_not_eligible" experiment2.unit_type = "user_id" @@ -76,16 +76,16 @@ experiment2.traffic_seed_lo = 511357582 experiment2.traffic_split = [0.99, 0.01] experiment2.full_on_variant = 0 - experiment2.applications = [ExperimentApplication.new("website")] + experiment2.applications = [Absmartly::ExperimentApplication.new("website")] experiment2.variants = [ - ExperimentVariant.new("A", nil), - ExperimentVariant.new("B", "{\"card.width\":\"80%\"}"), - ExperimentVariant.new("C", "{\"card.width\":\"75%\"}") + Absmartly::ExperimentVariant.new("A", nil), + Absmartly::ExperimentVariant.new("B", "{\"card.width\":\"80%\"}"), + Absmartly::ExperimentVariant.new("C", "{\"card.width\":\"75%\"}") ] experiment2.audience_strict = false experiment2.audience = "{}" - experiment3 = Experiment.new + experiment3 = Absmartly::Experiment.new experiment3.id = 4 experiment3.name = "exp_test_fullon" experiment3.unit_type = "session_id" @@ -97,17 +97,17 @@ experiment3.traffic_seed_lo = 330937933 experiment3.traffic_split = [0.0, 1.0] experiment3.full_on_variant = 2 - experiment3.applications = [ExperimentApplication.new("website")] + experiment3.applications = [Absmartly::ExperimentApplication.new("website")] experiment3.variants = [ - ExperimentVariant.new("A", nil), - ExperimentVariant.new("B", "{\"submit.color\":\"red\",\"submit.shape\":\"circle\"}"), - ExperimentVariant.new("C", "{\"submit.color\":\"blue\",\"submit.shape\":\"rect\"}"), - ExperimentVariant.new("D", "{\"submit.color\":\"green\",\"submit.shape\":\"square\"}") + Absmartly::ExperimentVariant.new("A", nil), + Absmartly::ExperimentVariant.new("B", "{\"submit.color\":\"red\",\"submit.shape\":\"circle\"}"), + Absmartly::ExperimentVariant.new("C", "{\"submit.color\":\"blue\",\"submit.shape\":\"rect\"}"), + Absmartly::ExperimentVariant.new("D", "{\"submit.color\":\"green\",\"submit.shape\":\"square\"}") ] experiment3.audience_strict = false experiment3.audience = "null" - expected = ContextData.new + expected = Absmartly::ContextData.new expected.experiments = [ experiment0, experiment1, diff --git a/spec/default_http_client_config_spec.rb b/spec/default_http_client_config_spec.rb index 14465a0..0f706e0 100644 --- a/spec/default_http_client_config_spec.rb +++ b/spec/default_http_client_config_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require "default_http_client_config" -require "context" +require "absmartly/default_http_client_config" +require "absmartly/context" -RSpec.describe DefaultHttpClientConfig do +RSpec.describe Absmartly::DefaultHttpClientConfig do it ".connect_timeout" do config = described_class.new config.connect_timeout = 123 diff --git a/spec/default_http_client_spec.rb b/spec/default_http_client_spec.rb index 8260aa3..d17bcd8 100644 --- a/spec/default_http_client_spec.rb +++ b/spec/default_http_client_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require "default_http_client" -require "default_http_client_config" +require "absmartly/default_http_client" +require "absmartly/default_http_client_config" -RSpec.describe DefaultHttpClient do +RSpec.describe Absmartly::DefaultHttpClient do describe "#initialize" do it "configures Faraday with custom pool_size" do - config = DefaultHttpClientConfig.create + config = Absmartly::DefaultHttpClientConfig.create config.pool_size = 42 expect_any_instance_of(Faraday::Connection).to receive(:adapter) @@ -17,7 +17,7 @@ end it "configures Faraday with custom pool_idle_timeout" do - config = DefaultHttpClientConfig.create + config = Absmartly::DefaultHttpClientConfig.create config.pool_idle_timeout = 15 block_called = false @@ -38,7 +38,7 @@ end it "uses default pool_size of 20" do - config = DefaultHttpClientConfig.create + config = Absmartly::DefaultHttpClientConfig.create expect_any_instance_of(Faraday::Connection).to receive(:adapter) .with(:net_http_persistent, pool_size: 20) @@ -48,7 +48,7 @@ end it "uses default pool_idle_timeout of 5" do - config = DefaultHttpClientConfig.create + config = Absmartly::DefaultHttpClientConfig.create block_called = false idle_timeout_value = nil @@ -68,7 +68,7 @@ end it "uses the net_http_persistent adapter" do - config = DefaultHttpClientConfig.create + config = Absmartly::DefaultHttpClientConfig.create client = described_class.create(config) adapter = client.session.builder.adapter diff --git a/spec/default_variable_parser_spec.rb b/spec/default_variable_parser_spec.rb index 0b99445..0fecc20 100644 --- a/spec/default_variable_parser_spec.rb +++ b/spec/default_variable_parser_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require "default_variable_parser" -require "context" +require "absmartly/default_variable_parser" +require "absmartly/context" -RSpec.describe DefaultVariableParser do +RSpec.describe Absmartly::DefaultVariableParser do it ".parse" do - context = instance_double(Context) + context = instance_double(Absmartly::Context) config_value = resource("variables.json") variable_parser = described_class.new @@ -28,7 +28,7 @@ end it ".parse does not throw" do - context = instance_double(Context) + context = instance_double(Absmartly::Context) config_value = resource("variables.json")[5..] variable_parser = described_class.new diff --git a/spec/hashing_spec.rb b/spec/hashing_spec.rb index 7acf003..16a8994 100644 --- a/spec/hashing_spec.rb +++ b/spec/hashing_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require "hashing" +require "absmartly/hashing" -RSpec.describe Hashing do +RSpec.describe Absmartly::Hashing do describe ".hash_unit" do tests = [ ["", "1B2M2Y8AsgTpgAmY7PhCfg"], @@ -24,7 +24,7 @@ tests.each do |test| it "given: #{test.first}, then: md5 must be #{test.last}" do - md5 = Hashing.hash_unit(test.first) + md5 = Absmartly::Hashing.hash_unit(test.first) expect(md5).to eq(test.last) end end diff --git a/spec/json_expr/expr_evaluator_spec.rb b/spec/json_expr/expr_evaluator_spec.rb index ae3c038..b74aa45 100644 --- a/spec/json_expr/expr_evaluator_spec.rb +++ b/spec/json_expr/expr_evaluator_spec.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true -require "json_expr/expr_evaluator" -require "json_expr/operator" -require "json_expr/evaluator" +require "absmartly/json_expr/expr_evaluator" +require "absmartly/json_expr/operator" +require "absmartly/json_expr/evaluator" -RSpec.describe ExprEvaluator do +RSpec.describe Absmartly::ExprEvaluator do describe ".evaluate" do it "considers list as and combinator" do - and_operator = instance_double(Operator) + and_operator = instance_double(Absmartly::Operator) allow(and_operator).to receive(:evaluate).and_return('value': true) - or_operator = instance_double(Operator) + or_operator = instance_double(Absmartly::Operator) allow(or_operator).to receive(:evaluate).and_return('value': true) - expect(and_operator.evaluate(EMPTY_MAP, EMPTY_MAP)).to eq('value': true) + expect(and_operator.evaluate(Absmartly::EMPTY_MAP, Absmartly::EMPTY_MAP)).to eq('value': true) - evaluator = described_class.new({ 'and': and_operator, 'or': or_operator }, EMPTY_MAP) + evaluator = described_class.new({ 'and': and_operator, 'or': or_operator }, Absmartly::EMPTY_MAP) args = [{ 'value': true }, { 'value': false }] expect(evaluator.evaluate(args)).not_to be_nil @@ -22,32 +22,32 @@ end it "returns null if operator not found" do - value_operator = instance_double(Operator) + value_operator = instance_double(Absmartly::Operator) allow(value_operator).to receive(:evaluate).and_return('value': true) - evaluator = described_class.new({ 'value': value_operator }, EMPTY_MAP) + evaluator = described_class.new({ 'value': value_operator }, Absmartly::EMPTY_MAP) expect(evaluator.evaluate('not_found': true)).to be_nil expect(value_operator).to have_received(:evaluate).exactly(0).time end it "returns the args if calls operator with args" do - value_operator = instance_double(Operator) + value_operator = instance_double(Absmartly::Operator) args = [1, 2, 3] - allow(value_operator).to receive(:evaluate).with(Evaluator, args).and_return(args) + allow(value_operator).to receive(:evaluate).with(Absmartly::Evaluator, args).and_return(args) - evaluator = described_class.new({ value: value_operator }, EMPTY_MAP) + evaluator = described_class.new({ value: value_operator }, Absmartly::EMPTY_MAP) expect(evaluator.evaluate(value: args)).to eq(args) - expect(value_operator).to have_received(:evaluate).with(Evaluator, args).once + expect(value_operator).to have_received(:evaluate).with(Absmartly::Evaluator, args).once end it "test boolean convert" do - evaluator = described_class.new(EMPTY_MAP, EMPTY_MAP) - expect(evaluator.boolean_convert(EMPTY_LIST)).to be_truthy - expect(evaluator.boolean_convert(EMPTY_MAP)).to be_truthy + evaluator = described_class.new(Absmartly::EMPTY_MAP, Absmartly::EMPTY_MAP) + expect(evaluator.boolean_convert(Absmartly::EMPTY_LIST)).to be_truthy + expect(evaluator.boolean_convert(Absmartly::EMPTY_MAP)).to be_truthy expect(evaluator.boolean_convert(nil)).to be_falsey expect(evaluator.boolean_convert(true)).to be_truthy @@ -64,10 +64,10 @@ end it "test number convert" do - evaluator = described_class.new(EMPTY_MAP, EMPTY_MAP) + evaluator = described_class.new(Absmartly::EMPTY_MAP, Absmartly::EMPTY_MAP) - expect(evaluator.number_convert(EMPTY_LIST)).to be_nil - expect(evaluator.number_convert(EMPTY_MAP)).to be_nil + expect(evaluator.number_convert(Absmartly::EMPTY_LIST)).to be_nil + expect(evaluator.number_convert(Absmartly::EMPTY_MAP)).to be_nil expect(evaluator.number_convert(nil)).to be_nil expect(evaluator.number_convert("")).to be_nil expect(evaluator.number_convert("abcd")).to be_nil @@ -101,11 +101,11 @@ end it "test string convert" do - evaluator = described_class.new(EMPTY_MAP, EMPTY_MAP) + evaluator = described_class.new(Absmartly::EMPTY_MAP, Absmartly::EMPTY_MAP) expect(evaluator.string_convert(nil)).to be_nil - expect(evaluator.string_convert(EMPTY_MAP)).to be_nil - expect(evaluator.string_convert(EMPTY_LIST)).to be_nil + expect(evaluator.string_convert(Absmartly::EMPTY_MAP)).to be_nil + expect(evaluator.string_convert(Absmartly::EMPTY_LIST)).to be_nil expect(evaluator.string_convert(true)).to eq("true") expect(evaluator.string_convert(false)).to eq("false") @@ -147,7 +147,7 @@ "f" => { "y" => { "x" => 3, "0" => 10 } } } - evaluator = described_class.new(EMPTY_MAP, vars) + evaluator = described_class.new(Absmartly::EMPTY_MAP, vars) expect(evaluator.extract_var("a")).to eq(1) expect(evaluator.extract_var("b")).to eq(true) @@ -177,7 +177,7 @@ end it "test compare null" do - evaluator = described_class.new(EMPTY_MAP, EMPTY_MAP) + evaluator = described_class.new(Absmartly::EMPTY_MAP, Absmartly::EMPTY_MAP) expect(evaluator.compare(nil, nil)).to eq(0) @@ -187,38 +187,38 @@ expect(evaluator.compare(nil, false)).to be_nil expect(evaluator.compare(nil, "")).to be_nil expect(evaluator.compare(nil, "abc")).to be_nil - expect(evaluator.compare(nil, EMPTY_MAP)).to be_nil - expect(evaluator.compare(nil, EMPTY_LIST)).to be_nil + expect(evaluator.compare(nil, Absmartly::EMPTY_MAP)).to be_nil + expect(evaluator.compare(nil, Absmartly::EMPTY_LIST)).to be_nil end it "test compare objects" do - evaluator = described_class.new(EMPTY_MAP, EMPTY_MAP) - - expect(evaluator.compare(EMPTY_MAP, 0)).to be_nil - expect(evaluator.compare(EMPTY_MAP, 1)).to be_nil - expect(evaluator.compare(EMPTY_MAP, true)).to be_nil - expect(evaluator.compare(EMPTY_MAP, false)).to be_nil - expect(evaluator.compare(EMPTY_MAP, "")).to be_nil - expect(evaluator.compare(EMPTY_MAP, "abc")).to be_nil - expect(evaluator.compare(EMPTY_MAP, EMPTY_MAP)).to eq(0) + evaluator = described_class.new(Absmartly::EMPTY_MAP, Absmartly::EMPTY_MAP) + + expect(evaluator.compare(Absmartly::EMPTY_MAP, 0)).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_MAP, 1)).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_MAP, true)).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_MAP, false)).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_MAP, "")).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_MAP, "abc")).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_MAP, Absmartly::EMPTY_MAP)).to eq(0) expect(evaluator.compare({ "a" => 1 }, { "a" => 1 })).to eq(0) expect(evaluator.compare({ "a" => 1 }, { "b" => 2 })).to be_nil - expect(evaluator.compare(EMPTY_MAP, EMPTY_LIST)).to be_nil - - expect(evaluator.compare(EMPTY_LIST, 0)).to be_nil - expect(evaluator.compare(EMPTY_LIST, 1)).to be_nil - expect(evaluator.compare(EMPTY_LIST, true)).to be_nil - expect(evaluator.compare(EMPTY_LIST, false)).to be_nil - expect(evaluator.compare(EMPTY_LIST, "")).to be_nil - expect(evaluator.compare(EMPTY_LIST, "abc")).to be_nil - expect(evaluator.compare(EMPTY_LIST, EMPTY_MAP)).to be_nil - expect(evaluator.compare(EMPTY_LIST, EMPTY_LIST)).to eq(0) + expect(evaluator.compare(Absmartly::EMPTY_MAP, Absmartly::EMPTY_LIST)).to be_nil + + expect(evaluator.compare(Absmartly::EMPTY_LIST, 0)).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_LIST, 1)).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_LIST, true)).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_LIST, false)).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_LIST, "")).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_LIST, "abc")).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_LIST, Absmartly::EMPTY_MAP)).to be_nil + expect(evaluator.compare(Absmartly::EMPTY_LIST, Absmartly::EMPTY_LIST)).to eq(0) expect(evaluator.compare([1, 2], [1, 2])).to eq(0) expect(evaluator.compare([1, 2], [3, 4])).to be_nil end it "test compare booleans" do - evaluator = described_class.new(EMPTY_MAP, EMPTY_MAP) + evaluator = described_class.new(Absmartly::EMPTY_MAP, Absmartly::EMPTY_MAP) expect(evaluator.compare(false, 0)).to eq(0) expect(evaluator.compare(false, 1)).to eq(-1) @@ -226,8 +226,8 @@ expect(evaluator.compare(false, false)).to eq(0) expect(evaluator.compare(false, "")).to eq(0) expect(evaluator.compare(false, "abc")).to eq(-1) - expect(evaluator.compare(false, EMPTY_MAP)).to eq(-1) - expect(evaluator.compare(false, EMPTY_LIST)).to eq(-1) + expect(evaluator.compare(false, Absmartly::EMPTY_MAP)).to eq(-1) + expect(evaluator.compare(false, Absmartly::EMPTY_LIST)).to eq(-1) expect(evaluator.compare(true, 0)).to eq(1) expect(evaluator.compare(true, 1)).to eq(0) @@ -235,12 +235,12 @@ expect(evaluator.compare(true, false)).to eq(1) expect(evaluator.compare(true, "")).to eq(1) expect(evaluator.compare(true, "abc")).to eq(0) - expect(evaluator.compare(true, EMPTY_MAP)).to eq(0) - expect(evaluator.compare(true, EMPTY_LIST)).to eq(0) + expect(evaluator.compare(true, Absmartly::EMPTY_MAP)).to eq(0) + expect(evaluator.compare(true, Absmartly::EMPTY_LIST)).to eq(0) end it "test compare numbers" do - evaluator = described_class.new(EMPTY_MAP, EMPTY_MAP) + evaluator = described_class.new(Absmartly::EMPTY_MAP, Absmartly::EMPTY_MAP) expect(evaluator.compare(0, 0)).to eq(0) expect(evaluator.compare(0, 1)).to eq(-1) @@ -248,8 +248,8 @@ expect(evaluator.compare(0, false)).to eq(0) expect(evaluator.compare(0, "")).to be_nil expect(evaluator.compare(0, "abc")).to be_nil - expect(evaluator.compare(0, EMPTY_MAP)).to be_nil - expect(evaluator.compare(0, EMPTY_LIST)).to be_nil + expect(evaluator.compare(0, Absmartly::EMPTY_MAP)).to be_nil + expect(evaluator.compare(0, Absmartly::EMPTY_LIST)).to be_nil expect(evaluator.compare(1, 0)).to eq(1) expect(evaluator.compare(1, 1)).to eq(0) @@ -257,8 +257,8 @@ expect(evaluator.compare(1, false)).to eq(1) expect(evaluator.compare(1, "")).to be_nil expect(evaluator.compare(1, "abc")).to be_nil - expect(evaluator.compare(1, EMPTY_MAP)).to be_nil - expect(evaluator.compare(1, EMPTY_LIST)).to be_nil + expect(evaluator.compare(1, Absmartly::EMPTY_MAP)).to be_nil + expect(evaluator.compare(1, Absmartly::EMPTY_LIST)).to be_nil expect(evaluator.compare(1.0, 1)).to eq(0) expect(evaluator.compare(1.5, 1)).to eq(1) @@ -280,7 +280,7 @@ end it "test compare strings" do - evaluator = described_class.new(EMPTY_MAP, EMPTY_MAP) + evaluator = described_class.new(Absmartly::EMPTY_MAP, Absmartly::EMPTY_MAP) expect(evaluator.compare("", "")).to eq(0) expect(evaluator.compare("abc", "abc")).to eq(0) @@ -288,10 +288,10 @@ expect(evaluator.compare("1", 1)).to eq(0) expect(evaluator.compare("true", true)).to eq(0) expect(evaluator.compare("false", false)).to eq(0) - expect(evaluator.compare("", EMPTY_MAP)).to be_nil - expect(evaluator.compare("abc", EMPTY_MAP)).to be_nil - expect(evaluator.compare("", EMPTY_LIST)).to be_nil - expect(evaluator.compare("abc", EMPTY_LIST)).to be_nil + expect(evaluator.compare("", Absmartly::EMPTY_MAP)).to be_nil + expect(evaluator.compare("abc", Absmartly::EMPTY_MAP)).to be_nil + expect(evaluator.compare("", Absmartly::EMPTY_LIST)).to be_nil + expect(evaluator.compare("abc", Absmartly::EMPTY_LIST)).to be_nil expect(evaluator.compare("abc", "bcd")).to eq(-1) expect(evaluator.compare("bcd", "abc")).to eq(1) diff --git a/spec/json_expr/json_expr_spec.rb b/spec/json_expr/json_expr_spec.rb index fa5aaa7..531cc22 100644 --- a/spec/json_expr/json_expr_spec.rb +++ b/spec/json_expr/json_expr_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require "json_expr/json_expr" +require "absmartly/json_expr/json_expr" -RSpec.describe JsonExpr do +RSpec.describe Absmartly::JsonExpr do describe ".evaluate_boolean_expr" do def value_for(x) { value: x } diff --git a/spec/json_expr/operators/and_combinator_spec.rb b/spec/json_expr/operators/and_combinator_spec.rb index b77fbda..64e3c2f 100644 --- a/spec/json_expr/operators/and_combinator_spec.rb +++ b/spec/json_expr/operators/and_combinator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/and_combinator" +require "absmartly/json_expr/operators/and_combinator" -RSpec.describe AndCombinator do +RSpec.describe Absmartly::AndCombinator do include_examples "shared operator" let(:combinator) { described_class.new } diff --git a/spec/json_expr/operators/equals_operator_spec.rb b/spec/json_expr/operators/equals_operator_spec.rb index 4de4e04..f41a936 100644 --- a/spec/json_expr/operators/equals_operator_spec.rb +++ b/spec/json_expr/operators/equals_operator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/equals_operator" +require "absmartly/json_expr/operators/equals_operator" -RSpec.describe EqualsOperator do +RSpec.describe Absmartly::EqualsOperator do include_examples "shared operator" let(:operator) { described_class.new } diff --git a/spec/json_expr/operators/greater_than_operator_spec.rb b/spec/json_expr/operators/greater_than_operator_spec.rb index cfade44..5146224 100644 --- a/spec/json_expr/operators/greater_than_operator_spec.rb +++ b/spec/json_expr/operators/greater_than_operator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/greater_than_operator" +require "absmartly/json_expr/operators/greater_than_operator" -RSpec.describe GreaterThanOperator do +RSpec.describe Absmartly::GreaterThanOperator do include_examples "shared operator" let(:operator) { described_class.new } diff --git a/spec/json_expr/operators/greater_than_or_equal_operator_spec.rb b/spec/json_expr/operators/greater_than_or_equal_operator_spec.rb index 6bc7b07..80d0aab 100644 --- a/spec/json_expr/operators/greater_than_or_equal_operator_spec.rb +++ b/spec/json_expr/operators/greater_than_or_equal_operator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/greater_than_or_equal_operator" +require "absmartly/json_expr/operators/greater_than_or_equal_operator" -RSpec.describe GreaterThanOrEqualOperator do +RSpec.describe Absmartly::GreaterThanOrEqualOperator do include_examples "shared operator" let(:operator) { described_class.new } diff --git a/spec/json_expr/operators/in_operator_spec.rb b/spec/json_expr/operators/in_operator_spec.rb index bbe6646..f4d5faa 100644 --- a/spec/json_expr/operators/in_operator_spec.rb +++ b/spec/json_expr/operators/in_operator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/in_operator" +require "absmartly/json_expr/operators/in_operator" -RSpec.describe InOperator do +RSpec.describe Absmartly::InOperator do include_examples "shared operator" let(:operator) { described_class.new } diff --git a/spec/json_expr/operators/less_than_operator_spec.rb b/spec/json_expr/operators/less_than_operator_spec.rb index 68d0c6f..ba5bfa3 100644 --- a/spec/json_expr/operators/less_than_operator_spec.rb +++ b/spec/json_expr/operators/less_than_operator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/less_than_operator" +require "absmartly/json_expr/operators/less_than_operator" -RSpec.describe LessThanOperator do +RSpec.describe Absmartly::LessThanOperator do include_examples "shared operator" let(:operator) { described_class.new } diff --git a/spec/json_expr/operators/less_than_or_equal_operator_spec.rb b/spec/json_expr/operators/less_than_or_equal_operator_spec.rb index 10d48e9..a40ea92 100644 --- a/spec/json_expr/operators/less_than_or_equal_operator_spec.rb +++ b/spec/json_expr/operators/less_than_or_equal_operator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/less_than_or_equal_operator" +require "absmartly/json_expr/operators/less_than_or_equal_operator" -RSpec.describe LessThanOrEqualOperator do +RSpec.describe Absmartly::LessThanOrEqualOperator do include_examples "shared operator" let(:operator) { described_class.new } diff --git a/spec/json_expr/operators/match_operator_spec.rb b/spec/json_expr/operators/match_operator_spec.rb index 8615b91..ca9cffe 100644 --- a/spec/json_expr/operators/match_operator_spec.rb +++ b/spec/json_expr/operators/match_operator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/match_operator" +require "absmartly/json_expr/operators/match_operator" -RSpec.describe MatchOperator do +RSpec.describe Absmartly::MatchOperator do include_examples "shared operator" let(:operator) { described_class.new } diff --git a/spec/json_expr/operators/nil_operator_spec.rb b/spec/json_expr/operators/nil_operator_spec.rb index 0b1bbb3..3807549 100644 --- a/spec/json_expr/operators/nil_operator_spec.rb +++ b/spec/json_expr/operators/nil_operator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/nil_operator" +require "absmartly/json_expr/operators/nil_operator" -RSpec.describe NilOperator do +RSpec.describe Absmartly::NilOperator do include_examples "shared operator" let(:operator) { described_class.new } diff --git a/spec/json_expr/operators/not_operator_spec.rb b/spec/json_expr/operators/not_operator_spec.rb index 38cbb42..2259301 100644 --- a/spec/json_expr/operators/not_operator_spec.rb +++ b/spec/json_expr/operators/not_operator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/not_operator" +require "absmartly/json_expr/operators/not_operator" -RSpec.describe NotOperator do +RSpec.describe Absmartly::NotOperator do include_examples "shared operator" let(:operator) { described_class.new } diff --git a/spec/json_expr/operators/or_combinator_spec.rb b/spec/json_expr/operators/or_combinator_spec.rb index a033e45..7c4ddf5 100644 --- a/spec/json_expr/operators/or_combinator_spec.rb +++ b/spec/json_expr/operators/or_combinator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/or_combinator" +require "absmartly/json_expr/operators/or_combinator" -RSpec.describe OrCombinator do +RSpec.describe Absmartly::OrCombinator do include_examples "shared operator" let(:combinator) { described_class.new } diff --git a/spec/json_expr/operators/shared_operator.rb b/spec/json_expr/operators/shared_operator.rb index cb045e7..cf48fd9 100644 --- a/spec/json_expr/operators/shared_operator.rb +++ b/spec/json_expr/operators/shared_operator.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "json_expr/evaluator" +require "absmartly/json_expr/evaluator" RSpec.shared_examples "shared operator" do before(:each) do @@ -10,7 +10,7 @@ attr_reader :evaluator def reset_evaluator - @evaluator = Evaluator.new + @evaluator = Absmartly::Evaluator.new allow(@evaluator).to receive(:evaluate).and_wrap_original do |_, *invocation| invocation[0] diff --git a/spec/json_expr/operators/value_operator_spec.rb b/spec/json_expr/operators/value_operator_spec.rb index 6535875..8daecfb 100644 --- a/spec/json_expr/operators/value_operator_spec.rb +++ b/spec/json_expr/operators/value_operator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/value_operator" +require "absmartly/json_expr/operators/value_operator" -RSpec.describe ValueOperator do +RSpec.describe Absmartly::ValueOperator do include_examples "shared operator" let(:operator) { described_class.new } @@ -14,8 +14,8 @@ expect(operator.evaluate(evaluator, true)).to eq(true) expect(operator.evaluate(evaluator, false)).to eq(false) expect(operator.evaluate(evaluator, "")).to eq("") - expect(operator.evaluate(evaluator, EMPTY_MAP)).to eq(EMPTY_MAP) - expect(operator.evaluate(evaluator, EMPTY_LIST)).to eq(EMPTY_LIST) + expect(operator.evaluate(evaluator, Absmartly::EMPTY_MAP)).to eq(Absmartly::EMPTY_MAP) + expect(operator.evaluate(evaluator, Absmartly::EMPTY_LIST)).to eq(Absmartly::EMPTY_LIST) expect(operator.evaluate(evaluator, nil)).to be_nil expect(evaluator).to have_received(:evaluate).exactly(0).time end diff --git a/spec/json_expr/operators/var_operator_spec.rb b/spec/json_expr/operators/var_operator_spec.rb index 0e6198d..36d881c 100644 --- a/spec/json_expr/operators/var_operator_spec.rb +++ b/spec/json_expr/operators/var_operator_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true require_relative "./shared_operator" -require "json_expr/operators/var_operator" +require "absmartly/json_expr/operators/var_operator" -RSpec.describe VarOperator do +RSpec.describe Absmartly::VarOperator do include_examples "shared operator" let(:operator) { described_class.new } diff --git a/spec/variant_assigner_spec.rb b/spec/variant_assigner_spec.rb index 098c7ef..d8076b3 100644 --- a/spec/variant_assigner_spec.rb +++ b/spec/variant_assigner_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require "variant_assigner" +require "absmartly/variant_assigner" -RSpec.describe VariantAssigner do +RSpec.describe Absmartly::VariantAssigner do describe ".choose_variant" do probs = [ 0.0, @@ -66,7 +66,7 @@ splits.each_with_index do |split, i| it "with split:#{split}, prob:#{probs[i]}" do - expect(VariantAssigner.choose_variant(split, probs[i])).to eq variants[i] + expect(Absmartly::VariantAssigner.choose_variant(split, probs[i])).to eq variants[i] end end end @@ -115,7 +115,7 @@ keys_with_variants.each do |key, variants| splits.each_with_index do |split, i| it "with key:#{key}, split: #{split}, seeds[]:#{seeds[i]}, variant:#{variants[i]}" do - @variant_assigner = VariantAssigner.new(key) + @variant_assigner = Absmartly::VariantAssigner.new(key) seed_hi, seed_lo = seeds[i] expect(@variant_assigner.assign(split, seed_hi, seed_lo)).to eq variants[i] end