featureflip 2.0.0 → 2.1.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/lib/featureflip/evaluation/evaluator.rb +74 -10
- data/lib/featureflip/http/client.rb +9 -1
- data/lib/featureflip/models/evaluation_detail.rb +2 -2
- data/lib/featureflip/models/flag.rb +10 -1
- data/lib/featureflip/shared_core.rb +12 -3
- data/lib/featureflip/store/flag_store.rb +4 -0
- data/lib/featureflip/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74310b5e1a7545b5d289a4f09d32c7e396f3f03bcb5af9265fe970667f2aedfd
|
|
4
|
+
data.tar.gz: 6f0fd700429f63dbba04b3311c88700e9281113f0b114a6c635797745ca1cddc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6eb646f7ba550b44639e2d294ad8b171d2a9931f5004afcdb7621db46abc18dfa047732da544e74a9167a509da1fd5018e71e49f7126e0753ee87f8ac9e2bccf
|
|
7
|
+
data.tar.gz: ca59f44a28682ac0fcc5924d4a19e3bb9df8ae76809dfa133f34a57d4bf24243e3cf4b2b7a7f82a54efc0249653a6d54a1fd6ca4c5d0b685fbe2438282e27776
|
|
@@ -4,20 +4,37 @@ require_relative "bucketing"
|
|
|
4
4
|
module Featureflip
|
|
5
5
|
module Evaluation
|
|
6
6
|
class Evaluator
|
|
7
|
+
# Mirrors packages/js-sdk/src/core/evaluator.ts. The guard uses `>` (not `>=`)
|
|
8
|
+
# so a chain of MAX_PREREQUISITE_DEPTH + 1 nested flags trips the cap — matches
|
|
9
|
+
# the JS reference implementation; see prerequisite_spec.rb depth test.
|
|
10
|
+
MAX_PREREQUISITE_DEPTH = 10
|
|
11
|
+
|
|
7
12
|
def initialize
|
|
8
13
|
@condition_evaluator = ConditionEvaluator.new
|
|
9
14
|
end
|
|
10
15
|
|
|
11
|
-
def evaluate(flag, context, get_segment: nil)
|
|
16
|
+
def evaluate(flag, context, get_segment: nil, all_flags: {})
|
|
17
|
+
evaluate_with_shared_memo(flag, context, get_segment: get_segment, all_flags: all_flags, memo: {})
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def evaluate_with_shared_memo(flag, context, all_flags:, memo:, get_segment: nil)
|
|
21
|
+
evaluate_internal(flag, context, get_segment, all_flags, 0, memo)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def evaluate_internal(flag, context, get_segment, all_flags, depth, memo)
|
|
27
|
+
if depth > MAX_PREREQUISITE_DEPTH
|
|
28
|
+
return off_result(flag, reason: "Error")
|
|
29
|
+
end
|
|
30
|
+
|
|
12
31
|
unless flag.enabled
|
|
13
|
-
|
|
14
|
-
return Models::EvaluationDetail.new(
|
|
15
|
-
value: variation&.value,
|
|
16
|
-
reason: "FlagDisabled",
|
|
17
|
-
variation_key: flag.off_variation
|
|
18
|
-
)
|
|
32
|
+
return off_result(flag, reason: "FlagDisabled")
|
|
19
33
|
end
|
|
20
34
|
|
|
35
|
+
prereq_failure = resolve_prerequisites(flag, context, get_segment, all_flags, depth, memo)
|
|
36
|
+
return prereq_failure if prereq_failure
|
|
37
|
+
|
|
21
38
|
sorted_rules = flag.rules.sort_by(&:priority)
|
|
22
39
|
sorted_rules.each do |rule|
|
|
23
40
|
conditions_match = if rule.segment_key && get_segment
|
|
@@ -38,25 +55,72 @@ module Featureflip
|
|
|
38
55
|
if conditions_match
|
|
39
56
|
variation_key = resolve_serve(rule.serve, context)
|
|
40
57
|
variation = flag.get_variation(variation_key)
|
|
41
|
-
|
|
58
|
+
result = Models::EvaluationDetail.new(
|
|
42
59
|
value: variation&.value,
|
|
43
60
|
reason: "RuleMatch",
|
|
44
61
|
rule_id: rule.id,
|
|
45
62
|
variation_key: variation_key
|
|
46
63
|
)
|
|
64
|
+
memo[flag.key] = result
|
|
65
|
+
return result
|
|
47
66
|
end
|
|
48
67
|
end
|
|
49
68
|
|
|
50
69
|
variation_key = resolve_serve(flag.fallthrough, context)
|
|
51
70
|
variation = flag.get_variation(variation_key)
|
|
52
|
-
Models::EvaluationDetail.new(
|
|
71
|
+
result = Models::EvaluationDetail.new(
|
|
53
72
|
value: variation&.value,
|
|
54
73
|
reason: "Fallthrough",
|
|
55
74
|
variation_key: variation_key
|
|
56
75
|
)
|
|
76
|
+
memo[flag.key] = result
|
|
77
|
+
result
|
|
57
78
|
end
|
|
58
79
|
|
|
59
|
-
|
|
80
|
+
# Returns nil when all prerequisites pass; otherwise returns the off-variation
|
|
81
|
+
# result for the flag and memoises it under flag.key. Mirrors the per-branch
|
|
82
|
+
# memo writes in the JS SDK evaluator.
|
|
83
|
+
def resolve_prerequisites(flag, context, get_segment, all_flags, depth, memo)
|
|
84
|
+
prerequisites = flag.prerequisites || []
|
|
85
|
+
prerequisites.each do |prereq|
|
|
86
|
+
key = prereq.prerequisite_flag_key
|
|
87
|
+
prereq_result = memo[key]
|
|
88
|
+
|
|
89
|
+
unless prereq_result
|
|
90
|
+
prereq_flag = all_flags[key]
|
|
91
|
+
unless prereq_flag
|
|
92
|
+
return memoise(memo, flag.key, off_result(flag, reason: "PrerequisiteFailed", prerequisite_key: key))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
prereq_result = evaluate_internal(prereq_flag, context, get_segment, all_flags, depth + 1, memo)
|
|
96
|
+
memo[key] = prereq_result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if prereq_result.reason == "Error"
|
|
100
|
+
return memoise(memo, flag.key, off_result(flag, reason: "Error"))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if prereq_result.variation_key != prereq.expected_variation_key
|
|
104
|
+
return memoise(memo, flag.key, off_result(flag, reason: "PrerequisiteFailed", prerequisite_key: key))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def memoise(memo, key, result)
|
|
111
|
+
memo[key] = result
|
|
112
|
+
result
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def off_result(flag, reason:, prerequisite_key: nil)
|
|
116
|
+
variation = flag.get_variation(flag.off_variation)
|
|
117
|
+
Models::EvaluationDetail.new(
|
|
118
|
+
value: variation&.value,
|
|
119
|
+
reason: reason,
|
|
120
|
+
variation_key: flag.off_variation,
|
|
121
|
+
prerequisite_key: prerequisite_key
|
|
122
|
+
)
|
|
123
|
+
end
|
|
60
124
|
|
|
61
125
|
def resolve_serve(serve, context)
|
|
62
126
|
if serve.type == "Fixed"
|
|
@@ -82,7 +82,15 @@ module Featureflip
|
|
|
82
82
|
variations: (data["variations"] || []).map { |v| Models::Variation.new(key: v["key"], value: v["value"]) },
|
|
83
83
|
rules: (data["rules"] || []).map { |r| parse_rule(r) },
|
|
84
84
|
fallthrough: parse_serve(data["fallthrough"]),
|
|
85
|
-
off_variation: data["offVariation"]
|
|
85
|
+
off_variation: data["offVariation"],
|
|
86
|
+
prerequisites: (data["prerequisites"] || []).map { |p| parse_prerequisite(p) }
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def parse_prerequisite(data)
|
|
91
|
+
Models::Prerequisite.new(
|
|
92
|
+
prerequisite_flag_key: data["prerequisiteFlagKey"],
|
|
93
|
+
expected_variation_key: data["expectedVariationKey"]
|
|
86
94
|
)
|
|
87
95
|
end
|
|
88
96
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Featureflip
|
|
2
2
|
module Models
|
|
3
|
-
EvaluationDetail = Struct.new(:value, :reason, :rule_id, :variation_key, keyword_init: true) do
|
|
4
|
-
def initialize(value:, reason:, rule_id: nil, variation_key: nil)
|
|
3
|
+
EvaluationDetail = Struct.new(:value, :reason, :rule_id, :variation_key, :prerequisite_key, keyword_init: true) do
|
|
4
|
+
def initialize(value:, reason:, rule_id: nil, variation_key: nil, prerequisite_key: nil)
|
|
5
5
|
super
|
|
6
6
|
end
|
|
7
7
|
end
|
|
@@ -28,7 +28,16 @@ module Featureflip
|
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Prerequisite = Struct.new(:prerequisite_flag_key, :expected_variation_key, keyword_init: true)
|
|
32
|
+
|
|
33
|
+
FlagConfiguration = Struct.new(
|
|
34
|
+
:key, :version, :type, :enabled, :variations, :rules, :fallthrough, :off_variation, :prerequisites,
|
|
35
|
+
keyword_init: true
|
|
36
|
+
) do
|
|
37
|
+
def initialize(key:, version:, type:, enabled:, variations:, rules:, fallthrough:, off_variation:, prerequisites: [])
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
|
|
32
41
|
def get_variation(key)
|
|
33
42
|
@variations_by_key ||= variations.each_with_object({}) { |v, h| h[v.key] = v }
|
|
34
43
|
@variations_by_key[key]
|
|
@@ -135,7 +135,12 @@ module Featureflip
|
|
|
135
135
|
return Models::EvaluationDetail.new(value: default_value, reason: "FlagNotFound")
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
-
result = @evaluator.evaluate(
|
|
138
|
+
result = @evaluator.evaluate(
|
|
139
|
+
flag,
|
|
140
|
+
context,
|
|
141
|
+
get_segment: method(:get_segment),
|
|
142
|
+
all_flags: @store.all_flags_map
|
|
143
|
+
)
|
|
139
144
|
value = result.value.nil? ? default_value : result.value
|
|
140
145
|
record_evaluation(key, context, result.variation_key)
|
|
141
146
|
|
|
@@ -143,10 +148,14 @@ module Featureflip
|
|
|
143
148
|
value: value,
|
|
144
149
|
reason: result.reason,
|
|
145
150
|
rule_id: result.rule_id,
|
|
146
|
-
variation_key: result.variation_key
|
|
151
|
+
variation_key: result.variation_key,
|
|
152
|
+
prerequisite_key: result.prerequisite_key
|
|
147
153
|
)
|
|
148
154
|
rescue StandardError
|
|
149
|
-
|
|
155
|
+
# Prerequisite-resolution failures return PrerequisiteFailed cleanly through
|
|
156
|
+
# the evaluator; this rescue only fires on unexpected exceptions (malformed
|
|
157
|
+
# config, programming errors), so prerequisite_key has no defined value.
|
|
158
|
+
Models::EvaluationDetail.new(value: default_value, reason: "Error", prerequisite_key: nil)
|
|
150
159
|
end
|
|
151
160
|
|
|
152
161
|
# --- Event methods ---
|
data/lib/featureflip/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: featureflip
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Featureflip
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|