Skip to content

Commit 145251b

Browse files
authored
Merge pull request #582 from splitio/FME-10329-fallback-models
Added models and updated config
2 parents ac959eb + bbb6848 commit 145251b

File tree

9 files changed

+258
-23
lines changed

9 files changed

+258
-23
lines changed

lib/splitclient-rb.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,11 @@
110110
require 'splitclient-rb/engine/models/treatment'
111111
require 'splitclient-rb/engine/models/split_http_response'
112112
require 'splitclient-rb/engine/models/evaluation_options'
113+
require 'splitclient-rb/engine/models/fallback_treatment.rb'
114+
require 'splitclient-rb/engine/models/fallback_treatments_configuration.rb'
113115
require 'splitclient-rb/engine/auth_api_client'
114116
require 'splitclient-rb/engine/back_off'
117+
require 'splitclient-rb/engine/fallback_treatment_calculator.rb'
115118
require 'splitclient-rb/engine/push_manager'
116119
require 'splitclient-rb/engine/status_manager'
117120
require 'splitclient-rb/engine/sync_manager'
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
module SplitIoClient
4+
module Engine
5+
class FallbackTreatmentCalculator
6+
attr_accessor :fallback_treatments_configuration, :label_prefix
7+
8+
def initialize(fallback_treatment_configuration)
9+
@label_prefix = 'fallback - '
10+
@fallback_treatments_configuration = fallback_treatment_configuration
11+
end
12+
13+
def resolve(flag_name, label)
14+
default_fallback_treatment = Engine::Models::FallbackTreatment.new(
15+
Engine::Models::Treatment::CONTROL,
16+
nil,
17+
label
18+
)
19+
return default_fallback_treatment if @fallback_treatments_configuration.nil?
20+
21+
if !@fallback_treatments_configuration.by_flag_fallback_treatment.nil? \
22+
&& !@fallback_treatments_configuration.by_flag_fallback_treatment.fetch(flag_name, nil).nil?
23+
return copy_with_label(
24+
@fallback_treatments_configuration.by_flag_fallback_treatment[flag_name],
25+
resolve_label(label)
26+
)
27+
end
28+
29+
return copy_with_label(@fallback_treatments_configuration.global_fallback_treatment, resolve_label(label)) \
30+
unless @fallback_treatments_configuration.global_fallback_treatment.nil?
31+
32+
default_fallback_treatment
33+
end
34+
35+
private
36+
37+
def resolve_label(label)
38+
return nil if label.nil?
39+
40+
@label_prefix + label
41+
end
42+
43+
def copy_with_label(fallback_treatment, label)
44+
Engine::Models::FallbackTreatment.new(fallback_treatment.treatment, fallback_treatment.config, label)
45+
end
46+
end
47+
end
48+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module SplitIoClient::Engine::Models
2+
class FallbackTreatment
3+
attr_accessor :treatment, :config, :label
4+
5+
def initialize(treatment, config=nil, label=nil)
6+
@treatment = treatment
7+
@config = config
8+
@label = label
9+
end
10+
end
11+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module SplitIoClient::Engine::Models
2+
class FallbackTreatmentsConfiguration
3+
attr_accessor :global_fallback_treatment, :by_flag_fallback_treatment
4+
5+
def initialize(global_fallback_treatment=nil, by_flag_fallback_treatment=nil)
6+
@global_fallback_treatment = build_global_fallback_treatment(global_fallback_treatment)
7+
@by_flag_fallback_treatment = build_by_flag_fallback_treatment(by_flag_fallback_treatment)
8+
end
9+
10+
private
11+
12+
def build_global_fallback_treatment(global_fallback_treatment)
13+
if global_fallback_treatment.is_a? String
14+
return FallbackTreatment.new(global_fallback_treatment)
15+
end
16+
17+
global_fallback_treatment
18+
end
19+
20+
def build_by_flag_fallback_treatment(by_flag_fallback_treatment)
21+
return nil unless by_flag_fallback_treatment.is_a? Hash
22+
processed_by_flag_fallback_treatment = Hash.new
23+
24+
by_flag_fallback_treatment.each do |key, value|
25+
if value.is_a? String
26+
processed_by_flag_fallback_treatment[key] = FallbackTreatment.new(value)
27+
next
28+
end
29+
30+
processed_by_flag_fallback_treatment[key] = value
31+
end
32+
33+
processed_by_flag_fallback_treatment
34+
end
35+
end
36+
end

lib/splitclient-rb/split_config.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ def initialize(opts = {})
123123
@on_demand_fetch_max_retries = SplitConfig.default_on_demand_fetch_max_retries
124124

125125
@flag_sets_filter = SplitConfig.sanitize_flag_set_filter(opts[:flag_sets_filter], @split_validator, opts[:cache_adapter], @logger)
126+
127+
@fallback_treatments_configuration = SplitConfig.sanitize_fallback_config(opts[:fallback_treatments], @split_validator, @logger)
126128
startup_log
127129
end
128130

@@ -303,6 +305,8 @@ def initialize(opts = {})
303305
# @return [Array]
304306
attr_accessor :flag_sets_filter
305307

308+
attr_accessor :fallback_treatments_configuration
309+
306310
def self.default_counter_refresh_rate(adapter)
307311
return 300 if adapter == :redis # Send bulk impressions count - Refresh rate: 5 min.
308312

@@ -697,5 +701,37 @@ def self.machine_ip(ip_addresses_enabled, ip, adapter)
697701

698702
return ''.freeze
699703
end
704+
705+
def self.sanitize_fallback_config(fallback_config, validator, logger)
706+
return fallback_config if fallback_config.nil?
707+
708+
processed = Engine::Models::FallbackTreatmentsConfiguration.new
709+
if !fallback_config.is_a?(Engine::Models::FallbackTreatmentsConfiguration)
710+
logger.warn('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.')
711+
return processed
712+
end
713+
714+
sanitized_global_fallback_treatment = fallback_config.global_fallback_treatment
715+
if !fallback_config.global_fallback_treatment.nil? && !validator.validate_fallback_treatment('Config', fallback_config.global_fallback_treatment)
716+
logger.warn('Config: global fallbacktreatment parameter is discarded.')
717+
sanitized_global_fallback_treatment = nil
718+
end
719+
720+
sanitized_flag_fallback_treatments = nil
721+
if !fallback_config.by_flag_fallback_treatment.nil? && fallback_config.by_flag_fallback_treatment.is_a?(Hash)
722+
sanitized_flag_fallback_treatments = Hash.new
723+
for feature_name in fallback_config.by_flag_fallback_treatment.keys()
724+
if !validator.valid_split_name?('Config', feature_name) || !validator.validate_fallback_treatment('Config', fallback_config.by_flag_fallback_treatment[feature_name])
725+
logger.warn("Config: fallback treatment parameter for feature flag #{feature_name} is discarded.")
726+
next
727+
end
728+
729+
sanitized_flag_fallback_treatments[feature_name] = fallback_config.by_flag_fallback_treatment[feature_name]
730+
end
731+
end
732+
processed = Engine::Models::FallbackTreatmentsConfiguration.new(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments)
733+
734+
processed
735+
end
700736
end
701737
end

lib/splitclient-rb/validators.rb

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ module SplitIoClient
44
class Validators
55

66
Flagset_regex = /^[a-z0-9][_a-z0-9]{0,49}$/
7+
Fallback_treatment_regex = /^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$/
8+
Fallback_treatment_size = 100
79

810
def initialize(config)
911
@config = config
@@ -68,7 +70,7 @@ def valid_flag_sets(method, flag_sets)
6870
log_invalid_flag_set_type(method)
6971
elsif flag_set.is_a?(String) && flag_set.empty?
7072
log_invalid_flag_set_type(method)
71-
elsif !flag_set.empty? && string_match?(flag_set.strip.downcase, method)
73+
elsif !flag_set.empty? && string_match?(flag_set.strip.downcase, method, Flagset_regex, :log_invalid_match)
7274
valid_flag_sets.add(flag_set.strip.downcase)
7375
else
7476
log_invalid_flag_set_type(method)
@@ -77,6 +79,46 @@ def valid_flag_sets(method, flag_sets)
7779
!valid_flag_sets.empty? ? valid_flag_sets.to_a.sort : []
7880
end
7981

82+
def validate_fallback_treatment(method, fallback_treatment)
83+
if !fallback_treatment.is_a? Engine::Models::FallbackTreatment
84+
@config.logger.warn("#{method}: Fallback treatment instance should be FallbackTreatment, input is discarded")
85+
return false
86+
end
87+
88+
if !fallback_treatment.treatment.is_a? String
89+
@config.logger.warn("#{method}: Fallback treatment value should be str type, input is discarded")
90+
return false
91+
end
92+
93+
return false unless string_match?(fallback_treatment.treatment, method, Fallback_treatment_regex, :log_invalid_fallback_treatment)
94+
95+
if fallback_treatment.treatment.size > Fallback_treatment_size
96+
@config.logger.warn("#{method}: Fallback treatment size should not exceed #{Fallback_treatment_size} characters")
97+
return false
98+
end
99+
100+
true
101+
end
102+
103+
def valid_split_name?(method, split_name)
104+
if split_name.nil?
105+
log_nil(:split_name, method)
106+
return false
107+
end
108+
109+
unless string?(split_name)
110+
log_invalid_type(:split_name, method)
111+
return false
112+
end
113+
114+
if empty_string?(split_name)
115+
log_empty_string(:split_name, method)
116+
return false
117+
end
118+
119+
true
120+
end
121+
80122
private
81123

82124
def string?(value)
@@ -91,9 +133,9 @@ def number_or_string?(value)
91133
(value.is_a?(Numeric) && !value.to_f.nan?) || string?(value)
92134
end
93135

94-
def string_match?(value, method)
95-
if Flagset_regex.match(value) == nil
96-
log_invalid_match(value, method)
136+
def string_match?(value, method, regex_exp, log_if_invalid)
137+
if regex_exp.match(value) == nil
138+
method(log_if_invalid).call(value, method)
97139
false
98140
else
99141
true
@@ -132,25 +174,6 @@ def log_key_too_long(key, method)
132174
@config.logger.error("#{method}: #{key} is too long - must be #{@config.max_key_size} characters or less")
133175
end
134176

135-
def valid_split_name?(method, split_name)
136-
if split_name.nil?
137-
log_nil(:split_name, method)
138-
return false
139-
end
140-
141-
unless string?(split_name)
142-
log_invalid_type(:split_name, method)
143-
return false
144-
end
145-
146-
if empty_string?(split_name)
147-
log_empty_string(:split_name, method)
148-
return false
149-
end
150-
151-
true
152-
end
153-
154177
def valid_key?(method, key)
155178
if key.nil?
156179
log_nil(:key, method)
@@ -326,5 +349,9 @@ def valid_properties?(properties)
326349

327350
true
328351
end
352+
353+
def log_invalid_fallback_treatment(key, method)
354+
@config.logger.warn("#{method}: Invalid treatment #{key}, Fallback treatment should match regex #{Fallback_treatment_regex}")
355+
end
329356
end
330357
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe SplitIoClient::Engine::FallbackTreatmentCalculator do
6+
context 'works' do
7+
it 'process fallback treatments' do
8+
fallback_config = SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new(SplitIoClient::Engine::Models::FallbackTreatment.new("on" ,"{}"))
9+
fallback_calculator = SplitIoClient::Engine::FallbackTreatmentCalculator.new(fallback_config)
10+
expect(fallback_calculator.fallback_treatments_configuration).to be fallback_config
11+
expect(fallback_calculator.label_prefix).to eq("fallback - ")
12+
13+
fallback_treatment = fallback_calculator.resolve("feature", "not ready")
14+
expect(fallback_treatment.treatment).to eq("on")
15+
expect(fallback_treatment.label).to eq("fallback - not ready")
16+
expect(fallback_treatment.config).to eq("{}")
17+
18+
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"}')})
19+
fallback_treatment = fallback_calculator.resolve(:feature, "not ready")
20+
expect(fallback_treatment.treatment).to eq("off")
21+
expect(fallback_treatment.label).to eq("fallback - not ready")
22+
expect(fallback_treatment.config).to eq('{"prop": "val"}')
23+
24+
fallback_treatment = fallback_calculator.resolve(:feature2, "not ready")
25+
expect(fallback_treatment.treatment).to eq("on")
26+
expect(fallback_treatment.label).to eq("fallback - not ready")
27+
expect(fallback_treatment.config).to eq("{}")
28+
end
29+
end
30+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration do
6+
context 'works' do
7+
it 'it converts string to fallback treatment' do
8+
fb_config = described_class.new("global", {:feature => "local"})
9+
expect(fb_config.global_fallback_treatment.is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to be true
10+
expect(fb_config.global_fallback_treatment.treatment).to be "global"
11+
12+
expect(fb_config.by_flag_fallback_treatment[:feature].is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to be true
13+
expect(fb_config.by_flag_fallback_treatment[:feature].treatment).to be "local"
14+
end
15+
end
16+
end

spec/splitclient/split_config_spec.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,5 +180,33 @@
180180
configs = SplitIoClient::SplitConfig.new(flag_sets_filter: ['1set', 12])
181181
expect(configs.flag_sets_filter).to eq ['1set']
182182
end
183+
184+
it 'test fallback treatment validations' do
185+
configs = SplitIoClient::SplitConfig.new(fallback_treatments: 0)
186+
expect(configs.fallback_treatments_configuration.is_a?(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration)).to eq true
187+
expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil
188+
expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq nil
189+
190+
configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new)
191+
expect(configs.fallback_treatments_configuration.is_a?(SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration)).to eq true
192+
expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil
193+
expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq nil
194+
195+
configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new('on-global', {'feature' => 'on_45-c'}))
196+
expect(configs.fallback_treatments_configuration.global_fallback_treatment.is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to eq true
197+
expect(configs.fallback_treatments_configuration.global_fallback_treatment.treatment).to eq 'on-global'
198+
expect(configs.fallback_treatments_configuration.global_fallback_treatment.config).to eq nil
199+
expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment['feature'].is_a?(SplitIoClient::Engine::Models::FallbackTreatment)).to eq true
200+
expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment['feature'].treatment).to eq 'on_45-c'
201+
expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment['feature'].config).to eq nil
202+
203+
configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new('on-gl/obal', {'feature' => "0" * 300}))
204+
expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil
205+
expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq Hash.new
206+
207+
configs = SplitIoClient::SplitConfig.new(fallback_treatments: SplitIoClient::Engine::Models::FallbackTreatmentsConfiguration.new(SplitIoClient::Engine::Models::FallbackTreatment.new('on-gl$#obal'), {"" => "treat"}))
208+
expect(configs.fallback_treatments_configuration.global_fallback_treatment).to eq nil
209+
expect(configs.fallback_treatments_configuration.by_flag_fallback_treatment).to eq Hash.new
210+
end
183211
end
184212
end

0 commit comments

Comments
 (0)