From 16746c265b504e02afc735a7206c09d83de32fac Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 2 Oct 2025 10:49:54 -0700 Subject: [PATCH 1/2] Added models and updated config --- lib/splitclient-rb.rb | 3 ++ .../engine/fallback_treatment_calculator.rb | 48 +++++++++++++++++++ .../engine/models/fallback_treatment.rb | 11 +++++ .../fallback_treatments_configuration.rb | 36 ++++++++++++++ lib/splitclient-rb/split_config.rb | 35 ++++++++++++++ lib/splitclient-rb/validators.rb | 35 ++++++++++++-- .../fallback_treatment_calculator_spec.rb | 30 ++++++++++++ .../fallback_treatments_configuration_spec.rb | 16 +++++++ 8 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 lib/splitclient-rb/engine/fallback_treatment_calculator.rb create mode 100644 lib/splitclient-rb/engine/models/fallback_treatment.rb create mode 100644 lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb create mode 100644 spec/engine/fallback_treatment_calculator_spec.rb create mode 100644 spec/engine/fallback_treatments_configuration_spec.rb diff --git a/lib/splitclient-rb.rb b/lib/splitclient-rb.rb index 30e0a2b3..5f4ebcc9 100644 --- a/lib/splitclient-rb.rb +++ b/lib/splitclient-rb.rb @@ -110,8 +110,11 @@ require 'splitclient-rb/engine/models/treatment' require 'splitclient-rb/engine/models/split_http_response' require 'splitclient-rb/engine/models/evaluation_options' +require 'splitclient-rb/engine/models/fallback_treatment.rb' +require 'splitclient-rb/engine/models/fallback_treatments_configuration.rb' require 'splitclient-rb/engine/auth_api_client' require 'splitclient-rb/engine/back_off' +require 'splitclient-rb/engine/fallback_treatment_calculator.rb' require 'splitclient-rb/engine/push_manager' require 'splitclient-rb/engine/status_manager' require 'splitclient-rb/engine/sync_manager' diff --git a/lib/splitclient-rb/engine/fallback_treatment_calculator.rb b/lib/splitclient-rb/engine/fallback_treatment_calculator.rb new file mode 100644 index 00000000..d736c84c --- /dev/null +++ b/lib/splitclient-rb/engine/fallback_treatment_calculator.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module SplitIoClient + module Engine + class FallbackTreatmentCalculator + attr_accessor :fallback_treatments_configuration, :label_prefix + + def initialize(fallback_treatment_configuration) + @label_prefix = 'fallback - ' + @fallback_treatments_configuration = fallback_treatment_configuration + end + + def resolve(flag_name, label) + default_fallback_treatment = Engine::Models::FallbackTreatment.new( + Engine::Models::Treatment::CONTROL, + nil, + label + ) + return default_fallback_treatment if @fallback_treatments_configuration.nil? + + if !@fallback_treatments_configuration.by_flag_fallback_treatment.nil? \ + && !@fallback_treatments_configuration.by_flag_fallback_treatment.fetch(flag_name, nil).nil? + return copy_with_label( + @fallback_treatments_configuration.by_flag_fallback_treatment[flag_name], + resolve_label(label) + ) + end + + return copy_with_label(@fallback_treatments_configuration.global_fallback_treatment, resolve_label(label)) \ + unless @fallback_treatments_configuration.global_fallback_treatment.nil? + + default_fallback_treatment + end + + private + + def resolve_label(label) + return nil if label.nil? + + @label_prefix + label + end + + def copy_with_label(fallback_treatment, label) + Engine::Models::FallbackTreatment.new(fallback_treatment.treatment, fallback_treatment.config, label) + end + end + end +end diff --git a/lib/splitclient-rb/engine/models/fallback_treatment.rb b/lib/splitclient-rb/engine/models/fallback_treatment.rb new file mode 100644 index 00000000..c89f439d --- /dev/null +++ b/lib/splitclient-rb/engine/models/fallback_treatment.rb @@ -0,0 +1,11 @@ +module SplitIoClient::Engine::Models + class FallbackTreatment + attr_accessor :treatment, :config, :label + + def initialize(treatment, config=nil, label=nil) + @treatment = treatment + @config = config + @label = label + end + end +end diff --git a/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb b/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb new file mode 100644 index 00000000..c4514fca --- /dev/null +++ b/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb @@ -0,0 +1,36 @@ +module SplitIoClient::Engine::Models + class FallbackTreatmentsConfiguration + attr_accessor :global_fallback_treatment, :by_flag_fallback_treatment + + def initialize(global_fallback_treatment=nil, by_flag_fallback_treatment=nil) + @global_fallback_treatment = build_global_fallback_treatment(global_fallback_treatment) + @by_flag_fallback_treatment = build_by_flag_fallback_treatment(by_flag_fallback_treatment) + end + + private + + def build_global_fallback_treatment(global_fallback_treatment) + if global_fallback_treatment.is_a? String + return FallbackTreatment.new(global_fallback_treatment) + end + + global_fallback_treatment + end + + def build_by_flag_fallback_treatment(by_flag_fallback_treatment) + return nil unless by_flag_fallback_treatment.is_a? Hash + processed_by_flag_fallback_treatment = Hash.new + + by_flag_fallback_treatment.each do |key, value| + if value.is_a? String + processed_by_flag_fallback_treatment[key] = FallbackTreatment.new(value) + next + end + + processed_by_flag_fallback_treatment[key] = value + end + + processed_by_flag_fallback_treatment + end + end +end diff --git a/lib/splitclient-rb/split_config.rb b/lib/splitclient-rb/split_config.rb index f1967c93..ec14edd7 100644 --- a/lib/splitclient-rb/split_config.rb +++ b/lib/splitclient-rb/split_config.rb @@ -123,6 +123,8 @@ def initialize(opts = {}) @on_demand_fetch_max_retries = SplitConfig.default_on_demand_fetch_max_retries @flag_sets_filter = SplitConfig.sanitize_flag_set_filter(opts[:flag_sets_filter], @split_validator, opts[:cache_adapter], @logger) + + @fallback_treatments_configuration = SplitConfig.sanitize_fallback_config(opts[:fallback_treatments], @split_validator, @logger) startup_log end @@ -303,6 +305,8 @@ def initialize(opts = {}) # @return [Array] attr_accessor :flag_sets_filter + attr_accessor :fallback_treatments_configuration + def self.default_counter_refresh_rate(adapter) return 300 if adapter == :redis # Send bulk impressions count - Refresh rate: 5 min. @@ -697,5 +701,36 @@ def self.machine_ip(ip_addresses_enabled, ip, adapter) return ''.freeze end + + def self.sanitize_fallback_config(fallback_config, validator, logger) + return fallback_config if fallback_config.nil? + + processed = Engine::Models::FallbackTreatmentsConfiguration.new + if !fallback_config.is_a?(Engine::Models::FallbackTreatmentsConfiguration) + logger.warn('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.') + return processed + end + + sanitized_global_fallback_treatment = fallback_config.global_fallback_treatment + if !fallback_config.global_fallback_treatment.nil? && !validator.validate_fallback_treatment(fallback_config.global_fallback_treatment) + logger.warn('Config: global fallbacktreatment parameter is discarded.') + sanitized_global_fallback_treatment = None + end + + sanitized_flag_fallback_treatments = Hash.new + if !fallback_config.by_flag_fallback_treatment.nil? && fallback_config.by_flag_fallback_treatment.is_a?(Hash) + for feature_name in fallback_config.by_flag_fallback_treatment.keys() + if !validator.valid_split_name?('Config', feature_name) || !validator.validate_fallback_treatment(fallback_config.by_flag_fallback_treatment[feature_name]) + logger.warn("Config: fallback treatment parameter for feature flag #{feature_name} is discarded.") + next + end + + sanitized_flag_fallback_treatments[feature_name] = fallback_config.by_flag_fallback_treatment[feature_name] + end + end + processed = Engine::Models::FallbackTreatmentsConfiguration.new(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments) + + processed + end end end diff --git a/lib/splitclient-rb/validators.rb b/lib/splitclient-rb/validators.rb index 0061115b..958e2e7c 100644 --- a/lib/splitclient-rb/validators.rb +++ b/lib/splitclient-rb/validators.rb @@ -4,6 +4,8 @@ module SplitIoClient class Validators Flagset_regex = /^[a-z0-9][_a-z0-9]{0,49}$/ + Fallback_treatment_regex = /^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$/ + Fallback_treatment_size = 100 def initialize(config) @config = config @@ -68,7 +70,7 @@ def valid_flag_sets(method, flag_sets) log_invalid_flag_set_type(method) elsif flag_set.is_a?(String) && flag_set.empty? log_invalid_flag_set_type(method) - elsif !flag_set.empty? && string_match?(flag_set.strip.downcase, method) + elsif !flag_set.empty? && string_match?(flag_set.strip.downcase, method, Flagset_regex, log_invalid_match) valid_flag_sets.add(flag_set.strip.downcase) else log_invalid_flag_set_type(method) @@ -91,9 +93,9 @@ def number_or_string?(value) (value.is_a?(Numeric) && !value.to_f.nan?) || string?(value) end - def string_match?(value, method) - if Flagset_regex.match(value) == nil - log_invalid_match(value, method) + def string_match?(value, method, regex_exp, log_if_invalid) + if regex_exp.match(value) == nil + log_if_invalid(value, method) false else true @@ -326,5 +328,30 @@ def valid_properties?(properties) true end + + def validate_fallback_treatment(method, fallback_treatment) + if !fallback_treatment.is_a? Engine::Models::FallbackTreatment + @config.logger.warn("#{method}: Fallback treatment instance should be FallbackTreatment, input is discarded") + return false + end + + if !fallback_treatment.treatment.is_a? String + @config.logger.warn("#{method}: Fallback treatment value should be str type, input is discarded") + return false + end + + return false unless string_match?(fallback_treatment.treatment, method, Fallback_treatment_regex) + + if fallback_treatment.treatment.size > Fallback_treatment_size + @config.logger.warn("#{method}: Fallback treatment size should not exceed %s characters", Fallback_treatment_size) + return false + end + + true + end + + def log_invalid_fallback_treatment(key, method) + @config.logger.warn("#{method}: Invalid treatment #{key}, Fallback treatment should match regex %s", Fallback_treatment_regex) + end end end diff --git a/spec/engine/fallback_treatment_calculator_spec.rb b/spec/engine/fallback_treatment_calculator_spec.rb new file mode 100644 index 00000000..4c02056a --- /dev/null +++ b/spec/engine/fallback_treatment_calculator_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SplitIoClient::Engine::FallbackTreatmentCalculator do + context 'works' do + it 'process fallback treatments' do + fallback_config = SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new(SplitIoClient::Engine::Models::FallbackTreatment.new("on" ,"{}")) + fallback_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(fallback_config) + expect(fallback_calculator.fallback_treatments_configuration).to be fallback_config + expect(fallback_calculator.label_prefix).to eq("fallback - ") + + fallback_treatment = fallback_calculator.resolve("feature", "not ready") + expect(fallback_treatment.treatment).to eq("on") + expect(fallback_treatment.label).to eq("fallback - not ready") + expect(fallback_treatment.config).to eq("{}") + + fallback_calculator.fallback_treatments_configuration = SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new(SplitIoClient::Engine::Models::FallbackTreatment.new("on" ,"{}"), {:feature => SplitIoClient::Engine::Models::FallbackTreatment.new("off" , '{"prop": "val"}')}) + fallback_treatment = fallback_calculator.resolve(:feature, "not ready") + expect(fallback_treatment.treatment).to eq("off") + expect(fallback_treatment.label).to eq("fallback - not ready") + expect(fallback_treatment.config).to eq('{"prop": "val"}') + + fallback_treatment = fallback_calculator.resolve(:feature2, "not ready") + expect(fallback_treatment.treatment).to eq("on") + expect(fallback_treatment.label).to eq("fallback - not ready") + expect(fallback_treatment.config).to eq("{}") + end + end +end diff --git a/spec/engine/fallback_treatments_configuration_spec.rb b/spec/engine/fallback_treatments_configuration_spec.rb new file mode 100644 index 00000000..cd732f63 --- /dev/null +++ b/spec/engine/fallback_treatments_configuration_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration do + context 'works' do + it 'it converts string to fallback treatment' do + fb_config = described_class.new("global", {:feature => "local"}) + expect(fb_config.global_fallback_treatment.is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to be true + expect(fb_config.global_fallback_treatment.treatment).to be "global" + + expect(fb_config.by_flag_fallback_treatment[:feature].is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to be true + expect(fb_config.by_flag_fallback_treatment[:feature].treatment).to be "local" + end + end +end From bbb684887d3fcd92068c7396d1bede92715f49af Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 3 Oct 2025 14:14:32 -0700 Subject: [PATCH 2/2] fixed validator and added tests --- lib/splitclient-rb/split_config.rb | 9 +-- lib/splitclient-rb/validators.rb | 86 +++++++++++++-------------- spec/splitclient/split_config_spec.rb | 28 +++++++++ 3 files changed, 76 insertions(+), 47 deletions(-) diff --git a/lib/splitclient-rb/split_config.rb b/lib/splitclient-rb/split_config.rb index ec14edd7..fe518bf0 100644 --- a/lib/splitclient-rb/split_config.rb +++ b/lib/splitclient-rb/split_config.rb @@ -712,15 +712,16 @@ def self.sanitize_fallback_config(fallback_config, validator, logger) end sanitized_global_fallback_treatment = fallback_config.global_fallback_treatment - if !fallback_config.global_fallback_treatment.nil? && !validator.validate_fallback_treatment(fallback_config.global_fallback_treatment) + if !fallback_config.global_fallback_treatment.nil? && !validator.validate_fallback_treatment('Config', fallback_config.global_fallback_treatment) logger.warn('Config: global fallbacktreatment parameter is discarded.') - sanitized_global_fallback_treatment = None + sanitized_global_fallback_treatment = nil end - sanitized_flag_fallback_treatments = Hash.new + sanitized_flag_fallback_treatments = nil if !fallback_config.by_flag_fallback_treatment.nil? && fallback_config.by_flag_fallback_treatment.is_a?(Hash) + sanitized_flag_fallback_treatments = Hash.new for feature_name in fallback_config.by_flag_fallback_treatment.keys() - if !validator.valid_split_name?('Config', feature_name) || !validator.validate_fallback_treatment(fallback_config.by_flag_fallback_treatment[feature_name]) + if !validator.valid_split_name?('Config', feature_name) || !validator.validate_fallback_treatment('Config', fallback_config.by_flag_fallback_treatment[feature_name]) logger.warn("Config: fallback treatment parameter for feature flag #{feature_name} is discarded.") next end diff --git a/lib/splitclient-rb/validators.rb b/lib/splitclient-rb/validators.rb index 958e2e7c..20554848 100644 --- a/lib/splitclient-rb/validators.rb +++ b/lib/splitclient-rb/validators.rb @@ -70,7 +70,7 @@ def valid_flag_sets(method, flag_sets) log_invalid_flag_set_type(method) elsif flag_set.is_a?(String) && flag_set.empty? log_invalid_flag_set_type(method) - elsif !flag_set.empty? && string_match?(flag_set.strip.downcase, method, Flagset_regex, log_invalid_match) + elsif !flag_set.empty? && string_match?(flag_set.strip.downcase, method, Flagset_regex, :log_invalid_match) valid_flag_sets.add(flag_set.strip.downcase) else log_invalid_flag_set_type(method) @@ -79,6 +79,46 @@ def valid_flag_sets(method, flag_sets) !valid_flag_sets.empty? ? valid_flag_sets.to_a.sort : [] end + def validate_fallback_treatment(method, fallback_treatment) + if !fallback_treatment.is_a? Engine::Models::FallbackTreatment + @config.logger.warn("#{method}: Fallback treatment instance should be FallbackTreatment, input is discarded") + return false + end + + if !fallback_treatment.treatment.is_a? String + @config.logger.warn("#{method}: Fallback treatment value should be str type, input is discarded") + return false + end + + return false unless string_match?(fallback_treatment.treatment, method, Fallback_treatment_regex, :log_invalid_fallback_treatment) + + if fallback_treatment.treatment.size > Fallback_treatment_size + @config.logger.warn("#{method}: Fallback treatment size should not exceed #{Fallback_treatment_size} characters") + return false + end + + true + end + + def valid_split_name?(method, split_name) + if split_name.nil? + log_nil(:split_name, method) + return false + end + + unless string?(split_name) + log_invalid_type(:split_name, method) + return false + end + + if empty_string?(split_name) + log_empty_string(:split_name, method) + return false + end + + true + end + private def string?(value) @@ -95,7 +135,7 @@ def number_or_string?(value) def string_match?(value, method, regex_exp, log_if_invalid) if regex_exp.match(value) == nil - log_if_invalid(value, method) + method(log_if_invalid).call(value, method) false else true @@ -134,25 +174,6 @@ def log_key_too_long(key, method) @config.logger.error("#{method}: #{key} is too long - must be #{@config.max_key_size} characters or less") end - def valid_split_name?(method, split_name) - if split_name.nil? - log_nil(:split_name, method) - return false - end - - unless string?(split_name) - log_invalid_type(:split_name, method) - return false - end - - if empty_string?(split_name) - log_empty_string(:split_name, method) - return false - end - - true - end - def valid_key?(method, key) if key.nil? log_nil(:key, method) @@ -329,29 +350,8 @@ def valid_properties?(properties) true end - def validate_fallback_treatment(method, fallback_treatment) - if !fallback_treatment.is_a? Engine::Models::FallbackTreatment - @config.logger.warn("#{method}: Fallback treatment instance should be FallbackTreatment, input is discarded") - return false - end - - if !fallback_treatment.treatment.is_a? String - @config.logger.warn("#{method}: Fallback treatment value should be str type, input is discarded") - return false - end - - return false unless string_match?(fallback_treatment.treatment, method, Fallback_treatment_regex) - - if fallback_treatment.treatment.size > Fallback_treatment_size - @config.logger.warn("#{method}: Fallback treatment size should not exceed %s characters", Fallback_treatment_size) - return false - end - - true - end - def log_invalid_fallback_treatment(key, method) - @config.logger.warn("#{method}: Invalid treatment #{key}, Fallback treatment should match regex %s", Fallback_treatment_regex) + @config.logger.warn("#{method}: Invalid treatment #{key}, Fallback treatment should match regex #{Fallback_treatment_regex}") end end end diff --git a/spec/splitclient/split_config_spec.rb b/spec/splitclient/split_config_spec.rb index 1c867aa4..4e05daff 100644 --- a/spec/splitclient/split_config_spec.rb +++ b/spec/splitclient/split_config_spec.rb @@ -180,5 +180,33 @@ configs = SplitIoClient::SplitConfig.new(flag_sets_filter: ['1set', 12]) expect(configs.flag_sets_filter).to eq ['1set'] end + + it 'test fallback treatment validations' do + configs = SplitIoClient::SplitConfig.new(fallback_treatments: 0) + expect(configs.fallback_treatments_configuration.is_a?(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration)).to eq true + expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq nil + + configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new) + expect(configs.fallback_treatments_configuration.is_a?(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration)).to eq true + expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq nil + + configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new('on-global', {'feature' => 'on_45-c'})) + expect(configs.fallback_treatments_configuration.global_fallback_treatment.is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to eq true + expect(configs.fallback_treatments_configuration.global_fallback_treatment.treatment).to eq 'on-global' + expect(configs.fallback_treatments_configuration.global_fallback_treatment.config).to eq nil + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment['feature'].is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to eq true + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment['feature'].treatment).to eq 'on_45-c' + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment['feature'].config).to eq nil + + configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new('on-gl/obal', {'feature' => "0" * 300})) + expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq Hash.new + + configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new(SplitIoClient::Engine::Models::FallbackTreatment.new('on-gl$#obal'), {"" => "treat"})) + expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil + expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq Hash.new + end end end