featurevisor 0.1.1
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +722 -0
- data/bin/cli.rb +142 -0
- data/bin/commands/assess_distribution.rb +236 -0
- data/bin/commands/benchmark.rb +274 -0
- data/bin/commands/test.rb +793 -0
- data/bin/commands.rb +10 -0
- data/bin/featurevisor +18 -0
- data/lib/featurevisor/bucketer.rb +95 -0
- data/lib/featurevisor/child_instance.rb +311 -0
- data/lib/featurevisor/compare_versions.rb +126 -0
- data/lib/featurevisor/conditions.rb +152 -0
- data/lib/featurevisor/datafile_reader.rb +350 -0
- data/lib/featurevisor/emitter.rb +60 -0
- data/lib/featurevisor/evaluate.rb +818 -0
- data/lib/featurevisor/events.rb +76 -0
- data/lib/featurevisor/hooks.rb +159 -0
- data/lib/featurevisor/instance.rb +463 -0
- data/lib/featurevisor/logger.rb +150 -0
- data/lib/featurevisor/murmurhash.rb +69 -0
- data/lib/featurevisor/version.rb +3 -0
- data/lib/featurevisor.rb +17 -0
- metadata +89 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Featurevisor
|
|
4
|
+
# Events module for generating event parameters
|
|
5
|
+
module Events
|
|
6
|
+
# Get parameters for sticky set event
|
|
7
|
+
# @param previous_sticky [Hash] Previous sticky features
|
|
8
|
+
# @param new_sticky [Hash] New sticky features
|
|
9
|
+
# @param replace [Boolean] Whether features were replaced
|
|
10
|
+
# @return [Hash] Event parameters
|
|
11
|
+
def self.get_params_for_sticky_set_event(previous_sticky = {}, new_sticky = {}, replace = false)
|
|
12
|
+
keys_before = previous_sticky.keys
|
|
13
|
+
keys_after = new_sticky.keys
|
|
14
|
+
|
|
15
|
+
all_keys = (keys_before + keys_after).uniq
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
features: all_keys,
|
|
19
|
+
replaced: replace
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get parameters for datafile set event
|
|
24
|
+
# @param previous_reader [DatafileReader] Previous datafile reader
|
|
25
|
+
# @param new_reader [DatafileReader] New datafile reader
|
|
26
|
+
# @return [Hash] Event parameters
|
|
27
|
+
def self.get_params_for_datafile_set_event(previous_reader, new_reader)
|
|
28
|
+
previous_revision = previous_reader.get_revision
|
|
29
|
+
previous_feature_keys = previous_reader.get_feature_keys
|
|
30
|
+
|
|
31
|
+
new_revision = new_reader.get_revision
|
|
32
|
+
new_feature_keys = new_reader.get_feature_keys
|
|
33
|
+
|
|
34
|
+
# results
|
|
35
|
+
removed_features = []
|
|
36
|
+
changed_features = []
|
|
37
|
+
added_features = []
|
|
38
|
+
|
|
39
|
+
# checking against existing datafile
|
|
40
|
+
previous_feature_keys.each do |previous_feature_key|
|
|
41
|
+
if !new_feature_keys.include?(previous_feature_key)
|
|
42
|
+
# feature was removed in new datafile
|
|
43
|
+
removed_features << previous_feature_key
|
|
44
|
+
next
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# feature exists in both datafiles, check if it was changed
|
|
48
|
+
previous_feature = previous_reader.get_feature(previous_feature_key)
|
|
49
|
+
new_feature = new_reader.get_feature(previous_feature_key)
|
|
50
|
+
|
|
51
|
+
if previous_feature && new_feature && previous_feature[:hash] != new_feature[:hash]
|
|
52
|
+
# feature was changed in new datafile
|
|
53
|
+
changed_features << previous_feature_key
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# checking against new datafile
|
|
58
|
+
new_feature_keys.each do |new_feature_key|
|
|
59
|
+
if !previous_feature_keys.include?(new_feature_key)
|
|
60
|
+
# feature was added in new datafile
|
|
61
|
+
added_features << new_feature_key
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# combine all affected feature keys
|
|
66
|
+
all_affected_features = (removed_features + changed_features + added_features).uniq
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
revision: new_revision,
|
|
70
|
+
previous_revision: previous_revision,
|
|
71
|
+
revision_changed: previous_revision != new_revision,
|
|
72
|
+
features: all_affected_features
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Featurevisor
|
|
4
|
+
# Hooks module for extending evaluation behavior
|
|
5
|
+
module Hooks
|
|
6
|
+
# Hook interface for extending evaluation behavior
|
|
7
|
+
class Hook
|
|
8
|
+
attr_reader :name
|
|
9
|
+
|
|
10
|
+
# Initialize a new hook
|
|
11
|
+
# @param options [Hash] Hook options
|
|
12
|
+
# @option options [String] :name Hook name
|
|
13
|
+
# @option options [Proc, nil] :before Before evaluation hook
|
|
14
|
+
# @option options [Proc, nil] :bucket_key Bucket key configuration hook
|
|
15
|
+
# @option options [Proc, nil] :bucket_value Bucket value configuration hook
|
|
16
|
+
# @option options [Proc, nil] :after After evaluation hook
|
|
17
|
+
def initialize(options)
|
|
18
|
+
@name = options[:name]
|
|
19
|
+
@before = options[:before]
|
|
20
|
+
@bucket_key = options[:bucket_key]
|
|
21
|
+
@bucket_value = options[:bucket_value]
|
|
22
|
+
@after = options[:after]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Call the before hook if defined
|
|
26
|
+
# @param options [Hash] Evaluation options
|
|
27
|
+
# @return [Hash] Modified evaluation options
|
|
28
|
+
def call_before(options)
|
|
29
|
+
return options unless @before
|
|
30
|
+
|
|
31
|
+
@before.call(options)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Call the bucket key hook if defined
|
|
35
|
+
# @param options [Hash] Bucket key options
|
|
36
|
+
# @return [String] Modified bucket key
|
|
37
|
+
def call_bucket_key(options)
|
|
38
|
+
return options[:bucket_key] unless @bucket_key
|
|
39
|
+
|
|
40
|
+
@bucket_key.call(options)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Call the bucket value hook if defined
|
|
44
|
+
# @param options [Hash] Bucket value options
|
|
45
|
+
# @return [Integer] Modified bucket value
|
|
46
|
+
def call_bucket_value(options)
|
|
47
|
+
return options[:bucket_value] unless @bucket_value
|
|
48
|
+
|
|
49
|
+
@bucket_value.call(options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Call the after hook if defined
|
|
53
|
+
# @param evaluation [Hash] Evaluation result
|
|
54
|
+
# @param options [Hash] Evaluation options
|
|
55
|
+
# @return [Hash] Modified evaluation result
|
|
56
|
+
def call_after(evaluation, options)
|
|
57
|
+
return evaluation unless @after
|
|
58
|
+
|
|
59
|
+
@after.call(evaluation, options)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# HooksManager class for managing hooks
|
|
64
|
+
class HooksManager
|
|
65
|
+
attr_reader :hooks, :logger
|
|
66
|
+
|
|
67
|
+
# Initialize a new HooksManager
|
|
68
|
+
# @param options [Hash] Options hash containing hooks and logger
|
|
69
|
+
# @option options [Array<Hook>] :hooks Array of hooks
|
|
70
|
+
# @option options [Logger] :logger Logger instance
|
|
71
|
+
def initialize(options)
|
|
72
|
+
@logger = options[:logger]
|
|
73
|
+
@hooks = []
|
|
74
|
+
|
|
75
|
+
if options[:hooks]
|
|
76
|
+
options[:hooks].each do |hook|
|
|
77
|
+
add(hook)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Add a hook to the manager
|
|
83
|
+
# @param hook [Hook] Hook to add
|
|
84
|
+
# @return [Proc, nil] Remove function or nil if hook already exists
|
|
85
|
+
def add(hook)
|
|
86
|
+
if @hooks.any? { |existing_hook| existing_hook.name == hook.name }
|
|
87
|
+
@logger.error("Hook with name \"#{hook.name}\" already exists.", {
|
|
88
|
+
name: hook.name,
|
|
89
|
+
hook: hook
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
@hooks << hook
|
|
96
|
+
|
|
97
|
+
# Return a remove function
|
|
98
|
+
-> { remove(hook.name) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Remove a hook by name
|
|
102
|
+
# @param name [String] Hook name to remove
|
|
103
|
+
def remove(name)
|
|
104
|
+
@hooks = @hooks.reject { |hook| hook.name == name }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get all hooks
|
|
108
|
+
# @return [Array<Hook>] Array of all hooks
|
|
109
|
+
def get_all
|
|
110
|
+
@hooks
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Run before hooks
|
|
114
|
+
# @param options [Hash] Evaluation options
|
|
115
|
+
# @return [Hash] Modified evaluation options
|
|
116
|
+
def run_before_hooks(options)
|
|
117
|
+
result = options
|
|
118
|
+
@hooks.each do |hook|
|
|
119
|
+
result = hook.call_before(result)
|
|
120
|
+
end
|
|
121
|
+
result
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Run bucket key hooks
|
|
125
|
+
# @param options [Hash] Bucket key options
|
|
126
|
+
# @return [String] Modified bucket key
|
|
127
|
+
def run_bucket_key_hooks(options)
|
|
128
|
+
bucket_key = options[:bucket_key]
|
|
129
|
+
@hooks.each do |hook|
|
|
130
|
+
bucket_key = hook.call_bucket_key(options.merge(bucket_key: bucket_key))
|
|
131
|
+
end
|
|
132
|
+
bucket_key
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Run bucket value hooks
|
|
136
|
+
# @param options [Hash] Bucket value options
|
|
137
|
+
# @return [Integer] Modified bucket value
|
|
138
|
+
def run_bucket_value_hooks(options)
|
|
139
|
+
bucket_value = options[:bucket_value]
|
|
140
|
+
@hooks.each do |hook|
|
|
141
|
+
bucket_value = hook.call_bucket_value(options.merge(bucket_value: bucket_value))
|
|
142
|
+
end
|
|
143
|
+
bucket_value
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Run after hooks
|
|
147
|
+
# @param evaluation [Hash] Evaluation result
|
|
148
|
+
# @param options [Hash] Evaluation options
|
|
149
|
+
# @return [Hash] Modified evaluation result
|
|
150
|
+
def run_after_hooks(evaluation, options)
|
|
151
|
+
result = evaluation
|
|
152
|
+
@hooks.each do |hook|
|
|
153
|
+
result = hook.call_after(result, options)
|
|
154
|
+
end
|
|
155
|
+
result
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Featurevisor
|
|
6
|
+
# Instance class for managing feature flag evaluations
|
|
7
|
+
class Instance
|
|
8
|
+
attr_reader :context, :logger, :sticky, :datafile_reader, :hooks_manager, :emitter
|
|
9
|
+
|
|
10
|
+
# Empty datafile template
|
|
11
|
+
EMPTY_DATAFILE = {
|
|
12
|
+
schemaVersion: "2",
|
|
13
|
+
revision: "unknown",
|
|
14
|
+
segments: {},
|
|
15
|
+
features: {}
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# Initialize a new Featurevisor instance
|
|
19
|
+
# @param options [Hash] Instance options
|
|
20
|
+
# @option options [Hash, String] :datafile Datafile content or JSON string
|
|
21
|
+
# @option options [Hash] :context Initial context
|
|
22
|
+
# @option options [String] :log_level Log level
|
|
23
|
+
# @option options [Logger] :logger Logger instance
|
|
24
|
+
# @option options [Hash] :sticky Sticky features
|
|
25
|
+
# @option options [Array<Hook>] :hooks Array of hooks
|
|
26
|
+
def initialize(options = {})
|
|
27
|
+
# from options
|
|
28
|
+
@context = options[:context] || {}
|
|
29
|
+
@logger = options[:logger] || Featurevisor.create_logger(level: options[:log_level] || "info")
|
|
30
|
+
@hooks_manager = Featurevisor::Hooks::HooksManager.new(
|
|
31
|
+
hooks: (options[:hooks] || []).map { |hook_data| Featurevisor::Hooks::Hook.new(hook_data) },
|
|
32
|
+
logger: @logger
|
|
33
|
+
)
|
|
34
|
+
@emitter = Featurevisor::Emitter.new
|
|
35
|
+
@sticky = options[:sticky] || {}
|
|
36
|
+
|
|
37
|
+
# datafile
|
|
38
|
+
@datafile_reader = Featurevisor::DatafileReader.new(
|
|
39
|
+
datafile: EMPTY_DATAFILE,
|
|
40
|
+
logger: @logger
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if options[:datafile]
|
|
44
|
+
@datafile_reader = Featurevisor::DatafileReader.new(
|
|
45
|
+
datafile: options[:datafile].is_a?(String) ? JSON.parse(options[:datafile]) : options[:datafile],
|
|
46
|
+
logger: @logger
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@logger.info("Featurevisor SDK initialized")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Set the log level
|
|
54
|
+
# @param level [String] Log level
|
|
55
|
+
def set_log_level(level)
|
|
56
|
+
@logger.set_level(level)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Set the datafile
|
|
60
|
+
# @param datafile [Hash, String] Datafile content or JSON string
|
|
61
|
+
def set_datafile(datafile)
|
|
62
|
+
begin
|
|
63
|
+
new_datafile_reader = Featurevisor::DatafileReader.new(
|
|
64
|
+
datafile: datafile.is_a?(String) ? JSON.parse(datafile) : datafile,
|
|
65
|
+
logger: @logger
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
details = Featurevisor::Events.get_params_for_datafile_set_event(@datafile_reader, new_datafile_reader)
|
|
69
|
+
@datafile_reader = new_datafile_reader
|
|
70
|
+
|
|
71
|
+
@logger.info("datafile set", details)
|
|
72
|
+
@emitter.trigger("datafile_set", details)
|
|
73
|
+
rescue => e
|
|
74
|
+
@logger.error("could not parse datafile", { error: e })
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Set sticky features
|
|
79
|
+
# @param sticky [Hash] Sticky features
|
|
80
|
+
# @param replace [Boolean] Whether to replace existing sticky features
|
|
81
|
+
def set_sticky(sticky, replace = false)
|
|
82
|
+
previous_sticky_features = @sticky || {}
|
|
83
|
+
|
|
84
|
+
if replace
|
|
85
|
+
@sticky = sticky
|
|
86
|
+
else
|
|
87
|
+
@sticky = {
|
|
88
|
+
**@sticky,
|
|
89
|
+
**sticky
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
params = Featurevisor::Events.get_params_for_sticky_set_event(previous_sticky_features, @sticky, replace)
|
|
94
|
+
|
|
95
|
+
@logger.info("sticky features set", params)
|
|
96
|
+
@emitter.trigger("sticky_set", params)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get the revision
|
|
100
|
+
# @return [String] Revision string
|
|
101
|
+
def get_revision
|
|
102
|
+
@datafile_reader.get_revision
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Get a feature by key
|
|
106
|
+
# @param feature_key [String] Feature key
|
|
107
|
+
# @return [Hash, nil] Feature data or nil if not found
|
|
108
|
+
def get_feature(feature_key)
|
|
109
|
+
@datafile_reader.get_feature(feature_key)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Add a hook
|
|
113
|
+
# @param hook [Hook] Hook to add
|
|
114
|
+
# @return [Proc, nil] Remove function or nil if hook already exists
|
|
115
|
+
def add_hook(hook)
|
|
116
|
+
@hooks_manager.add(hook)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Subscribe to an event
|
|
120
|
+
# @param event_name [String] Event name
|
|
121
|
+
# @param callback [Proc] Callback function
|
|
122
|
+
# @return [Proc] Unsubscribe function
|
|
123
|
+
def on(event_name, callback)
|
|
124
|
+
@emitter.on(event_name, callback)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Close the instance
|
|
128
|
+
def close
|
|
129
|
+
@emitter.clear_all
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Set context
|
|
133
|
+
# @param context [Hash] Context to set
|
|
134
|
+
# @param replace [Boolean] Whether to replace existing context
|
|
135
|
+
def set_context(context, replace = false)
|
|
136
|
+
if replace
|
|
137
|
+
@context = context
|
|
138
|
+
else
|
|
139
|
+
@context = { **@context, **context }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
@emitter.trigger("context_set", {
|
|
143
|
+
context: @context,
|
|
144
|
+
replaced: replace
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
@logger.debug(replace ? "context replaced" : "context updated", {
|
|
148
|
+
context: @context,
|
|
149
|
+
replaced: replace
|
|
150
|
+
})
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Get context
|
|
154
|
+
# @param context [Hash, nil] Additional context to merge
|
|
155
|
+
# @return [Hash] Merged context
|
|
156
|
+
def get_context(context = nil)
|
|
157
|
+
if context
|
|
158
|
+
{
|
|
159
|
+
**@context,
|
|
160
|
+
**context
|
|
161
|
+
}
|
|
162
|
+
else
|
|
163
|
+
@context
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Spawn a child instance
|
|
168
|
+
# @param context [Hash] Child context
|
|
169
|
+
# @param options [Hash] Override options
|
|
170
|
+
# @return [ChildInstance] Child instance
|
|
171
|
+
def spawn(context = {}, options = {})
|
|
172
|
+
Featurevisor::ChildInstance.new(
|
|
173
|
+
parent: self,
|
|
174
|
+
context: get_context(context),
|
|
175
|
+
sticky: options[:sticky]
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Evaluate a flag
|
|
180
|
+
# @param feature_key [String] Feature key
|
|
181
|
+
# @param context [Hash] Context
|
|
182
|
+
# @param options [Hash] Override options
|
|
183
|
+
# @return [Hash] Evaluation result
|
|
184
|
+
def evaluate_flag(feature_key, context = {}, options = {})
|
|
185
|
+
Featurevisor::Evaluate.evaluate_with_hooks(
|
|
186
|
+
get_evaluation_dependencies(context, options).merge(
|
|
187
|
+
type: "flag",
|
|
188
|
+
feature_key: feature_key
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Check if a feature is enabled
|
|
194
|
+
# @param feature_key [String] Feature key
|
|
195
|
+
# @param context [Hash] Context
|
|
196
|
+
# @param options [Hash] Override options
|
|
197
|
+
# @return [Boolean] True if feature is enabled
|
|
198
|
+
def is_enabled(feature_key, context = {}, options = {})
|
|
199
|
+
begin
|
|
200
|
+
evaluation = evaluate_flag(feature_key, context, options)
|
|
201
|
+
evaluation[:enabled] == true
|
|
202
|
+
rescue => e
|
|
203
|
+
@logger.error("isEnabled", { feature_key: feature_key, error: e })
|
|
204
|
+
false
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Evaluate a variation
|
|
209
|
+
# @param feature_key [String] Feature key
|
|
210
|
+
# @param context [Hash] Context
|
|
211
|
+
# @param options [Hash] Override options
|
|
212
|
+
# @return [Hash] Evaluation result
|
|
213
|
+
def evaluate_variation(feature_key, context = {}, options = {})
|
|
214
|
+
Featurevisor::Evaluate.evaluate_with_hooks(
|
|
215
|
+
get_evaluation_dependencies(context, options).merge(
|
|
216
|
+
type: "variation",
|
|
217
|
+
feature_key: feature_key
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Get variation value
|
|
223
|
+
# @param feature_key [String] Feature key
|
|
224
|
+
# @param context [Hash] Context
|
|
225
|
+
# @param options [Hash] Override options
|
|
226
|
+
# @return [String, nil] Variation value or nil
|
|
227
|
+
def get_variation(feature_key, context = {}, options = {})
|
|
228
|
+
begin
|
|
229
|
+
evaluation = evaluate_variation(feature_key, context, options)
|
|
230
|
+
|
|
231
|
+
if evaluation[:variation_value]
|
|
232
|
+
evaluation[:variation_value]
|
|
233
|
+
elsif evaluation[:variation]
|
|
234
|
+
evaluation[:variation][:value]
|
|
235
|
+
else
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
rescue => e
|
|
239
|
+
@logger.error("getVariation", { feature_key: feature_key, error: e })
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Evaluate a variable
|
|
245
|
+
# @param feature_key [String] Feature key
|
|
246
|
+
# @param variable_key [String] Variable key
|
|
247
|
+
# @param context [Hash] Context
|
|
248
|
+
# @param options [Hash] Override options
|
|
249
|
+
# @return [Hash] Evaluation result
|
|
250
|
+
def evaluate_variable(feature_key, variable_key, context = {}, options = {})
|
|
251
|
+
Featurevisor::Evaluate.evaluate_with_hooks(
|
|
252
|
+
get_evaluation_dependencies(context, options).merge(
|
|
253
|
+
type: "variable",
|
|
254
|
+
feature_key: feature_key,
|
|
255
|
+
variable_key: variable_key
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Get variable value
|
|
261
|
+
# @param feature_key [String] Feature key
|
|
262
|
+
# @param variable_key [String] Variable key
|
|
263
|
+
# @param context [Hash] Context
|
|
264
|
+
# @param options [Hash] Override options
|
|
265
|
+
# @return [Object, nil] Variable value or nil
|
|
266
|
+
def get_variable(feature_key, variable_key, context = {}, options = {})
|
|
267
|
+
begin
|
|
268
|
+
evaluation = evaluate_variable(feature_key, variable_key, context, options)
|
|
269
|
+
|
|
270
|
+
if !evaluation[:variable_value].nil?
|
|
271
|
+
if evaluation[:variable_schema] &&
|
|
272
|
+
evaluation[:variable_schema][:type] == "json" &&
|
|
273
|
+
evaluation[:variable_value].is_a?(String)
|
|
274
|
+
JSON.parse(evaluation[:variable_value], symbolize_names: true)
|
|
275
|
+
else
|
|
276
|
+
evaluation[:variable_value]
|
|
277
|
+
end
|
|
278
|
+
else
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
rescue => e
|
|
282
|
+
@logger.error("getVariable", { feature_key: feature_key, variable_key: variable_key, error: e })
|
|
283
|
+
nil
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Get variable as boolean
|
|
288
|
+
# @param feature_key [String] Feature key
|
|
289
|
+
# @param variable_key [String] Variable key
|
|
290
|
+
# @param context [Hash] Context
|
|
291
|
+
# @param options [Hash] Override options
|
|
292
|
+
# @return [Boolean, nil] Boolean value or nil
|
|
293
|
+
def get_variable_boolean(feature_key, variable_key, context = {}, options = {})
|
|
294
|
+
variable_value = get_variable(feature_key, variable_key, context, options)
|
|
295
|
+
get_value_by_type(variable_value, "boolean")
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Get variable as string
|
|
299
|
+
# @param feature_key [String] Feature key
|
|
300
|
+
# @param variable_key [String] Variable key
|
|
301
|
+
# @param context [Hash] Context
|
|
302
|
+
# @param options [Hash] Override options
|
|
303
|
+
# @return [String, nil] String value or nil
|
|
304
|
+
def get_variable_string(feature_key, variable_key, context = {}, options = {})
|
|
305
|
+
variable_value = get_variable(feature_key, variable_key, context, options)
|
|
306
|
+
get_value_by_type(variable_value, "string")
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Get variable as integer
|
|
310
|
+
# @param feature_key [String] Feature key
|
|
311
|
+
# @param variable_key [String] Variable key
|
|
312
|
+
# @param context [Hash] Context
|
|
313
|
+
# @param options [Hash] Override options
|
|
314
|
+
# @return [Integer, nil] Integer value or nil
|
|
315
|
+
def get_variable_integer(feature_key, variable_key, context = {}, options = {})
|
|
316
|
+
variable_value = get_variable(feature_key, variable_key, context, options)
|
|
317
|
+
get_value_by_type(variable_value, "integer")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Get variable as double
|
|
321
|
+
# @param feature_key [String] Feature key
|
|
322
|
+
# @param variable_key [String] Variable key
|
|
323
|
+
# @param context [Hash] Context
|
|
324
|
+
# @param options [Hash] Override options
|
|
325
|
+
# @return [Float, nil] Float value or nil
|
|
326
|
+
def get_variable_double(feature_key, variable_key, context = {}, options = {})
|
|
327
|
+
variable_value = get_variable(feature_key, variable_key, context, options)
|
|
328
|
+
get_value_by_type(variable_value, "double")
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Get variable as array
|
|
332
|
+
# @param feature_key [String] Feature key
|
|
333
|
+
# @param variable_key [String] Variable key
|
|
334
|
+
# @param context [Hash] Context
|
|
335
|
+
# @param options [Hash] Override options
|
|
336
|
+
# @return [Array, nil] Array value or nil
|
|
337
|
+
def get_variable_array(feature_key, variable_key, context = {}, options = {})
|
|
338
|
+
variable_value = get_variable(feature_key, variable_key, context, options)
|
|
339
|
+
get_value_by_type(variable_value, "array")
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Get variable as object
|
|
343
|
+
# @param feature_key [String] Feature key
|
|
344
|
+
# @param variable_key [String] Variable key
|
|
345
|
+
# @param context [Hash] Context
|
|
346
|
+
# @param options [Hash] Override options
|
|
347
|
+
# @return [Hash, nil] Object value or nil
|
|
348
|
+
def get_variable_object(feature_key, variable_key, context = {}, options = {})
|
|
349
|
+
variable_value = get_variable(feature_key, variable_key, context, options)
|
|
350
|
+
get_value_by_type(variable_value, "object")
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Get variable as JSON
|
|
354
|
+
# @param feature_key [String] Feature key
|
|
355
|
+
# @param variable_key [String] Variable key
|
|
356
|
+
# @param context [Hash] Context
|
|
357
|
+
# @param options [Hash] Override options
|
|
358
|
+
# @return [Object, nil] JSON value or nil
|
|
359
|
+
def get_variable_json(feature_key, variable_key, context = {}, options = {})
|
|
360
|
+
variable_value = get_variable(feature_key, variable_key, context, options)
|
|
361
|
+
get_value_by_type(variable_value, "json")
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Get all evaluations
|
|
365
|
+
# @param context [Hash] Context
|
|
366
|
+
# @param feature_keys [Array<String>] Feature keys to evaluate
|
|
367
|
+
# @param options [Hash] Override options
|
|
368
|
+
# @return [Hash] All evaluations
|
|
369
|
+
def get_all_evaluations(context = {}, feature_keys = [], options = {})
|
|
370
|
+
result = {}
|
|
371
|
+
|
|
372
|
+
keys = feature_keys.size > 0 ? feature_keys : @datafile_reader.get_feature_keys
|
|
373
|
+
|
|
374
|
+
keys.each do |feature_key|
|
|
375
|
+
# Convert symbol keys to strings for evaluation functions
|
|
376
|
+
feature_key_str = feature_key.to_s
|
|
377
|
+
|
|
378
|
+
# isEnabled
|
|
379
|
+
evaluated_feature = {
|
|
380
|
+
enabled: is_enabled(feature_key_str, context, options)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# variation
|
|
384
|
+
if @datafile_reader.has_variations?(feature_key_str)
|
|
385
|
+
variation = get_variation(feature_key_str, context, options)
|
|
386
|
+
evaluated_feature[:variation] = variation if variation
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# variables
|
|
390
|
+
variable_keys = @datafile_reader.get_variable_keys(feature_key_str)
|
|
391
|
+
if variable_keys.size > 0
|
|
392
|
+
evaluated_feature[:variables] = {}
|
|
393
|
+
|
|
394
|
+
variable_keys.each do |variable_key|
|
|
395
|
+
evaluated_feature[:variables][variable_key] = get_variable(
|
|
396
|
+
feature_key_str,
|
|
397
|
+
variable_key,
|
|
398
|
+
context,
|
|
399
|
+
options
|
|
400
|
+
)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
result[feature_key] = evaluated_feature
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
result
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
private
|
|
411
|
+
|
|
412
|
+
# Get evaluation dependencies
|
|
413
|
+
# @param context [Hash] Context
|
|
414
|
+
# @param options [Hash] Override options
|
|
415
|
+
# @return [Hash] Evaluation dependencies
|
|
416
|
+
def get_evaluation_dependencies(context, options = {})
|
|
417
|
+
{
|
|
418
|
+
context: get_context(context),
|
|
419
|
+
logger: @logger,
|
|
420
|
+
hooks_manager: @hooks_manager,
|
|
421
|
+
datafile_reader: @datafile_reader,
|
|
422
|
+
sticky: options[:sticky] ? { **(@sticky || {}), **options[:sticky] } : @sticky,
|
|
423
|
+
default_variation_value: options[:default_variation_value],
|
|
424
|
+
default_variable_value: options[:default_variable_value]
|
|
425
|
+
}
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Get value by type
|
|
429
|
+
# @param value [Object] Value to convert
|
|
430
|
+
# @param type [String] Target type
|
|
431
|
+
# @return [Object] Converted value
|
|
432
|
+
def get_value_by_type(value, type)
|
|
433
|
+
return nil if value.nil?
|
|
434
|
+
|
|
435
|
+
case type
|
|
436
|
+
when "string"
|
|
437
|
+
value.is_a?(String) ? value : nil
|
|
438
|
+
when "integer"
|
|
439
|
+
value.is_a?(String) ? Integer(value, 10) : (value.is_a?(Integer) ? value : nil)
|
|
440
|
+
when "double"
|
|
441
|
+
value.is_a?(String) ? Float(value) : (value.is_a?(Numeric) ? value.to_f : nil)
|
|
442
|
+
when "boolean"
|
|
443
|
+
value == true
|
|
444
|
+
when "array"
|
|
445
|
+
value.is_a?(Array) ? value : nil
|
|
446
|
+
when "object"
|
|
447
|
+
value.is_a?(Hash) ? value : nil
|
|
448
|
+
# @NOTE: `json` is not handled here intentionally
|
|
449
|
+
else
|
|
450
|
+
value
|
|
451
|
+
end
|
|
452
|
+
rescue
|
|
453
|
+
nil
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Create a new Featurevisor instance
|
|
458
|
+
# @param options [Hash] Instance options
|
|
459
|
+
# @return [Instance] New instance
|
|
460
|
+
def self.create_instance(options = {})
|
|
461
|
+
Instance.new(options)
|
|
462
|
+
end
|
|
463
|
+
end
|