Skip to content

Commit

Permalink
Fix evaluating force property in rule when rule is JsonObject and def…
Browse files Browse the repository at this point in the history
…ined as null (#112)

* fix evaluating force property in rule when rule is JsonObject and defined as null

* replace hasForceProperty logic with wrapper to distinguish if force field is present in response or not

---------

Co-authored-by: Bohdan Akimenko <[email protected]>
  • Loading branch information
vazarkevych and Bohdan-Kim authored Jan 31, 2025
1 parent 2aa2f88 commit 9e9ce7c
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 12 deletions.
8 changes: 5 additions & 3 deletions lib/src/main/java/growthbook/sdk/java/FeatureEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(
}

// Loop through the feature rules (if any)
for (FeatureRule<ValueType> rule : feature.getRules()) {
List<FeatureRule<ValueType>> featureRules = feature.getRules();
for (int i = 0; i < featureRules.size(); i++) {
FeatureRule<ValueType> rule = featureRules.get(i);
// If there are prerequisite flag(s), evaluate them
if (rule.getParentConditions() != null) {
for (ParentCondition parentCondition : rule.getParentConditions()) {
Expand Down Expand Up @@ -225,7 +227,7 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(
}

// Feature value is being forced
if (rule.getForce() != null) {
if (rule.getForce().isPresent()) {

// If the rule has a condition, and it evaluates to false, skip this rule and continue to the next one
if (rule.getCondition() != null) {
Expand Down Expand Up @@ -301,7 +303,7 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(
}
}

ValueType value = (ValueType) GrowthBookJsonUtils.unwrap(rule.getForce());
ValueType value = (ValueType) GrowthBookJsonUtils.unwrap(rule.getForce().getValue());

// Apply the force rule
FeatureResult<ValueType> forcedRuleFeatureValue = FeatureResult
Expand Down
80 changes: 77 additions & 3 deletions lib/src/main/java/growthbook/sdk/java/FeatureRule.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package growthbook.sdk.java;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.annotation.Nullable;
import java.lang.reflect.Type;
import java.util.ArrayList;

/**
Expand All @@ -28,7 +34,8 @@
@Data
@Builder
@AllArgsConstructor
public class FeatureRule<ValueType> {
@NoArgsConstructor
public class FeatureRule<ValueType> implements JsonDeserializer<FeatureRule<ValueType>> {
/**
* Unique feature rule id
*/
Expand All @@ -50,8 +57,7 @@ public class FeatureRule<ValueType> {
/**
* Immediately force a specific value (ignore every other option besides condition and coverage)
*/
@Nullable
ValueType force;
OptionalField<ValueType> force;

/**
* Run an experiment (A/B test) and randomly choose between these variations
Expand Down Expand Up @@ -172,4 +178,72 @@ public class FeatureRule<ValueType> {
*/
@Nullable
ArrayList<TrackData<ValueType>> tracks;

@Override
public FeatureRule<ValueType> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
JsonObject jsonObject = json.getAsJsonObject();
FeatureRule.FeatureRuleBuilder<ValueType> builder = FeatureRule.builder();

builder.id(jsonObject.has("id") ? context.deserialize(jsonObject.get("id"), String.class) : null);
builder.key(jsonObject.has("key") ? context.deserialize(jsonObject.get("key"), String.class) : null);
builder.coverage(jsonObject.has("coverage") ? context.deserialize(jsonObject.get("coverage"), Float.class) : null);


if (jsonObject.has("force")) {
JsonElement forceElement = jsonObject.get("force");
if (!forceElement.isJsonNull()) {
ValueType forceValue = context.deserialize(forceElement, new TypeToken<ValueType>() {}.getType());
builder.force(new OptionalField<>(true, forceValue));
} else {
builder.force(new OptionalField<>(true, null));
}
} else {
builder.force(new OptionalField<>(false, null));
}

if (jsonObject.has("variations")) {
builder.variations(context.deserialize(jsonObject.get("variations"), new TypeToken<ArrayList<ValueType>>() {}.getType()));
}

if (jsonObject.has("weights")) {
builder.weights(context.deserialize(jsonObject.get("weights"), new TypeToken<ArrayList<Float>>() {}.getType()));
}

builder.namespace(jsonObject.has("namespace") ? context.deserialize(jsonObject.get("namespace"), Namespace.class) : null);
builder.hashAttribute(jsonObject.has("hashAttribute") ? context.deserialize(jsonObject.get("hashAttribute"), String.class) : "id");
builder.condition(jsonObject.has("condition") ? context.deserialize(jsonObject.get("condition"), JsonObject.class) : null);

if (jsonObject.has("parentConditions")) {
builder.parentConditions(context.deserialize(jsonObject.get("parentConditions"), new TypeToken<ArrayList<ParentCondition>>() {}.getType()));
}

builder.hashVersion(jsonObject.has("hashVersion") ? context.deserialize(jsonObject.get("hashVersion"), Integer.class) : null);
builder.range(jsonObject.has("range") ? context.deserialize(jsonObject.get("range"), BucketRange.class) : null);

if (jsonObject.has("ranges")) {
builder.ranges(context.deserialize(jsonObject.get("ranges"), new TypeToken<ArrayList<BucketRange>>() {}.getType()));
}

if (jsonObject.has("meta")) {
builder.meta(context.deserialize(jsonObject.get("meta"), new TypeToken<ArrayList<VariationMeta>>() {}.getType()));
}

if (jsonObject.has("filters")) {
builder.filters(context.deserialize(jsonObject.get("filters"), new TypeToken<ArrayList<Filter>>() {}.getType()));
}

builder.seed(jsonObject.has("seed") ? context.deserialize(jsonObject.get("seed"), String.class) : null);
builder.name(jsonObject.has("name") ? context.deserialize(jsonObject.get("name"), String.class) : null);
builder.phase(jsonObject.has("phase") ? context.deserialize(jsonObject.get("phase"), String.class) : null);
builder.fallbackAttribute(jsonObject.has("fallbackAttribute") ? context.deserialize(jsonObject.get("fallbackAttribute"), String.class) : null);
builder.disableStickyBucketing(jsonObject.has("disableStickyBucketing") ? context.deserialize(jsonObject.get("disableStickyBucketing"), Boolean.class) : null);
builder.bucketVersion(jsonObject.has("bucketVersion") ? context.deserialize(jsonObject.get("bucketVersion"), Integer.class) : null);
builder.minBucketVersion(jsonObject.has("minBucketVersion") ? context.deserialize(jsonObject.get("minBucketVersion"), Integer.class) : null);

if (jsonObject.has("tracks")) {
builder.tracks(context.deserialize(jsonObject.get("tracks"), new TypeToken<ArrayList<TrackData<ValueType>>>() {}.getType()));
}

return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ private GrowthBookJsonUtils() {
// FeatureResult
gsonBuilder.registerTypeAdapter(FeatureResult.class, FeatureResult.getSerializer());

gsonBuilder.registerTypeAdapter(FeatureRule.class, new FeatureRule<>());

gsonBuilder.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE);

gson = gsonBuilder.create();
Expand Down
23 changes: 23 additions & 0 deletions lib/src/main/java/growthbook/sdk/java/OptionalField.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package growthbook.sdk.java;

import javax.annotation.Nullable;

public class OptionalField<ValueType> {
private final boolean isPresent;
@Nullable
private final ValueType value;

public OptionalField(boolean isPresent, @Nullable ValueType value) {
this.isPresent = isPresent;
this.value = value;
}

public boolean isPresent() {
return isPresent;
}

@Nullable
public ValueType getValue() {
return value;
}
}
12 changes: 6 additions & 6 deletions lib/src/test/java/growthbook/sdk/java/FeatureRuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ void canBeConstructed() {
null,
"my-key",
0.5f,
100,
new OptionalField<>(true,100),
variations,
weights,
namespace,
Expand All @@ -51,8 +51,8 @@ void canBeConstructed() {

assertEquals(0.5f, subject.coverage);
assertEquals(0.5f, subject.getCoverage());
assertEquals(100, subject.force);
assertEquals(100, subject.getForce());
assertEquals(100, subject.getForce().getValue());
assertEquals(100, subject.getForce().getValue());
assertEquals(namespace, subject.namespace);
assertEquals(namespace, subject.getNamespace());
assertEquals("_id", subject.hashAttribute);
Expand All @@ -79,16 +79,16 @@ void canBeBuilt() {
FeatureRule<String> subject = FeatureRule
.<String>builder()
.coverage(0.5f)
.force("forced-value")
.force(new OptionalField<>(true, "forced-value"))
.namespace(namespace)
.weights(weights)
.hashAttribute("_id")
.build();

assertEquals(0.5f, subject.coverage);
assertEquals(0.5f, subject.getCoverage());
assertEquals("forced-value", subject.force);
assertEquals("forced-value", subject.getForce());
assertEquals("forced-value", subject.getForce().getValue());
assertEquals("forced-value", subject.getForce().getValue());
assertEquals(namespace, subject.namespace);
assertEquals(namespace, subject.getNamespace());
assertEquals(weights, subject.weights);
Expand Down

0 comments on commit 9e9ce7c

Please sign in to comment.