prefab-cloud-ruby 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.envrc.sample +3 -0
- data/.github/workflows/ruby.yml +4 -0
- data/.gitmodules +3 -0
- data/Gemfile +12 -12
- data/Gemfile.lock +16 -14
- data/README.md +1 -1
- data/Rakefile +13 -14
- data/VERSION +1 -1
- data/lib/prefab/auth_interceptor.rb +2 -1
- data/lib/prefab/cancellable_interceptor.rb +8 -7
- data/lib/prefab/client.rb +33 -24
- data/lib/prefab/config_client.rb +55 -66
- data/lib/prefab/config_loader.rb +7 -114
- data/lib/prefab/config_resolver.rb +27 -57
- data/lib/prefab/config_value_unwrapper.rb +23 -0
- data/lib/prefab/criteria_evaluator.rb +96 -0
- data/lib/prefab/errors/invalid_api_key_error.rb +1 -1
- data/lib/prefab/feature_flag_client.rb +13 -145
- data/lib/prefab/internal_logger.rb +6 -5
- data/lib/prefab/local_config_parser.rb +110 -0
- data/lib/prefab/logger_client.rb +26 -31
- data/lib/prefab/murmer3.rb +3 -4
- data/lib/prefab/noop_cache.rb +5 -7
- data/lib/prefab/noop_stats.rb +2 -3
- data/lib/prefab/options.rb +11 -9
- data/lib/prefab/ratelimit_client.rb +11 -13
- data/lib/prefab/sse_logger.rb +3 -2
- data/lib/prefab/weighted_value_resolver.rb +42 -0
- data/lib/prefab/yaml_config_parser.rb +32 -0
- data/lib/prefab-cloud-ruby.rb +7 -2
- data/lib/prefab_pb.rb +49 -43
- data/lib/prefab_services_pb.rb +0 -1
- data/prefab-cloud-ruby.gemspec +28 -19
- data/test/.prefab.unit_tests.config.yaml +3 -2
- data/test/integration_test.rb +98 -0
- data/test/integration_test_helpers.rb +37 -0
- data/test/test_client.rb +32 -31
- data/test/test_config_client.rb +21 -20
- data/test/test_config_loader.rb +48 -37
- data/test/test_config_resolver.rb +312 -135
- data/test/test_config_value_unwrapper.rb +83 -0
- data/test/test_criteria_evaluator.rb +533 -0
- data/test/test_feature_flag_client.rb +35 -347
- data/test/test_helper.rb +18 -14
- data/test/test_integration.rb +33 -0
- data/test/test_local_config_parser.rb +78 -0
- data/test/test_logger.rb +47 -46
- data/test/test_weighted_value_resolver.rb +65 -0
- metadata +24 -27
- data/lib/prefab/config_helper.rb +0 -31
- data/run_test_harness_server.sh +0 -8
- data/test/harness_server.rb +0 -64
data/lib/prefab/config_loader.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'yaml'
|
4
3
|
module Prefab
|
5
4
|
class ConfigLoader
|
6
5
|
attr_reader :highwater_mark
|
@@ -19,21 +18,19 @@ module Prefab
|
|
19
18
|
@api_config.each_key do |k|
|
20
19
|
rtn[k] = @api_config[k]
|
21
20
|
end
|
22
|
-
rtn
|
23
|
-
rtn
|
21
|
+
rtn.merge(@local_overrides)
|
24
22
|
end
|
25
23
|
|
26
24
|
def set(config, source)
|
27
25
|
# don't overwrite newer values
|
28
|
-
if @api_config[config.key] && @api_config[config.key][:config].id >= config.id
|
29
|
-
return
|
30
|
-
end
|
26
|
+
return if @api_config[config.key] && @api_config[config.key][:config].id >= config.id
|
31
27
|
|
32
28
|
if config.rows.empty?
|
33
29
|
@api_config.delete(config.key)
|
34
30
|
else
|
35
31
|
if @api_config[config.key]
|
36
|
-
@base_client.log_internal Logger::DEBUG,
|
32
|
+
@base_client.log_internal Logger::DEBUG,
|
33
|
+
"Replace #{config.key} with value from #{source} #{@api_config[config.key][:config].id} -> #{config.id}"
|
37
34
|
end
|
38
35
|
@api_config[config.key] = { source: source, config: config }
|
39
36
|
end
|
@@ -56,7 +53,7 @@ module Prefab
|
|
56
53
|
|
57
54
|
def load_classpath_config
|
58
55
|
classpath_dir = @prefab_options.prefab_config_classpath_dir
|
59
|
-
rtn = load_glob(File.join(classpath_dir,
|
56
|
+
rtn = load_glob(File.join(classpath_dir, '.prefab.default.config.yaml'))
|
60
57
|
@prefab_options.prefab_envs.each do |env|
|
61
58
|
rtn = rtn.merge load_glob(File.join(classpath_dir, ".prefab.#{env}.config.yaml"))
|
62
59
|
end
|
@@ -65,7 +62,7 @@ module Prefab
|
|
65
62
|
|
66
63
|
def load_local_overrides
|
67
64
|
override_dir = @prefab_options.prefab_config_override_dir
|
68
|
-
rtn = load_glob(File.join(override_dir,
|
65
|
+
rtn = load_glob(File.join(override_dir, '.prefab.default.config.yaml'))
|
69
66
|
@prefab_options.prefab_envs.each do |env|
|
70
67
|
rtn = rtn.merge load_glob(File.join(override_dir, ".prefab.#{env}.config.yaml"))
|
71
68
|
end
|
@@ -75,113 +72,9 @@ module Prefab
|
|
75
72
|
def load_glob(glob)
|
76
73
|
rtn = {}
|
77
74
|
Dir.glob(glob).each do |file|
|
78
|
-
@base_client.
|
79
|
-
yaml = load(file)
|
80
|
-
yaml.each do |k, v|
|
81
|
-
load_kv(k, v, rtn, file)
|
82
|
-
end
|
75
|
+
Prefab::YAMLConfigParser.new(file, @base_client).merge(rtn)
|
83
76
|
end
|
84
77
|
rtn
|
85
78
|
end
|
86
|
-
|
87
|
-
def load_kv(k, v, rtn, file)
|
88
|
-
if v.class == Hash
|
89
|
-
if v['feature_flag']
|
90
|
-
rtn[k] = feature_flag_config(file, k, v)
|
91
|
-
else
|
92
|
-
v.each do |nest_k, nest_v|
|
93
|
-
nested_key = "#{k}.#{nest_k}"
|
94
|
-
nested_key = k if nest_k == "_"
|
95
|
-
load_kv(nested_key, nest_v, rtn, file)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
else
|
99
|
-
rtn[k] = {
|
100
|
-
source: file,
|
101
|
-
match: "default",
|
102
|
-
config: Prefab::Config.new(
|
103
|
-
key: k,
|
104
|
-
rows: [
|
105
|
-
Prefab::ConfigRow.new(value: Prefab::ConfigValue.new(value_from(k, v)))
|
106
|
-
]
|
107
|
-
)
|
108
|
-
}
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
def load(filename)
|
113
|
-
if File.exist? filename
|
114
|
-
@base_client.log_internal Logger::INFO, "Load #{filename}"
|
115
|
-
YAML.load_file(filename)
|
116
|
-
else
|
117
|
-
@base_client.log_internal Logger::INFO, "No file #{filename}"
|
118
|
-
{}
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def value_from(key, raw)
|
123
|
-
case raw
|
124
|
-
when String
|
125
|
-
if key.start_with? Prefab::LoggerClient::BASE_KEY
|
126
|
-
prefab_log_level_resolve = Prefab::LogLevel.resolve(raw.upcase.to_sym) || Prefab::LogLevel::NOT_SET_LOG_LEVEL
|
127
|
-
{ log_level: prefab_log_level_resolve }
|
128
|
-
else
|
129
|
-
{ string: raw }
|
130
|
-
end
|
131
|
-
when Integer
|
132
|
-
{ int: raw }
|
133
|
-
when TrueClass, FalseClass
|
134
|
-
{ bool: raw }
|
135
|
-
when Float
|
136
|
-
{ double: raw }
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
def feature_flag_config(file, key, value)
|
141
|
-
criteria = Prefab::Criteria.new(operator: 'ALWAYS_TRUE')
|
142
|
-
|
143
|
-
if value['criteria']
|
144
|
-
criteria = Prefab::Criteria.new(criteria_values(value['criteria']))
|
145
|
-
end
|
146
|
-
|
147
|
-
row = Prefab::ConfigRow.new(
|
148
|
-
value: Prefab::ConfigValue.new(
|
149
|
-
feature_flag: Prefab::FeatureFlag.new(
|
150
|
-
active: true,
|
151
|
-
inactive_variant_idx: -1, # not supported
|
152
|
-
rules: [
|
153
|
-
Prefab::Rule.new(
|
154
|
-
variant_weights: [
|
155
|
-
Prefab::VariantWeight.new(variant_idx: 0, weight: 1000)
|
156
|
-
],
|
157
|
-
criteria: criteria
|
158
|
-
)
|
159
|
-
]
|
160
|
-
)
|
161
|
-
)
|
162
|
-
)
|
163
|
-
|
164
|
-
unless value.has_key?('value')
|
165
|
-
raise Prefab::Error, "Feature flag config `#{key}` #{file} must have a `value`"
|
166
|
-
end
|
167
|
-
|
168
|
-
{
|
169
|
-
source: file,
|
170
|
-
match: key,
|
171
|
-
config: Prefab::Config.new(
|
172
|
-
key: key,
|
173
|
-
variants: [Prefab::FeatureFlagVariant.new(value_from(key, value['value']))],
|
174
|
-
rows: [row]
|
175
|
-
)
|
176
|
-
}
|
177
|
-
end
|
178
|
-
|
179
|
-
def criteria_values(criteria_hash)
|
180
|
-
if RUBY_VERSION < '2.7'
|
181
|
-
criteria_hash.transform_keys(&:to_sym)
|
182
|
-
else
|
183
|
-
criteria_hash
|
184
|
-
end
|
185
|
-
end
|
186
79
|
end
|
187
80
|
end
|
@@ -1,17 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Prefab
|
3
4
|
class ConfigResolver
|
4
|
-
include Prefab::ConfigHelper
|
5
|
-
NAMESPACE_DELIMITER = "."
|
6
|
-
|
7
5
|
attr_accessor :project_env_id # this will be set by the config_client when it gets an API response
|
8
6
|
|
9
7
|
def initialize(base_client, config_loader)
|
10
8
|
@lock = Concurrent::ReadWriteLock.new
|
11
9
|
@local_store = {}
|
12
|
-
@
|
10
|
+
@additional_properties = { Prefab::CriteriaEvaluator::NAMESPACE_KEY => base_client.options.namespace }
|
13
11
|
@config_loader = config_loader
|
14
|
-
@project_env_id = 0
|
12
|
+
@project_env_id = 0 # we don't know this yet, it is set from the API results
|
13
|
+
@base_client = base_client
|
15
14
|
make_local
|
16
15
|
end
|
17
16
|
|
@@ -22,82 +21,53 @@ module Prefab
|
|
22
21
|
v = @local_store[k]
|
23
22
|
elements = [k.slice(0..49).ljust(50)]
|
24
23
|
if v.nil?
|
25
|
-
elements <<
|
24
|
+
elements << 'tombstone'
|
26
25
|
else
|
27
|
-
|
28
|
-
|
29
|
-
elements <<
|
26
|
+
config = evaluate(v[:config], {})
|
27
|
+
value = Prefab::ConfigValueUnwrapper.unwrap(config, k, {})
|
28
|
+
elements << value.to_s.slice(0..34).ljust(35)
|
29
|
+
elements << value.class.to_s.slice(0..6).ljust(7)
|
30
30
|
elements << "Match: #{v[:match]}".slice(0..29).ljust(30)
|
31
31
|
elements << "Source: #{v[:source]}"
|
32
32
|
end
|
33
|
-
str += elements.join(
|
33
|
+
str += elements.join(' | ') << "\n"
|
34
34
|
end
|
35
35
|
end
|
36
36
|
str
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
40
|
-
|
41
|
-
config ? value_of(config[:value]) : nil
|
42
|
-
end
|
39
|
+
def raw(key)
|
40
|
+
via_key = @local_store[key]
|
43
41
|
|
44
|
-
|
45
|
-
config = _get(property)
|
46
|
-
config ? config[:config] : nil
|
42
|
+
via_key ? via_key[:config] : nil
|
47
43
|
end
|
48
44
|
|
49
|
-
def
|
45
|
+
def get(key, lookup_key, properties = {})
|
50
46
|
@lock.with_read_lock do
|
51
|
-
|
47
|
+
raw_config = raw(key)
|
48
|
+
|
49
|
+
return nil unless raw_config
|
50
|
+
|
51
|
+
evaluate(raw(key), lookup_key, properties)
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
+
def evaluate(config, lookup_key, properties = {})
|
56
|
+
props = properties.merge(@additional_properties).merge(Prefab::CriteriaEvaluator::LOOKUP_KEY => lookup_key)
|
57
|
+
|
58
|
+
Prefab::CriteriaEvaluator.new(config,
|
59
|
+
project_env_id: @project_env_id, resolver: self, base_client: @base_client).evaluate(props)
|
60
|
+
end
|
61
|
+
|
55
62
|
def update
|
56
63
|
make_local
|
57
64
|
end
|
58
65
|
|
59
66
|
private
|
60
67
|
|
61
|
-
# Should client a.b.c see key in namespace a.b? yes
|
62
|
-
# Should client a.b.c see key in namespace a.b.c? yes
|
63
|
-
# Should client a.b.c see key in namespace a.b.d? no
|
64
|
-
# Should client a.b.c see key in namespace ""? yes
|
65
|
-
#
|
66
|
-
def starts_with_ns?(key_namespace, client_namespace)
|
67
|
-
zipped = key_namespace.split(NAMESPACE_DELIMITER).zip(client_namespace.split(NAMESPACE_DELIMITER))
|
68
|
-
mapped = zipped.map do |k, c|
|
69
|
-
(k.nil? || k.empty?) || k == c
|
70
|
-
end
|
71
|
-
[mapped.all?, mapped.size]
|
72
|
-
end
|
73
|
-
|
74
68
|
def make_local
|
75
|
-
store = {}
|
76
|
-
@config_loader.calc_config.each do |key, config_resolver_obj|
|
77
|
-
config = config_resolver_obj[:config]
|
78
|
-
sortable = config.rows.map do |row|
|
79
|
-
if row.project_env_id != 0
|
80
|
-
if row.project_env_id == @project_env_id
|
81
|
-
if !row.namespace.empty?
|
82
|
-
(starts_with, count) = starts_with_ns?(row.namespace, @namespace)
|
83
|
-
# rubocop:disable BlockNesting
|
84
|
-
{ sortable: 2 + count, match: "nm:#{row.namespace}", value: row.value, config: config} if starts_with
|
85
|
-
else
|
86
|
-
{ sortable: 1, match: "env:#{row.project_env_id}", value: row.value, config: config}
|
87
|
-
end
|
88
|
-
end
|
89
|
-
else
|
90
|
-
match = config_resolver_obj[:match] || "default"
|
91
|
-
{ sortable: 0, match: match, value: row.value, config: config}
|
92
|
-
end
|
93
|
-
end.compact
|
94
|
-
to_store = sortable.sort_by { |h| h[:sortable] }.last
|
95
|
-
to_store[:source] = config_resolver_obj[:source]
|
96
|
-
store[key] = to_store
|
97
|
-
end
|
98
|
-
|
99
69
|
@lock.with_write_lock do
|
100
|
-
@local_store =
|
70
|
+
@local_store = @config_loader.calc_config
|
101
71
|
end
|
102
72
|
end
|
103
73
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class ConfigValueUnwrapper
|
5
|
+
def self.unwrap(config_value, config_key, properties)
|
6
|
+
return nil unless config_value
|
7
|
+
|
8
|
+
case config_value.type
|
9
|
+
when :int, :string, :double, :bool, :log_level
|
10
|
+
config_value.public_send(config_value.type)
|
11
|
+
when :string_list
|
12
|
+
config_value.string_list.values
|
13
|
+
when :weighted_values
|
14
|
+
lookup_key = properties[Prefab::CriteriaEvaluator::LOOKUP_KEY]
|
15
|
+
weights = config_value.weighted_values.weighted_values
|
16
|
+
value = Prefab::WeightedValueResolver.new(weights, config_key, lookup_key).resolve
|
17
|
+
unwrap(value.value, config_key, properties)
|
18
|
+
else
|
19
|
+
raise "Unknown type: #{config_value.type}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prefab
|
4
|
+
class CriteriaEvaluator
|
5
|
+
LOOKUP_KEY = 'LOOKUP'
|
6
|
+
NAMESPACE_KEY = 'NAMESPACE'
|
7
|
+
NO_MATCHING_ROWS = [].freeze
|
8
|
+
|
9
|
+
def initialize(config, project_env_id:, resolver:, base_client:)
|
10
|
+
@config = config
|
11
|
+
@project_env_id = project_env_id
|
12
|
+
@resolver = resolver
|
13
|
+
@base_client = base_client
|
14
|
+
end
|
15
|
+
|
16
|
+
def evaluate(properties)
|
17
|
+
# TODO: optimize this and perhaps do it elsewhere
|
18
|
+
props = properties.transform_keys(&:to_s)
|
19
|
+
|
20
|
+
matching_environment_row_values.each do |conditional_value|
|
21
|
+
return conditional_value.value if all_criteria_match?(conditional_value, props)
|
22
|
+
end
|
23
|
+
|
24
|
+
default_row_values.each do |conditional_value|
|
25
|
+
return conditional_value.value if all_criteria_match?(conditional_value, props)
|
26
|
+
end
|
27
|
+
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def all_criteria_match?(conditional_value, props)
|
32
|
+
conditional_value.criteria.all? do |criterion|
|
33
|
+
evaluate_criteron(criterion, props)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def evaluate_criteron(criterion, properties)
|
38
|
+
value_from_properties = properties[criterion.property_name]
|
39
|
+
|
40
|
+
case criterion.operator
|
41
|
+
when :LOOKUP_KEY_IN, :PROP_IS_ONE_OF
|
42
|
+
matches?(criterion, value_from_properties, properties)
|
43
|
+
when :LOOKUP_KEY_NOT_IN, :PROP_IS_NOT_ONE_OF
|
44
|
+
!matches?(criterion, value_from_properties, properties)
|
45
|
+
when :IN_SEG
|
46
|
+
in_segment?(criterion, properties)
|
47
|
+
when :NOT_IN_SEG
|
48
|
+
!in_segment?(criterion, properties)
|
49
|
+
when :PROP_ENDS_WITH_ONE_OF
|
50
|
+
return false unless value_from_properties
|
51
|
+
|
52
|
+
criterion.value_to_match.string_list.values.any? do |ending|
|
53
|
+
value_from_properties.end_with?(ending)
|
54
|
+
end
|
55
|
+
when :PROP_DOES_NOT_END_WITH_ONE_OF
|
56
|
+
return true unless value_from_properties
|
57
|
+
|
58
|
+
criterion.value_to_match.string_list.values.none? do |ending|
|
59
|
+
value_from_properties.end_with?(ending)
|
60
|
+
end
|
61
|
+
when :HIERARCHICAL_MATCH
|
62
|
+
value_from_properties.start_with?(criterion.value_to_match.string)
|
63
|
+
when :ALWAYS_TRUE
|
64
|
+
true
|
65
|
+
else
|
66
|
+
@base_client.log.info("Unknown Operator: #{criterion.operator}")
|
67
|
+
false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def matching_environment_row_values
|
74
|
+
@config.rows.find { |row| row.project_env_id == @project_env_id }&.values || NO_MATCHING_ROWS
|
75
|
+
end
|
76
|
+
|
77
|
+
def default_row_values
|
78
|
+
@config.rows.find { |row| row.project_env_id != @project_env_id }&.values || NO_MATCHING_ROWS
|
79
|
+
end
|
80
|
+
|
81
|
+
def in_segment?(criterion, properties)
|
82
|
+
@resolver.get(criterion.value_to_match.string, properties[LOOKUP_KEY], properties).bool
|
83
|
+
end
|
84
|
+
|
85
|
+
def matches?(criterion, value_from_properties, properties)
|
86
|
+
criterion_value_or_values = Prefab::ConfigValueUnwrapper.unwrap(criterion.value_to_match, @config.key, properties)
|
87
|
+
|
88
|
+
case criterion_value_or_values
|
89
|
+
when Google::Protobuf::RepeatedField
|
90
|
+
criterion_value_or_values.include?(value_from_properties)
|
91
|
+
else
|
92
|
+
criterion_value_or_values == value_from_properties
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -5,7 +5,7 @@ module Prefab
|
|
5
5
|
class InvalidApiKeyError < Prefab::Error
|
6
6
|
def initialize(key)
|
7
7
|
if key.nil? || key.empty?
|
8
|
-
message =
|
8
|
+
message = 'No API key. Set PREFAB_API_KEY env var or use PREFAB_DATASOURCES=LOCAL_ONLY'
|
9
9
|
|
10
10
|
super(message)
|
11
11
|
else
|
@@ -1,176 +1,44 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Prefab
|
3
4
|
class FeatureFlagClient
|
4
|
-
include Prefab::ConfigHelper
|
5
|
-
MAX_32_FLOAT = 4294967294.0
|
6
|
-
|
7
5
|
def initialize(base_client)
|
8
6
|
@base_client = base_client
|
9
7
|
end
|
10
8
|
|
11
|
-
def upsert(feature_name, feature_obj)
|
12
|
-
@base_client.config_client.upsert(feature_name, Prefab::ConfigValue.new(feature_flag: feature_obj))
|
13
|
-
end
|
14
|
-
|
15
9
|
def feature_is_on?(feature_name)
|
16
10
|
feature_is_on_for?(feature_name, nil)
|
17
11
|
end
|
18
12
|
|
19
13
|
def feature_is_on_for?(feature_name, lookup_key, attributes: {})
|
20
|
-
@base_client.stats.increment(
|
14
|
+
@base_client.stats.increment('prefab.featureflag.on', tags: ["feature:#{feature_name}"])
|
21
15
|
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
def get(feature_name, lookup_key=nil, attributes={}, default: false)
|
26
|
-
variant = _get(feature_name, lookup_key, attributes, default: default)
|
16
|
+
variant = @base_client.config_client.get(feature_name, false, attributes, lookup_key)
|
27
17
|
|
28
|
-
|
18
|
+
is_on?(variant)
|
29
19
|
end
|
30
20
|
|
31
|
-
|
21
|
+
def get(feature_name, lookup_key = nil, attributes = {}, default: false)
|
22
|
+
value = _get(feature_name, lookup_key, attributes)
|
32
23
|
|
33
|
-
|
34
|
-
if variant_maybe.nil?
|
35
|
-
default != Prefab::Client::NO_DEFAULT_PROVIDED ? default : nil
|
36
|
-
else
|
37
|
-
value_of_variant(variant_maybe)
|
38
|
-
end
|
24
|
+
value.nil? ? default : value
|
39
25
|
end
|
40
26
|
|
41
|
-
|
42
|
-
feature_obj = @base_client.config_client.get(feature_name, default)
|
43
|
-
config_obj = @base_client.config_client.get_config_obj(feature_name)
|
44
|
-
|
45
|
-
return nil if feature_obj.nil? || config_obj.nil?
|
46
|
-
|
47
|
-
if feature_obj == !!feature_obj
|
48
|
-
return feature_obj
|
49
|
-
end
|
27
|
+
private
|
50
28
|
|
51
|
-
|
52
|
-
|
29
|
+
def _get(feature_name, lookup_key = nil, attributes = {})
|
30
|
+
@base_client.config_client.get(feature_name, nil, attributes, lookup_key)
|
53
31
|
end
|
54
32
|
|
55
33
|
def is_on?(variant)
|
56
|
-
if variant.nil?
|
57
|
-
return false
|
58
|
-
end
|
34
|
+
return false if variant.nil?
|
59
35
|
|
60
|
-
if variant == !!variant
|
61
|
-
return variant
|
62
|
-
end
|
36
|
+
return variant if variant == !!variant
|
63
37
|
|
64
38
|
variant.bool
|
65
|
-
rescue
|
39
|
+
rescue StandardError
|
66
40
|
@base_client.log.info("is_on? methods only work for boolean feature flags variants. This feature flags variant is '#{variant}'. Returning false")
|
67
41
|
false
|
68
42
|
end
|
69
|
-
|
70
|
-
def get_variant(feature_name, lookup_key, attributes, feature_obj, variants)
|
71
|
-
if !feature_obj.active
|
72
|
-
return get_variant_obj(variants, feature_obj.inactive_variant_idx)
|
73
|
-
end
|
74
|
-
|
75
|
-
#default to inactive
|
76
|
-
variant_weights = [Prefab::VariantWeight.new(variant_idx: feature_obj.inactive_variant_idx, weight: 1)]
|
77
|
-
|
78
|
-
# if rules.match
|
79
|
-
feature_obj.rules.each do |rule|
|
80
|
-
if criteria_match?(rule.criteria, lookup_key, attributes)
|
81
|
-
variant_weights = rule.variant_weights
|
82
|
-
break
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
percent_through_distribution = rand()
|
87
|
-
if lookup_key
|
88
|
-
percent_through_distribution = get_user_pct(feature_name, lookup_key)
|
89
|
-
end
|
90
|
-
|
91
|
-
variant_idx = get_variant_idx_from_weights(variant_weights, percent_through_distribution, feature_name)
|
92
|
-
|
93
|
-
return get_variant_obj(variants, variant_idx)
|
94
|
-
end
|
95
|
-
|
96
|
-
def get_variant_obj(variants, idx)
|
97
|
-
# our array is 0 based, but the idx are 1 based so the protos are clearly set
|
98
|
-
return variants[idx - 1] if variants.length >= idx
|
99
|
-
nil
|
100
|
-
end
|
101
|
-
|
102
|
-
def get_variant_idx_from_weights(variant_weights, percent_through_distribution, feature_name)
|
103
|
-
distrubution_space = variant_weights.inject(0) { |sum, v| sum + v.weight }
|
104
|
-
bucket = distrubution_space * percent_through_distribution
|
105
|
-
sum = 0
|
106
|
-
variant_weights.each do |variant_weight|
|
107
|
-
if bucket < sum + variant_weight.weight
|
108
|
-
return variant_weight.variant_idx
|
109
|
-
else
|
110
|
-
sum += variant_weight.weight
|
111
|
-
end
|
112
|
-
end
|
113
|
-
# variants didn't add up to 100%
|
114
|
-
@base_client.log.info("Variants of #{feature_name} did not add to 100%")
|
115
|
-
return variant_weights.last.variant_idx
|
116
|
-
end
|
117
|
-
|
118
|
-
def get_user_pct(feature, lookup_key)
|
119
|
-
to_hash = "#{feature}#{lookup_key}"
|
120
|
-
int_value = Murmur3.murmur3_32(to_hash)
|
121
|
-
int_value / MAX_32_FLOAT
|
122
|
-
end
|
123
|
-
|
124
|
-
def criteria_match?(criteria, lookup_key, attributes)
|
125
|
-
case criteria.operator
|
126
|
-
when :ALWAYS_TRUE
|
127
|
-
true
|
128
|
-
when :LOOKUP_KEY_IN
|
129
|
-
criteria.values.include?(lookup_key)
|
130
|
-
when :LOOKUP_KEY_NOT_IN
|
131
|
-
!criteria.values.include?(lookup_key)
|
132
|
-
when :IN_SEG
|
133
|
-
segment_matches?(criteria.values, lookup_key, attributes)
|
134
|
-
when :NOT_IN_SEG
|
135
|
-
!segment_matches?(criteria.values, lookup_key, attributes)
|
136
|
-
when :PROP_IS_ONE_OF
|
137
|
-
criteria.values.include?(attribute_value(attributes, criteria.property))
|
138
|
-
when :PROP_IS_NOT_ONE_OF
|
139
|
-
!criteria.values.include?(attribute_value(attributes, criteria.property))
|
140
|
-
when :PROP_ENDS_WITH_ONE_OF
|
141
|
-
criteria.values.any? { |value| attribute_value(attributes, criteria.property)&.end_with?(value) }
|
142
|
-
when :PROP_DOES_NOT_END_WITH_ONE_OF
|
143
|
-
criteria.values.none? { |value| attribute_value(attributes, criteria.property)&.end_with?(value) }
|
144
|
-
else
|
145
|
-
@base_client.log.info("Unknown Operator: #{criteria.operator}")
|
146
|
-
false
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
def attribute_value(attributes, property)
|
151
|
-
attributes[property] || attributes[property.to_sym]
|
152
|
-
end
|
153
|
-
|
154
|
-
# evaluate each segment key and return whether any match
|
155
|
-
# there should be an associated segment available as a standard config obj
|
156
|
-
def segment_matches?(segment_keys, lookup_key, attributes)
|
157
|
-
segment_keys.any? do |segment_key|
|
158
|
-
segment = @base_client.config_client.get(segment_key)
|
159
|
-
if segment.nil?
|
160
|
-
@base_client.log.info("Missing Segment")
|
161
|
-
false
|
162
|
-
else
|
163
|
-
segment_match?(segment, lookup_key, attributes)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
# does a given segment match?
|
169
|
-
def segment_match?(segment, lookup_key, attributes)
|
170
|
-
segment.criterion.any? do |criteria|
|
171
|
-
criteria_match?(criteria, lookup_key, attributes)
|
172
|
-
end
|
173
|
-
end
|
174
43
|
end
|
175
44
|
end
|
176
|
-
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Prefab
|
3
4
|
class InternalLogger < Logger
|
4
5
|
def initialize(path, logger)
|
@@ -6,23 +7,23 @@ module Prefab
|
|
6
7
|
@logger = logger
|
7
8
|
end
|
8
9
|
|
9
|
-
def debug(progname = nil
|
10
|
+
def debug(progname = nil)
|
10
11
|
@logger.log_internal yield, @path, progname, DEBUG
|
11
12
|
end
|
12
13
|
|
13
|
-
def info(progname = nil
|
14
|
+
def info(progname = nil)
|
14
15
|
@logger.log_internal yield, @path, progname, INFO
|
15
16
|
end
|
16
17
|
|
17
|
-
def warn(progname = nil
|
18
|
+
def warn(progname = nil)
|
18
19
|
@logger.log_internal yield, @path, progname, WARN
|
19
20
|
end
|
20
21
|
|
21
|
-
def error(progname = nil
|
22
|
+
def error(progname = nil)
|
22
23
|
@logger.log_internal yield, @path, progname, ERROR
|
23
24
|
end
|
24
25
|
|
25
|
-
def fatal(progname = nil
|
26
|
+
def fatal(progname = nil)
|
26
27
|
@logger.log_internal yield, @path, progname, FATAL
|
27
28
|
end
|
28
29
|
end
|