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.
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Featurevisor
6
+ class RequirementError < StandardError; end
7
+ class RequirementCycleError < RequirementError; end
8
+
9
+ # One evaluation owns its prerequisite stack and completed prerequisite values.
10
+ class EvaluationSession
11
+ def initialize
12
+ @root = nil
13
+ @root_current = nil
14
+ @active = nil
15
+ @memo = nil
16
+ @sticky_inputs = nil
17
+ @segments = nil
18
+ end
19
+
20
+ def dependency_of
21
+ @active ? @active.last&.dig(:current) : @root_current
22
+ end
23
+
24
+ def update_current(options)
25
+ current = dependency_of
26
+ return if current && current[:type] == options[:type] &&
27
+ current[:feature_key] == options[:feature_key] && current[:variable_key] == options[:variable_key]
28
+
29
+ identity = identity_of(options)
30
+ if @active
31
+ @active.last[:current] = identity
32
+ else
33
+ @root_current = identity
34
+ end
35
+ end
36
+
37
+ def run(options, prerequisite: false)
38
+ # A root needs its original and redirected identities, but no dependency
39
+ # stack, memo or retained sticky maps until a handoff is actually reached.
40
+ if !@root && !@active && !prerequisite
41
+ @root = @root_current = identity_of(options)
42
+ begin
43
+ return yield
44
+ ensure
45
+ clear
46
+ end
47
+ end
48
+
49
+ @active ||= @root ? [{ identity: @root, current: @root_current }] : []
50
+ identity = identity_of(options)
51
+ raise RequirementCycleError, "Circular evaluation requirement" if @active.any? { |frame| frame[:identity] == identity || frame[:current] == identity }
52
+ raise RequirementError, "Evaluation dependency depth exceeded" if @active.length + (@segments&.length || 0) > 256
53
+
54
+ requester = dependency_of
55
+ key = prerequisite ? fingerprint(identity, requester, options) : nil
56
+ borrowed = options[:dependency_purpose] == "composition"
57
+ return @memo[key] if key && @memo&.key?(key)
58
+
59
+ @active << { identity: identity, current: identity }
60
+ begin
61
+ result = yield
62
+ if prerequisite && options[:__featurevisor_project_variable]
63
+ result = options[:__featurevisor_project_variable].call(result)
64
+ end
65
+ if key && key == fingerprint(identity, requester, options)
66
+ minimal = minimal_result(result, borrowed)
67
+ (@memo ||= {})[key] = borrowed ? minimal : duplicate(minimal) if minimal
68
+ end
69
+ result
70
+ rescue => e
71
+ @pending_error = { identity: identity, requester: requester, key: key }
72
+ raise
73
+ ensure
74
+ @active.pop
75
+ clear if @active.empty?
76
+ end
77
+ end
78
+
79
+ def segment(key)
80
+ @segments ||= []
81
+ raise RequirementCycleError, "Circular dependency" if @segments.include?(key)
82
+ active_length = @active ? @active.length : (@root ? 1 : 0)
83
+ raise RequirementError, "Evaluation dependency depth exceeded" if @segments.length + [active_length - 1, 0].max >= 256
84
+ @segments << key
85
+ begin
86
+ yield
87
+ ensure
88
+ @segments.pop
89
+ end
90
+ end
91
+
92
+ def remember(options, result)
93
+ return unless options[:__featurevisor_prerequisite]
94
+
95
+ identity = identity_of(options)
96
+ pending = @pending_error
97
+ @pending_error = nil
98
+ return unless pending && pending[:identity] == identity && pending[:requester] == dependency_of
99
+
100
+ key = fingerprint(identity, dependency_of, options)
101
+ return unless key && key == pending[:key]
102
+ borrowed = options[:dependency_purpose] == "composition"
103
+ minimal = minimal_result(result, borrowed)
104
+ (@memo ||= {})[key] = borrowed ? minimal : duplicate(minimal) if minimal
105
+ end
106
+
107
+ private
108
+
109
+ def clear
110
+ @root = @root_current = nil
111
+ @active&.clear
112
+ @memo&.clear
113
+ @sticky_inputs&.clear
114
+ @segments&.clear
115
+ @active = nil
116
+ @pending_error = nil
117
+ end
118
+
119
+ def identity_of(options)
120
+ identity = {}
121
+ identity[:type] = options[:type] unless options[:type].nil?
122
+ identity[:feature_key] = options[:feature_key] unless options[:feature_key].nil?
123
+ identity[:variable_key] = options[:variable_key] unless options[:variable_key].nil?
124
+ identity
125
+ end
126
+
127
+ def fingerprint(identity, requester, options)
128
+ @sticky_inputs ||= {}
129
+ # Keep referenced maps alive until the root ends so object_id cannot be reused.
130
+ [options[:sticky], options[:sticky_variables]].each do |input|
131
+ @sticky_inputs[input.object_id] = input if input
132
+ end
133
+ JSON.generate(canonical([
134
+ identity, requester, options[:dependency_purpose], options[:context], options[:sticky]&.object_id, options[:sticky_variables]&.object_id,
135
+ options[:__featurevisor_generation]&.call,
136
+ options[:__featurevisor_child_generation]&.call,
137
+ options[:datafile]&.object_id
138
+ ]))
139
+ rescue ArgumentError, TypeError, JSON::GeneratorError
140
+ nil
141
+ end
142
+
143
+ def canonical(value, ancestors = [])
144
+ raise ArgumentError, "Too deep" if ancestors.length > 64
145
+
146
+ case value
147
+ when NilClass, TrueClass, FalseClass, String, Integer
148
+ value
149
+ when Float
150
+ raise ArgumentError, "Nonfinite number" unless value.finite?
151
+ value
152
+ when Array
153
+ raise ArgumentError, "Circular value" if ancestors.include?(value.object_id)
154
+ ["array", value.map { |entry| canonical(entry, ancestors + [value.object_id]) }]
155
+ when Hash
156
+ raise ArgumentError, "Circular value" if ancestors.include?(value.object_id)
157
+ keys = value.keys
158
+ raise ArgumentError, "Unsupported key" unless keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
159
+ pairs = keys.sort_by { |key| [key.to_s, key.is_a?(String) ? 0 : 1] }.map do |key|
160
+ [key.is_a?(String) ? "string" : "symbol", key.to_s,
161
+ canonical(value[key], ancestors + [value.object_id])]
162
+ end
163
+ ["object", pairs]
164
+ else
165
+ raise ArgumentError, "Unsupported value"
166
+ end
167
+ end
168
+
169
+ def minimal_result(result, borrowed = false)
170
+ return nil unless result.is_a?(Hash)
171
+
172
+ keys = %i[type feature_key variable_key reason enabled variation_value variable_value __featurevisor_decoded_json]
173
+ value = result.select { |key, _| keys.include?(key) }
174
+ value[:variation] = { value: result[:variation][:value] } if result[:variation].is_a?(Hash)
175
+ value[:variable_schema] = { type: result[:variable_schema][:type] } if result[:variable_schema].is_a?(Hash)
176
+ if result[:variable].is_a?(Hash)
177
+ value[:variable] = { type: result[:variable][:type] }
178
+ value[:variable][:compose] = true if result[:variable][:compose]
179
+ end
180
+ return value if borrowed
181
+ canonical(value)
182
+ duplicate(value)
183
+ rescue ArgumentError, TypeError
184
+ nil
185
+ end
186
+
187
+ def duplicate(value)
188
+ Marshal.load(Marshal.dump(value))
189
+ end
190
+ end
191
+ end
@@ -33,7 +33,7 @@ module Featurevisor
33
33
  # @param previous_reader [InstanceEvaluationDataProvider] Previous datafile
34
34
  # @param new_reader [InstanceEvaluationDataProvider] New datafile
35
35
  # @return [Hash] Event parameters
36
- def self.get_params_for_datafile_set_event(previous_reader, new_reader, replace = false)
36
+ def self.get_params_for_datafile_set_event(previous_reader, new_reader, replace = false, incoming = nil)
37
37
  previous_revision = previous_reader.get_revision
38
38
  previous_feature_keys = previous_reader.get_feature_keys
39
39
 
@@ -42,6 +42,17 @@ module Featurevisor
42
42
  previous_variable_keys = previous_reader.get_variable_keys
43
43
  new_variable_keys = new_reader.get_variable_keys
44
44
 
45
+ if previous_feature_keys.empty? && previous_variable_keys.empty? && previous_reader.get_segment_keys.empty?
46
+ return {
47
+ revision: new_revision,
48
+ previousRevision: previous_revision,
49
+ revisionChanged: previous_revision != new_revision,
50
+ features: new_feature_keys,
51
+ variables: new_variable_keys,
52
+ replaced: replace
53
+ }
54
+ end
55
+
45
56
  # results
46
57
  removed_features = []
47
58
  changed_features = []
@@ -49,18 +60,20 @@ module Featurevisor
49
60
 
50
61
  # checking against existing datafile
51
62
  previous_feature_keys.each do |previous_feature_key|
52
- if !new_feature_keys.include?(previous_feature_key)
63
+ if !new_reader.features.key?(previous_feature_key)
53
64
  # feature was removed in new datafile
54
65
  removed_features << previous_feature_key
55
66
  next
56
67
  end
57
68
 
58
69
  # feature exists in both datafiles, check if it was changed
59
- previous_feature = previous_reader.get_feature(previous_feature_key)
60
- new_feature = new_reader.get_feature(previous_feature_key)
70
+ previous_feature = previous_reader.features[previous_feature_key]
71
+ new_feature = new_reader.features[previous_feature_key]
61
72
 
62
73
  if previous_feature && new_feature &&
63
- (previous_feature[:hash].nil? || new_feature[:hash].nil? || previous_feature[:hash] != new_feature[:hash])
74
+ (previous_feature[:hash].nil? || previous_feature[:hash].empty? ||
75
+ new_feature[:hash].nil? || new_feature[:hash].empty? ||
76
+ previous_feature[:hash] != new_feature[:hash])
64
77
  # feature was changed in new datafile
65
78
  changed_features << previous_feature_key
66
79
  end
@@ -68,7 +81,7 @@ module Featurevisor
68
81
 
69
82
  # checking against new datafile
70
83
  new_feature_keys.each do |new_feature_key|
71
- if !previous_feature_keys.include?(new_feature_key)
84
+ if !previous_reader.features.key?(new_feature_key)
72
85
  # feature was added in new datafile
73
86
  added_features << new_feature_key
74
87
  end
@@ -77,41 +90,33 @@ module Featurevisor
77
90
  # combine all affected feature keys
78
91
  all_affected_features = (removed_features + changed_features + added_features).uniq
79
92
  all_affected_variables = (previous_variable_keys + new_variable_keys).uniq.select do |key|
80
- previous = previous_reader.get_global_variable(key)
81
- current = new_reader.get_global_variable(key)
82
- previous.nil? || current.nil? || previous[:hash].nil? || current[:hash].nil? || previous[:hash] != current[:hash]
93
+ previous = previous_reader.variables[key]
94
+ current = new_reader.variables[key]
95
+ previous.nil? || current.nil? ||
96
+ previous[:hash].nil? || previous[:hash].empty? || current[:hash].nil? || current[:hash].empty? ||
97
+ previous[:hash] != current[:hash]
83
98
  end
84
99
 
85
100
 
101
+ new_datafile = new_reader.get_datafile
86
102
  changed_segments = (previous_reader.get_segment_keys + new_reader.get_segment_keys).uniq.select do |key|
87
- previous_reader.get_segment(key) != new_reader.get_segment(key)
88
- end.map(&:to_s)
89
- feature_keys = (previous_feature_keys + new_feature_keys).uniq
90
- loop do
91
- before = all_affected_features.length
92
- feature_keys.each do |key|
93
- next if all_affected_features.include?(key)
94
-
95
- candidates = [previous_reader.get_feature(key), new_reader.get_feature(key)].compact
96
- all_affected_features << key if candidates.any? do |feature|
97
- dependency = feature_dependencies(feature)
98
- !(dependency[:segments].map(&:to_s) & changed_segments).empty? ||
99
- !(dependency[:features].map(&:to_s) & all_affected_features.map(&:to_s)).empty?
100
- end
101
- end
102
- break if before == all_affected_features.length
103
+ previous = previous_reader.segments[key]
104
+ current = new_reader.segments[key]
105
+ !previous.equal?(current) && previous != current
103
106
  end
104
-
105
- (previous_variable_keys + new_variable_keys).uniq.each do |key|
106
- next if all_affected_variables.include?(key)
107
-
108
- candidates = [previous_reader.get_global_variable(key), new_reader.get_global_variable(key)].compact
109
- all_affected_variables << key if candidates.any? do |variable|
110
- dependency = global_variable_dependencies(variable)
111
- !(dependency[:segments].map(&:to_s) & changed_segments).empty? ||
112
- !(dependency[:features].map(&:to_s) & all_affected_features.map(&:to_s)).empty?
107
+ index = previous_reader.dependency_index || Dependencies.create_index(previous_reader.get_datafile)
108
+ if replace || incoming.nil?
109
+ incoming = Dependencies::COLLECTIONS.to_h do |name, _|
110
+ [name, (previous_reader.public_send(name).keys + new_datafile[name].keys).uniq.to_h { |key| [key, true] }]
113
111
  end
114
112
  end
113
+ Dependencies.add_changes(all_affected_features, all_affected_variables, changed_segments,
114
+ index)
115
+ index.update(new_datafile, incoming, previous_reader.get_datafile)
116
+ index.reorder(new_datafile) if replace
117
+ Dependencies.add_changes(all_affected_features, all_affected_variables, changed_segments,
118
+ index)
119
+ new_reader.dependency_index = index
115
120
 
116
121
  {
117
122
  revision: new_revision,
@@ -123,71 +128,5 @@ module Featurevisor
123
128
  }
124
129
  end
125
130
 
126
- def self.required_feature_keys(values)
127
- items = values.is_a?(Array) ? values : [values].compact
128
- items.filter_map do |required|
129
- required.is_a?(String) ? required : (required[:feature] || required[:key])
130
- end
131
- end
132
- private_class_method :required_feature_keys
133
-
134
- def self.segment_keys(value)
135
- return [] if value.nil? || value == "*"
136
- if value.is_a?(String)
137
- if value.start_with?("{", "[")
138
- begin
139
- return segment_keys(JSON.parse(value, symbolize_names: true))
140
- rescue JSON::ParserError
141
- return []
142
- end
143
- end
144
- return [value]
145
- end
146
- return value.flat_map { |item| segment_keys(item) }.uniq if value.is_a?(Array)
147
- return [] unless value.is_a?(Hash)
148
-
149
- %i[and or not].flat_map { |key| segment_keys(value[key] || value[key.to_s]) }.uniq
150
- end
151
- private_class_method :segment_keys
152
-
153
- def self.override_dependencies(groups)
154
- overrides = groups.is_a?(Hash) ? groups.values.flatten : []
155
- {
156
- segments: overrides.flat_map { |override| segment_keys(override[:segments]) }.uniq,
157
- features: overrides.flat_map { |override| required_feature_keys(override[:requiredFeatures]) }.uniq
158
- }
159
- end
160
- private_class_method :override_dependencies
161
-
162
- def self.feature_dependencies(feature)
163
- requirements = feature.key?(:requiredFeatures) ? feature[:requiredFeatures] : feature[:required]
164
- segments = []
165
- features = required_feature_keys(requirements)
166
- Array(feature[:traffic]).each do |traffic|
167
- segments.concat(segment_keys(traffic[:segments]))
168
- nested = override_dependencies(traffic[:variableOverrides])
169
- segments.concat(nested[:segments])
170
- features.concat(nested[:features])
171
- end
172
- Array(feature[:force]).each { |force| segments.concat(segment_keys(force[:segments])) }
173
- Array(feature[:variations]).each do |variation|
174
- nested = override_dependencies(variation[:variableOverrides])
175
- segments.concat(nested[:segments])
176
- features.concat(nested[:features])
177
- end
178
- { segments: segments.uniq, features: features.uniq }
179
- end
180
- private_class_method :feature_dependencies
181
-
182
- def self.global_variable_dependencies(variable)
183
- segments = []
184
- features = required_feature_keys(variable[:requiredFeatures])
185
- Array(variable[:overrides]).each do |override|
186
- segments.concat(segment_keys(override[:segments]))
187
- features.concat(required_feature_keys(override[:requiredFeatures]))
188
- end
189
- { segments: segments.uniq, features: features.uniq }
190
- end
191
- private_class_method :global_variable_dependencies
192
131
  end
193
132
  end