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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d0798d6f9dde45580a5345467cfcbc108b01a0bda1ed9c0224a6e58305ec9bd
4
- data.tar.gz: d7a7ceca0d72477e0987ee83b0af3bdcee069f47add6ecc89a42bb7c9cb635e4
3
+ metadata.gz: f3d59e135c8bde6e1955c965c186ade207bbb58961bbb95ad949497940efbd8d
4
+ data.tar.gz: 9a25588388685e6e08f206b4ed623b491e949df2fc448797ba4305cdfdedfd4f
5
5
  SHA512:
6
- metadata.gz: 2546ac239e8b8c0b120caa1d8286ad9a58e4fe23900a58cb63343ad469e9e324548ebac72ea194c102c023e5de2ef2e88fd344c80d15beb49dfc18803a3bd711
7
- data.tar.gz: 6393ec68c36a42813a537fa699e9801132606d273264c142b8d75082c0fc7d260160b27aac53306d49a14bd2d895de2da57e8284690c8affb031076a76500a47
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?(condition.operator)
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(condition.operator, str_value, targets)
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[Equals NotEquals In NotIn].freeze
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 "Equals"
137
+ when "equals"
115
138
  ci_targets.any? { |t| ci_value == t }
116
- when "NotEquals"
139
+ when "notequals"
117
140
  ci_targets.all? { |t| ci_value != t }
118
- when "Contains"
141
+ when "contains"
119
142
  ci_targets.any? { |t| ci_value.include?(t) }
120
- when "NotContains"
143
+ when "notcontains"
121
144
  ci_targets.all? { |t| !ci_value.include?(t) }
122
- when "StartsWith"
145
+ when "startswith"
123
146
  ci_targets.any? { |t| ci_value.start_with?(t) }
124
- when "EndsWith"
147
+ when "endswith"
125
148
  ci_targets.any? { |t| ci_value.end_with?(t) }
126
- when "In"
149
+ when "in"
127
150
  ci_targets.include?(ci_value)
128
- when "NotIn"
151
+ when "notin"
129
152
  !ci_targets.include?(ci_value)
130
- when "MatchesRegex"
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 "GreaterThan"
171
+ when "greaterthan"
149
172
  ci_targets.any? { |t| compare_numeric(ci_value, t, :>) }
150
- when "GreaterThanOrEqual"
173
+ when "greaterthanorequal"
151
174
  ci_targets.any? { |t| compare_numeric(ci_value, t, :>=) }
152
- when "LessThan"
175
+ when "lessthan"
153
176
  ci_targets.any? { |t| compare_numeric(ci_value, t, :<) }
154
- when "LessThanOrEqual"
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 "Before"
185
+ when "before"
163
186
  targets.any? { |t| compare_datetime(value, t, :<) }
164
- when "After"
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 "SemverEquals"
193
+ when "semverequals"
171
194
  targets.any? { |t| compare_semver(value, t, :==) }
172
- when "SemverGreaterThan"
195
+ when "semvergreaterthan"
173
196
  targets.any? { |t| compare_semver(value, t, :>) }
174
- when "SemverGreaterThanOrEqual"
197
+ when "semvergreaterthanorequal"
175
198
  targets.any? { |t| compare_semver(value, t, :>=) }
176
- when "SemverLessThan"
199
+ when "semverlessthan"
177
200
  targets.any? { |t| compare_semver(value, t, :<) }
178
- when "SemverLessThanOrEqual"
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?(condition.operator)
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[Equals In].freeze
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
@@ -1,3 +1,3 @@
1
1
  module Featureflip
2
- VERSION = "2.7.0"
2
+ VERSION = "2.8.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.7.0
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-26 00:00:00.000000000 Z
11
+ date: 2026-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger