featurevisor 0.2.0 → 1.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 +172 -75
- data/bin/cli.rb +15 -2
- data/bin/commands/assess_distribution.rb +34 -15
- data/bin/commands/benchmark.rb +65 -63
- data/bin/commands/test.rb +250 -100
- data/lib/featurevisor/child_instance.rb +22 -24
- data/lib/featurevisor/datafile_reader.rb +18 -13
- data/lib/featurevisor/emitter.rb +2 -2
- data/lib/featurevisor/evaluate.rb +106 -100
- data/lib/featurevisor/events.rb +5 -4
- data/lib/featurevisor/instance.rb +225 -39
- data/lib/featurevisor/logger.rb +0 -7
- data/lib/featurevisor/modules.rb +184 -0
- data/lib/featurevisor/version.rb +1 -1
- data/lib/featurevisor.rb +4 -1
- metadata +16 -2
- data/lib/featurevisor/hooks.rb +0 -159
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "securerandom"
|
|
4
5
|
|
|
5
6
|
module Featurevisor
|
|
6
7
|
# Instance class for managing feature flag evaluations
|
|
7
8
|
class Instance
|
|
8
|
-
attr_reader :context, :logger, :sticky, :datafile_reader, :hooks_manager, :emitter
|
|
9
|
-
|
|
10
9
|
# Empty datafile template
|
|
11
10
|
EMPTY_DATAFILE = {
|
|
12
11
|
schemaVersion: "2",
|
|
@@ -20,34 +19,44 @@ module Featurevisor
|
|
|
20
19
|
# @option options [Hash, String] :datafile Datafile content or JSON string
|
|
21
20
|
# @option options [Hash] :context Initial context
|
|
22
21
|
# @option options [String] :log_level Log level
|
|
23
|
-
# @option options [Logger] :logger Logger instance
|
|
24
22
|
# @option options [Hash] :sticky Sticky features
|
|
25
|
-
# @option options [Array<
|
|
23
|
+
# @option options [Array<Hash, FeaturevisorModule>] :modules Array of modules
|
|
24
|
+
# @option options [Proc] :on_diagnostic Diagnostic handler
|
|
26
25
|
def initialize(options = {})
|
|
27
26
|
# from options
|
|
28
27
|
@context = options[:context] || {}
|
|
29
|
-
@logger =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
logger: @logger
|
|
28
|
+
@logger = Logger.new(
|
|
29
|
+
level: options[:log_level] || "info",
|
|
30
|
+
handler: method(:handle_internal_log)
|
|
33
31
|
)
|
|
32
|
+
@on_diagnostic = options[:on_diagnostic] || options[:onDiagnostic]
|
|
34
33
|
@emitter = Featurevisor::Emitter.new
|
|
35
34
|
@sticky = options[:sticky] || {}
|
|
35
|
+
@closed = false
|
|
36
|
+
@module_diagnostic_subscriptions = []
|
|
36
37
|
|
|
37
38
|
# datafile
|
|
38
|
-
@datafile_reader =
|
|
39
|
+
@datafile_reader = DatafileReader.new(
|
|
39
40
|
datafile: EMPTY_DATAFILE,
|
|
40
41
|
logger: @logger
|
|
41
42
|
)
|
|
42
43
|
|
|
44
|
+
@modules_manager = Featurevisor::Modules::ModulesManager.new(
|
|
45
|
+
modules: options[:modules] || [],
|
|
46
|
+
report_diagnostic: method(:report_diagnostic),
|
|
47
|
+
module_api_factory: method(:create_module_api),
|
|
48
|
+
clear_module_diagnostic_subscriptions: method(:clear_module_diagnostic_subscriptions)
|
|
49
|
+
)
|
|
50
|
+
|
|
43
51
|
if options[:datafile]
|
|
44
|
-
|
|
45
|
-
datafile: options[:datafile].is_a?(String) ? JSON.parse(options[:datafile], symbolize_names: true) : options[:datafile],
|
|
46
|
-
logger: @logger
|
|
47
|
-
)
|
|
52
|
+
set_datafile(options[:datafile], true)
|
|
48
53
|
end
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
report_diagnostic(
|
|
56
|
+
level: "info",
|
|
57
|
+
code: "sdk_initialized",
|
|
58
|
+
message: "SDK initialized"
|
|
59
|
+
)
|
|
51
60
|
end
|
|
52
61
|
|
|
53
62
|
# Set the log level
|
|
@@ -56,22 +65,61 @@ module Featurevisor
|
|
|
56
65
|
@logger.set_level(level)
|
|
57
66
|
end
|
|
58
67
|
|
|
68
|
+
def handle_internal_log(level, message, details = nil)
|
|
69
|
+
details = (details || {}).dup
|
|
70
|
+
code = details[:reason] || details["reason"] || message
|
|
71
|
+
code = "deprecated_feature" if message == "feature is deprecated"
|
|
72
|
+
code = "deprecated_variable" if message == "variable is deprecated"
|
|
73
|
+
code = "feature_not_found" if message == "feature not found"
|
|
74
|
+
code = "variable_not_found" if message == "variable schema not found"
|
|
75
|
+
code = "no_variations" if message == "no variations"
|
|
76
|
+
code = "invalid_bucket_by" if message == "invalid bucketBy"
|
|
77
|
+
report_diagnostic(level: level, code: code.to_s, message: message, details: details)
|
|
78
|
+
end
|
|
79
|
+
private :handle_internal_log
|
|
80
|
+
|
|
59
81
|
# Set the datafile
|
|
60
82
|
# @param datafile [Hash, String] Datafile content or JSON string
|
|
61
|
-
|
|
83
|
+
# @param replace [Boolean] Whether to replace instead of merge
|
|
84
|
+
def set_datafile(datafile, replace = false)
|
|
85
|
+
return if @closed
|
|
86
|
+
|
|
62
87
|
begin
|
|
63
|
-
|
|
64
|
-
|
|
88
|
+
parsed_datafile = if datafile.is_a?(String)
|
|
89
|
+
JSON.parse(datafile, symbolize_names: true)
|
|
90
|
+
elsif datafile.is_a?(Hash)
|
|
91
|
+
JSON.parse(JSON.generate(datafile), symbolize_names: true)
|
|
92
|
+
else
|
|
93
|
+
datafile
|
|
94
|
+
end
|
|
95
|
+
unless parsed_datafile.is_a?(Hash) && parsed_datafile[:schemaVersion].is_a?(String) &&
|
|
96
|
+
parsed_datafile[:revision].is_a?(String) && parsed_datafile[:segments].is_a?(Hash) &&
|
|
97
|
+
parsed_datafile[:features].is_a?(Hash)
|
|
98
|
+
raise ArgumentError, "Invalid datafile"
|
|
99
|
+
end
|
|
100
|
+
next_datafile = replace ? parsed_datafile : merge_datafiles(@datafile_reader.get_datafile, parsed_datafile)
|
|
101
|
+
new_datafile_reader = DatafileReader.new(
|
|
102
|
+
datafile: next_datafile,
|
|
65
103
|
logger: @logger
|
|
66
104
|
)
|
|
67
105
|
|
|
68
|
-
details = Featurevisor::Events.get_params_for_datafile_set_event(@datafile_reader, new_datafile_reader)
|
|
106
|
+
details = Featurevisor::Events.get_params_for_datafile_set_event(@datafile_reader, new_datafile_reader, replace)
|
|
69
107
|
@datafile_reader = new_datafile_reader
|
|
70
108
|
|
|
71
|
-
|
|
109
|
+
report_diagnostic(
|
|
110
|
+
level: "info",
|
|
111
|
+
code: "datafile_set",
|
|
112
|
+
message: "Datafile set",
|
|
113
|
+
details: details
|
|
114
|
+
)
|
|
72
115
|
@emitter.trigger("datafile_set", details)
|
|
73
116
|
rescue => e
|
|
74
|
-
|
|
117
|
+
report_diagnostic(
|
|
118
|
+
level: "error",
|
|
119
|
+
code: "invalid_datafile",
|
|
120
|
+
message: "Could not parse datafile",
|
|
121
|
+
original_error: e
|
|
122
|
+
)
|
|
75
123
|
end
|
|
76
124
|
end
|
|
77
125
|
|
|
@@ -92,7 +140,12 @@ module Featurevisor
|
|
|
92
140
|
|
|
93
141
|
params = Featurevisor::Events.get_params_for_sticky_set_event(previous_sticky_features, @sticky, replace)
|
|
94
142
|
|
|
95
|
-
|
|
143
|
+
report_diagnostic(
|
|
144
|
+
level: "info",
|
|
145
|
+
code: "sticky_set",
|
|
146
|
+
message: "Sticky features set",
|
|
147
|
+
details: params
|
|
148
|
+
)
|
|
96
149
|
@emitter.trigger("sticky_set", params)
|
|
97
150
|
end
|
|
98
151
|
|
|
@@ -102,6 +155,26 @@ module Featurevisor
|
|
|
102
155
|
@datafile_reader.get_revision
|
|
103
156
|
end
|
|
104
157
|
|
|
158
|
+
def get_schema_version
|
|
159
|
+
@datafile_reader.get_schema_version
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def get_segment(segment_key)
|
|
163
|
+
@datafile_reader.get_segment(segment_key)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def get_feature_keys
|
|
167
|
+
@datafile_reader.get_feature_keys
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def get_variable_keys(feature_key)
|
|
171
|
+
@datafile_reader.get_variable_keys(feature_key)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def has_variations?(feature_key)
|
|
175
|
+
@datafile_reader.has_variations?(feature_key)
|
|
176
|
+
end
|
|
177
|
+
|
|
105
178
|
# Get a feature by key
|
|
106
179
|
# @param feature_key [String] Feature key
|
|
107
180
|
# @return [Hash, nil] Feature data or nil if not found
|
|
@@ -109,11 +182,15 @@ module Featurevisor
|
|
|
109
182
|
@datafile_reader.get_feature(feature_key)
|
|
110
183
|
end
|
|
111
184
|
|
|
112
|
-
# Add a
|
|
113
|
-
# @param
|
|
114
|
-
# @return [Proc, nil] Remove function or nil if
|
|
115
|
-
def
|
|
116
|
-
@
|
|
185
|
+
# Add a module
|
|
186
|
+
# @param mod [Hash, FeaturevisorModule] Module to add
|
|
187
|
+
# @return [Proc, nil] Remove function or nil if module already exists
|
|
188
|
+
def add_module(mod)
|
|
189
|
+
@modules_manager.add(mod)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def remove_module(name_or_module)
|
|
193
|
+
@modules_manager.remove(name_or_module)
|
|
117
194
|
end
|
|
118
195
|
|
|
119
196
|
# Subscribe to an event
|
|
@@ -126,6 +203,9 @@ module Featurevisor
|
|
|
126
203
|
|
|
127
204
|
# Close the instance
|
|
128
205
|
def close
|
|
206
|
+
@closed = true
|
|
207
|
+
@modules_manager.close_all
|
|
208
|
+
@module_diagnostic_subscriptions = []
|
|
129
209
|
@emitter.clear_all
|
|
130
210
|
end
|
|
131
211
|
|
|
@@ -144,10 +224,15 @@ module Featurevisor
|
|
|
144
224
|
replaced: replace
|
|
145
225
|
})
|
|
146
226
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
227
|
+
report_diagnostic(
|
|
228
|
+
level: "debug",
|
|
229
|
+
code: "context_set",
|
|
230
|
+
message: replace ? "Context replaced" : "Context updated",
|
|
231
|
+
details: {
|
|
232
|
+
context: @context,
|
|
233
|
+
replaced: replace
|
|
234
|
+
}
|
|
235
|
+
)
|
|
151
236
|
end
|
|
152
237
|
|
|
153
238
|
# Get context
|
|
@@ -182,7 +267,7 @@ module Featurevisor
|
|
|
182
267
|
# @param options [Hash] Override options
|
|
183
268
|
# @return [Hash] Evaluation result
|
|
184
269
|
def evaluate_flag(feature_key, context = {}, options = {})
|
|
185
|
-
Featurevisor::Evaluate.
|
|
270
|
+
Featurevisor::Evaluate.evaluate_with_modules(
|
|
186
271
|
get_evaluation_dependencies(context, options).merge(
|
|
187
272
|
type: "flag",
|
|
188
273
|
feature_key: feature_key
|
|
@@ -211,7 +296,7 @@ module Featurevisor
|
|
|
211
296
|
# @param options [Hash] Override options
|
|
212
297
|
# @return [Hash] Evaluation result
|
|
213
298
|
def evaluate_variation(feature_key, context = {}, options = {})
|
|
214
|
-
Featurevisor::Evaluate.
|
|
299
|
+
Featurevisor::Evaluate.evaluate_with_modules(
|
|
215
300
|
get_evaluation_dependencies(context, options).merge(
|
|
216
301
|
type: "variation",
|
|
217
302
|
feature_key: feature_key
|
|
@@ -248,7 +333,7 @@ module Featurevisor
|
|
|
248
333
|
# @param options [Hash] Override options
|
|
249
334
|
# @return [Hash] Evaluation result
|
|
250
335
|
def evaluate_variable(feature_key, variable_key, context = {}, options = {})
|
|
251
|
-
Featurevisor::Evaluate.
|
|
336
|
+
Featurevisor::Evaluate.evaluate_with_modules(
|
|
252
337
|
get_evaluation_dependencies(context, options).merge(
|
|
253
338
|
type: "variable",
|
|
254
339
|
feature_key: feature_key,
|
|
@@ -417,9 +502,9 @@ module Featurevisor
|
|
|
417
502
|
{
|
|
418
503
|
context: get_context(context),
|
|
419
504
|
logger: @logger,
|
|
420
|
-
|
|
505
|
+
modules_manager: @modules_manager,
|
|
421
506
|
datafile_reader: @datafile_reader,
|
|
422
|
-
sticky: options[:
|
|
507
|
+
sticky: options[:__featurevisor_child_sticky] || @sticky,
|
|
423
508
|
default_variation_value: options[:default_variation_value],
|
|
424
509
|
default_variable_value: options[:default_variable_value]
|
|
425
510
|
}
|
|
@@ -436,11 +521,12 @@ module Featurevisor
|
|
|
436
521
|
when "string"
|
|
437
522
|
value.is_a?(String) ? value : nil
|
|
438
523
|
when "integer"
|
|
439
|
-
|
|
524
|
+
return value if value.is_a?(Integer)
|
|
525
|
+
value.is_a?(Float) && value.finite? && value == value.to_i ? value.to_i : nil
|
|
440
526
|
when "double"
|
|
441
|
-
value.is_a?(
|
|
527
|
+
value.is_a?(Numeric) && value.finite? ? value.to_f : nil
|
|
442
528
|
when "boolean"
|
|
443
|
-
value == true
|
|
529
|
+
value == true || value == false ? value : nil
|
|
444
530
|
when "array"
|
|
445
531
|
value.is_a?(Array) ? value : nil
|
|
446
532
|
when "object"
|
|
@@ -452,12 +538,112 @@ module Featurevisor
|
|
|
452
538
|
rescue
|
|
453
539
|
nil
|
|
454
540
|
end
|
|
541
|
+
|
|
542
|
+
def merge_datafiles(previous, incoming)
|
|
543
|
+
previous ||= EMPTY_DATAFILE
|
|
544
|
+
incoming ||= EMPTY_DATAFILE
|
|
545
|
+
|
|
546
|
+
{
|
|
547
|
+
schemaVersion: incoming[:schemaVersion],
|
|
548
|
+
revision: incoming[:revision],
|
|
549
|
+
featurevisorVersion: incoming[:featurevisorVersion],
|
|
550
|
+
segments: {
|
|
551
|
+
**(previous[:segments] || {}),
|
|
552
|
+
**(incoming[:segments] || {})
|
|
553
|
+
},
|
|
554
|
+
features: {
|
|
555
|
+
**(previous[:features] || {}),
|
|
556
|
+
**(incoming[:features] || {})
|
|
557
|
+
}
|
|
558
|
+
}.compact
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def create_module_api(mod)
|
|
562
|
+
instance = self
|
|
563
|
+
{
|
|
564
|
+
get_revision: -> { instance.get_revision },
|
|
565
|
+
on_diagnostic: lambda do |handler, options = {}|
|
|
566
|
+
subscription = {
|
|
567
|
+
id: SecureRandom.uuid,
|
|
568
|
+
module_id: mod.id,
|
|
569
|
+
handler: handler,
|
|
570
|
+
level: options[:level] || options[:log_level] || "info"
|
|
571
|
+
}
|
|
572
|
+
@module_diagnostic_subscriptions << subscription
|
|
573
|
+
-> { @module_diagnostic_subscriptions.reject! { |item| item[:id] == subscription[:id] } }
|
|
574
|
+
end,
|
|
575
|
+
report_diagnostic: ->(diagnostic) { report_diagnostic(diagnostic, mod) }
|
|
576
|
+
}
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def clear_module_diagnostic_subscriptions(mod)
|
|
580
|
+
@module_diagnostic_subscriptions.reject! { |item| item[:module_id] == mod.id }
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def report_diagnostic(diagnostic, source_module = nil)
|
|
584
|
+
diagnostic = (diagnostic || {}).dup
|
|
585
|
+
diagnostic[:level] ||= "info"
|
|
586
|
+
diagnostic[:module] = source_module.name if source_module && source_module.name
|
|
587
|
+
details = (diagnostic[:details] || {}).dup
|
|
588
|
+
legacy_module_name = diagnostic.delete(:module_name)
|
|
589
|
+
legacy_original_error = diagnostic.delete(:original_error)
|
|
590
|
+
diagnostic[:moduleName] = legacy_module_name if !diagnostic.key?(:moduleName) && !legacy_module_name.nil?
|
|
591
|
+
diagnostic[:originalError] = legacy_original_error if !diagnostic.key?(:originalError) && !legacy_original_error.nil?
|
|
592
|
+
diagnostic.each do |key, value|
|
|
593
|
+
next if %i[level code message module moduleName originalError details].include?(key)
|
|
594
|
+
|
|
595
|
+
details[key] = value
|
|
596
|
+
end
|
|
597
|
+
diagnostic.select! { |key, _| %i[level code message module moduleName originalError details].include?(key) }
|
|
598
|
+
diagnostic[:details] = details
|
|
599
|
+
|
|
600
|
+
@module_diagnostic_subscriptions.dup.each do |subscription|
|
|
601
|
+
next if source_module && subscription[:module_id] == source_module.id
|
|
602
|
+
next unless should_report_diagnostic?(diagnostic[:level], subscription[:level])
|
|
603
|
+
|
|
604
|
+
begin
|
|
605
|
+
subscription[:handler].call(diagnostic)
|
|
606
|
+
rescue => e
|
|
607
|
+
Kernel.warn("[Featurevisor] Diagnostic handler failed: #{e}")
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
if @on_diagnostic
|
|
612
|
+
if should_report_diagnostic?(diagnostic[:level], @logger.level)
|
|
613
|
+
begin
|
|
614
|
+
@on_diagnostic.call(diagnostic)
|
|
615
|
+
rescue => e
|
|
616
|
+
Kernel.warn("[Featurevisor] Diagnostic handler failed: #{e}")
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
else
|
|
620
|
+
Logger.new(level: @logger.level).log(
|
|
621
|
+
diagnostic[:level],
|
|
622
|
+
diagnostic[:message],
|
|
623
|
+
diagnostic
|
|
624
|
+
)
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
if diagnostic[:level] == "error"
|
|
628
|
+
@emitter.trigger("error", diagnostic: diagnostic)
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def should_report_diagnostic?(diagnostic_level, subscriber_level)
|
|
633
|
+
levels = Featurevisor::LOG_LEVELS
|
|
634
|
+
diagnostic_index = levels.index(diagnostic_level || "info")
|
|
635
|
+
subscriber_index = levels.index(subscriber_level || "info")
|
|
636
|
+
|
|
637
|
+
return false if diagnostic_index.nil? || subscriber_index.nil?
|
|
638
|
+
|
|
639
|
+
subscriber_index >= diagnostic_index
|
|
640
|
+
end
|
|
455
641
|
end
|
|
456
642
|
|
|
457
643
|
# Create a new Featurevisor instance
|
|
458
644
|
# @param options [Hash] Instance options
|
|
459
645
|
# @return [Instance] New instance
|
|
460
|
-
def self.
|
|
646
|
+
def self.create_featurevisor(options = {})
|
|
461
647
|
Instance.new(options)
|
|
462
648
|
end
|
|
463
649
|
end
|
data/lib/featurevisor/logger.rb
CHANGED
|
@@ -113,13 +113,6 @@ module Featurevisor
|
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
# Create a new logger instance
|
|
117
|
-
# @param options [Hash] Logger options
|
|
118
|
-
# @return [Logger] New logger instance
|
|
119
|
-
def self.create_logger(options = {})
|
|
120
|
-
Logger.new(options)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
116
|
# Default log handler function
|
|
124
117
|
# @param level [String] Log level
|
|
125
118
|
# @param message [String] Log message
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Featurevisor
|
|
6
|
+
# Modules extend evaluation behavior and SDK lifecycle.
|
|
7
|
+
module Modules
|
|
8
|
+
class FeaturevisorModule
|
|
9
|
+
attr_reader :id, :name
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
@id = SecureRandom.uuid
|
|
13
|
+
@name = options[:name]
|
|
14
|
+
@setup = options[:setup]
|
|
15
|
+
@before = options[:before]
|
|
16
|
+
@bucket_key = options[:bucket_key]
|
|
17
|
+
@bucket_value = options[:bucket_value]
|
|
18
|
+
@after = options[:after]
|
|
19
|
+
@close = options[:close]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call_setup(api)
|
|
23
|
+
@setup.call(api) if @setup
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call_before(options)
|
|
27
|
+
return options unless @before
|
|
28
|
+
|
|
29
|
+
@before.call(options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call_bucket_key(options)
|
|
33
|
+
return options[:bucket_key] unless @bucket_key
|
|
34
|
+
|
|
35
|
+
@bucket_key.call(options)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call_bucket_value(options)
|
|
39
|
+
return options[:bucket_value] unless @bucket_value
|
|
40
|
+
|
|
41
|
+
@bucket_value.call(options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call_after(evaluation, options)
|
|
45
|
+
return evaluation unless @after
|
|
46
|
+
|
|
47
|
+
@after.call(evaluation, options)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def call_close
|
|
51
|
+
@close.call if @close
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class ModulesManager
|
|
56
|
+
attr_reader :modules
|
|
57
|
+
|
|
58
|
+
def initialize(options = {})
|
|
59
|
+
@modules = []
|
|
60
|
+
@report_diagnostic = options[:report_diagnostic]
|
|
61
|
+
@module_api_factory = options[:module_api_factory]
|
|
62
|
+
@clear_module_diagnostic_subscriptions = options[:clear_module_diagnostic_subscriptions]
|
|
63
|
+
|
|
64
|
+
(options[:modules] || []).each do |mod|
|
|
65
|
+
add(mod)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def add(mod)
|
|
70
|
+
mod = FeaturevisorModule.new(mod) if mod.is_a?(Hash)
|
|
71
|
+
return nil unless mod
|
|
72
|
+
|
|
73
|
+
if mod.name && !mod.name.to_s.empty? && @modules.any? { |existing| existing.name == mod.name }
|
|
74
|
+
report(
|
|
75
|
+
{
|
|
76
|
+
level: "error",
|
|
77
|
+
code: "duplicate_module",
|
|
78
|
+
message: "Duplicate module name",
|
|
79
|
+
module_name: mod.name
|
|
80
|
+
},
|
|
81
|
+
nil
|
|
82
|
+
)
|
|
83
|
+
return nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
begin
|
|
87
|
+
mod.call_setup(@module_api_factory.call(mod)) if @module_api_factory
|
|
88
|
+
rescue => e
|
|
89
|
+
@clear_module_diagnostic_subscriptions.call(mod) if @clear_module_diagnostic_subscriptions
|
|
90
|
+
report(
|
|
91
|
+
{
|
|
92
|
+
level: "error",
|
|
93
|
+
code: "module_setup_error",
|
|
94
|
+
message: "Module setup failed",
|
|
95
|
+
module_name: mod.name,
|
|
96
|
+
original_error: e
|
|
97
|
+
},
|
|
98
|
+
nil
|
|
99
|
+
)
|
|
100
|
+
close_module(mod)
|
|
101
|
+
return nil
|
|
102
|
+
end
|
|
103
|
+
@modules << mod
|
|
104
|
+
|
|
105
|
+
-> { remove(mod) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def remove(name_or_module)
|
|
109
|
+
removed = []
|
|
110
|
+
@modules = @modules.reject do |mod|
|
|
111
|
+
matches = name_or_module.equal?(mod) || mod.name == name_or_module
|
|
112
|
+
removed << mod if matches
|
|
113
|
+
matches
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
removed.each do |mod|
|
|
117
|
+
@clear_module_diagnostic_subscriptions.call(mod) if @clear_module_diagnostic_subscriptions
|
|
118
|
+
close_module(mod)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def get_all
|
|
123
|
+
@modules
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def run_before_modules(options)
|
|
127
|
+
@modules.reduce(options) do |result, mod|
|
|
128
|
+
mod.call_before(result)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def run_bucket_key_modules(options)
|
|
133
|
+
bucket_key = options[:bucket_key]
|
|
134
|
+
@modules.each do |mod|
|
|
135
|
+
bucket_key = mod.call_bucket_key(options.merge(bucket_key: bucket_key))
|
|
136
|
+
end
|
|
137
|
+
bucket_key
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def run_bucket_value_modules(options)
|
|
141
|
+
bucket_value = options[:bucket_value]
|
|
142
|
+
@modules.each do |mod|
|
|
143
|
+
bucket_value = mod.call_bucket_value(options.merge(bucket_value: bucket_value))
|
|
144
|
+
end
|
|
145
|
+
bucket_value
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def run_after_modules(evaluation, options)
|
|
149
|
+
@modules.reduce(evaluation) do |result, mod|
|
|
150
|
+
mod.call_after(result, options)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def close_all
|
|
155
|
+
@modules.each do |mod|
|
|
156
|
+
@clear_module_diagnostic_subscriptions.call(mod) if @clear_module_diagnostic_subscriptions
|
|
157
|
+
close_module(mod)
|
|
158
|
+
end
|
|
159
|
+
@modules = []
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def close_module(mod)
|
|
165
|
+
mod.call_close
|
|
166
|
+
rescue => e
|
|
167
|
+
report(
|
|
168
|
+
{
|
|
169
|
+
level: "error",
|
|
170
|
+
code: "module_close_error",
|
|
171
|
+
message: "Module close failed",
|
|
172
|
+
module_name: mod.name,
|
|
173
|
+
original_error: e
|
|
174
|
+
},
|
|
175
|
+
nil
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def report(diagnostic, mod)
|
|
180
|
+
@report_diagnostic.call(diagnostic, mod) if @report_diagnostic
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
data/lib/featurevisor/version.rb
CHANGED
data/lib/featurevisor.rb
CHANGED
|
@@ -6,7 +6,7 @@ require_relative "featurevisor/emitter"
|
|
|
6
6
|
require_relative "featurevisor/conditions"
|
|
7
7
|
require_relative "featurevisor/datafile_reader"
|
|
8
8
|
require_relative "featurevisor/bucketer"
|
|
9
|
-
require_relative "featurevisor/
|
|
9
|
+
require_relative "featurevisor/modules"
|
|
10
10
|
require_relative "featurevisor/evaluate"
|
|
11
11
|
require_relative "featurevisor/instance"
|
|
12
12
|
require_relative "featurevisor/child_instance"
|
|
@@ -14,4 +14,7 @@ require_relative "featurevisor/events"
|
|
|
14
14
|
|
|
15
15
|
module Featurevisor
|
|
16
16
|
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
private_constant :DatafileReader
|
|
19
|
+
private_constant :Logger
|
|
17
20
|
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: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fahad Heylaal
|
|
@@ -9,6 +9,20 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: benchmark
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: rspec
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -60,9 +74,9 @@ files:
|
|
|
60
74
|
- lib/featurevisor/emitter.rb
|
|
61
75
|
- lib/featurevisor/evaluate.rb
|
|
62
76
|
- lib/featurevisor/events.rb
|
|
63
|
-
- lib/featurevisor/hooks.rb
|
|
64
77
|
- lib/featurevisor/instance.rb
|
|
65
78
|
- lib/featurevisor/logger.rb
|
|
79
|
+
- lib/featurevisor/modules.rb
|
|
66
80
|
- lib/featurevisor/murmurhash.rb
|
|
67
81
|
- lib/featurevisor/version.rb
|
|
68
82
|
homepage: https://featurevisor.com
|