featureflip 2.7.0 → 2.8.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/condition_evaluator.rb +50 -27
- 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: f3d59e135c8bde6e1955c965c186ade207bbb58961bbb95ad949497940efbd8d
|
|
4
|
+
data.tar.gz: 9a25588388685e6e08f206b4ed623b491e949df2fc448797ba4305cdfdedfd4f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad445faee786f603e3702df154e78afe5ae8d3ca6868be6c4d5dcc97f03fc4006f364cf40d9c2a73d5e0d43744f5074228186b6b3f69a1a97fd03ca678aa037a
|
|
7
|
+
data.tar.gz: 5bdd2687ecec69386d9cde9f8b0c5190bd4bee4e49f3187f3762263be21e28e6d3dd09517c4b44451f4ddcc969fa70543a7177f91ba6f2b39fd7a785bf590147
|
|
@@ -14,14 +14,19 @@ module Featureflip
|
|
|
14
14
|
|
|
15
15
|
return condition.negate if attr_value.nil?
|
|
16
16
|
|
|
17
|
+
# Resolve the operator ONCE (#2374). Every subsequent decision that keys
|
|
18
|
+
# off it -- numeric coercion, dispatch -- must read the same normalised
|
|
19
|
+
# label, or a mis-cased operator takes some paths and not others.
|
|
20
|
+
operator = normalize_operator(condition.operator)
|
|
21
|
+
|
|
17
22
|
# Issue #1458: when the attribute is a native numeric (Integer/Float —
|
|
18
23
|
# Ruby's `true`/`false` are NOT Numeric, so booleans are naturally
|
|
19
24
|
# excluded), the equality-family operators coerce the condition values to
|
|
20
25
|
# numbers and compare numerically, so 1.0 matches "1". This mirrors the
|
|
21
26
|
# engine's type-aware path and runs BEFORE stringification — a String
|
|
22
27
|
# attribute (even "1.0") stays on the string path below.
|
|
23
|
-
if attr_value.is_a?(Numeric) && NUMERIC_EQUALITY_OPERATORS.include?(
|
|
24
|
-
return evaluate_numeric_equality(condition, attr_value)
|
|
28
|
+
if attr_value.is_a?(Numeric) && NUMERIC_EQUALITY_OPERATORS.include?(operator)
|
|
29
|
+
return evaluate_numeric_equality(condition, operator, attr_value)
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
# Pass the raw (case-preserved) strings to the operator dispatcher.
|
|
@@ -31,7 +36,7 @@ module Featureflip
|
|
|
31
36
|
str_value = attr_value.to_s
|
|
32
37
|
targets = condition.values.map(&:to_s)
|
|
33
38
|
|
|
34
|
-
result = evaluate_operator(
|
|
39
|
+
result = evaluate_operator(operator, str_value, targets)
|
|
35
40
|
|
|
36
41
|
# Issue #2262: an unrecognised operator fails CLOSED. `!nil` is `true`
|
|
37
42
|
# in Ruby, so without this guard a negated unknown operator would match
|
|
@@ -66,7 +71,7 @@ module Featureflip
|
|
|
66
71
|
# The equality-family operators that get type-aware numeric coercion when
|
|
67
72
|
# the attribute is a native Numeric (Issue #1458). Relational/string ops
|
|
68
73
|
# are deliberately excluded — only these four coerce.
|
|
69
|
-
NUMERIC_EQUALITY_OPERATORS = %w[
|
|
74
|
+
NUMERIC_EQUALITY_OPERATORS = %w[equals notequals in notin].freeze
|
|
70
75
|
private_constant :NUMERIC_EQUALITY_OPERATORS
|
|
71
76
|
|
|
72
77
|
# A parsed semantic version: the release core as dot-separated numeric
|
|
@@ -105,29 +110,47 @@ module Featureflip
|
|
|
105
110
|
DAYS_IN_MONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31].freeze
|
|
106
111
|
private_constant :DAYS_IN_MONTH
|
|
107
112
|
|
|
113
|
+
# The single definition of "recognised operator" shared by the four
|
|
114
|
+
# string-typed SDKs (js, go, ruby, php) -- see #2374. Strips underscores
|
|
115
|
+
# and folds case, so the canonical PascalCase the API emits ("NotEquals"),
|
|
116
|
+
# the concatenated form go accepted ("notequals", "NOTEQUALS") and the
|
|
117
|
+
# snake_case form php accepted ("not_equals") all resolve to one label.
|
|
118
|
+
#
|
|
119
|
+
# This SDK previously matched the PascalCase labels EXACTLY, so every
|
|
120
|
+
# mis-cased spelling was simply unknown and failed closed. Normalising by
|
|
121
|
+
# removal makes the shared rule a superset of what each SDK accepted
|
|
122
|
+
# before, so no SDK gets stricter and no configuration that evaluated
|
|
123
|
+
# before stops doing so. The concatenated labels stay unambiguous under
|
|
124
|
+
# this mapping -- no two operator names collide once underscores go.
|
|
125
|
+
def normalize_operator(operator)
|
|
126
|
+
operator.to_s.delete("_").downcase
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `operator` arrives already normalised, so the labels below are the
|
|
130
|
+
# concatenated lowercase form rather than the wire PascalCase.
|
|
108
131
|
def evaluate_operator(operator, value, targets)
|
|
109
132
|
# Case-insensitive views for the string/relational/date operators.
|
|
110
133
|
ci_value = value.downcase
|
|
111
134
|
ci_targets = targets.map(&:downcase)
|
|
112
135
|
|
|
113
136
|
case operator
|
|
114
|
-
when "
|
|
137
|
+
when "equals"
|
|
115
138
|
ci_targets.any? { |t| ci_value == t }
|
|
116
|
-
when "
|
|
139
|
+
when "notequals"
|
|
117
140
|
ci_targets.all? { |t| ci_value != t }
|
|
118
|
-
when "
|
|
141
|
+
when "contains"
|
|
119
142
|
ci_targets.any? { |t| ci_value.include?(t) }
|
|
120
|
-
when "
|
|
143
|
+
when "notcontains"
|
|
121
144
|
ci_targets.all? { |t| !ci_value.include?(t) }
|
|
122
|
-
when "
|
|
145
|
+
when "startswith"
|
|
123
146
|
ci_targets.any? { |t| ci_value.start_with?(t) }
|
|
124
|
-
when "
|
|
147
|
+
when "endswith"
|
|
125
148
|
ci_targets.any? { |t| ci_value.end_with?(t) }
|
|
126
|
-
when "
|
|
149
|
+
when "in"
|
|
127
150
|
ci_targets.include?(ci_value)
|
|
128
|
-
when "
|
|
151
|
+
when "notin"
|
|
129
152
|
!ci_targets.include?(ci_value)
|
|
130
|
-
when "
|
|
153
|
+
when "matchesregex"
|
|
131
154
|
# Case-sensitive matching on the original-case value and pattern,
|
|
132
155
|
# mirroring the engine (RegexOptions.None). Case-insensitivity is
|
|
133
156
|
# opt-in via the (?i) inline flag in the pattern.
|
|
@@ -145,13 +168,13 @@ module Featureflip
|
|
|
145
168
|
# against ANY condition value (mirroring the server engine), not just
|
|
146
169
|
# values[0]. `.any?` over an empty array is false, so empty values
|
|
147
170
|
# returns false without error.
|
|
148
|
-
when "
|
|
171
|
+
when "greaterthan"
|
|
149
172
|
ci_targets.any? { |t| compare_numeric(ci_value, t, :>) }
|
|
150
|
-
when "
|
|
173
|
+
when "greaterthanorequal"
|
|
151
174
|
ci_targets.any? { |t| compare_numeric(ci_value, t, :>=) }
|
|
152
|
-
when "
|
|
175
|
+
when "lessthan"
|
|
153
176
|
ci_targets.any? { |t| compare_numeric(ci_value, t, :<) }
|
|
154
|
-
when "
|
|
177
|
+
when "lessthanorequal"
|
|
155
178
|
ci_targets.any? { |t| compare_numeric(ci_value, t, :<=) }
|
|
156
179
|
# Date operators compare against the RAW value/targets, not the
|
|
157
180
|
# lowercased copies: downcasing breaks ISO-8601 parsing (the "Z" UTC
|
|
@@ -159,23 +182,23 @@ module Featureflip
|
|
|
159
182
|
# to UTC instants — offsets are honored, no-offset strings are assumed
|
|
160
183
|
# UTC, and a bare integer is treated as Unix seconds — so an unparseable
|
|
161
184
|
# operand matches nothing instead of falling back to a string compare.
|
|
162
|
-
when "
|
|
185
|
+
when "before"
|
|
163
186
|
targets.any? { |t| compare_datetime(value, t, :<) }
|
|
164
|
-
when "
|
|
187
|
+
when "after"
|
|
165
188
|
targets.any? { |t| compare_datetime(value, t, :>) }
|
|
166
189
|
# Semantic-version operators compare against the RAW value/targets:
|
|
167
190
|
# prerelease precedence is case-sensitive (semver §11), so the casing
|
|
168
191
|
# preserved by `evaluate_condition` must not be folded here. An
|
|
169
192
|
# unparseable version matches nothing, like the numeric/date operators.
|
|
170
|
-
when "
|
|
193
|
+
when "semverequals"
|
|
171
194
|
targets.any? { |t| compare_semver(value, t, :==) }
|
|
172
|
-
when "
|
|
195
|
+
when "semvergreaterthan"
|
|
173
196
|
targets.any? { |t| compare_semver(value, t, :>) }
|
|
174
|
-
when "
|
|
197
|
+
when "semvergreaterthanorequal"
|
|
175
198
|
targets.any? { |t| compare_semver(value, t, :>=) }
|
|
176
|
-
when "
|
|
199
|
+
when "semverlessthan"
|
|
177
200
|
targets.any? { |t| compare_semver(value, t, :<) }
|
|
178
|
-
when "
|
|
201
|
+
when "semverlessthanorequal"
|
|
179
202
|
targets.any? { |t| compare_semver(value, t, :<=) }
|
|
180
203
|
else
|
|
181
204
|
# Unrecognised operator. `nil` — NOT `false` — so the caller can tell
|
|
@@ -190,21 +213,21 @@ module Featureflip
|
|
|
190
213
|
# numerically against the attribute. Equals/In match if ANY value is equal;
|
|
191
214
|
# NotEquals/NotIn are their negation. The `negate` flag is then applied,
|
|
192
215
|
# mirroring `evaluate_condition`'s tail.
|
|
193
|
-
def evaluate_numeric_equality(condition, attr_value)
|
|
216
|
+
def evaluate_numeric_equality(condition, operator, attr_value)
|
|
194
217
|
target = attr_value.to_f
|
|
195
218
|
any_equal = condition.values.any? do |v|
|
|
196
219
|
n = parse_numeric(v)
|
|
197
220
|
n && n == target
|
|
198
221
|
end
|
|
199
222
|
|
|
200
|
-
positive = NUMERIC_EQUALITY_POSITIVE_OPERATORS.include?(
|
|
223
|
+
positive = NUMERIC_EQUALITY_POSITIVE_OPERATORS.include?(operator)
|
|
201
224
|
result = positive ? any_equal : !any_equal
|
|
202
225
|
condition.negate ? !result : result
|
|
203
226
|
end
|
|
204
227
|
|
|
205
228
|
# Equals/In are the "positive" members of the equality family (match on
|
|
206
229
|
# equality); NotEquals/NotIn negate the same any-equal test.
|
|
207
|
-
NUMERIC_EQUALITY_POSITIVE_OPERATORS = %w[
|
|
230
|
+
NUMERIC_EQUALITY_POSITIVE_OPERATORS = %w[equals in].freeze
|
|
208
231
|
private_constant :NUMERIC_EQUALITY_POSITIVE_OPERATORS
|
|
209
232
|
|
|
210
233
|
# Strict literal parse of a condition value to a Float, reusing the same
|
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.8.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-08
|
|
11
|
+
date: 2026-09-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: logger
|