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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b4009e153b6dc621cee9ed4d8f1aaa7f199396e5d8a8266d949b3367b0f06b8
4
- data.tar.gz: 1f45c271ce3d180c362cfa095d6027f0d5d48f51cc259fd31215baa8959732d6
3
+ metadata.gz: 74310b5e1a7545b5d289a4f09d32c7e396f3f03bcb5af9265fe970667f2aedfd
4
+ data.tar.gz: 6f0fd700429f63dbba04b3311c88700e9281113f0b114a6c635797745ca1cddc
5
5
  SHA512:
6
- metadata.gz: debfee24678c85c688e94935396ef142f3b1f31aaec0b7788c6fc09e51e03e805104f273e53bda66686d4eded49baff6e33f58bb7e0b2543cbdf5cfac1cf9289
7
- data.tar.gz: ff2cc0874823a1dcd23c3fc29dd70deffb46b0a08fc6c9efce620299bb3200b6edbf78d5352579e2b476725e5870d9bcde1f2a48faafc8e5ccca11e113e750ba
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
- variation = flag.get_variation(flag.off_variation)
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
- return Models::EvaluationDetail.new(
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
- private
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
- FlagConfiguration = Struct.new(:key, :version, :type, :enabled, :variations, :rules, :fallthrough, :off_variation, keyword_init: true) do
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(flag, context, get_segment: method(:get_segment))
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
- Models::EvaluationDetail.new(value: default_value, reason: "Error")
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 ---
@@ -28,6 +28,10 @@ module Featureflip
28
28
  @mutex.synchronize { @flags.values }
29
29
  end
30
30
 
31
+ def all_flags_map
32
+ @mutex.synchronize { @flags.dup }
33
+ end
34
+
31
35
  def upsert(flag)
32
36
  @mutex.synchronize do
33
37
  existing = @flags[flag.key]
@@ -1,3 +1,3 @@
1
1
  module Featureflip
2
- VERSION = "2.0.0"
2
+ VERSION = "2.1.0"
3
3
  end
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.0.0
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-04-11 00:00:00.000000000 Z
11
+ date: 2026-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec