featurevisor 3.0.0 → 4.0.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/README.md +307 -193
- data/bin/cli.rb +84 -17
- data/bin/commands/assess_distribution.rb +10 -52
- data/bin/commands/benchmark.rb +10 -48
- data/bin/commands/node_cli.rb +20 -0
- data/bin/commands/runtime_project.rb +63 -0
- data/bin/commands/test.rb +771 -259
- data/bin/commands.rb +1 -0
- data/bin/segment_tests.rb +89 -0
- data/bin/test_checks.rb +767 -0
- data/lib/featurevisor/child_instance.rb +15 -2
- data/lib/featurevisor/compare_versions.rb +18 -36
- data/lib/featurevisor/conditions.rb +91 -9
- data/lib/featurevisor/dependencies.rb +150 -0
- data/lib/featurevisor/emitter.rb +4 -0
- data/lib/featurevisor/evaluate.rb +64 -115
- data/lib/featurevisor/evaluation_data_provider.rb +100 -51
- data/lib/featurevisor/evaluation_session.rb +191 -0
- data/lib/featurevisor/events.rb +39 -100
- data/lib/featurevisor/instance.rb +258 -59
- data/lib/featurevisor/modules.rb +6 -46
- data/lib/featurevisor/mutations.rb +213 -0
- data/lib/featurevisor/normalize.rb +105 -0
- data/lib/featurevisor/unified_conditions.rb +79 -0
- data/lib/featurevisor/version.rb +1 -1
- data/lib/featurevisor.rb +7 -0
- metadata +10 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Featurevisor
|
|
4
|
+
module Mutations
|
|
5
|
+
DANGEROUS_KEYS = %w[__proto__ prototype constructor].freeze
|
|
6
|
+
OPERATIONS = %w[set append prepend appendUnique removeValue before after remove].freeze
|
|
7
|
+
MAX_SAFE_INTEGER = 9_007_199_254_740_991
|
|
8
|
+
private_constant :DANGEROUS_KEYS, :OPERATIONS, :MAX_SAFE_INTEGER
|
|
9
|
+
|
|
10
|
+
def self.apply(value, operations)
|
|
11
|
+
raise ArgumentError, "Invalid variable mutation operations" unless operations.is_a?(Array)
|
|
12
|
+
|
|
13
|
+
operations.reduce(value) do |current, operation|
|
|
14
|
+
validate(operation)
|
|
15
|
+
update_at_path(current, operation, 0)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.validate(operation)
|
|
20
|
+
unless operation.is_a?(Hash) && operation.key?(:op) && operation.key?(:path)
|
|
21
|
+
raise ArgumentError, "Invalid variable mutation operation"
|
|
22
|
+
end
|
|
23
|
+
op = operation[:op]
|
|
24
|
+
raise ArgumentError, "Invalid variable mutation operation" unless OPERATIONS.include?(op)
|
|
25
|
+
if op != "remove" && !operation.key?(:value)
|
|
26
|
+
raise ArgumentError, "Variable mutation operation requires a value"
|
|
27
|
+
end
|
|
28
|
+
if %w[appendUnique removeValue].include?(op)
|
|
29
|
+
value = operation[:value]
|
|
30
|
+
unless primitive?(value) && (!value.is_a?(Numeric) || !value.respond_to?(:finite?) || value.finite?)
|
|
31
|
+
raise ArgumentError, "Variable mutation operation requires a primitive value"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
raise ArgumentError, "Invalid variable mutation path" unless operation[:path].is_a?(Array)
|
|
35
|
+
|
|
36
|
+
operation[:path].each do |part|
|
|
37
|
+
key = if part.is_a?(String)
|
|
38
|
+
part
|
|
39
|
+
elsif !array_index(part).nil?
|
|
40
|
+
nil
|
|
41
|
+
else
|
|
42
|
+
select = selector(part)
|
|
43
|
+
raise ArgumentError, "Invalid variable mutation path" unless select
|
|
44
|
+
select[:property]
|
|
45
|
+
end
|
|
46
|
+
raise ArgumentError, "Unsafe variable mutation key" if key && DANGEROUS_KEYS.include?(key)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
private_class_method :validate
|
|
50
|
+
|
|
51
|
+
def self.primitive?(value)
|
|
52
|
+
value.nil? || value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false
|
|
53
|
+
end
|
|
54
|
+
private_class_method :primitive?
|
|
55
|
+
|
|
56
|
+
def self.same?(left, right)
|
|
57
|
+
return true if left.nil? && right.nil?
|
|
58
|
+
return left == right if left.is_a?(Numeric) && right.is_a?(Numeric)
|
|
59
|
+
return left == right if left.is_a?(String) && right.is_a?(String)
|
|
60
|
+
return left == right if [true, false].include?(left) && [true, false].include?(right)
|
|
61
|
+
|
|
62
|
+
left.equal?(right)
|
|
63
|
+
end
|
|
64
|
+
private_class_method :same?
|
|
65
|
+
|
|
66
|
+
def self.array_index(value)
|
|
67
|
+
return nil unless value.is_a?(Numeric)
|
|
68
|
+
return nil if value.respond_to?(:finite?) && !value.finite?
|
|
69
|
+
return nil unless value >= 0 && value <= MAX_SAFE_INTEGER && value.to_i == value
|
|
70
|
+
|
|
71
|
+
value.to_i
|
|
72
|
+
end
|
|
73
|
+
private_class_method :array_index
|
|
74
|
+
|
|
75
|
+
def self.selector(part)
|
|
76
|
+
return nil unless part.is_a?(Hash)
|
|
77
|
+
select = part[:select] || part["select"]
|
|
78
|
+
return nil unless select.is_a?(Hash)
|
|
79
|
+
property = select[:property] || select["property"]
|
|
80
|
+
values = select[:values] || select["values"]
|
|
81
|
+
return nil unless property.is_a?(String) && values.is_a?(Array) && values.all? { |value| primitive?(value) }
|
|
82
|
+
|
|
83
|
+
{ property: property, values: values }
|
|
84
|
+
end
|
|
85
|
+
private_class_method :selector
|
|
86
|
+
|
|
87
|
+
def self.hash_key(hash, key)
|
|
88
|
+
return key if hash.key?(key)
|
|
89
|
+
symbol = key.to_sym
|
|
90
|
+
hash.key?(symbol) ? symbol : symbol
|
|
91
|
+
end
|
|
92
|
+
private_class_method :hash_key
|
|
93
|
+
|
|
94
|
+
def self.selected_index(array, select)
|
|
95
|
+
array.each_with_index do |candidate, index|
|
|
96
|
+
next unless candidate.is_a?(Hash)
|
|
97
|
+
key = hash_key(candidate, select[:property])
|
|
98
|
+
next unless candidate.key?(key)
|
|
99
|
+
value = candidate[key]
|
|
100
|
+
return index if primitive?(value) && select[:values].any? { |expected| same?(value, expected) }
|
|
101
|
+
end
|
|
102
|
+
-1
|
|
103
|
+
end
|
|
104
|
+
private_class_method :selected_index
|
|
105
|
+
|
|
106
|
+
def self.path_key(container, part)
|
|
107
|
+
if part.is_a?(String)
|
|
108
|
+
raise ArgumentError, "Variable mutation expected an object" unless container.is_a?(Hash)
|
|
109
|
+
return hash_key(container, part)
|
|
110
|
+
end
|
|
111
|
+
raise ArgumentError, "Variable mutation expected an array" unless container.is_a?(Array)
|
|
112
|
+
|
|
113
|
+
index = array_index(part)
|
|
114
|
+
return index unless index.nil?
|
|
115
|
+
|
|
116
|
+
select = selector(part)
|
|
117
|
+
raise ArgumentError, "Invalid variable mutation path" unless select
|
|
118
|
+
|
|
119
|
+
selected_index(container, select)
|
|
120
|
+
end
|
|
121
|
+
private_class_method :path_key
|
|
122
|
+
|
|
123
|
+
def self.exists?(container, key)
|
|
124
|
+
container.is_a?(Hash) ? container.key?(key) : key >= 0 && key < container.length
|
|
125
|
+
end
|
|
126
|
+
private_class_method :exists?
|
|
127
|
+
|
|
128
|
+
def self.replace(container, key, value)
|
|
129
|
+
result = container.dup
|
|
130
|
+
if result.is_a?(Array) && key == result.length
|
|
131
|
+
result << value
|
|
132
|
+
else
|
|
133
|
+
result[key] = value
|
|
134
|
+
end
|
|
135
|
+
result
|
|
136
|
+
end
|
|
137
|
+
private_class_method :replace
|
|
138
|
+
|
|
139
|
+
def self.update_array(array, op, item)
|
|
140
|
+
if op == "removeValue"
|
|
141
|
+
return array unless array.any? { |value| same?(value, item) }
|
|
142
|
+
return array.reject { |value| same?(value, item) }
|
|
143
|
+
end
|
|
144
|
+
return array if op == "appendUnique" && array.any? { |value| same?(value, item) }
|
|
145
|
+
|
|
146
|
+
op == "prepend" ? [item, *array] : [*array, item]
|
|
147
|
+
end
|
|
148
|
+
private_class_method :update_array
|
|
149
|
+
|
|
150
|
+
def self.update_final(container, part, operation)
|
|
151
|
+
return container if container.nil?
|
|
152
|
+
op = operation[:op]
|
|
153
|
+
if %w[before after].include?(op) && part.is_a?(String)
|
|
154
|
+
raise ArgumentError, "Variable mutation before/after requires an array element path"
|
|
155
|
+
end
|
|
156
|
+
key = path_key(container, part)
|
|
157
|
+
found = exists?(container, key)
|
|
158
|
+
|
|
159
|
+
if op == "remove"
|
|
160
|
+
return container unless found
|
|
161
|
+
result = container.dup
|
|
162
|
+
result.is_a?(Array) ? result.delete_at(key) : result.delete(key)
|
|
163
|
+
return result
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
item = operation[:value]
|
|
167
|
+
if op == "set"
|
|
168
|
+
return container if key.is_a?(Integer) && (key.negative? || key > container.length)
|
|
169
|
+
return container if found && same?(container[key], item)
|
|
170
|
+
return replace(container, key, item)
|
|
171
|
+
end
|
|
172
|
+
if %w[before after].include?(op)
|
|
173
|
+
return container unless found
|
|
174
|
+
result = container.dup
|
|
175
|
+
result.insert(key + (op == "after" ? 1 : 0), item)
|
|
176
|
+
return result
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
current = found ? container[key] : nil
|
|
180
|
+
return container if current.nil? && key.is_a?(Integer)
|
|
181
|
+
return container if current.nil? && op == "removeValue"
|
|
182
|
+
raise ArgumentError, "Variable mutation expected an array" if !current.nil? && !current.is_a?(Array)
|
|
183
|
+
array = current || []
|
|
184
|
+
result = update_array(array, op, item)
|
|
185
|
+
result.equal?(current) ? container : replace(container, key, result)
|
|
186
|
+
end
|
|
187
|
+
private_class_method :update_final
|
|
188
|
+
|
|
189
|
+
def self.update_at_path(value, operation, offset)
|
|
190
|
+
path = operation[:path]
|
|
191
|
+
if offset == path.length
|
|
192
|
+
return same?(value, operation[:value]) ? value : operation[:value] if operation[:op] == "set"
|
|
193
|
+
raise ArgumentError, "Cannot remove the root variable value" if operation[:op] == "remove"
|
|
194
|
+
if %w[before after].include?(operation[:op])
|
|
195
|
+
raise ArgumentError, "Variable mutation before/after requires an array element path"
|
|
196
|
+
end
|
|
197
|
+
return value if value.nil?
|
|
198
|
+
raise ArgumentError, "Variable mutation expected an array" unless value.is_a?(Array)
|
|
199
|
+
return update_array(value, operation[:op], operation[:value])
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
return update_final(value, path[offset], operation) if offset == path.length - 1
|
|
203
|
+
return value if value.nil?
|
|
204
|
+
key = path_key(value, path[offset])
|
|
205
|
+
return value unless exists?(value, key)
|
|
206
|
+
current = value[key]
|
|
207
|
+
return value if current.nil?
|
|
208
|
+
result = update_at_path(current, operation, offset + 1)
|
|
209
|
+
result.equal?(current) ? value : replace(value, key, result)
|
|
210
|
+
end
|
|
211
|
+
private_class_method :update_at_path
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Featurevisor
|
|
6
|
+
module Normalize
|
|
7
|
+
class InvalidConditions < Hash
|
|
8
|
+
def initialize
|
|
9
|
+
super
|
|
10
|
+
self[:or] = []
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class ParseFailure < StandardError; end
|
|
15
|
+
|
|
16
|
+
def self.legacy(value, features = false)
|
|
17
|
+
return value if value.is_a?(InvalidConditions)
|
|
18
|
+
case value
|
|
19
|
+
when String
|
|
20
|
+
features ? { feature: value } : value
|
|
21
|
+
when Array
|
|
22
|
+
value.empty? ? "*" : value.map { |entry| legacy(entry, features) }
|
|
23
|
+
when Hash
|
|
24
|
+
group = %i[and or not].find { |name| value[name].is_a?(Array) }
|
|
25
|
+
return value unless group
|
|
26
|
+
return "*" if group == :and && value[group].empty?
|
|
27
|
+
{ group => value[group].map { |entry| legacy(entry, features) } }
|
|
28
|
+
else
|
|
29
|
+
value
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.entity(kind, key, source, legacy = false)
|
|
34
|
+
errors = []
|
|
35
|
+
parse = lambda do |value|
|
|
36
|
+
next value unless value.is_a?(String) && value.lstrip.match?(/\A[\{\[\"]/)
|
|
37
|
+
begin
|
|
38
|
+
JSON.parse(value, symbolize_names: true)
|
|
39
|
+
rescue JSON::ParserError => error
|
|
40
|
+
errors << { level: "error", code: "conditions_parse_error", message: "Error parsing conditions",
|
|
41
|
+
original_error: error,
|
|
42
|
+
details: { conditions: value, entityType: kind.to_s, entityKey: key.to_s } }
|
|
43
|
+
InvalidConditions.new
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
select = lambda do |entry, old = false|
|
|
47
|
+
result = entry.reject { |name, _| %i[segments requiredFeatures requiredVariables required].include?(name) }
|
|
48
|
+
parts = []
|
|
49
|
+
if entry.key?(:conditions)
|
|
50
|
+
value = parse.call(entry[:conditions])
|
|
51
|
+
parts << (old ? self.legacy(value) : value)
|
|
52
|
+
end
|
|
53
|
+
parts << self.legacy(parse.call(entry[:segments])) if entry.key?(:segments)
|
|
54
|
+
if entry.key?(:requiredFeatures)
|
|
55
|
+
features = entry[:requiredFeatures]
|
|
56
|
+
features = [features] unless features.is_a?(Array)
|
|
57
|
+
parts << features.map { |value| self.legacy(value, true) } unless features.empty?
|
|
58
|
+
end
|
|
59
|
+
if parts.any? { |part| part.is_a?(InvalidConditions) }
|
|
60
|
+
result[:conditions] = InvalidConditions.new
|
|
61
|
+
elsif parts.length == 1
|
|
62
|
+
result[:conditions] = parts.first
|
|
63
|
+
elsif !parts.empty?
|
|
64
|
+
result[:conditions] = parts.flat_map { |part| part.is_a?(Array) ? (part.empty? ? [{ and: [] }] : part) : [part] }
|
|
65
|
+
end
|
|
66
|
+
result
|
|
67
|
+
end
|
|
68
|
+
with_overrides = lambda do |entry|
|
|
69
|
+
next entry unless entry[:variableOverrides]
|
|
70
|
+
entry.merge(variableOverrides: entry[:variableOverrides].transform_values do |values|
|
|
71
|
+
values.map { |override| select.call(override, legacy) }
|
|
72
|
+
end)
|
|
73
|
+
end
|
|
74
|
+
if kind.to_s == "segment"
|
|
75
|
+
value = parse.call(source[:conditions])
|
|
76
|
+
result = source.merge(conditions: legacy ? self.legacy(value) : value)
|
|
77
|
+
else
|
|
78
|
+
result = select.call(source)
|
|
79
|
+
if kind.to_s == "feature"
|
|
80
|
+
result[:traffic] = (source[:traffic] || []).map { |entry| with_overrides.call(select.call(entry)) }
|
|
81
|
+
result[:force] = source[:force].map { |entry| select.call(entry, legacy) } if source[:force]
|
|
82
|
+
result[:variations] = source[:variations].map { |entry| with_overrides.call(entry) } if source[:variations]
|
|
83
|
+
else
|
|
84
|
+
%i[overrides overrideProgram].each do |name|
|
|
85
|
+
result[name] = source[name].map { |entry| select.call(entry, legacy && name == :overrides) } if source[name]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
[result, errors]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.datafile(source)
|
|
93
|
+
errors = []
|
|
94
|
+
result = source.dup
|
|
95
|
+
{ features: :feature, segments: :segment, variables: :variable }.each do |name, kind|
|
|
96
|
+
next unless source[name]
|
|
97
|
+
result[name] = source[name].each_with_object({}) do |(key, entry), values|
|
|
98
|
+
values[key], diagnostics = entity(kind, key, entry, true)
|
|
99
|
+
errors.concat(diagnostics)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
[result, errors]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Featurevisor
|
|
4
|
+
module UnifiedConditions
|
|
5
|
+
def self.matches?(conditions, options)
|
|
6
|
+
match(conditions, options)
|
|
7
|
+
rescue Normalize::ParseFailure
|
|
8
|
+
false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.match(conditions, options)
|
|
12
|
+
raise Normalize::ParseFailure if conditions.is_a?(Normalize::InvalidConditions)
|
|
13
|
+
return true if conditions == "*"
|
|
14
|
+
if conditions.is_a?(String)
|
|
15
|
+
datafile = options[:__featurevisor_datafile_source]&.call || options[:datafile]
|
|
16
|
+
segment = datafile.get_segment(conditions)
|
|
17
|
+
return false unless segment
|
|
18
|
+
raise Normalize::ParseFailure if segment[:conditions].is_a?(Normalize::InvalidConditions)
|
|
19
|
+
if datafile.attribute_only_conditions?(segment[:conditions])
|
|
20
|
+
return datafile.all_conditions_are_matched(segment[:conditions], options[:context])
|
|
21
|
+
end
|
|
22
|
+
session = options[:__featurevisor_session] ||= EvaluationSession.new
|
|
23
|
+
return session.segment(conditions) { match(segment[:conditions], options) }
|
|
24
|
+
end
|
|
25
|
+
if conditions.is_a?(Array)
|
|
26
|
+
return !conditions.empty? && conditions.all? { |child| match(child, options) }
|
|
27
|
+
end
|
|
28
|
+
return false unless conditions.is_a?(Hash)
|
|
29
|
+
if conditions.key?(:variable)
|
|
30
|
+
begin
|
|
31
|
+
return options[:__featurevisor_instance].send(:variable_condition_matches?, conditions, options)
|
|
32
|
+
rescue RequirementError
|
|
33
|
+
raise
|
|
34
|
+
rescue => error
|
|
35
|
+
raise RequirementError, error.message
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
if conditions.key?(:feature)
|
|
39
|
+
clean = options.reject { |key, _| %i[feature_key variable_key default_variable_value default_variation_value].include?(key) }
|
|
40
|
+
clean = clean.merge(__featurevisor_prerequisite: true, dependency_purpose: "condition")
|
|
41
|
+
flag = Evaluate.evaluate_with_modules(clean.merge(type: "flag", feature_key: conditions[:feature]))
|
|
42
|
+
enabled = conditions[:enabled].nil? ? true : conditions[:enabled]
|
|
43
|
+
return false unless (flag[:enabled] == true) == enabled
|
|
44
|
+
return true unless conditions.key?(:variation)
|
|
45
|
+
variation = Evaluate.evaluate_with_modules(clean.merge(type: "variation", feature_key: conditions[:feature]))
|
|
46
|
+
value = variation.key?(:variation_value) ? variation[:variation_value] : variation.dig(:variation, :value)
|
|
47
|
+
return value == conditions[:variation]
|
|
48
|
+
end
|
|
49
|
+
if conditions.key?(:attribute)
|
|
50
|
+
return options[:datafile].all_conditions_are_matched(conditions, options[:context])
|
|
51
|
+
end
|
|
52
|
+
%i[and or not].each do |group|
|
|
53
|
+
next unless conditions[group].is_a?(Array)
|
|
54
|
+
children = conditions[group]
|
|
55
|
+
return false if children.empty?
|
|
56
|
+
return children.any? { |child| match(child, options) } if group == :or
|
|
57
|
+
result = children.all? { |child| match(child, options) }
|
|
58
|
+
return group == :not ? !result : result
|
|
59
|
+
end
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
private_class_method :match
|
|
63
|
+
|
|
64
|
+
def self.attribute_only?(conditions)
|
|
65
|
+
case conditions
|
|
66
|
+
when Array
|
|
67
|
+
conditions.all? { |child| attribute_only?(child) }
|
|
68
|
+
when Hash
|
|
69
|
+
return false if conditions.is_a?(Normalize::InvalidConditions) || conditions.key?(:feature) || conditions.key?(:variable)
|
|
70
|
+
return true if conditions.key?(:attribute)
|
|
71
|
+
group = %i[and or not].find { |name| conditions.key?(name) }
|
|
72
|
+
group.nil? || attribute_only?(conditions[group])
|
|
73
|
+
else
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
private_class_method :attribute_only?
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/featurevisor/version.rb
CHANGED
data/lib/featurevisor.rb
CHANGED
|
@@ -4,9 +4,14 @@ require_relative "featurevisor/compare_versions"
|
|
|
4
4
|
require_relative "featurevisor/diagnostics"
|
|
5
5
|
require_relative "featurevisor/emitter"
|
|
6
6
|
require_relative "featurevisor/conditions"
|
|
7
|
+
require_relative "featurevisor/normalize"
|
|
8
|
+
require_relative "featurevisor/mutations"
|
|
9
|
+
require_relative "featurevisor/dependencies"
|
|
7
10
|
require_relative "featurevisor/evaluation_data_provider"
|
|
8
11
|
require_relative "featurevisor/bucketer"
|
|
9
12
|
require_relative "featurevisor/modules"
|
|
13
|
+
require_relative "featurevisor/evaluation_session"
|
|
14
|
+
require_relative "featurevisor/unified_conditions"
|
|
10
15
|
require_relative "featurevisor/evaluate"
|
|
11
16
|
require_relative "featurevisor/instance"
|
|
12
17
|
require_relative "featurevisor/child_instance"
|
|
@@ -17,4 +22,6 @@ module Featurevisor
|
|
|
17
22
|
|
|
18
23
|
private_constant :InstanceEvaluationDataProvider
|
|
19
24
|
private_constant :DiagnosticReporter
|
|
25
|
+
private_constant :Mutations
|
|
26
|
+
private_constant :Dependencies
|
|
20
27
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: featurevisor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fahad Heylaal
|
|
@@ -69,21 +69,30 @@ files:
|
|
|
69
69
|
- bin/commands.rb
|
|
70
70
|
- bin/commands/assess_distribution.rb
|
|
71
71
|
- bin/commands/benchmark.rb
|
|
72
|
+
- bin/commands/node_cli.rb
|
|
73
|
+
- bin/commands/runtime_project.rb
|
|
72
74
|
- bin/commands/test.rb
|
|
73
75
|
- bin/featurevisor
|
|
76
|
+
- bin/segment_tests.rb
|
|
77
|
+
- bin/test_checks.rb
|
|
74
78
|
- lib/featurevisor.rb
|
|
75
79
|
- lib/featurevisor/bucketer.rb
|
|
76
80
|
- lib/featurevisor/child_instance.rb
|
|
77
81
|
- lib/featurevisor/compare_versions.rb
|
|
78
82
|
- lib/featurevisor/conditions.rb
|
|
83
|
+
- lib/featurevisor/dependencies.rb
|
|
79
84
|
- lib/featurevisor/diagnostics.rb
|
|
80
85
|
- lib/featurevisor/emitter.rb
|
|
81
86
|
- lib/featurevisor/evaluate.rb
|
|
82
87
|
- lib/featurevisor/evaluation_data_provider.rb
|
|
88
|
+
- lib/featurevisor/evaluation_session.rb
|
|
83
89
|
- lib/featurevisor/events.rb
|
|
84
90
|
- lib/featurevisor/instance.rb
|
|
85
91
|
- lib/featurevisor/modules.rb
|
|
86
92
|
- lib/featurevisor/murmurhash.rb
|
|
93
|
+
- lib/featurevisor/mutations.rb
|
|
94
|
+
- lib/featurevisor/normalize.rb
|
|
95
|
+
- lib/featurevisor/unified_conditions.rb
|
|
87
96
|
- lib/featurevisor/version.rb
|
|
88
97
|
homepage: https://featurevisor.com
|
|
89
98
|
licenses:
|