Skip to content

Commit d69e746

Browse files
committed
Addressed comments and fixed minor bugs
Signed-off-by: Marcin Olko <molko@google.com>
1 parent a26fafc commit d69e746

3 files changed

Lines changed: 152 additions & 103 deletions

File tree

providers/flagd/src/evaluator/flagd_ops.cpp

Lines changed: 152 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ class SemanticVersion {
142142
return absl::InvalidArgumentError("Empty pre-release identifier");
143143
}
144144

145-
bool is_numeric = std::all_of(ident.begin(), ident.end(), ::isdigit);
145+
bool is_numeric =
146+
std::all_of(ident.begin(), ident.end(),
147+
[](unsigned char chr) { return std::isdigit(chr); });
146148
if (is_numeric && HasLeadingZero(ident)) {
147149
return absl::InvalidArgumentError(
148150
"Numeric pre-release identifiers MUST NOT contain leading zeros");
@@ -174,22 +176,19 @@ class SemanticVersion {
174176

175177
// Numeric identifiers have lower precedence than non-numeric identifiers.
176178
bool lhs_is_num =
177-
std::all_of(lhs_part.begin(), lhs_part.end(), ::isdigit);
179+
std::all_of(lhs_part.begin(), lhs_part.end(),
180+
[](unsigned char c) { return std::isdigit(c); });
178181
bool rhs_is_num =
179-
std::all_of(rhs_part.begin(), rhs_part.end(), ::isdigit);
182+
std::all_of(rhs_part.begin(), rhs_part.end(),
183+
[](unsigned char c) { return std::isdigit(c); });
180184

181185
if (lhs_is_num && rhs_is_num) {
182-
uint64_t lhs_num;
183-
uint64_t rhs_num;
184-
// Numeric identifiers are compared numerically.
185-
if (absl::SimpleAtoi(lhs_part, &lhs_num) &&
186-
absl::SimpleAtoi(rhs_part, &rhs_num)) {
187-
if (lhs_num != rhs_num) return lhs_num < rhs_num ? -1 : 1;
188-
} else {
189-
// Fallback to lexicographical comparison if parsing fails (e.g.,
190-
// overflow)
191-
if (lhs_part != rhs_part) return lhs_part < rhs_part ? -1 : 1;
186+
// Compare numerically by length first, then lexicographically.
187+
// This supports arbitrary precision integers without overflow.
188+
if (lhs_part.length() != rhs_part.length()) {
189+
return lhs_part.length() < rhs_part.length() ? -1 : 1;
192190
}
191+
if (lhs_part != rhs_part) return lhs_part < rhs_part ? -1 : 1;
193192
} else if (lhs_is_num && !rhs_is_num) {
194193
return -1;
195194
} else if (!lhs_is_num && rhs_is_num) {
@@ -216,6 +215,121 @@ struct Distribution {
216215
int32_t weight;
217216
};
218217

218+
struct FractionalContext {
219+
std::vector<Distribution> distributions;
220+
uint64_t sum_of_weights;
221+
};
222+
223+
// Resolves the bucketing value based on the rule and data.
224+
// If the first argument evaluates to a string, it's used as the bucketing
225+
// property. Otherwise, it falls back to flagKey + targetingKey.
226+
absl::StatusOr<std::string> ResolveBucketingValue(
227+
const json_logic::JsonLogic& eval, const nlohmann::json& values,
228+
const nlohmann::json& data, bool& first_value_used) {
229+
absl::StatusOr<nlohmann::json> bucketing_property_eval =
230+
eval.Apply(values[0], data);
231+
if (!bucketing_property_eval.ok()) return bucketing_property_eval.status();
232+
233+
if (bucketing_property_eval.value().is_string()) {
234+
first_value_used = true;
235+
return bucketing_property_eval.value().get<std::string>();
236+
}
237+
238+
first_value_used = false;
239+
// Fallback logic from spec: Concatenate flagKey and targetingKey if property
240+
// is missing
241+
std::string flag_key;
242+
if (data.contains("$flagd") && data["$flagd"].is_object() &&
243+
data["$flagd"].contains("flagKey") &&
244+
data["$flagd"]["flagKey"].is_string()) {
245+
flag_key = data["$flagd"]["flagKey"].get<std::string>();
246+
}
247+
248+
std::string targeting_key;
249+
if (data.contains("targetingKey") && data["targetingKey"].is_string()) {
250+
targeting_key = data["targetingKey"].get<std::string>();
251+
}
252+
return absl::StrCat(flag_key, targeting_key);
253+
}
254+
255+
// Parses the distributions from the values array.
256+
absl::StatusOr<FractionalContext> ParseDistributions(
257+
const json_logic::JsonLogic& eval, const nlohmann::json& values,
258+
const nlohmann::json& data, bool first_value_used) {
259+
std::vector<Distribution> distributions;
260+
uint64_t sum_of_weights = 0;
261+
262+
for (size_t i = first_value_used ? 1 : 0; i < values.size(); i++) {
263+
absl::StatusOr<nlohmann::json> item = eval.Apply(values[i], data);
264+
if (!item.ok()) return item.status();
265+
if (!item.value().is_array() || item.value().empty()) {
266+
return absl::InvalidArgumentError("Invalid distribution element");
267+
}
268+
269+
if (!item.value()[0].is_string()) {
270+
return absl::InvalidArgumentError("Variant name must be a string");
271+
}
272+
273+
int32_t weight = 1;
274+
if (item.value().size() >= 2 && item.value()[1].is_number()) {
275+
weight = item.value()[1].get<int32_t>();
276+
if (weight < 0) {
277+
return absl::InvalidArgumentError("Weight must be non-negative.");
278+
}
279+
}
280+
281+
distributions.push_back({item.value()[0].get<std::string>(), weight});
282+
sum_of_weights += weight;
283+
}
284+
285+
if (distributions.empty()) {
286+
return absl::InvalidArgumentError("No distributions found");
287+
}
288+
289+
if (sum_of_weights == 0) {
290+
return absl::InvalidArgumentError("Sum of weights must be positive");
291+
}
292+
293+
if (sum_of_weights >=
294+
static_cast<uint64_t>(std::numeric_limits<int32_t>::max())) {
295+
return absl::InvalidArgumentError("Sum of weights exceeds maximum limit");
296+
}
297+
298+
return FractionalContext{std::move(distributions), sum_of_weights};
299+
}
300+
301+
// Calculates the hash value for the given input using MurmurHash3.
302+
absl::StatusOr<uint32_t> CalculateHash(const std::string& input) {
303+
if (input.length() > static_cast<size_t>(std::numeric_limits<int>::max())) {
304+
return absl::InvalidArgumentError(
305+
"Input string is too long for MurmurHash3");
306+
}
307+
uint32_t hash_value;
308+
MurmurHash3_x86_32(input.data(), static_cast<int>(input.length()), 0,
309+
&hash_value);
310+
return hash_value;
311+
}
312+
313+
// Calculates the bucket and selects the variant based on the hash value.
314+
absl::StatusOr<std::string> SelectVariant(
315+
const std::vector<Distribution>& distributions, uint64_t sum_of_weights,
316+
uint32_t hash_value) {
317+
// High-precision bucketing using 64-bit math to distribute hash over
318+
// sum_of_weights
319+
uint64_t bucket = (static_cast<uint64_t>(hash_value) * sum_of_weights) >>
320+
std::numeric_limits<uint32_t>::digits;
321+
322+
uint64_t range_end = 0;
323+
for (const Distribution& dist : distributions) {
324+
range_end += dist.weight;
325+
if (bucket < range_end) {
326+
return dist.variant;
327+
}
328+
}
329+
330+
return absl::InternalError("Fractional bucketing failed to find a variant");
331+
}
332+
219333
} // namespace
220334

221335
absl::StatusOr<nlohmann::json> StartsWith(const json_logic::JsonLogic& eval,
@@ -271,8 +385,19 @@ absl::StatusOr<nlohmann::json> SemVer(const json_logic::JsonLogic& eval,
271385
if (operation == "<") return cmp < 0;
272386
if (operation == ">=") return cmp >= 0;
273387
if (operation == "<=") return cmp <= 0;
274-
if (operation == "^") return ver1.GetMajor() == ver2.GetMajor();
388+
if (operation == "^") {
389+
if (ver1.Compare(ver2) < 0) return false;
390+
if (ver2.GetMajor() > 0) {
391+
return ver1.GetMajor() == ver2.GetMajor();
392+
}
393+
if (ver2.GetMinor() > 0) {
394+
return ver1.GetMajor() == 0 && ver1.GetMinor() == ver2.GetMinor();
395+
}
396+
return ver1.GetMajor() == 0 && ver1.GetMinor() == 0 &&
397+
ver1.GetPatch() == ver2.GetPatch();
398+
}
275399
if (operation == "~") {
400+
if (ver1.Compare(ver2) < 0) return false;
276401
return ver1.GetMajor() == ver2.GetMajor() &&
277402
ver1.GetMinor() == ver2.GetMinor();
278403
}
@@ -294,95 +419,24 @@ absl::StatusOr<nlohmann::json> Fractional(const json_logic::JsonLogic& eval,
294419
"fractional evaluation data has length under 2");
295420
}
296421

297-
// 1. Get the target property value used for bucketing the values
298-
absl::StatusOr<nlohmann::json> bucketing_property_eval =
299-
eval.Apply(values[0], data);
300-
if (!bucketing_property_eval.ok()) return bucketing_property_eval.status();
301-
302-
std::string bucketing_property_value;
303422
bool first_value_used = false;
304-
if (bucketing_property_eval.value().is_string()) {
305-
bucketing_property_value =
306-
bucketing_property_eval.value().get<std::string>();
307-
first_value_used = true;
308-
} else {
309-
// Fallback logic from spec: Concatenate flagKey and targetingKey if
310-
// property is missing
311-
std::string flag_key;
312-
if (data.contains("$flagd") && data["$flagd"].is_object() &&
313-
data["$flagd"].contains("flagKey") &&
314-
data["$flagd"]["flagKey"].is_string()) {
315-
flag_key = data["$flagd"]["flagKey"].get<std::string>();
316-
}
317-
318-
std::string targeting_key;
319-
if (data.contains("targetingKey") && data["targetingKey"].is_string()) {
320-
targeting_key = data["targetingKey"].get<std::string>();
321-
}
322-
bucketing_property_value = absl::StrCat(flag_key, targeting_key);
323-
}
324-
325-
// 2. Parse the fractional distribution
326-
std::vector<Distribution> distributions;
327-
uint64_t sum_of_weights = 0;
328-
329-
for (size_t i = first_value_used ? 1 : 0; i < values.size(); i++) {
330-
absl::StatusOr<nlohmann::json> item = eval.Apply(values[i], data);
331-
if (!item.ok()) return item.status();
332-
if (!item.value().is_array() || item.value().empty()) {
333-
return absl::InvalidArgumentError("Invalid distribution element");
334-
}
335-
336-
if (!item.value()[0].is_string()) {
337-
return absl::InvalidArgumentError("Variant name must be a string");
338-
}
339-
340-
int32_t weight = 1;
341-
if (item.value().size() >= 2 && item.value()[1].is_number()) {
342-
weight = item.value()[1].get<int32_t>();
343-
if (weight < 0) {
344-
return absl::InvalidArgumentError("Weight must be non-negative.");
345-
}
346-
}
423+
absl::StatusOr<std::string> bucketing_property_res =
424+
ResolveBucketingValue(eval, values, data, first_value_used);
425+
if (!bucketing_property_res.ok()) return bucketing_property_res.status();
347426

348-
distributions.push_back({item.value()[0].get<std::string>(), weight});
349-
sum_of_weights += weight;
350-
}
427+
absl::StatusOr<FractionalContext> context_res =
428+
ParseDistributions(eval, values, data, first_value_used);
429+
if (!context_res.ok()) return context_res.status();
351430

352-
if (distributions.empty()) {
353-
return absl::InvalidArgumentError("No distributions found");
354-
}
431+
absl::StatusOr<uint32_t> hash_res =
432+
CalculateHash(bucketing_property_res.value());
433+
if (!hash_res.ok()) return hash_res.status();
355434

356-
if (sum_of_weights == 0) {
357-
return absl::InvalidArgumentError("Sum of weights must be positive");
358-
}
435+
absl::StatusOr<std::string> variant_res = SelectVariant(
436+
context_res->distributions, context_res->sum_of_weights, hash_res.value());
437+
if (!variant_res.ok()) return variant_res.status();
359438

360-
if (sum_of_weights >=
361-
static_cast<uint64_t>(std::numeric_limits<int32_t>::max())) {
362-
return absl::InvalidArgumentError("Sum of weights exceeds maximum limit");
363-
}
364-
365-
// 3. Calculate hash and determine bucket
366-
uint32_t hash_value;
367-
MurmurHash3_x86_32(bucketing_property_value.data(),
368-
static_cast<int>(bucketing_property_value.length()), 0,
369-
&hash_value);
370-
371-
// High-precision bucketing using 64-bit math to distribute hash over
372-
// sum_of_weights
373-
uint64_t bucket = (static_cast<uint64_t>(hash_value) * sum_of_weights) >>
374-
std::numeric_limits<uint32_t>::digits;
375-
376-
// 4. Return the variant corresponding to the bucket
377-
uint64_t range_end = 0;
378-
for (const Distribution& dist : distributions) {
379-
range_end += dist.weight;
380-
if (bucket < range_end) {
381-
return dist.variant;
382-
}
383-
}
384-
385-
return absl::InternalError("Fractional bucketing failed to find a variant");
439+
return variant_res.value();
386440
}
387441

388442
} // namespace flagd

providers/flagd/tests/evaluator/flagd_fractional_op_test.cpp

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ using nlohmann::json;
1414
class FlagdOpsTest : public ::testing::Test {
1515
protected:
1616
void SetUp() override {
17-
// Assuming these are registered
18-
json_logic_.RegisterOperation("starts_with", flagd::StartsWith);
19-
json_logic_.RegisterOperation("ends_with", flagd::EndsWith);
20-
json_logic_.RegisterOperation("sem_ver", flagd::SemVer);
2117
json_logic_.RegisterOperation("fractional", flagd::Fractional);
2218
}
2319

providers/flagd/tests/evaluator/flagd_ops_test.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ class FlagdOpsTest : public ::testing::Test {
1515
json_logic_.RegisterOperation("starts_with", flagd::StartsWith);
1616
json_logic_.RegisterOperation("ends_with", flagd::EndsWith);
1717
json_logic_.RegisterOperation("sem_ver", flagd::SemVer);
18-
json_logic_.RegisterOperation("fractional", flagd::Fractional);
1918
}
2019

2120
JsonLogic json_logic_;

0 commit comments

Comments
 (0)