diff --git a/lib/src/main/java/growthbook/sdk/java/BucketRange.java b/lib/src/main/java/growthbook/sdk/java/BucketRange.java index f1c8386d..237d0dc5 100644 --- a/lib/src/main/java/growthbook/sdk/java/BucketRange.java +++ b/lib/src/main/java/growthbook/sdk/java/BucketRange.java @@ -1,12 +1,13 @@ package growthbook.sdk.java; -import com.google.gson.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonSerializer; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import org.apache.commons.math3.util.Precision; - -import java.lang.reflect.Type; import java.util.Objects; /** @@ -37,6 +38,7 @@ static BucketRange fromJson(JsonElement jsonElement) { /** * Converts the bucket range to the serialized tuple + * * @return JSON string of the bucket range */ public String toJson() { @@ -88,23 +90,13 @@ static JsonElement getJson(BucketRange object) { * @return serializer for {@link BucketRange} */ public static JsonSerializer getSerializer() { - return new JsonSerializer() { - @Override - public JsonElement serialize(BucketRange src, Type typeOfSrc, JsonSerializationContext context) { - return BucketRange.getJson(src); - } - }; + return (src, typeOfSrc, context) -> BucketRange.getJson(src); } /** * @return deserializer for {@link BucketRange} */ public static JsonDeserializer getDeserializer() { - return new JsonDeserializer() { - @Override - public BucketRange deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return BucketRange.fromJson(json); - } - }; + return (json, typeOfT, context) -> BucketRange.fromJson(json); } } diff --git a/lib/src/main/java/growthbook/sdk/java/ConditionEvaluator.java b/lib/src/main/java/growthbook/sdk/java/ConditionEvaluator.java index f4306292..3292f9ae 100644 --- a/lib/src/main/java/growthbook/sdk/java/ConditionEvaluator.java +++ b/lib/src/main/java/growthbook/sdk/java/ConditionEvaluator.java @@ -5,10 +5,13 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; - import javax.annotation.Nullable; import java.lang.reflect.Type; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -84,7 +87,7 @@ Boolean isOperatorObject(JsonElement object) { Set> entries = ((JsonObject) object).entrySet(); - if (entries.size() == 0) { + if (entries.isEmpty()) { return true; } @@ -98,10 +101,10 @@ Boolean isOperatorObject(JsonElement object) { /** * Given attributes and a dot-separated path string, - * @return the value at that path (or null if the path doesn't exist) * * @param attributes User attributes * @param path String path, e.g. path.to.something + * @return the value at that path (or null if the path doesn't exist) */ @Nullable Object getPath(JsonElement attributes, String path) { @@ -144,7 +147,7 @@ Object getPath(JsonElement attributes, String path) { * } * } * - * + *

* And the following attributes: * *

@@ -161,7 +164,7 @@ Object getPath(JsonElement attributes, String path) {
      * i.e. $eq, $ne, $lt, $lte, $gt, $gte, $regex
      * 

* - *

+ *

* There are 2 operators where conditionValue is an array, * i.e. $in, $nin *

@@ -181,9 +184,9 @@ Object getPath(JsonElement attributes, String path) { * i.e. $exists, $type, $not *

* - * @param actual Nullable JSON element + * @param actual Nullable JSON element * @param operatorString String value of the operator - * @param expected The conditions to use to verify that the attributes match, based on the operator + * @param expected The conditions to use to verify that the attributes match, based on the operator * @return if it's a match */ Boolean evalOperatorCondition(String operatorString, @Nullable JsonElement actual, JsonElement expected) { @@ -207,21 +210,24 @@ Boolean evalOperatorCondition(String operatorString, @Nullable JsonElement actua if (DataType.STRING == attributeDataType) { String value = actual.getAsString(); - Type listType = new TypeToken>() {}.getType(); + Type listType = new TypeToken>() { + }.getType(); ArrayList conditionsList = jsonUtils.gson.fromJson(expected, listType); return conditionsList.contains(value); } if (DataType.NUMBER == attributeDataType) { Float value = actual.getAsFloat(); - Type listType = new TypeToken>() {}.getType(); + Type listType = new TypeToken>() { + }.getType(); ArrayList conditionsList = jsonUtils.gson.fromJson(expected, listType); return conditionsList.contains(value); } if (DataType.BOOLEAN == attributeDataType) { Boolean value = actual.getAsBoolean(); - Type listType = new TypeToken>() {}.getType(); + Type listType = new TypeToken>() { + }.getType(); ArrayList conditionsList = jsonUtils.gson.fromJson(expected, listType); return conditionsList.contains(value); } @@ -242,21 +248,24 @@ Boolean evalOperatorCondition(String operatorString, @Nullable JsonElement actua if (DataType.STRING == attributeDataType) { String value = actual.getAsString(); - Type listType = new TypeToken>() {}.getType(); + Type listType = new TypeToken>() { + }.getType(); ArrayList conditionsList = jsonUtils.gson.fromJson(expected, listType); return !conditionsList.contains(value); } if (DataType.NUMBER == attributeDataType) { Float value = actual.getAsFloat(); - Type listType = new TypeToken>() {}.getType(); + Type listType = new TypeToken>() { + }.getType(); ArrayList conditionsList = jsonUtils.gson.fromJson(expected, listType); return !conditionsList.contains(value); } if (DataType.BOOLEAN == attributeDataType) { Boolean value = actual.getAsBoolean(); - Type listType = new TypeToken>() {}.getType(); + Type listType = new TypeToken>() { + }.getType(); ArrayList conditionsList = jsonUtils.gson.fromJson(expected, listType); return !conditionsList.contains(value); } @@ -266,7 +275,7 @@ Boolean evalOperatorCondition(String operatorString, @Nullable JsonElement actua case GT: if (actual == null || DataType.NULL.equals(attributeDataType)) { return (!expected.isJsonPrimitive() || expected.getAsJsonPrimitive().isNumber()) - && 0.0 > expected.getAsDouble(); + && 0.0 > expected.getAsDouble(); } if (actual.getAsJsonPrimitive().isNumber()) { return actual.getAsNumber().floatValue() > expected.getAsNumber().floatValue(); @@ -279,7 +288,7 @@ Boolean evalOperatorCondition(String operatorString, @Nullable JsonElement actua case GTE: if (actual == null || DataType.NULL.equals(attributeDataType)) { return (!expected.isJsonPrimitive() || expected.getAsJsonPrimitive().isNumber()) - && 0.0 >= expected.getAsDouble(); + && 0.0 >= expected.getAsDouble(); } if (actual.getAsJsonPrimitive().isNumber()) { return actual.getAsNumber().floatValue() >= expected.getAsNumber().floatValue(); @@ -292,7 +301,7 @@ Boolean evalOperatorCondition(String operatorString, @Nullable JsonElement actua case LT: if (actual == null || DataType.NULL.equals(attributeDataType)) { return (!expected.isJsonPrimitive() || expected.getAsJsonPrimitive().isNumber()) - && 0.0 < expected.getAsDouble(); + && 0.0 < expected.getAsDouble(); } if (actual.getAsString().toLowerCase().matches("\\d+")) { return Double.parseDouble(actual.getAsString()) < expected.getAsDouble(); @@ -308,7 +317,7 @@ Boolean evalOperatorCondition(String operatorString, @Nullable JsonElement actua case LTE: if (actual == null || DataType.NULL.equals(attributeDataType)) { return (!expected.isJsonPrimitive() || expected.getAsJsonPrimitive().isNumber()) - && 0.0 <= expected.getAsDouble(); + && 0.0 <= expected.getAsDouble(); } if (actual.getAsJsonPrimitive().isNumber()) { return actual.getAsNumber().floatValue() <= expected.getAsNumber().floatValue(); @@ -384,40 +393,46 @@ Boolean evalOperatorCondition(String operatorString, @Nullable JsonElement actua } case VERSION_GT: - if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) return false; + if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) + return false; return StringUtils.paddedVersionString(actual.getAsString()) - .compareTo(StringUtils.paddedVersionString(expected.getAsString())) > 0; + .compareTo(StringUtils.paddedVersionString(expected.getAsString())) > 0; case VERSION_GTE: - if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) return false; + if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) + return false; return StringUtils.paddedVersionString(actual.getAsString()) - .compareTo(StringUtils.paddedVersionString(expected.getAsString())) >= 0; + .compareTo(StringUtils.paddedVersionString(expected.getAsString())) >= 0; case VERSION_LT: - if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) return false; + if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) + return false; return StringUtils.paddedVersionString(actual.getAsString()) - .compareTo(StringUtils.paddedVersionString(expected.getAsString())) < 0; + .compareTo(StringUtils.paddedVersionString(expected.getAsString())) < 0; case VERSION_LTE: - if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) return false; + if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) + return false; return StringUtils.paddedVersionString(actual.getAsString()) - .compareTo(StringUtils.paddedVersionString(expected.getAsString())) <= 0; + .compareTo(StringUtils.paddedVersionString(expected.getAsString())) <= 0; case VERSION_NE: - if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) return false; + if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) + return false; return StringUtils.paddedVersionString(actual.getAsString()) - .compareTo(StringUtils.paddedVersionString(expected.getAsString())) != 0; + .compareTo(StringUtils.paddedVersionString(expected.getAsString())) != 0; case VERSION_EQ: - if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) return false; + if (actual == null || expected == null || DataType.NULL.equals(attributeDataType)) + return false; return StringUtils.paddedVersionString(actual.getAsString()) - .compareTo(StringUtils.paddedVersionString(expected.getAsString())) == 0; + .compareTo(StringUtils.paddedVersionString(expected.getAsString())) == 0; default: return false; @@ -427,6 +442,7 @@ Boolean evalOperatorCondition(String operatorString, @Nullable JsonElement actua /** * Compares two primitives for equality. + * * @param a left side primitive * @param b right side primitive * @param dataType The data type of the primitives @@ -528,7 +544,7 @@ else if (evaluateCondition(actualElement.toString(), expected.toString())) { * @return if matches */ Boolean evalOr(JsonElement attributes, JsonArray conditions) { - if (conditions.size() == 0) { + if (conditions.isEmpty()) { return true; } @@ -570,7 +586,7 @@ private Boolean isIn(JsonElement actual, JsonArray expected) { JsonArray actualArr = actual.getAsJsonArray(); - if (actualArr.size() == 0) return false; + if (actualArr.isEmpty()) return false; DataType attributeDataType = GrowthBookJsonUtils.getElementType(actualArr.get(0)); ArrayList actualAsList = jsonUtils.gson.fromJson(actualArr, listType); diff --git a/lib/src/main/java/growthbook/sdk/java/Experiment.java b/lib/src/main/java/growthbook/sdk/java/Experiment.java index 1da194fc..ea6140ab 100644 --- a/lib/src/main/java/growthbook/sdk/java/Experiment.java +++ b/lib/src/main/java/growthbook/sdk/java/Experiment.java @@ -5,7 +5,6 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; - import javax.annotation.Nullable; import java.util.ArrayList; @@ -48,9 +47,18 @@ public class Experiment { /** * Optional targeting condition */ - String conditionJson; + JsonElement conditionJson; + + /** + * Each item defines a prerequisite where a `condition` must evaluate against + * a parent feature's value (identified by `id`). If `gate` is true, then this is a blocking + * feature-level prerequisite; otherwise it applies to the current rule only. + */ + @Nullable + ArrayList parentConditions; @Nullable + @Deprecated Namespace namespace; /** @@ -100,6 +108,7 @@ public class Experiment { /** * Get a Gson JsonElement of the experiment + * * @return JsonElement */ public String toJson() { @@ -116,7 +125,8 @@ public String toString() { /** * Get a Gson JsonElement of the experiment - * @param object experiment + * + * @param object experiment * @param value type for the experiment * @return JsonElement */ diff --git a/lib/src/main/java/growthbook/sdk/java/ExperimentEvaluator.java b/lib/src/main/java/growthbook/sdk/java/ExperimentEvaluator.java index b135feaf..55f04c84 100644 --- a/lib/src/main/java/growthbook/sdk/java/ExperimentEvaluator.java +++ b/lib/src/main/java/growthbook/sdk/java/ExperimentEvaluator.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; import javax.annotation.Nullable; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; /** @@ -13,6 +14,8 @@ class ExperimentEvaluator implements IExperimentEvaluator { private final ConditionEvaluator conditionEvaluator = new ConditionEvaluator(); + private final GrowthBookJsonUtils jsonUtils = GrowthBookJsonUtils.getInstance(); + @Override public ExperimentResult evaluateExperiment(Experiment experiment, @@ -127,10 +130,10 @@ public ExperimentResult evaluateExperiment(Experiment ExperimentResult evaluateExperiment(Experiment parenConditions = experiment.getParentConditions(); + if (parenConditions != null) { + for (ParentCondition parentCondition : parenConditions) { + FeatureResult parentResult = new FeatureEvaluator().evaluateFeature( + parentCondition.getId(), + context, + null, + jsonUtils.gson.fromJson( + parentCondition.getCondition(), + JsonObject.class + ) + ); + + if (parentResult.source.equals(FeatureResultSource.CYCLIC_PREREQUISITE)) { + return getExperimentResult(context, experiment, -1, false, featureId, null, null, attributeOverrides); + } + + Map evalObj = new HashMap<>(); + if (parentResult.getValue() != null) { + evalObj.put("value", parentResult.getValue()); + } + String attributesJson = GrowthBookJsonUtils.getInstance().gson.toJson(evalObj); + + boolean evalCondition = conditionEvaluator.evaluateCondition( + attributesJson, + parentCondition.getCondition().toString() + ); + + // blocking prerequisite eval failed: feature evaluation fails + if (!evalCondition) { + System.out.println("Feature blocked by prerequisite"); + return getExperimentResult(context, experiment, -1, false, featureId, null, null, attributeOverrides); + } + } + } } + String seed = experiment.getSeed(); if (seed == null) { seed = experiment.getKey(); diff --git a/lib/src/main/java/growthbook/sdk/java/ExperimentResult.java b/lib/src/main/java/growthbook/sdk/java/ExperimentResult.java index de878878..ca53f122 100644 --- a/lib/src/main/java/growthbook/sdk/java/ExperimentResult.java +++ b/lib/src/main/java/growthbook/sdk/java/ExperimentResult.java @@ -4,11 +4,11 @@ import com.google.gson.annotations.SerializedName; import lombok.Builder; import lombok.Data; - import javax.annotation.Nullable; /** * The result of an {@link GrowthBook#run(Experiment)} call + * * @param generic type for the value type for this experiment's variations. */ @Data @@ -51,17 +51,17 @@ public class ExperimentResult { /** * The result of running an {@link Experiment} given a specific {@link GBContext} * - * @param value The array value of the assigned variation - * @param variationId The array index of the assigned variation - * @param inExperiment Whether the user is part of the experiment or not + * @param value The array value of the assigned variation + * @param variationId The array index of the assigned variation + * @param inExperiment Whether the user is part of the experiment or not * @param hashAttribute The user attribute used to assign a variation (default: "id") - * @param hashValue The value of that attribute - * @param featureId The id of the feature (if any) that the experiment came from - * @param hashUsed If a hash was used to assign a variation - * @param key The experiment key, if any - * @param name The human-readable name of the assigned variation - * @param bucket The hash value used to assign a variation (float from 0 to 1) - * @param passThrough Used for holdout groups + * @param hashValue The value of that attribute + * @param featureId The id of the feature (if any) that the experiment came from + * @param hashUsed If a hash was used to assign a variation + * @param key The experiment key, if any + * @param name The human-readable name of the assigned variation + * @param bucket The hash value used to assign a variation (float from 0 to 1) + * @param passThrough Used for holdout groups */ @Builder public ExperimentResult( @@ -101,6 +101,7 @@ public ExperimentResult( /** * Serialized JSON string of the {@link ExperimentResult} + * * @return JSON string */ public String toJson() { diff --git a/lib/src/main/java/growthbook/sdk/java/ExperimentRunCallback.java b/lib/src/main/java/growthbook/sdk/java/ExperimentRunCallback.java index a13e7fa4..0d3f63d0 100644 --- a/lib/src/main/java/growthbook/sdk/java/ExperimentRunCallback.java +++ b/lib/src/main/java/growthbook/sdk/java/ExperimentRunCallback.java @@ -6,6 +6,7 @@ public interface ExperimentRunCallback { /** * A callback to be executed with an {@link ExperimentResult} whenever an experiment is run. + * * @param experimentResult {@link ExperimentResult} */ void onRun(ExperimentResult experimentResult); diff --git a/lib/src/main/java/growthbook/sdk/java/Feature.java b/lib/src/main/java/growthbook/sdk/java/Feature.java index 0b595e26..b559e642 100644 --- a/lib/src/main/java/growthbook/sdk/java/Feature.java +++ b/lib/src/main/java/growthbook/sdk/java/Feature.java @@ -9,6 +9,7 @@ *
  • defaultValue (any) - The default value (should use null if not specified)
  • *
  • rules (FeatureRule[]) - Array of FeatureRule objects that determine when and how the defaultValue gets overridden
  • * + * * @param value type for the feature */ public class Feature { @@ -20,6 +21,7 @@ public class Feature { /** * The default value for a feature evaluation + * * @return value of the feature */ public Object getDefaultValue() { @@ -28,6 +30,7 @@ public Object getDefaultValue() { /** * Returns the rules for evaluating the feature + * * @return rules list */ @Nullable diff --git a/lib/src/main/java/growthbook/sdk/java/FeatureEvalContext.java b/lib/src/main/java/growthbook/sdk/java/FeatureEvalContext.java new file mode 100644 index 00000000..2b987883 --- /dev/null +++ b/lib/src/main/java/growthbook/sdk/java/FeatureEvalContext.java @@ -0,0 +1,14 @@ +package growthbook.sdk.java; + +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +public class FeatureEvalContext { + private String id; + private Set evaluatedFeatures; +} diff --git a/lib/src/main/java/growthbook/sdk/java/FeatureEvaluator.java b/lib/src/main/java/growthbook/sdk/java/FeatureEvaluator.java index cc1d2968..ef594d28 100644 --- a/lib/src/main/java/growthbook/sdk/java/FeatureEvaluator.java +++ b/lib/src/main/java/growthbook/sdk/java/FeatureEvaluator.java @@ -2,12 +2,14 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; - import javax.annotation.Nullable; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.HashSet; +import java.util.Map; /** * INTERNAL: Implementation of feature evaluation @@ -23,6 +25,7 @@ class FeatureEvaluator implements IFeatureEvaluator { private final GrowthBookJsonUtils jsonUtils = GrowthBookJsonUtils.getInstance(); private final ConditionEvaluator conditionEvaluator = new ConditionEvaluator(); private final ExperimentEvaluator experimentEvaluator = new ExperimentEvaluator(); + private final FeatureEvalContext featureEvalContext = new FeatureEvalContext(null, new HashSet<>()); // Takes Context and Feature Key // Returns Calculated Feature Result against that key @@ -33,6 +36,7 @@ public FeatureResult evaluateFeature( Class valueTypeClass, JsonObject attributeOverrides ) throws ClassCastException { + featureEvalContext.setId(key); // This callback serves for listening for feature usage events FeatureUsageCallback featureUsageCallback = context.getFeatureUsageCallback(); @@ -44,6 +48,23 @@ public FeatureResult evaluateFeature( .build(); try { + // block that handle recursion + System.out.println("evaluateFeature: circular dependency detected:"); + if (featureEvalContext.getEvaluatedFeatures().contains(key)) { + FeatureResult featureResultWhenCircularDependencyDetected = FeatureResult + .builder() + .value(null) + .source(FeatureResultSource.CYCLIC_PREREQUISITE) + .build(); + if (featureUsageCallback != null) { + featureUsageCallback.onFeatureUsage(key, featureResultWhenCircularDependencyDetected); + } + + return featureResultWhenCircularDependencyDetected; + } + + featureEvalContext.getEvaluatedFeatures().add(key); + // Check for feature values forced by URL if (context.getAllowUrlOverride()) { ValueType forcedValue = evaluateForcedFeatureValueFromUrl(key, context.getUrl(), valueTypeClass); @@ -127,6 +148,66 @@ public FeatureResult evaluateFeature( // Loop through the feature rules (if any) for (FeatureRule rule : feature.getRules()) { + // If there are prerequisite flag(s), evaluate them + if (rule.getParentConditions() != null) { + for (ParentCondition parentCondition : rule.getParentConditions()) { + FeatureResult parentResult = evaluateFeature( + parentCondition.getId(), + context, + valueTypeClass, + attributeOverrides); + + // break out for cyclic prerequisites + if (parentResult.getSource().equals(FeatureResultSource.CYCLIC_PREREQUISITE)) { + FeatureResult featureResultWhenCircularDependencyDetected = + FeatureResult + .builder() + .value(null) + .source(FeatureResultSource.CYCLIC_PREREQUISITE) + .build(); + + if (featureUsageCallback != null) { + featureUsageCallback.onFeatureUsage(key, featureResultWhenCircularDependencyDetected); + } + return featureResultWhenCircularDependencyDetected; + } + + Map evalObj = new HashMap<>(); + if (parentResult.getValue() != null) { + evalObj.put("value", parentResult.getValue()); + } + String attributesJsonString = GrowthBookJsonUtils.getInstance().gson.toJson(evalObj); + + boolean evalCondition = conditionEvaluator.evaluateCondition( + attributesJsonString, + String.valueOf(parentCondition.getCondition()) + ); + + // blocking prerequisite eval failed: feature evaluation fails + if (!evalCondition) { + // blocking prerequisite eval failed: feature evaluation fails + if (parentCondition.getGate()) { + System.out.println("Feature blocked by prerequisite"); + + FeatureResult featureResultWhenBlockedByPrerequisite = + FeatureResult + .builder() + .value(null) + .source(FeatureResultSource.PREREQUISITE) + .build(); + + if (featureUsageCallback != null) { + featureUsageCallback.onFeatureUsage(key, featureResultWhenBlockedByPrerequisite); + } + return featureResultWhenBlockedByPrerequisite; + } + // non-blocking prerequisite eval failed: break out + // of parentConditions loop, jump to the next rule + + continue; + } + } + } // If there are filters for who is included (e.g. namespaces) List filters = rule.getFilters(); @@ -198,13 +279,14 @@ public FeatureResult evaluateFeature( // String key = ruleKey; String attributeValue = context.getAttributes().get(ruleKey) == null ? null : context.getAttributes().get(ruleKey).getAsString(); if (attributeValue == null || attributeValue.isEmpty()) { - Float hashFNV = GrowthBookUtils.hash(attributeValue, 1, key); - if (hashFNV == null) { - hashFNV = 0f; - } - if (hashFNV > rule.getCoverage()) { - continue; - } + continue; + } + Float hashFNV = GrowthBookUtils.hash(attributeValue, 1, key); + if (hashFNV == null) { + hashFNV = 0f; + } + if (hashFNV > rule.getCoverage()) { + continue; } } } diff --git a/lib/src/main/java/growthbook/sdk/java/FeatureFetchException.java b/lib/src/main/java/growthbook/sdk/java/FeatureFetchException.java index 030e78b0..d7f41da1 100644 --- a/lib/src/main/java/growthbook/sdk/java/FeatureFetchException.java +++ b/lib/src/main/java/growthbook/sdk/java/FeatureFetchException.java @@ -5,14 +5,14 @@ /** * This error is thrown by {@link GBFeaturesRepository} * You can call getErrorCode() to get an enum of various error types you can handle. - * + *

    * CONFIGURATION_ERROR: - * - an encryptionKey was provided but the endpoint does not support encryption so decryption fails - * - no features were found for an unencrypted endpoint + * - an encryptionKey was provided but the endpoint does not support encryption so decryption fails + * - no features were found for an unencrypted endpoint * NO_RESPONSE_ERROR: - * - there was no response body + * - there was no response body * UNKNOWN: - * - there was an unknown error that occurred when attempting to make the request. + * - there was an unknown error that occurred when attempting to make the request. */ public class FeatureFetchException extends Exception { @@ -28,13 +28,13 @@ public class FeatureFetchException extends Exception { */ public enum FeatureFetchErrorCode { /** - * - an encryptionKey was provided but the endpoint does not support encryption so decryption fails - * - no features were found for an unencrypted endpoint + * - an encryptionKey was provided but the endpoint does not support encryption so decryption fails + * - no features were found for an unencrypted endpoint */ CONFIGURATION_ERROR, /** - * - there was no response body + * - there was no response body */ NO_RESPONSE_ERROR, @@ -44,7 +44,7 @@ public enum FeatureFetchErrorCode { SSE_CONNECTION_ERROR, /** - * - there was an unknown error that occurred when attempting to make the request. + * - there was an unknown error that occurred when attempting to make the request. */ UNKNOWN, } @@ -52,7 +52,8 @@ public enum FeatureFetchErrorCode { /** * Create an exception with error code and custom message - * @param errorCode {@link FeatureFetchErrorCode} + * + * @param errorCode {@link FeatureFetchErrorCode} * @param errorMessage Custom error message string */ public FeatureFetchException(FeatureFetchErrorCode errorCode, String errorMessage) { @@ -62,6 +63,7 @@ public FeatureFetchException(FeatureFetchErrorCode errorCode, String errorMessag /** * Create an exception with error code + * * @param errorCode {@link FeatureFetchErrorCode} */ public FeatureFetchException(FeatureFetchErrorCode errorCode) { diff --git a/lib/src/main/java/growthbook/sdk/java/FeatureRefreshCallback.java b/lib/src/main/java/growthbook/sdk/java/FeatureRefreshCallback.java index 81f5f653..c5101e76 100644 --- a/lib/src/main/java/growthbook/sdk/java/FeatureRefreshCallback.java +++ b/lib/src/main/java/growthbook/sdk/java/FeatureRefreshCallback.java @@ -7,12 +7,14 @@ public interface FeatureRefreshCallback { /** * See {@link GBFeaturesRepository#onFeaturesRefresh(FeatureRefreshCallback)} - * @param featuresJson Features as JSON string + * + * @param featuresJson Features as JSON string */ void onRefresh(String featuresJson); /** * See {@link GBFeaturesRepository#onFeaturesRefresh(FeatureRefreshCallback)} + * * @param throwable Exception on refreshCallback */ void onError(Throwable throwable); diff --git a/lib/src/main/java/growthbook/sdk/java/FeatureResult.java b/lib/src/main/java/growthbook/sdk/java/FeatureResult.java index ff4ee9d1..f65383c2 100644 --- a/lib/src/main/java/growthbook/sdk/java/FeatureResult.java +++ b/lib/src/main/java/growthbook/sdk/java/FeatureResult.java @@ -1,13 +1,14 @@ package growthbook.sdk.java; -import com.google.gson.*; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializer; import com.google.gson.annotations.SerializedName; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; - import javax.annotation.Nullable; -import java.lang.reflect.Type; /** * Results for a {@link FeatureEvaluator#evaluateFeature(String, GBContext, Class, JsonObject)} @@ -46,6 +47,7 @@ public class FeatureResult { /** * Get a Gson JsonElement of the {@link FeatureResult} + * * @return a Gson JsonElement */ public String toJson() { @@ -54,6 +56,7 @@ public String toJson() { /** * Evaluates to true when the feature is on + * * @return Boolean */ public Boolean isOn() { @@ -84,6 +87,7 @@ public Boolean isOn() { /** * Evaluates to true when the feature is off + * * @return Boolean */ public Boolean isOff() { @@ -92,9 +96,10 @@ public Boolean isOff() { /** * Get a Gson JsonElement of the {@link FeatureResult} - * @param object {@link FeatureResult} - * @return a Gson JsonElement + * + * @param object {@link FeatureResult} * @param value type for the feature + * @return a Gson JsonElement */ public static JsonElement getJson(FeatureResult object) { JsonObject jsonObject = new JsonObject(); @@ -126,15 +131,11 @@ public static JsonElement getJson(FeatureResult object) { /** * a Gson serializer for {@link FeatureResult} - * @return Gson serializer + * * @param {@link FeatureResult} + * @return Gson serializer */ public static JsonSerializer> getSerializer() { - return new JsonSerializer>() { - @Override - public JsonElement serialize(FeatureResult src, Type typeOfSrc, JsonSerializationContext context) { - return FeatureResult.getJson(src); - } - }; + return (src, typeOfSrc, context) -> FeatureResult.getJson(src); } } diff --git a/lib/src/main/java/growthbook/sdk/java/FeatureResultSource.java b/lib/src/main/java/growthbook/sdk/java/FeatureResultSource.java index c1e96b01..c7ab0264 100644 --- a/lib/src/main/java/growthbook/sdk/java/FeatureResultSource.java +++ b/lib/src/main/java/growthbook/sdk/java/FeatureResultSource.java @@ -32,7 +32,17 @@ public enum FeatureResultSource { * When the value is assigned due to an experiment condition */ @SerializedName("experiment") EXPERIMENT("experiment"), - ; + + /** + * CyclicPrerequisite Value for the Feature is being processed + */ + @SerializedName("cyclicPrerequisite") CYCLIC_PREREQUISITE("cyclicPrerequisite"), + + /** + * Prerequisite Value for the Feature is being processed + */ + @SerializedName("prerequisite") PREREQUISITE("prerequisite"); + private final String rawValue; FeatureResultSource(String rawValue) { @@ -47,6 +57,7 @@ public String toString() { /** * Get a nullable enum Operator from the string value. Use this instead of valueOf() + * * @param stringValue string to try to parse as an operator * @return nullable Operator */ diff --git a/lib/src/main/java/growthbook/sdk/java/FeatureRule.java b/lib/src/main/java/growthbook/sdk/java/FeatureRule.java index a268281e..2599ac77 100644 --- a/lib/src/main/java/growthbook/sdk/java/FeatureRule.java +++ b/lib/src/main/java/growthbook/sdk/java/FeatureRule.java @@ -21,12 +21,16 @@ *

  • namespace (Namespace) - Adds the experiment to a namespace
  • *
  • hashAttribute (string) - What user attribute should be used to assign variations (defaults to id)
  • * + * * @param generic type for the value type for this experiment's variations. */ @Data @Builder @AllArgsConstructor public class FeatureRule { + @Nullable + String id; + @Nullable String key; @@ -51,6 +55,9 @@ public class FeatureRule { @Nullable JsonElement condition; + @Nullable + ArrayList parentConditions; + @Nullable Integer hashVersion; diff --git a/lib/src/main/java/growthbook/sdk/java/Filter.java b/lib/src/main/java/growthbook/sdk/java/Filter.java index 6f87f43c..4696984a 100644 --- a/lib/src/main/java/growthbook/sdk/java/Filter.java +++ b/lib/src/main/java/growthbook/sdk/java/Filter.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; - import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; @@ -22,9 +21,10 @@ public class Filter { /** * Object used for mutual exclusion and filtering users out of experiments based on random hashes. - * @param seed The seed used in the hash - * @param ranges Array of ranges that are included - * @param attribute The attribute to use (default: "id") + * + * @param seed The seed used in the hash + * @param ranges Array of ranges that are included + * @param attribute The attribute to use (default: "id") * @param hashVersion The hash version to use (default: 2) */ @Builder diff --git a/lib/src/main/java/growthbook/sdk/java/GBContext.java b/lib/src/main/java/growthbook/sdk/java/GBContext.java index ce3bc6a9..235fd346 100644 --- a/lib/src/main/java/growthbook/sdk/java/GBContext.java +++ b/lib/src/main/java/growthbook/sdk/java/GBContext.java @@ -24,15 +24,16 @@ public class GBContext { /** * The {@link GBContextBuilder} is recommended for constructing a Context. * Alternatively, you can use this static method instead of the builder. - * @param attributesJson User attributes as JSON string - * @param featuresJson Features response as JSON string, or the encrypted payload. Encrypted payload requires `encryptionKey` - * @param encryptionKey Optional encryption key. If this is not null, featuresJson should be an encrypted payload. - * @param enabled Whether globally all experiments are enabled (default: true) - * @param isQaMode If true, random assignment is disabled and only explicitly forced variations are used. - * @param url A URL string that is used for experiment evaluation, as well as forcing feature values. - * @param allowUrlOverrides Boolean flag to allow URL overrides (default: false) + * + * @param attributesJson User attributes as JSON string + * @param featuresJson Features response as JSON string, or the encrypted payload. Encrypted payload requires `encryptionKey` + * @param encryptionKey Optional encryption key. If this is not null, featuresJson should be an encrypted payload. + * @param enabled Whether globally all experiments are enabled (default: true) + * @param isQaMode If true, random assignment is disabled and only explicitly forced variations are used. + * @param url A URL string that is used for experiment evaluation, as well as forcing feature values. + * @param allowUrlOverrides Boolean flag to allow URL overrides (default: false) * @param forcedVariationsMap Force specific experiments to always assign a specific variation (used for QA) - * @param trackingCallback A function that takes {@link Experiment} and {@link ExperimentResult} as arguments. + * @param trackingCallback A function that takes {@link Experiment} and {@link ExperimentResult} as arguments. */ @Builder public GBContext( @@ -114,6 +115,7 @@ private void setFeatures(@Nullable JsonObject features) { /** * You can update the attributes JSON with new user attributes to evaluate against. + * * @param attributesJson updated user attributes */ public void setAttributesJson(String attributesJson) { @@ -139,6 +141,7 @@ private void setAttributes(@Nullable JsonObject attributes) { /** * You can update the features JSON with new features to evaluate against. + * * @param featuresJson updated features */ @@ -164,10 +167,12 @@ public void setFeaturesJson(String featuresJson) { /** * The builder class to help create a context. You can use {@link #builder()} or the {@link GBContext} constructor */ - public static class GBContextBuilder {} // This stub is required for JavaDoc and is filled by Lombuk + public static class GBContextBuilder { + } // This stub is required for JavaDoc and is filled by Lombuk /** * The builder class to help create a context. You can use this builder or the constructor + * * @return {@link CustomGBContextBuilder} */ public static GBContextBuilder builder() { diff --git a/lib/src/main/java/growthbook/sdk/java/GBFeaturesRepository.java b/lib/src/main/java/growthbook/sdk/java/GBFeaturesRepository.java index 0770e6cf..1ddde88d 100644 --- a/lib/src/main/java/growthbook/sdk/java/GBFeaturesRepository.java +++ b/lib/src/main/java/growthbook/sdk/java/GBFeaturesRepository.java @@ -4,12 +4,16 @@ import com.google.gson.JsonObject; import lombok.Builder; import lombok.Getter; -import okhttp3.*; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; import okhttp3.sse.EventSource; import okhttp3.sse.EventSourceListener; import okhttp3.sse.EventSources; import org.jetbrains.annotations.NotNull; - import javax.annotation.Nullable; import java.io.IOException; import java.time.Instant; @@ -36,7 +40,8 @@ public class GBFeaturesRepository implements IGBFeaturesRepository { @Getter private FeatureRefreshStrategy refreshStrategy; - @Nullable @Getter + @Nullable + @Getter private final String encryptionKey; @Getter @@ -55,8 +60,10 @@ public class GBFeaturesRepository implements IGBFeaturesRepository { private Boolean initialized = false; private Boolean sseAllowed = false; - @Nullable private Request sseRequest = null; - @Nullable private EventSource sseEventSource = null; + @Nullable + private Request sseRequest = null; + @Nullable + private EventSource sseEventSource = null; /** * Allows you to get the features JSON from the provided {@link GBFeaturesRepository#getFeaturesEndpoint()}. @@ -67,37 +74,39 @@ public class GBFeaturesRepository implements IGBFeaturesRepository { /** * Create a new GBFeaturesRepository - * @param apiHost The GrowthBook API host (default: https://cdn.growthbook.io) - * @param clientKey Your client ID, e.g. sdk-abc123 + * + * @param apiHost The GrowthBook API host (default: https://cdn.growthbook.io) + * @param clientKey Your client ID, e.g. sdk-abc123 * @param encryptionKey optional key for decrypting encrypted payload * @param swrTtlSeconds How often the cache should be invalidated when using {@link FeatureRefreshStrategy#STALE_WHILE_REVALIDATE} (default: 60) */ @Builder public GBFeaturesRepository( - @Nullable String apiHost, - String clientKey, - @Nullable String encryptionKey, - @Nullable FeatureRefreshStrategy refreshStrategy, - @Nullable Integer swrTtlSeconds + @Nullable String apiHost, + String clientKey, + @Nullable String encryptionKey, + @Nullable FeatureRefreshStrategy refreshStrategy, + @Nullable Integer swrTtlSeconds ) { this(apiHost, clientKey, encryptionKey, refreshStrategy, swrTtlSeconds, null); } /** * Create a new GBFeaturesRepository - * @param apiHost The GrowthBook API host (default: https://cdn.growthbook.io) - * @param clientKey Your client ID, e.g. sdk-abc123 + * + * @param apiHost The GrowthBook API host (default: https://cdn.growthbook.io) + * @param clientKey Your client ID, e.g. sdk-abc123 * @param encryptionKey optional key for decrypting encrypted payload * @param swrTtlSeconds How often the cache should be invalidated when using {@link FeatureRefreshStrategy#STALE_WHILE_REVALIDATE} (default: 60) - * @param okHttpClient HTTP client (optional) + * @param okHttpClient HTTP client (optional) */ public GBFeaturesRepository( - @Nullable String apiHost, - String clientKey, - @Nullable String encryptionKey, - @Nullable FeatureRefreshStrategy refreshStrategy, - @Nullable Integer swrTtlSeconds, - @Nullable OkHttpClient okHttpClient + @Nullable String apiHost, + String clientKey, + @Nullable String encryptionKey, + @Nullable FeatureRefreshStrategy refreshStrategy, + @Nullable Integer swrTtlSeconds, + @Nullable OkHttpClient okHttpClient ) { if (clientKey == null) throw new IllegalArgumentException("clientKey cannot be null"); @@ -144,7 +153,8 @@ public String getFeaturesJson() { * Subscribe to feature refresh events * This callback is called when the features are successfully refreshed or there is an error when refreshing. * This is called even if the features have not changed. - * @param callback This callback will be called when features are refreshed + * + * @param callback This callback will be called when features are refreshed */ @Override public void onFeaturesRefresh(FeatureRefreshCallback callback) { @@ -160,8 +170,8 @@ private void enqueueFeatureRefreshRequest() { GBFeaturesRepository self = this; Request request = new Request.Builder() - .url(this.featuresEndpoint) - .build(); + .url(this.featuresEndpoint) + .build(); this.okHttpClient.newCall(request).enqueue(new Callback() { @Override @@ -224,19 +234,19 @@ private void createEventSourceListenerAndStartListening(Boolean retryOnFailure) if (this.sseHttpClient == null) { this.sseHttpClient = new OkHttpClient.Builder() - .addInterceptor(new GBFeaturesRepositoryRequestInterceptor()) - .retryOnConnectionFailure(true) - .connectTimeout(0, TimeUnit.SECONDS) - .readTimeout(0, TimeUnit.SECONDS) - .writeTimeout(0, TimeUnit.SECONDS) - .build(); + .addInterceptor(new GBFeaturesRepositoryRequestInterceptor()) + .retryOnConnectionFailure(true) + .connectTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .build(); } this.sseRequest = new Request.Builder() - .url(this.eventsEndpoint) - .header("Accept", "application/json; q=0.5") - .addHeader("Accept", "text/event-stream") - .build(); + .url(this.eventsEndpoint) + .header("Accept", "application/json; q=0.5") + .addHeader("Accept", "text/event-stream") + .build(); GBEventSourceListener gbEventSourceListener = new GBEventSourceListener( @@ -274,8 +284,8 @@ public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable t, @ }; this.sseEventSource = EventSources - .createFactory(this.sseHttpClient) - .newEventSource(sseRequest, gbEventSourceListener); + .createFactory(this.sseHttpClient) + .newEventSource(sseRequest, gbEventSourceListener); this.sseHttpClient.newCall(sseRequest).enqueue(new Callback() { @Override @@ -296,9 +306,9 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO */ private OkHttpClient initializeHttpClient() { OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(new GBFeaturesRepositoryRequestInterceptor()) - .retryOnConnectionFailure(true) - .build(); + .addInterceptor(new GBFeaturesRepositoryRequestInterceptor()) + .retryOnConnectionFailure(true) + .build(); return client; } @@ -324,8 +334,8 @@ private void fetchFeatures() throws FeatureFetchException { } Request request = new Request.Builder() - .url(this.featuresEndpoint) - .build(); + .url(this.featuresEndpoint) + .build(); try (Response response = this.okHttpClient.newCall(request).execute()) { String sseSupportHeader = response.header("x-sse-support"); @@ -336,20 +346,21 @@ private void fetchFeatures() throws FeatureFetchException { e.printStackTrace(); throw new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, - e.getMessage() + FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, + e.getMessage() ); } } /** * Reads the response JSON properties `features` or `encryptedFeatures`, and decrypts if necessary + * * @param responseJsonString JSON response object */ private void onResponseJson(String responseJsonString) throws FeatureFetchException { try { JsonObject jsonObject = GrowthBookJsonUtils.getInstance() - .gson.fromJson(responseJsonString, JsonObject.class); + .gson.fromJson(responseJsonString, JsonObject.class); // Features will be refreshed as either an encrypted or un-encrypted JSON string String refreshedFeatures; @@ -359,8 +370,8 @@ private void onResponseJson(String responseJsonString) throws FeatureFetchExcept JsonElement encryptedFeaturesJsonElement = jsonObject.get("encryptedFeatures"); if (encryptedFeaturesJsonElement == null) { throw new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR, - "encryptionKey provided but endpoint not encrypted" + FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR, + "encryptionKey provided but endpoint not encrypted" ); } @@ -371,8 +382,8 @@ private void onResponseJson(String responseJsonString) throws FeatureFetchExcept JsonElement featuresJsonElement = jsonObject.get("features"); if (featuresJsonElement == null) { throw new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR, - "No features found" + FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR, + "No features found" ); } @@ -386,8 +397,8 @@ private void onResponseJson(String responseJsonString) throws FeatureFetchExcept e.printStackTrace(); throw new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, - e.getMessage() + FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, + e.getMessage() ); } } @@ -406,6 +417,7 @@ private void onRefreshFailed(Throwable throwable) { /** * Handles the successful features fetching response + * * @param response Successful response */ private void onSuccess(Response response) throws FeatureFetchException { @@ -413,7 +425,7 @@ private void onSuccess(Response response) throws FeatureFetchException { ResponseBody responseBody = response.body(); if (responseBody == null) { throw new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.NO_RESPONSE_ERROR + FeatureFetchException.FeatureFetchErrorCode.NO_RESPONSE_ERROR ); } @@ -422,14 +434,15 @@ private void onSuccess(Response response) throws FeatureFetchException { e.printStackTrace(); throw new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, - e.getMessage() + FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, + e.getMessage() ); } } private interface GBEventSourceHandler { void onClose(EventSource eventSource); + void onFeaturesResponse(String featuresJsonResponse) throws FeatureFetchException; } diff --git a/lib/src/main/java/growthbook/sdk/java/GrowthBook.java b/lib/src/main/java/growthbook/sdk/java/GrowthBook.java index 468d28af..10d92734 100644 --- a/lib/src/main/java/growthbook/sdk/java/GrowthBook.java +++ b/lib/src/main/java/growthbook/sdk/java/GrowthBook.java @@ -3,7 +3,6 @@ import com.google.gson.JsonObject; import growthbook.sdk.java.stickyBucketing.InMemoryStickyBucketServiceImpl; import growthbook.sdk.java.stickyBucketing.StickyBucketService; - import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashMap; @@ -29,6 +28,7 @@ public class GrowthBook implements IGrowthBook { /** * Initialize the GrowthBook SDK with a provided {@link GBContext} + * * @param context {@link GBContext} */ public GrowthBook(GBContext context) { @@ -57,9 +57,9 @@ public GrowthBook() { /** * INTERNAL: Constructor with injected dependencies. Useful for testing but not intended to be used * - * @param context Context - * @param featureEvaluator FeatureEvaluator - * @param conditionEvaluator ConditionEvaluator + * @param context Context + * @param featureEvaluator FeatureEvaluator + * @param conditionEvaluator ConditionEvaluator * @param experimentEvaluator ExperimentEvaluator */ GrowthBook(GBContext context, FeatureEvaluator featureEvaluator, ConditionEvaluator conditionEvaluator, ExperimentEvaluator experimentEvaluator) { @@ -86,10 +86,10 @@ public void setAttributes(String attributesJsonString) { } @Override - public ExperimentResult run(Experiment experiment) { + public ExperimentResult run(Experiment experiment) { ExperimentResult result = experimentEvaluatorEvaluator.evaluateExperiment(experiment, this.context, null, attributeOverrides); - this.callbacks.forEach( callback -> { + this.callbacks.forEach(callback -> { callback.onRun(result); }); @@ -162,17 +162,18 @@ public Float getFeatureValue(String featureKey, Float defaultValue) { @Override public Integer getFeatureValue(String featureKey, Integer defaultValue) { try { - // Type erasure occurs so a Double ends up being returned - Double maybeValue = (Double) this.featureEvaluator.evaluateFeature(featureKey, context, Double.class, attributeOverrides).getValue(); + Object maybeValue = this.featureEvaluator.evaluateFeature(featureKey, context, Object.class, attributeOverrides).getValue(); if (maybeValue == null) { return defaultValue; } - try { - return maybeValue.intValue(); - } catch (NumberFormatException e) { - return defaultValue; + if (maybeValue instanceof Double) { + return ((Double) maybeValue).intValue(); + } else if (maybeValue instanceof Long) { + return ((Long) maybeValue).intValue(); + } else { + return defaultValue; // или можно бросить исключение, если требуется строгое соответствие типу Integer } } catch (Exception e) { e.printStackTrace(); @@ -216,8 +217,19 @@ public Boolean evaluateCondition(String attributesJsonString, String conditionJs @Override public Double getFeatureValue(String featureKey, Double defaultValue) { try { - Double maybeValue = (Double) this.featureEvaluator.evaluateFeature(featureKey, context, Double.class, attributeOverrides).getValue(); - return maybeValue == null ? defaultValue : maybeValue; + Object maybeValue = this.featureEvaluator.evaluateFeature(featureKey, context, Object.class, attributeOverrides).getValue(); + + if (maybeValue == null) { + return defaultValue; + } + + if (maybeValue instanceof Double) { + return (Double) maybeValue; + } else if (maybeValue instanceof Long) { + return ((Long) maybeValue).doubleValue(); + } else { + return defaultValue; + } } catch (Exception e) { e.printStackTrace(); return defaultValue; diff --git a/lib/src/main/java/growthbook/sdk/java/GrowthBookJsonUtils.java b/lib/src/main/java/growthbook/sdk/java/GrowthBookJsonUtils.java index 9ee580cc..b9230931 100644 --- a/lib/src/main/java/growthbook/sdk/java/GrowthBookJsonUtils.java +++ b/lib/src/main/java/growthbook/sdk/java/GrowthBookJsonUtils.java @@ -4,7 +4,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonPrimitive; - +import com.google.gson.ToNumberPolicy; import javax.annotation.Nullable; import java.math.BigDecimal; import java.math.BigInteger; @@ -38,11 +38,14 @@ private GrowthBookJsonUtils() { // FeatureResult gsonBuilder.registerTypeAdapter(FeatureResult.class, FeatureResult.getSerializer()); + gsonBuilder.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE); + gson = gsonBuilder.create(); } /** * The JSON utils singleton + * * @return an instance of {@link GrowthBookJsonUtils} */ public static GrowthBookJsonUtils getInstance() { @@ -58,6 +61,7 @@ public static GrowthBookJsonUtils getInstance() { /** * Unwrap an object. If it's not a JsonElement, you'll get the object right back + * * @param o the JSON element to unwrap. * @return unwrapped or original object */ @@ -101,7 +105,7 @@ private static Number unwrapNumber(final Number n) { if (bigDecimal.scale() <= 0) { if (bigDecimal.abs().compareTo(new BigDecimal(Integer.MAX_VALUE)) <= 0) { unwrapped = bigDecimal.intValue(); - } else if (bigDecimal.abs().compareTo(new BigDecimal(Long.MAX_VALUE)) <= 0){ + } else if (bigDecimal.abs().compareTo(new BigDecimal(Long.MAX_VALUE)) <= 0) { unwrapped = bigDecimal.longValue(); } else { unwrapped = bigDecimal; @@ -122,6 +126,7 @@ private static Number unwrapNumber(final Number n) { /** * A convenience method to help work with types of JSON elements + * * @param element unknown JsonElement * @return {@link DataType} */ diff --git a/lib/src/main/java/growthbook/sdk/java/GrowthBookUtils.java b/lib/src/main/java/growthbook/sdk/java/GrowthBookUtils.java index ad2b2cc0..713fb546 100644 --- a/lib/src/main/java/growthbook/sdk/java/GrowthBookUtils.java +++ b/lib/src/main/java/growthbook/sdk/java/GrowthBookUtils.java @@ -1,18 +1,23 @@ package growthbook.sdk.java; -import com.google.gson.*; - +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import growthbook.sdk.java.stickyBucketing.StickyAssignmentsDocument; import growthbook.sdk.java.stickyBucketing.StickyBucketService; -import lombok.val; import org.jetbrains.annotations.NotNull; - import javax.annotation.Nullable; import java.net.MalformedURLException; import java.net.URL; - -// 8 references from java.util package are used in this file -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; /** * INTERNAL: Implementation of for internal utility methods to support {@link growthbook.sdk.java.GrowthBook} @@ -550,7 +555,7 @@ public static Map getStickyBucketAttributes(GBContext context, JsonElement features = featuresJsonElement != null ? featuresJsonElement : context.getFeatures(); if (features != null) { - for (val id : features.getAsJsonObject().keySet()) { + for (String id : features.getAsJsonObject().keySet()) { Feature feature = GrowthBookJsonUtils .getInstance() .gson @@ -638,9 +643,9 @@ public static Map getStickyBucketAssignments( } } - if (stickyAssignmentsDocuments.get(hashKey) != null) { - mergedAssignments.putAll(stickyAssignmentsDocuments.get(hashKey).getAssignments()); - } + if (stickyAssignmentsDocuments.get(hashKey) != null) { + mergedAssignments.putAll(stickyAssignmentsDocuments.get(hashKey).getAssignments()); + } return mergedAssignments; } diff --git a/lib/src/main/java/growthbook/sdk/java/IGrowthBook.java b/lib/src/main/java/growthbook/sdk/java/IGrowthBook.java index e502114d..8620a012 100644 --- a/lib/src/main/java/growthbook/sdk/java/IGrowthBook.java +++ b/lib/src/main/java/growthbook/sdk/java/IGrowthBook.java @@ -1,12 +1,11 @@ package growthbook.sdk.java; import growthbook.sdk.java.stickyBucketing.StickyBucketService; - import javax.annotation.Nullable; interface IGrowthBook { - ExperimentResult run(Experiment experiment); + ExperimentResult run(Experiment experiment); void subscribe(ExperimentRunCallback callback); @@ -19,12 +18,14 @@ interface IGrowthBook { /** * Call this with the JSON string returned from API. + * * @param featuresJsonString features JSON from the GrowthBook API */ void setFeatures(String featuresJsonString); /** * Update the user's attributes + * * @param attributesJsonString user attributes JSON */ void setAttributes(String attributesJsonString); @@ -34,11 +35,13 @@ interface IGrowthBook { void setInMemoryStickyBucketService(); Boolean isOn(String featureKey); + Boolean isOff(String featureKey); /** * Get the feature value as a boolean - * @param featureKey name of the feature + * + * @param featureKey name of the feature * @param defaultValue boolean value to return * @return the found value or defaultValue */ @@ -46,7 +49,8 @@ interface IGrowthBook { /** * Get the feature value as a string - * @param featureKey name of the feature + * + * @param featureKey name of the feature * @param defaultValue string value to return * @return the found value or defaultValue */ @@ -54,7 +58,8 @@ interface IGrowthBook { /** * Get the feature value as a float - * @param featureKey name of the feature + * + * @param featureKey name of the feature * @param defaultValue float value to return * @return the found value or defaultValue */ @@ -62,7 +67,8 @@ interface IGrowthBook { /** * Get the feature value as an integer - * @param featureKey name of the feature + * + * @param featureKey name of the feature * @param defaultValue integer value to return * @return the found value or defaultValue */ @@ -70,7 +76,8 @@ interface IGrowthBook { /** * Get the feature value as a double - * @param featureKey name of the feature + * + * @param featureKey name of the feature * @param defaultValue integer value to return * @return the found value or defaultValue */ @@ -78,7 +85,8 @@ interface IGrowthBook { /** * Get the feature value as an Object. This may be useful for implementations that do not use Gson. - * @param featureKey feature identifier + * + * @param featureKey feature identifier * @param defaultValue default object value * @return Object */ @@ -87,11 +95,12 @@ interface IGrowthBook { /** * Get the feature value as a Gson-deserializable. * If your class requires a custom deserializer, use {@link #getFeatureValue(String, Object)} instead and deserialize it with your own Gson instance. - * @param featureKey feature identifier - * @param defaultValue default generic class + * + * @param featureKey feature identifier + * @param defaultValue default generic class * @param gsonDeserializableClass the class of the generic, e.g. MyFeature.class + * @param Gson deserializable type * @return ValueType instance - * @param Gson deserializable type */ ValueType getFeatureValue(String featureKey, ValueType defaultValue, Class gsonDeserializableClass); diff --git a/lib/src/main/java/growthbook/sdk/java/Namespace.java b/lib/src/main/java/growthbook/sdk/java/Namespace.java index 4691ca00..27a7bb60 100644 --- a/lib/src/main/java/growthbook/sdk/java/Namespace.java +++ b/lib/src/main/java/growthbook/sdk/java/Namespace.java @@ -1,13 +1,14 @@ package growthbook.sdk.java; -import com.google.gson.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonSerializer; import com.google.gson.annotations.Expose; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import java.lang.reflect.Type; - /** * A tuple that specifies what part of a namespace an experiment includes. If two experiments are in the same namespace and their ranges don't overlap, they wil be mutually exclusive. *

    @@ -59,6 +60,7 @@ static Namespace fromJson(JsonElement jsonElement) { /** * A JSON string for the namespace, resulting in a triple value [id, rangeStart, rangeEnd] + * * @return JSON string */ public String toJson() { @@ -72,27 +74,19 @@ public String toString() { /** * a Gson deserializer for {@link Namespace} + * * @return a deserializer for {@link Namespace} */ public static JsonDeserializer getDeserializer() { - return new JsonDeserializer() { - @Override - public Namespace deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return Namespace.fromJson(json); - } - }; + return (json, typeOfT, context) -> Namespace.fromJson(json); } /** * a Gson serializer for {@link Namespace} + * * @return a serializer for {@link Namespace} */ public static JsonSerializer getSerializer() { - return new JsonSerializer() { - @Override - public JsonElement serialize(Namespace src, Type typeOfSrc, JsonSerializationContext context) { - return Namespace.getJson(src); - } - }; + return (src, typeOfSrc, context) -> Namespace.getJson(src); } } diff --git a/lib/src/main/java/growthbook/sdk/java/ParentCondition.java b/lib/src/main/java/growthbook/sdk/java/ParentCondition.java new file mode 100644 index 00000000..b265bc84 --- /dev/null +++ b/lib/src/main/java/growthbook/sdk/java/ParentCondition.java @@ -0,0 +1,15 @@ +package growthbook.sdk.java; + +import com.google.gson.JsonObject; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@AllArgsConstructor +@RequiredArgsConstructor +public class ParentCondition { + private String id; + private JsonObject condition; + private Boolean gate; +} diff --git a/lib/src/main/java/growthbook/sdk/java/VariationMeta.java b/lib/src/main/java/growthbook/sdk/java/VariationMeta.java index 6c68895c..9a2a8b12 100644 --- a/lib/src/main/java/growthbook/sdk/java/VariationMeta.java +++ b/lib/src/main/java/growthbook/sdk/java/VariationMeta.java @@ -4,7 +4,6 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; - import javax.annotation.Nullable; @@ -15,10 +14,13 @@ @Builder @AllArgsConstructor public class VariationMeta { - @Nullable String key; + @Nullable + String key; - @Nullable String name; + @Nullable + String name; @SerializedName("passthrough") - @Nullable Boolean passThrough; + @Nullable + Boolean passThrough; } diff --git a/lib/src/test/java/growthbook/sdk/java/ConditionEvaluatorTest.java b/lib/src/test/java/growthbook/sdk/java/ConditionEvaluatorTest.java index f20a220a..0b04e860 100644 --- a/lib/src/test/java/growthbook/sdk/java/ConditionEvaluatorTest.java +++ b/lib/src/test/java/growthbook/sdk/java/ConditionEvaluatorTest.java @@ -1,5 +1,10 @@ package growthbook.sdk.java; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -9,11 +14,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import java.util.ArrayList; -import static org.junit.jupiter.api.Assertions.*; - class ConditionEvaluatorTest { final TestCasesJsonHelper helper = TestCasesJsonHelper.getInstance(); @@ -21,8 +23,8 @@ class ConditionEvaluatorTest { final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); static final String[] expectedExceptionStrings = { - "Expected BEGIN_ARRAY but was NUMBER at path $", - "java.util.regex.PatternSyntaxException: Dangling meta character '?' near index 3" + "Expected BEGIN_ARRAY but was NUMBER at path $", + "java.util.regex.PatternSyntaxException: Dangling meta character '?' near index 3" }; @BeforeEach @@ -116,61 +118,6 @@ void test_getPath() { assertNull(evaluator.getPath(attributes, "job.company")); } - @Test - void test_paddedVersionString_eq() { - JsonArray testCases = helper.versionCompareTestCases_eq(); - - for (int i = 0; i < testCases.size(); i++) { - JsonArray test = (JsonArray) testCases.get(i); - String version = test.get(0).getAsString(); - String otherVersion = test.get(1).getAsString(); - Boolean equals = test.get(2).getAsBoolean(); - - String paddedVersion = StringUtils.paddedVersionString(version); - String paddedOther = StringUtils.paddedVersionString(otherVersion); - - assertEquals(paddedVersion.compareTo(paddedOther) == 0, equals); - } - } - - @Test - void test_paddedVersionString_lt() { - JsonArray testCases = helper.versionCompareTestCases_lt(); - - for (int i = 0; i < testCases.size(); i++) { - JsonArray test = (JsonArray) testCases.get(i); - String version = test.get(0).getAsString(); - String otherVersion = test.get(1).getAsString(); - Boolean equals = test.get(2).getAsBoolean(); - - String paddedVersion = StringUtils.paddedVersionString(version); - String paddedOther = StringUtils.paddedVersionString(otherVersion); - -// System.out.printf("%s < %s = %s - actual: %s\n", paddedVersion, paddedOther, equals, paddedVersion.compareTo(paddedOther) < 0); - - assertEquals(paddedVersion.compareTo(paddedOther) < 0, equals); - } - } - - @Test - void test_paddedVersionString_gt() { - JsonArray testCases = helper.versionCompareTestCases_gt(); - - for (int i = 0; i < testCases.size(); i++) { - JsonArray test = (JsonArray) testCases.get(i); - String version = test.get(0).getAsString(); - String otherVersion = test.get(1).getAsString(); - Boolean equals = test.get(2).getAsBoolean(); - - String paddedVersion = StringUtils.paddedVersionString(version); - String paddedOther = StringUtils.paddedVersionString(otherVersion); - -// System.out.printf("%s > %s = %s - actual: %s\n", paddedVersion, paddedOther, equals, paddedVersion.compareTo(paddedOther) > 0); - - assertEquals(paddedVersion.compareTo(paddedOther) > 0, equals); - } - } - private boolean unexpectedExceptionOccurred(String stacktrace) { if (stacktrace.isEmpty()) { return false; @@ -180,7 +127,7 @@ private boolean unexpectedExceptionOccurred(String stacktrace) { return false; } } - System.out.println(stacktrace.toString()); + System.out.println(stacktrace); return true; } diff --git a/lib/src/test/java/growthbook/sdk/java/DecryptionUtilsTest.java b/lib/src/test/java/growthbook/sdk/java/DecryptionUtilsTest.java index 4d336bd2..64a3d334 100644 --- a/lib/src/test/java/growthbook/sdk/java/DecryptionUtilsTest.java +++ b/lib/src/test/java/growthbook/sdk/java/DecryptionUtilsTest.java @@ -1,12 +1,14 @@ package growthbook.sdk.java; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.google.gson.JsonArray; import com.google.gson.JsonElement; import growthbook.sdk.java.testhelpers.TestCasesJsonHelper; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - class DecryptionUtilsTest { final TestCasesJsonHelper helper = TestCasesJsonHelper.getInstance(); diff --git a/lib/src/test/java/growthbook/sdk/java/EvaluateFeatureWithStickyBucketingFeatureTest.java b/lib/src/test/java/growthbook/sdk/java/EvaluateFeatureWithStickyBucketingFeatureTest.java index 40c0f590..703154d7 100644 --- a/lib/src/test/java/growthbook/sdk/java/EvaluateFeatureWithStickyBucketingFeatureTest.java +++ b/lib/src/test/java/growthbook/sdk/java/EvaluateFeatureWithStickyBucketingFeatureTest.java @@ -1,5 +1,7 @@ package growthbook.sdk.java; +import static org.junit.jupiter.api.Assertions.assertEquals; + import com.google.common.reflect.TypeToken; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -11,10 +13,11 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - -import java.util.*; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; public class EvaluateFeatureWithStickyBucketingFeatureTest { private static TestCasesJsonHelper helper; @@ -36,9 +39,9 @@ void setUp() { @Test void testsStickyBucketingFeature() { - ArrayList passedTests = new ArrayList<>(); - ArrayList failedTests = new ArrayList<>(); - ArrayList failingIndexes = new ArrayList<>(); + List passedTests = new ArrayList<>(); + List failedTests = new ArrayList<>(); + List failingIndexes = new ArrayList<>(); JsonArray stickyBucketTestCases = helper.getStickyBucketTestCases(); for (int i = 0; i < stickyBucketTestCases.size(); i++) { JsonArray testCase = stickyBucketTestCases.get(i).getAsJsonArray(); @@ -99,8 +102,8 @@ void testsStickyBucketingFeature() { }.getType() ); - String status = "\n" + testCase.get(0).getAsString() + expectedExperimentResult + "&" + expectedStickyAssignmentsDocument + "\n\n" - + "\n" + actualExperimentResult + "&" + context.getStickyBucketAssignmentDocs(); + String status = "\n" + description + expectedExperimentResult + "&" + expectedStickyAssignmentsDocument + "\n\n" + + "\n" + actualExperimentResult + "&" + actualStickyBucketAssignmentDocs; if (Objects.equals(actualExperimentResult, expectedExperimentResult) && expectedStickyAssignmentsDocument.equals(context.getStickyBucketAssignmentDocs())) { passedTests.add(status); @@ -114,6 +117,5 @@ void testsStickyBucketingFeature() { } assertEquals(0, failedTests.size()); - } } diff --git a/lib/src/test/java/growthbook/sdk/java/ExperimentResultTest.java b/lib/src/test/java/growthbook/sdk/java/ExperimentResultTest.java index 60626868..27690a5b 100644 --- a/lib/src/test/java/growthbook/sdk/java/ExperimentResultTest.java +++ b/lib/src/test/java/growthbook/sdk/java/ExperimentResultTest.java @@ -1,12 +1,13 @@ package growthbook.sdk.java; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + import com.google.gson.reflect.TypeToken; import org.junit.jupiter.api.Test; - import java.lang.reflect.Type; -import static org.junit.jupiter.api.Assertions.*; - class ExperimentResultTest { final GrowthBookJsonUtils jsonUtils = GrowthBookJsonUtils.getInstance(); diff --git a/lib/src/test/java/growthbook/sdk/java/ExperimentTest.java b/lib/src/test/java/growthbook/sdk/java/ExperimentTest.java index 62fb2883..618f859f 100644 --- a/lib/src/test/java/growthbook/sdk/java/ExperimentTest.java +++ b/lib/src/test/java/growthbook/sdk/java/ExperimentTest.java @@ -1,13 +1,14 @@ package growthbook.sdk.java; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import org.junit.jupiter.api.Test; - import java.lang.reflect.Type; import java.util.ArrayList; -import static org.junit.jupiter.api.Assertions.*; - class ExperimentTest { final GrowthBookJsonUtils jsonUtils = GrowthBookJsonUtils.getInstance(); @@ -21,7 +22,6 @@ void canBeConstructed() { ArrayList variations = new ArrayList<>(); Namespace namespace = Namespace.builder().build(); - String conditionJson = "{}"; Experiment experiment = new Experiment( "my_experiment", @@ -29,7 +29,8 @@ void canBeConstructed() { weights, true, 0.5f, - conditionJson, + new JsonObject(), + null, namespace, 1, "_id", @@ -53,10 +54,11 @@ void canBeConstructed() { assertEquals("my_experiment", experiment.getKey()); assertEquals("_id", experiment.hashAttribute); assertEquals("_id", experiment.getHashAttribute()); + assert experiment.weights != null; assertEquals(0.3f, experiment.weights.get(0)); assertEquals(0.7f, experiment.weights.get(1)); - assertTrue(experiment.isActive); - assertTrue(experiment.getIsActive()); + assertEquals(Boolean.TRUE, experiment.isActive); + assertEquals(Boolean.TRUE, experiment.getIsActive()); } @Test @@ -85,8 +87,8 @@ void canBeBuilt() { assertEquals("my_experiment", experiment.getKey()); assertEquals("_id", experiment.hashAttribute); assertEquals("_id", experiment.getHashAttribute()); - assertTrue(experiment.isActive); - assertTrue(experiment.getIsActive()); + assertEquals(Boolean.TRUE, experiment.isActive); + assertEquals(Boolean.TRUE, experiment.getIsActive()); } @Test @@ -127,13 +129,13 @@ void test_canBeSerialized() { .weights(weights) .isActive(true) .coverage(0.5f) - .conditionJson("{}") + .conditionJson(new JsonObject()) .namespace(namespace) .force(1) .hashAttribute("_id") .build(); - assertEquals("{\"key\":\"my_experiment\",\"variations\":[100,200],\"weights\":[0.3,0.7],\"active\":true,\"coverage\":0.5,\"conditionJson\":\"{}\",\"namespace\":[\"pricing\",0.0,1.0],\"force\":1,\"hashAttribute\":\"_id\"}", subject.toJson()); + assertEquals("{\"key\":\"my_experiment\",\"variations\":[100,200],\"weights\":[0.3,0.7],\"active\":true,\"coverage\":0.5,\"conditionJson\":{},\"namespace\":[\"pricing\",0.0,1.0],\"force\":1,\"hashAttribute\":\"_id\"}", subject.toJson()); } @Test @@ -142,8 +144,9 @@ void test_canBeDeserialized() { Experiment subject = jsonUtils.gson.fromJson("{\"key\":\"my_experiment\",\"variations\":[100,200],\"weights\":[0.3,0.7],\"active\":true,\"coverage\":0.5,\"namespace\":[\"pricing\",0.0,1.0],\"force\":1,\"hashAttribute\":\"_id\"}", experimentType); + assert subject.getNamespace() != null; assertEquals("pricing", subject.getNamespace().getId()); - assertTrue(subject.getIsActive()); + assertEquals(Boolean.TRUE, subject.getIsActive()); assertEquals(100, subject.getVariations().get(0)); assertEquals(200, subject.getVariations().get(1)); } diff --git a/lib/src/test/java/growthbook/sdk/java/FeatureFetchExceptionTest.java b/lib/src/test/java/growthbook/sdk/java/FeatureFetchExceptionTest.java index 4ac704b8..073cd377 100644 --- a/lib/src/test/java/growthbook/sdk/java/FeatureFetchExceptionTest.java +++ b/lib/src/test/java/growthbook/sdk/java/FeatureFetchExceptionTest.java @@ -1,8 +1,8 @@ package growthbook.sdk.java; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; class FeatureFetchExceptionTest { @@ -10,65 +10,65 @@ class FeatureFetchExceptionTest { void exceptionsHaveEnumErrorCodesAndMessages() { // CONFIGURATION_ERROR FeatureFetchException configExc = new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR + FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR ); assertEquals( - FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR, - configExc.getErrorCode() + FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR, + configExc.getErrorCode() ); FeatureFetchException configExcWithError = new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR, - "config with message" + FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR, + "config with message" ); assertEquals( - FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR, - configExcWithError.getErrorCode() + FeatureFetchException.FeatureFetchErrorCode.CONFIGURATION_ERROR, + configExcWithError.getErrorCode() ); assertEquals( - "CONFIGURATION_ERROR : config with message", - configExcWithError.getMessage() + "CONFIGURATION_ERROR : config with message", + configExcWithError.getMessage() ); // NO_RESPONSE_ERROR FeatureFetchException noResponseExc = new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.NO_RESPONSE_ERROR + FeatureFetchException.FeatureFetchErrorCode.NO_RESPONSE_ERROR ); assertEquals( - FeatureFetchException.FeatureFetchErrorCode.NO_RESPONSE_ERROR, - noResponseExc.getErrorCode() + FeatureFetchException.FeatureFetchErrorCode.NO_RESPONSE_ERROR, + noResponseExc.getErrorCode() ); FeatureFetchException noResponseExcWithMessage = new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.NO_RESPONSE_ERROR, - "no response with message" + FeatureFetchException.FeatureFetchErrorCode.NO_RESPONSE_ERROR, + "no response with message" ); assertEquals( - FeatureFetchException.FeatureFetchErrorCode.NO_RESPONSE_ERROR, - noResponseExcWithMessage.getErrorCode() + FeatureFetchException.FeatureFetchErrorCode.NO_RESPONSE_ERROR, + noResponseExcWithMessage.getErrorCode() ); assertEquals( - "NO_RESPONSE_ERROR : no response with message", - noResponseExcWithMessage.getMessage() + "NO_RESPONSE_ERROR : no response with message", + noResponseExcWithMessage.getMessage() ); // UNKNOWN FeatureFetchException unknownExc = new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.UNKNOWN + FeatureFetchException.FeatureFetchErrorCode.UNKNOWN ); assertEquals( - FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, - unknownExc.getErrorCode() + FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, + unknownExc.getErrorCode() ); FeatureFetchException unknownExcWithMessage = new FeatureFetchException( - FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, - "unknown with message" + FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, + "unknown with message" ); assertEquals( - FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, - unknownExcWithMessage.getErrorCode() + FeatureFetchException.FeatureFetchErrorCode.UNKNOWN, + unknownExcWithMessage.getErrorCode() ); assertEquals( - "UNKNOWN : unknown with message", - unknownExcWithMessage.getMessage() + "UNKNOWN : unknown with message", + unknownExcWithMessage.getMessage() ); } } diff --git a/lib/src/test/java/growthbook/sdk/java/FeatureResultTest.java b/lib/src/test/java/growthbook/sdk/java/FeatureResultTest.java index 5dfd6176..a96fab23 100644 --- a/lib/src/test/java/growthbook/sdk/java/FeatureResultTest.java +++ b/lib/src/test/java/growthbook/sdk/java/FeatureResultTest.java @@ -1,8 +1,11 @@ package growthbook.sdk.java; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; class FeatureResultTest { @Test @@ -21,7 +24,7 @@ void canBeConstructed() { @Test void canBeBuilt() { - FeatureResult subject = FeatureResult + FeatureResult subject = FeatureResult .builder() .value("hello") .experiment(null) @@ -67,10 +70,10 @@ void featureResultSourceOutputsCorrectlyToJson() { @Test void featureResult_isOn_withNonZeroValue_returnsTrue_forIntegers() { FeatureResult subject = FeatureResult - .builder() - .value(1) - .source(FeatureResultSource.FORCE) - .build(); + .builder() + .value(1) + .source(FeatureResultSource.FORCE) + .build(); assertTrue(subject.isOn()); assertFalse(subject.isOff()); @@ -79,10 +82,10 @@ void featureResult_isOn_withNonZeroValue_returnsTrue_forIntegers() { @Test void featureResult_isOn_withZeroValue_returnsFalse_forIntegers() { FeatureResult subject = FeatureResult - .builder() - .value(0) - .source(FeatureResultSource.FORCE) - .build(); + .builder() + .value(0) + .source(FeatureResultSource.FORCE) + .build(); assertFalse(subject.isOn()); assertTrue(subject.isOff()); @@ -93,10 +96,10 @@ void featureResult_isOn_withZeroValue_returnsFalse_forIntegers() { @Test void featureResult_isOn_withNonZeroValue_returnsTrue_forFloats() { FeatureResult subject = FeatureResult - .builder() - .value(1.0f) - .source(FeatureResultSource.FORCE) - .build(); + .builder() + .value(1.0f) + .source(FeatureResultSource.FORCE) + .build(); assertTrue(subject.isOn()); assertFalse(subject.isOff()); @@ -105,10 +108,10 @@ void featureResult_isOn_withNonZeroValue_returnsTrue_forFloats() { @Test void featureResult_isOn_withZeroValue_returnsFalse_forFloats() { FeatureResult subject = FeatureResult - .builder() - .value(0.0f) - .source(FeatureResultSource.FORCE) - .build(); + .builder() + .value(0.0f) + .source(FeatureResultSource.FORCE) + .build(); assertFalse(subject.isOn()); assertTrue(subject.isOff()); @@ -119,10 +122,10 @@ void featureResult_isOn_withZeroValue_returnsFalse_forFloats() { @Test void featureResult_isOn_withNonZeroValue_returnsTrue_forDoubles() { FeatureResult subject = FeatureResult - .builder() - .value(1.0) - .source(FeatureResultSource.FORCE) - .build(); + .builder() + .value(1.0) + .source(FeatureResultSource.FORCE) + .build(); assertTrue(subject.isOn()); assertFalse(subject.isOff()); @@ -131,10 +134,10 @@ void featureResult_isOn_withNonZeroValue_returnsTrue_forDoubles() { @Test void featureResult_isOn_withZeroValue_returnsFalse_forDoubles() { FeatureResult subject = FeatureResult - .builder() - .value(0) - .source(FeatureResultSource.FORCE) - .build(); + .builder() + .value(0) + .source(FeatureResultSource.FORCE) + .build(); assertFalse(subject.isOn()); assertTrue(subject.isOff()); @@ -145,10 +148,10 @@ void featureResult_isOn_withZeroValue_returnsFalse_forDoubles() { @Test void featureResult_isOn_withNonEmptyValue_returnsTrue_forStrings() { FeatureResult subject = FeatureResult - .builder() - .value("hello, world!") - .source(FeatureResultSource.FORCE) - .build(); + .builder() + .value("hello, world!") + .source(FeatureResultSource.FORCE) + .build(); assertTrue(subject.isOn()); assertFalse(subject.isOff()); @@ -157,10 +160,10 @@ void featureResult_isOn_withNonEmptyValue_returnsTrue_forStrings() { @Test void featureResult_isOn_withEmptyValue_returnsFalse_forStrings() { FeatureResult subject = FeatureResult - .builder() - .value("") - .source(FeatureResultSource.FORCE) - .build(); + .builder() + .value("") + .source(FeatureResultSource.FORCE) + .build(); assertFalse(subject.isOn()); assertTrue(subject.isOff()); diff --git a/lib/src/test/java/growthbook/sdk/java/FeatureRuleTest.java b/lib/src/test/java/growthbook/sdk/java/FeatureRuleTest.java index 888eabf5..13d981b7 100644 --- a/lib/src/test/java/growthbook/sdk/java/FeatureRuleTest.java +++ b/lib/src/test/java/growthbook/sdk/java/FeatureRuleTest.java @@ -1,12 +1,11 @@ package growthbook.sdk.java; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import org.junit.jupiter.api.Test; +import java.util.ArrayList; + class FeatureRuleTest { @Test @@ -25,6 +24,7 @@ void canBeConstructed() { .build(); FeatureRule subject = new FeatureRule( + null, "my-key", 0.5f, 100, @@ -45,6 +45,7 @@ void canBeConstructed() { null, null, null, + null, null ); diff --git a/lib/src/test/java/growthbook/sdk/java/FilterTest.java b/lib/src/test/java/growthbook/sdk/java/FilterTest.java index 5ff1f9e1..f2b3bdb7 100644 --- a/lib/src/test/java/growthbook/sdk/java/FilterTest.java +++ b/lib/src/test/java/growthbook/sdk/java/FilterTest.java @@ -1,11 +1,10 @@ package growthbook.sdk.java; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; import java.util.ArrayList; -import static org.junit.jupiter.api.Assertions.*; - class FilterTest { @Test void test_canBeBuilt() { diff --git a/lib/src/test/java/growthbook/sdk/java/GBContextTest.java b/lib/src/test/java/growthbook/sdk/java/GBContextTest.java index 43ab919d..d48faeae 100644 --- a/lib/src/test/java/growthbook/sdk/java/GBContextTest.java +++ b/lib/src/test/java/growthbook/sdk/java/GBContextTest.java @@ -1,16 +1,19 @@ package growthbook.sdk.java; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; - import java.util.HashMap; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.verify; - class GBContextTest { private AutoCloseable closeable; @Mock @@ -76,7 +79,7 @@ void hasGetterSetterForInitialState() { .build(); // Initial state OK - assertTrue(subject.getEnabled()); + assertEquals(Boolean.TRUE, subject.getEnabled()); assertFalse(subject.getIsQaMode()); assertEquals("http://localhost:3000", subject.getUrl()); @@ -85,7 +88,7 @@ void hasGetterSetterForInitialState() { subject.setIsQaMode(true); subject.setUrl("https://docs.growthbook.io/lib/build-your-own"); - assertFalse(subject.getEnabled()); + assertNotEquals(Boolean.TRUE, subject.getEnabled()); assertTrue(subject.getIsQaMode()); assertEquals("https://docs.growthbook.io/lib/build-your-own", subject.getUrl()); } @@ -168,6 +171,7 @@ void supportsEncryptedFeaturesUsingBuilder() { String expectedFeaturesJson = "{\"greeting\":{\"defaultValue\":\"hello\",\"rules\":[{\"condition\":{\"country\":\"france\"},\"force\":\"bonjour\"},{\"condition\":{\"country\":\"mexico\"},\"force\":\"hola\"}]}}"; assertNotNull(subject); + assert subject.getFeaturesJson() != null; assertEquals(expectedFeaturesJson.trim(), subject.getFeaturesJson().trim()); } diff --git a/lib/src/test/java/growthbook/sdk/java/GBFeaturesRepositoryRefreshingTest.java b/lib/src/test/java/growthbook/sdk/java/GBFeaturesRepositoryRefreshingTest.java index 9b223fbc..37dcf686 100644 --- a/lib/src/test/java/growthbook/sdk/java/GBFeaturesRepositoryRefreshingTest.java +++ b/lib/src/test/java/growthbook/sdk/java/GBFeaturesRepositoryRefreshingTest.java @@ -1,16 +1,23 @@ package growthbook.sdk.java; -import okhttp3.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; - import java.io.IOException; import java.util.concurrent.TimeUnit; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - - /** * // TODO: Fix concurrency issue with these tests and re-enable * These tests use Thread.sleep() to pass time to test the callback logic. @@ -25,20 +32,20 @@ public class GBFeaturesRepositoryRefreshingTest { void refreshesFeaturesWhenGetFeaturesCalledAfterCacheExpired() throws IOException, FeatureFetchException, InterruptedException { Integer ttlSeconds = 5; // cache invalidates every 5 seconds String fakeResponseJson = "{\n" + - " \"status\": 200,\n" + - " \"features\": {},\n" + - " \"dateUpdated\": \"2023-01-25T00:51:26.772Z\",\n" + - " \"encryptedFeatures\": \"jfLnSxjChWcbyHaIF30RNw==.iz8DywkSk4+WhNqnIwvr/PdvAwaRNjN3RE30JeOezGAQ/zZ2yoVyVo4w0nLHYqOje5MbhmL0ssvlH0ojk/BxqdSzXD4Wzo3DXfKV81Nzi1aSdiCMnVAIYEzjPl1IKZC3fl88YDBNV3F6YnR9Lemy9yzT03cvMZ0NZ9t5LZO2xS2MhpPYNcAfAlfxXhBGXj6UFDoNKGAtGKdc/zmJsUVQGLtHmqLspVynnJlPPo9nXG+87bt6SjSfQfySUgHm28hb4VmDhVmCx0N37buolVr3pzjZ1QK+tyMKIV7x4/Gu06k8sm0eU4HjG5DFsPgTR7qDu/N5Nk5UTRpG7aSXTUErxhHSJ7MQaxH/Dp/71zVEicaJ0qZE3oPRnU187QVBfdVLLRbqq2QU7Yu0GyJ1jjuf6TA+759OgifHdm17SX43L94Qe62CMU7JQyAqt7h7XmTTQBG664HYwgHJ0ju/9jySC4KUlRxNsixH1tJfznnEXqxgSozn4J61UprTqcmlxLZ1hZPCcRew3mm9DMAG9+YEiL8MhaIwsw8oVq9GirN1S8G3m/6UxQHxZVraPvMRXpGt5VpzEDJ0Po+phrIAhPuIbNpgb08b6Ej4Xh9XXeOLtIcpuj+gNpc4pR4tqF2IOwET\"\n" + - "}"; + " \"status\": 200,\n" + + " \"features\": {},\n" + + " \"dateUpdated\": \"2023-01-25T00:51:26.772Z\",\n" + + " \"encryptedFeatures\": \"jfLnSxjChWcbyHaIF30RNw==.iz8DywkSk4+WhNqnIwvr/PdvAwaRNjN3RE30JeOezGAQ/zZ2yoVyVo4w0nLHYqOje5MbhmL0ssvlH0ojk/BxqdSzXD4Wzo3DXfKV81Nzi1aSdiCMnVAIYEzjPl1IKZC3fl88YDBNV3F6YnR9Lemy9yzT03cvMZ0NZ9t5LZO2xS2MhpPYNcAfAlfxXhBGXj6UFDoNKGAtGKdc/zmJsUVQGLtHmqLspVynnJlPPo9nXG+87bt6SjSfQfySUgHm28hb4VmDhVmCx0N37buolVr3pzjZ1QK+tyMKIV7x4/Gu06k8sm0eU4HjG5DFsPgTR7qDu/N5Nk5UTRpG7aSXTUErxhHSJ7MQaxH/Dp/71zVEicaJ0qZE3oPRnU187QVBfdVLLRbqq2QU7Yu0GyJ1jjuf6TA+759OgifHdm17SX43L94Qe62CMU7JQyAqt7h7XmTTQBG664HYwgHJ0ju/9jySC4KUlRxNsixH1tJfznnEXqxgSozn4J61UprTqcmlxLZ1hZPCcRew3mm9DMAG9+YEiL8MhaIwsw8oVq9GirN1S8G3m/6UxQHxZVraPvMRXpGt5VpzEDJ0Po+phrIAhPuIbNpgb08b6Ej4Xh9XXeOLtIcpuj+gNpc4pR4tqF2IOwET\"\n" + + "}"; String encryptionKey = "o0maZL/O7AphxcbRvaJIzw=="; OkHttpClient mockOkHttpClient = mockHttpClient(fakeResponseJson); GBFeaturesRepository subject = new GBFeaturesRepository( - "http://localhost:80", - "sdk-abc123", - encryptionKey, - FeatureRefreshStrategy.STALE_WHILE_REVALIDATE, - ttlSeconds, - mockOkHttpClient + "http://localhost:80", + "sdk-abc123", + encryptionKey, + FeatureRefreshStrategy.STALE_WHILE_REVALIDATE, + ttlSeconds, + mockOkHttpClient ); subject.initialize(); @@ -60,21 +67,21 @@ void refreshesFeaturesWhenGetFeaturesCalledAfterCacheExpired() throws IOExceptio void doesNotRefreshFeaturesWhenGetFeaturesCalledWithinCacheTime() throws IOException, FeatureFetchException, InterruptedException { Integer ttlSeconds = 5; // cache invalidates every 5 seconds String fakeResponseJson = "{\n" + - " \"status\": 200,\n" + - " \"features\": {},\n" + - " \"dateUpdated\": \"2024-01-25T00:51:26.772Z\",\n" + - " \"encryptedFeatures\": \"jfLnSxjChWcbyHaIF30RNw==.iz8DywkSk4+WhNqnIwvr/PdvAwaRNjN3RE30JeOezGAQ/zZ2yoVyVo4w0nLHYqOje5MbhmL0ssvlH0ojk/BxqdSzXD4Wzo3DXfKV81Nzi1aSdiCMnVAIYEzjPl1IKZC3fl88YDBNV3F6YnR9Lemy9yzT03cvMZ0NZ9t5LZO2xS2MhpPYNcAfAlfxXhBGXj6UFDoNKGAtGKdc/zmJsUVQGLtHmqLspVynnJlPPo9nXG+87bt6SjSfQfySUgHm28hb4VmDhVmCx0N37buolVr3pzjZ1QK+tyMKIV7x4/Gu06k8sm0eU4HjG5DFsPgTR7qDu/N5Nk5UTRpG7aSXTUErxhHSJ7MQaxH/Dp/71zVEicaJ0qZE3oPRnU187QVBfdVLLRbqq2QU7Yu0GyJ1jjuf6TA+759OgifHdm17SX43L94Qe62CMU7JQyAqt7h7XmTTQBG664HYwgHJ0ju/9jySC4KUlRxNsixH1tJfznnEXqxgSozn4J61UprTqcmlxLZ1hZPCcRew3mm9DMAG9+YEiL8MhaIwsw8oVq9GirN1S8G3m/6UxQHxZVraPvMRXpGt5VpzEDJ0Po+phrIAhPuIbNpgb08b6Ej4Xh9XXeOLtIcpuj+gNpc4pR4tqF2IOwET\"\n" + - "}"; + " \"status\": 200,\n" + + " \"features\": {},\n" + + " \"dateUpdated\": \"2024-01-25T00:51:26.772Z\",\n" + + " \"encryptedFeatures\": \"jfLnSxjChWcbyHaIF30RNw==.iz8DywkSk4+WhNqnIwvr/PdvAwaRNjN3RE30JeOezGAQ/zZ2yoVyVo4w0nLHYqOje5MbhmL0ssvlH0ojk/BxqdSzXD4Wzo3DXfKV81Nzi1aSdiCMnVAIYEzjPl1IKZC3fl88YDBNV3F6YnR9Lemy9yzT03cvMZ0NZ9t5LZO2xS2MhpPYNcAfAlfxXhBGXj6UFDoNKGAtGKdc/zmJsUVQGLtHmqLspVynnJlPPo9nXG+87bt6SjSfQfySUgHm28hb4VmDhVmCx0N37buolVr3pzjZ1QK+tyMKIV7x4/Gu06k8sm0eU4HjG5DFsPgTR7qDu/N5Nk5UTRpG7aSXTUErxhHSJ7MQaxH/Dp/71zVEicaJ0qZE3oPRnU187QVBfdVLLRbqq2QU7Yu0GyJ1jjuf6TA+759OgifHdm17SX43L94Qe62CMU7JQyAqt7h7XmTTQBG664HYwgHJ0ju/9jySC4KUlRxNsixH1tJfznnEXqxgSozn4J61UprTqcmlxLZ1hZPCcRew3mm9DMAG9+YEiL8MhaIwsw8oVq9GirN1S8G3m/6UxQHxZVraPvMRXpGt5VpzEDJ0Po+phrIAhPuIbNpgb08b6Ej4Xh9XXeOLtIcpuj+gNpc4pR4tqF2IOwET\"\n" + + "}"; String encryptionKey = "o0maZL/O7AphxcbRvaJIzw=="; OkHttpClient mockOkHttpClient = mockHttpClient(fakeResponseJson); GBFeaturesRepository subject = new GBFeaturesRepository( - "http://localhost:80", - "sdk-abc123", - encryptionKey, - FeatureRefreshStrategy.STALE_WHILE_REVALIDATE, - ttlSeconds, - mockOkHttpClient + "http://localhost:80", + "sdk-abc123", + encryptionKey, + FeatureRefreshStrategy.STALE_WHILE_REVALIDATE, + ttlSeconds, + mockOkHttpClient ); subject.initialize(); @@ -95,20 +102,20 @@ void doesNotRefreshFeaturesWhenGetFeaturesCalledWithinCacheTime() throws IOExcep void refreshesFeaturesWhenGetFeaturesCalledAfterCacheExpired_multipleTimes() throws IOException, FeatureFetchException, InterruptedException { Integer ttlSeconds = 5; // cache invalidates every 5 seconds String fakeResponseJson = "{\n" + - " \"status\": 200,\n" + - " \"features\": {},\n" + - " \"dateUpdated\": \"2023-01-25T00:51:26.772Z\",\n" + - " \"encryptedFeatures\": \"jfLnSxjChWcbyHaIF30RNw==.iz8DywkSk4+WhNqnIwvr/PdvAwaRNjN3RE30JeOezGAQ/zZ2yoVyVo4w0nLHYqOje5MbhmL0ssvlH0ojk/BxqdSzXD4Wzo3DXfKV81Nzi1aSdiCMnVAIYEzjPl1IKZC3fl88YDBNV3F6YnR9Lemy9yzT03cvMZ0NZ9t5LZO2xS2MhpPYNcAfAlfxXhBGXj6UFDoNKGAtGKdc/zmJsUVQGLtHmqLspVynnJlPPo9nXG+87bt6SjSfQfySUgHm28hb4VmDhVmCx0N37buolVr3pzjZ1QK+tyMKIV7x4/Gu06k8sm0eU4HjG5DFsPgTR7qDu/N5Nk5UTRpG7aSXTUErxhHSJ7MQaxH/Dp/71zVEicaJ0qZE3oPRnU187QVBfdVLLRbqq2QU7Yu0GyJ1jjuf6TA+759OgifHdm17SX43L94Qe62CMU7JQyAqt7h7XmTTQBG664HYwgHJ0ju/9jySC4KUlRxNsixH1tJfznnEXqxgSozn4J61UprTqcmlxLZ1hZPCcRew3mm9DMAG9+YEiL8MhaIwsw8oVq9GirN1S8G3m/6UxQHxZVraPvMRXpGt5VpzEDJ0Po+phrIAhPuIbNpgb08b6Ej4Xh9XXeOLtIcpuj+gNpc4pR4tqF2IOwET\"\n" + - "}"; + " \"status\": 200,\n" + + " \"features\": {},\n" + + " \"dateUpdated\": \"2023-01-25T00:51:26.772Z\",\n" + + " \"encryptedFeatures\": \"jfLnSxjChWcbyHaIF30RNw==.iz8DywkSk4+WhNqnIwvr/PdvAwaRNjN3RE30JeOezGAQ/zZ2yoVyVo4w0nLHYqOje5MbhmL0ssvlH0ojk/BxqdSzXD4Wzo3DXfKV81Nzi1aSdiCMnVAIYEzjPl1IKZC3fl88YDBNV3F6YnR9Lemy9yzT03cvMZ0NZ9t5LZO2xS2MhpPYNcAfAlfxXhBGXj6UFDoNKGAtGKdc/zmJsUVQGLtHmqLspVynnJlPPo9nXG+87bt6SjSfQfySUgHm28hb4VmDhVmCx0N37buolVr3pzjZ1QK+tyMKIV7x4/Gu06k8sm0eU4HjG5DFsPgTR7qDu/N5Nk5UTRpG7aSXTUErxhHSJ7MQaxH/Dp/71zVEicaJ0qZE3oPRnU187QVBfdVLLRbqq2QU7Yu0GyJ1jjuf6TA+759OgifHdm17SX43L94Qe62CMU7JQyAqt7h7XmTTQBG664HYwgHJ0ju/9jySC4KUlRxNsixH1tJfznnEXqxgSozn4J61UprTqcmlxLZ1hZPCcRew3mm9DMAG9+YEiL8MhaIwsw8oVq9GirN1S8G3m/6UxQHxZVraPvMRXpGt5VpzEDJ0Po+phrIAhPuIbNpgb08b6Ej4Xh9XXeOLtIcpuj+gNpc4pR4tqF2IOwET\"\n" + + "}"; String encryptionKey = "o0maZL/O7AphxcbRvaJIzw=="; OkHttpClient mockOkHttpClient = mockHttpClient(fakeResponseJson); GBFeaturesRepository subject = new GBFeaturesRepository( - "http://localhost:80", - "sdk-abc123", - encryptionKey, - FeatureRefreshStrategy.STALE_WHILE_REVALIDATE, - ttlSeconds, - mockOkHttpClient + "http://localhost:80", + "sdk-abc123", + encryptionKey, + FeatureRefreshStrategy.STALE_WHILE_REVALIDATE, + ttlSeconds, + mockOkHttpClient ); subject.initialize(); @@ -138,6 +145,7 @@ void refreshesFeaturesWhenGetFeaturesCalledAfterCacheExpired_multipleTimes() thr /** * Create a mock instance of {@link OkHttpClient} + * * @param serializedBody JSON string response * @return mock {@link OkHttpClient} */ @@ -147,14 +155,14 @@ private static OkHttpClient mockHttpClient(final String serializedBody) throws I Call remoteCall = mock(Call.class); Response response = new Response.Builder() - .request(new Request.Builder().url("http://url.com").build()) - .protocol(Protocol.HTTP_1_1) - .code(200).message("").body( - ResponseBody.create( - serializedBody, - MediaType.parse("application/json") - )) - .build(); + .request(new Request.Builder().url("http://url.com").build()) + .protocol(Protocol.HTTP_1_1) + .code(200).message("").body( + ResponseBody.create( + serializedBody, + MediaType.parse("application/json") + )) + .build(); when(remoteCall.execute()).thenReturn(response); when(okHttpClient.newCall(any())).thenReturn(remoteCall); diff --git a/lib/src/test/java/growthbook/sdk/java/GBFeaturesRepositoryTest.java b/lib/src/test/java/growthbook/sdk/java/GBFeaturesRepositoryTest.java index d06663a4..594c64a0 100644 --- a/lib/src/test/java/growthbook/sdk/java/GBFeaturesRepositoryTest.java +++ b/lib/src/test/java/growthbook/sdk/java/GBFeaturesRepositoryTest.java @@ -1,11 +1,7 @@ package growthbook.sdk.java; -import okhttp3.*; -import org.junit.jupiter.api.Test; - -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; @@ -15,17 +11,28 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.Test; +import java.io.IOException; + class GBFeaturesRepositoryTest { @Test void canBeConstructed_withNullEncryptionKey() { GBFeaturesRepository subject = new GBFeaturesRepository( - "https://cdn.growthbook.io", - "java_NsrWldWd5bxQJZftGsWKl7R2yD2LtAK8C8EUYh9L8", - null, - null, - null, - null + "https://cdn.growthbook.io", + "java_NsrWldWd5bxQJZftGsWKl7R2yD2LtAK8C8EUYh9L8", + null, + null, + null, + null ); assertNotNull(subject); @@ -36,12 +43,12 @@ void canBeConstructed_withNullEncryptionKey() { @Test void canBeConstructed_withEncryptionKey() { GBFeaturesRepository subject = new GBFeaturesRepository( - "https://cdn.growthbook.io", - "sdk-862b5mHcP9XPugqD", - "BhB1wORFmZLTDjbvstvS8w==", - null, - null, - null + "https://cdn.growthbook.io", + "sdk-862b5mHcP9XPugqD", + "BhB1wORFmZLTDjbvstvS8w==", + null, + null, + null ); assertNotNull(subject); @@ -52,10 +59,10 @@ void canBeConstructed_withEncryptionKey() { @Test void canBeBuilt_withNullEncryptionKey() { GBFeaturesRepository subject = GBFeaturesRepository - .builder() - .apiHost("https://cdn.growthbook.io") - .clientKey("java_NsrWldWd5bxQJZftGsWKl7R2yD2LtAK8C8EUYh9L8") - .build(); + .builder() + .apiHost("https://cdn.growthbook.io") + .clientKey("java_NsrWldWd5bxQJZftGsWKl7R2yD2LtAK8C8EUYh9L8") + .build(); assertNotNull(subject); assertEquals("https://cdn.growthbook.io/api/features/java_NsrWldWd5bxQJZftGsWKl7R2yD2LtAK8C8EUYh9L8", subject.getFeaturesEndpoint()); @@ -64,11 +71,11 @@ void canBeBuilt_withNullEncryptionKey() { @Test void canBeBuilt_withEncryptionKey() { GBFeaturesRepository subject = GBFeaturesRepository - .builder() - .apiHost("https://cdn.growthbook.io") - .clientKey("sdk-862b5mHcP9XPugqD") - .encryptionKey("BhB1wORFmZLTDjbvstvS8w==") - .build(); + .builder() + .apiHost("https://cdn.growthbook.io") + .clientKey("sdk-862b5mHcP9XPugqD") + .encryptionKey("BhB1wORFmZLTDjbvstvS8w==") + .build(); assertNotNull(subject); assertEquals("https://cdn.growthbook.io/api/features/sdk-862b5mHcP9XPugqD", subject.getFeaturesEndpoint()); @@ -95,12 +102,12 @@ void canFetchUnencryptedFeatures_mockedResponse() throws FeatureFetchException, OkHttpClient mockOkHttpClient = mockHttpClient(fakeResponseJson); GBFeaturesRepository subject = new GBFeaturesRepository( - "http://localhost:80", - "sdk-abc123", - null, - null, - null, - mockOkHttpClient + "http://localhost:80", + "sdk-abc123", + null, + null, + null, + mockOkHttpClient ); subject.initialize(); @@ -131,21 +138,21 @@ void canFetchEncryptedFeatures_real() throws FeatureFetchException { @Test void canFetchEncryptedFeatures_mockedResponse() throws IOException, FeatureFetchException { String fakeResponseJson = "{\n" + - " \"status\": 200,\n" + - " \"features\": {},\n" + - " \"dateUpdated\": \"2023-01-25T00:51:26.772Z\",\n" + - " \"encryptedFeatures\": \"jfLnSxjChWcbyHaIF30RNw==.iz8DywkSk4+WhNqnIwvr/PdvAwaRNjN3RE30JeOezGAQ/zZ2yoVyVo4w0nLHYqOje5MbhmL0ssvlH0ojk/BxqdSzXD4Wzo3DXfKV81Nzi1aSdiCMnVAIYEzjPl1IKZC3fl88YDBNV3F6YnR9Lemy9yzT03cvMZ0NZ9t5LZO2xS2MhpPYNcAfAlfxXhBGXj6UFDoNKGAtGKdc/zmJsUVQGLtHmqLspVynnJlPPo9nXG+87bt6SjSfQfySUgHm28hb4VmDhVmCx0N37buolVr3pzjZ1QK+tyMKIV7x4/Gu06k8sm0eU4HjG5DFsPgTR7qDu/N5Nk5UTRpG7aSXTUErxhHSJ7MQaxH/Dp/71zVEicaJ0qZE3oPRnU187QVBfdVLLRbqq2QU7Yu0GyJ1jjuf6TA+759OgifHdm17SX43L94Qe62CMU7JQyAqt7h7XmTTQBG664HYwgHJ0ju/9jySC4KUlRxNsixH1tJfznnEXqxgSozn4J61UprTqcmlxLZ1hZPCcRew3mm9DMAG9+YEiL8MhaIwsw8oVq9GirN1S8G3m/6UxQHxZVraPvMRXpGt5VpzEDJ0Po+phrIAhPuIbNpgb08b6Ej4Xh9XXeOLtIcpuj+gNpc4pR4tqF2IOwET\"\n" + - "}"; + " \"status\": 200,\n" + + " \"features\": {},\n" + + " \"dateUpdated\": \"2023-01-25T00:51:26.772Z\",\n" + + " \"encryptedFeatures\": \"jfLnSxjChWcbyHaIF30RNw==.iz8DywkSk4+WhNqnIwvr/PdvAwaRNjN3RE30JeOezGAQ/zZ2yoVyVo4w0nLHYqOje5MbhmL0ssvlH0ojk/BxqdSzXD4Wzo3DXfKV81Nzi1aSdiCMnVAIYEzjPl1IKZC3fl88YDBNV3F6YnR9Lemy9yzT03cvMZ0NZ9t5LZO2xS2MhpPYNcAfAlfxXhBGXj6UFDoNKGAtGKdc/zmJsUVQGLtHmqLspVynnJlPPo9nXG+87bt6SjSfQfySUgHm28hb4VmDhVmCx0N37buolVr3pzjZ1QK+tyMKIV7x4/Gu06k8sm0eU4HjG5DFsPgTR7qDu/N5Nk5UTRpG7aSXTUErxhHSJ7MQaxH/Dp/71zVEicaJ0qZE3oPRnU187QVBfdVLLRbqq2QU7Yu0GyJ1jjuf6TA+759OgifHdm17SX43L94Qe62CMU7JQyAqt7h7XmTTQBG664HYwgHJ0ju/9jySC4KUlRxNsixH1tJfznnEXqxgSozn4J61UprTqcmlxLZ1hZPCcRew3mm9DMAG9+YEiL8MhaIwsw8oVq9GirN1S8G3m/6UxQHxZVraPvMRXpGt5VpzEDJ0Po+phrIAhPuIbNpgb08b6Ej4Xh9XXeOLtIcpuj+gNpc4pR4tqF2IOwET\"\n" + + "}"; String encryptionKey = "o0maZL/O7AphxcbRvaJIzw=="; OkHttpClient mockOkHttpClient = mockHttpClient(fakeResponseJson); GBFeaturesRepository subject = new GBFeaturesRepository( - "http://localhost:80", - "abc-123", - encryptionKey, - null, - null, - mockOkHttpClient + "http://localhost:80", + "abc-123", + encryptionKey, + null, + null, + mockOkHttpClient ); subject.initialize(); @@ -242,6 +249,7 @@ void testUserAgentHeaders() throws FeatureFetchException { /** * Create a mock instance of {@link OkHttpClient} + * * @param serializedBody JSON string response * @return mock {@link OkHttpClient} */ @@ -251,14 +259,14 @@ private static OkHttpClient mockHttpClient(final String serializedBody) throws I Call remoteCall = mock(Call.class); Response response = new Response.Builder() - .request(new Request.Builder().url("http://url.com").build()) - .protocol(Protocol.HTTP_1_1) - .code(200).message("").body( - ResponseBody.create( - serializedBody, - MediaType.parse("application/json") - )) - .build(); + .request(new Request.Builder().url("http://url.com").build()) + .protocol(Protocol.HTTP_1_1) + .code(200).message("").body( + ResponseBody.create( + serializedBody, + MediaType.parse("application/json") + )) + .build(); when(remoteCall.execute()).thenReturn(response); when(okHttpClient.newCall(any())).thenReturn(remoteCall); diff --git a/lib/src/test/java/growthbook/sdk/java/GrowthBookJsonUtilsTest.java b/lib/src/test/java/growthbook/sdk/java/GrowthBookJsonUtilsTest.java index c2d49fb0..b5c7f885 100644 --- a/lib/src/test/java/growthbook/sdk/java/GrowthBookJsonUtilsTest.java +++ b/lib/src/test/java/growthbook/sdk/java/GrowthBookJsonUtilsTest.java @@ -1,11 +1,11 @@ package growthbook.sdk.java; +import static org.junit.jupiter.api.Assertions.assertEquals; + import com.google.gson.Gson; import com.google.gson.JsonElement; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - class GrowthBookJsonUtilsTest { final GrowthBookJsonUtils subject = GrowthBookJsonUtils.getInstance(); diff --git a/lib/src/test/java/growthbook/sdk/java/GrowthBookTest.java b/lib/src/test/java/growthbook/sdk/java/GrowthBookTest.java index c8153a8b..e3fc485c 100644 --- a/lib/src/test/java/growthbook/sdk/java/GrowthBookTest.java +++ b/lib/src/test/java/growthbook/sdk/java/GrowthBookTest.java @@ -3,6 +3,16 @@ */ package growthbook.sdk.java; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -13,15 +23,11 @@ import growthbook.sdk.java.testhelpers.TestContext; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; - import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; import java.util.Objects; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - class GrowthBookTest { final TestCasesJsonHelper helper = TestCasesJsonHelper.getInstance(); @@ -129,8 +135,8 @@ void test_evalFeature() { System.out.printf("\n Actual result = %s", result); System.out.printf( - "\n\n valuePasses = %s, onPasses = %s, offPasses = %s, sourcePasses = %s, hashValuePasses = %s, keyPasses = %s, bucketPasses = %s", - valuePasses, onPasses, offPasses, sourcePasses, hashValuePasses, keyPasses, bucketPasses + "\n\n valuePasses = %s, onPasses = %s, offPasses = %s, sourcePasses = %s, hashValuePasses = %s, keyPasses = %s, bucketPasses = %s", + valuePasses, onPasses, offPasses, sourcePasses, hashValuePasses, keyPasses, bucketPasses ); // System.out.println("\n\n-------------------------------"); @@ -152,10 +158,10 @@ void test_evalFeature_callsFeatureUsageCallback() { FeatureUsageCallback featureUsageCallback = mock(FeatureUsageCallback.class); String features = TestCasesJsonHelper.getInstance().getDemoFeaturesJson(); GBContext context = GBContext - .builder() - .featuresJson(features) - .featureUsageCallback(featureUsageCallback) - .build(); + .builder() + .featuresJson(features) + .featureUsageCallback(featureUsageCallback) + .build(); GrowthBook subject = new GrowthBook(context); String value = subject.getFeatureValue("h1-title", "unknown feature key"); @@ -209,7 +215,7 @@ void test_runExperiment() { for (int i = 0; i < testCases.size(); i++) { JsonArray itemArray = (JsonArray) testCases.get(i); - if (itemArray instanceof JsonArray) { + if (itemArray != null) { String testDescription = itemArray.get(0).getAsString(); JsonElement featuresJson = itemArray.get(1).getAsJsonObject().get("features"); @@ -242,8 +248,7 @@ void test_runExperiment() { JsonObject experimentObject = jsonUtils.gson.fromJson(experimentElement.getAsJsonObject(), JsonObject.class); JsonElement conditionElement = experimentObject.get("condition"); if (conditionElement != null) { - String conditionJson = conditionElement.toString(); - experiment.setConditionJson(conditionJson); + experiment.setConditionJson(conditionElement); } } @@ -297,8 +302,10 @@ void test_isOn_returns_true() { .build(); GrowthBook subject = new GrowthBook(context); - assertTrue(subject.isOn(featureKey)); - assertFalse(subject.isOff(featureKey)); + FeatureResult feature = subject.evalFeature(featureKey, Object.class); + + assertTrue(feature.isOn()); + assertFalse(feature.isOff()); } @Test @@ -642,12 +649,12 @@ void test_withUrl_getFeatureValue_forcesBooleanValue_true() { String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); Boolean result = subject.getFeatureValue("dark_mode", false); @@ -662,12 +669,12 @@ void test_withUrl_getFeatureValue_forcesBooleanValue_on() { String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=on&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); Boolean result = subject.getFeatureValue("dark_mode", false); @@ -682,12 +689,12 @@ void test_withUrl_getFeatureValue_forcesBooleanValue_1() { String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=1&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); Boolean result = subject.getFeatureValue("dark_mode", false); @@ -702,12 +709,12 @@ void test_withUrl_getFeatureValue_forcesBooleanValue_false() { String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=false&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); Boolean result = subject.getFeatureValue("dark_mode", true); @@ -722,12 +729,12 @@ void test_withUrl_getFeatureValue_forcesBooleanValue_off() { String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=off&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); Boolean result = subject.getFeatureValue("dark_mode", true); @@ -742,12 +749,12 @@ void test_withUrl_getFeatureValue_forcesBooleanValue_0() { String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=0&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); Boolean result = subject.getFeatureValue("dark_mode", true); @@ -762,12 +769,12 @@ void test_withUrl_getFeatureValue_forcesIntegerValue() { String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=2&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); Integer result = subject.getFeatureValue("donut_price", 999); @@ -781,12 +788,12 @@ void test_withUrl_getFeatureValue_forcesFloatValue() { String attributes = "{}"; String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); Float result = subject.getFeatureValue("donut_price", 9999f); @@ -800,12 +807,12 @@ void test_withUrl_getFeatureValue_forcesStringValue() { String attributes = "{}"; String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); String result = subject.getFeatureValue("banner_text", "???"); @@ -820,12 +827,12 @@ void test_withUrl_getFeatureValue_forcesJsonValue_Gson() { String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); MealOrder emptyMealOrder = new MealOrder(MealType.STANDARD, "Donut"); @@ -844,12 +851,12 @@ void test_withUrl_getFeatureValue_jsonValueFromString() { String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); String resultAsString = (String) subject.getFeatureValue("meal_overrides_gluten_free", "{\"meal_type\": \"standard\", \"dessert\": \"Donut\"}"); @@ -867,12 +874,12 @@ void test_withUrl_getFeatureValue_withForcedJsonValue_returnsDefaultValueWhenInv String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); // We try to deserialize an unsupported class from the URL but it cannot deserialize properly, so we get the default value @@ -924,7 +931,8 @@ public MealOrder(MealType mealType, String dessert) { } static class GBTestingFoo { - @SerializedName("foo") String foo = "FOO!"; + @SerializedName("foo") + String foo = "FOO!"; @Override public boolean equals(Object obj) { @@ -945,12 +953,12 @@ void test_withUrl_evaluateFeature_forcesBooleanValue() { String attributes = "{}"; String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); FeatureResult result = subject.evalFeature("dark_mode", Boolean.class); @@ -965,12 +973,12 @@ void test_withUrl_evaluateFeature_forcesStringValue() { String attributes = "{}"; String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); FeatureResult result = subject.evalFeature("banner_text", String.class); @@ -985,12 +993,12 @@ void test_withUrl_evaluateFeature_forcesFloatValue() { String attributes = "{}"; String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); FeatureResult result = subject.evalFeature("donut_price", Float.class); @@ -1005,12 +1013,12 @@ void test_withUrl_evaluateFeature_forcesIntegerValue() { String attributes = "{}"; String url = "http://localhost:8080/url-feature-force?gb~meal_overrides_gluten_free=%7B%22meal_type%22%3A%20%22gf%22%2C%20%22dessert%22%3A%20%22French%20Vanilla%20Ice%20Cream%22%7D&gb~dark_mode=true&gb~donut_price=4&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!"; GBContext context = GBContext - .builder() - .featuresJson(featuresJson) - .attributesJson(attributes) - .url(url) - .allowUrlOverrides(true) - .build(); + .builder() + .featuresJson(featuresJson) + .attributesJson(attributes) + .url(url) + .allowUrlOverrides(true) + .build(); GrowthBook subject = new GrowthBook(context); FeatureResult result = subject.evalFeature("donut_price", Integer.class); diff --git a/lib/src/test/java/growthbook/sdk/java/GrowthBookUtilsTest.java b/lib/src/test/java/growthbook/sdk/java/GrowthBookUtilsTest.java index 6dc0507c..79af4c57 100644 --- a/lib/src/test/java/growthbook/sdk/java/GrowthBookUtilsTest.java +++ b/lib/src/test/java/growthbook/sdk/java/GrowthBookUtilsTest.java @@ -1,17 +1,17 @@ package growthbook.sdk.java; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import com.google.gson.JsonArray; import com.google.gson.reflect.TypeToken; import growthbook.sdk.java.testhelpers.TestCasesJsonHelper; import org.junit.jupiter.api.Test; - import javax.annotation.Nullable; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; class GrowthBookUtilsTest { final TestCasesJsonHelper helper = TestCasesJsonHelper.getInstance(); @@ -29,15 +29,15 @@ void test_hashFowlerNollVoAlgo() { hnvCases.forEach(jsonElement -> { JsonArray kv = (JsonArray) jsonElement; - String seed = kv.get(3).getAsString(); - String input = kv.get(0).getAsString(); + String seed = kv.get(0).getAsString(); + String input = kv.get(1).getAsString(); Integer hashVersion = kv.get(2).getAsInt(); Float expected = null; - if (!kv.get(1).isJsonNull()) { + if (!kv.get(3).isJsonNull()) { // In the case of unsupported hash versions, this method returns null - expected = kv.get(1).getAsFloat(); + expected = kv.get(3).getAsFloat(); } assertEquals(expected, GrowthBookUtils.hash(input, hashVersion, seed)); diff --git a/lib/src/test/java/growthbook/sdk/java/MathUtilsTest.java b/lib/src/test/java/growthbook/sdk/java/MathUtilsTest.java index 2753071b..0ffb3de7 100644 --- a/lib/src/test/java/growthbook/sdk/java/MathUtilsTest.java +++ b/lib/src/test/java/growthbook/sdk/java/MathUtilsTest.java @@ -1,9 +1,8 @@ package growthbook.sdk.java; -import growthbook.sdk.java.MathUtils; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; class MathUtilsTest { diff --git a/lib/src/test/java/growthbook/sdk/java/NamespaceTest.java b/lib/src/test/java/growthbook/sdk/java/NamespaceTest.java index e2487b76..bfebf873 100644 --- a/lib/src/test/java/growthbook/sdk/java/NamespaceTest.java +++ b/lib/src/test/java/growthbook/sdk/java/NamespaceTest.java @@ -1,12 +1,11 @@ package growthbook.sdk.java; +import static org.junit.jupiter.api.Assertions.assertEquals; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import growthbook.sdk.java.Namespace; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - class NamespaceTest { @Test void isGsonSerializable() { diff --git a/lib/src/test/java/growthbook/sdk/java/OperatorTest.java b/lib/src/test/java/growthbook/sdk/java/OperatorTest.java index bf5f226d..30c26ed8 100644 --- a/lib/src/test/java/growthbook/sdk/java/OperatorTest.java +++ b/lib/src/test/java/growthbook/sdk/java/OperatorTest.java @@ -1,8 +1,9 @@ package growthbook.sdk.java; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; class OperatorTest { @Test diff --git a/lib/src/test/java/growthbook/sdk/java/StringUtilsTest.java b/lib/src/test/java/growthbook/sdk/java/StringUtilsTest.java index 085597f5..0dca0d76 100644 --- a/lib/src/test/java/growthbook/sdk/java/StringUtilsTest.java +++ b/lib/src/test/java/growthbook/sdk/java/StringUtilsTest.java @@ -1,8 +1,8 @@ package growthbook.sdk.java; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; class StringUtilsTest { diff --git a/lib/src/test/java/growthbook/sdk/java/TrackDataTest.java b/lib/src/test/java/growthbook/sdk/java/TrackDataTest.java index 9cd39dd6..728b6539 100644 --- a/lib/src/test/java/growthbook/sdk/java/TrackDataTest.java +++ b/lib/src/test/java/growthbook/sdk/java/TrackDataTest.java @@ -1,20 +1,20 @@ package growthbook.sdk.java; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; class TrackDataTest { @Test void canBeConstructed() { Experiment experiment = Experiment.builder() - .key("my_experiment") - .force(100) - .build(); + .key("my_experiment") + .force(100) + .build(); ExperimentResult experimentResult = ExperimentResult.builder() - .inExperiment(true) - .key("my_experiment") - .build(); + .inExperiment(true) + .key("my_experiment") + .build(); TrackData subject = new TrackData<>(experiment, experimentResult); diff --git a/lib/src/test/java/growthbook/sdk/java/UrlUtilsTest.java b/lib/src/test/java/growthbook/sdk/java/UrlUtilsTest.java index ffbbac97..e6d430eb 100644 --- a/lib/src/test/java/growthbook/sdk/java/UrlUtilsTest.java +++ b/lib/src/test/java/growthbook/sdk/java/UrlUtilsTest.java @@ -1,15 +1,12 @@ package growthbook.sdk.java; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; - class UrlUtilsTest { @Test diff --git a/lib/src/test/java/growthbook/sdk/java/VariationMetaTest.java b/lib/src/test/java/growthbook/sdk/java/VariationMetaTest.java index bfc8e321..913f4e5c 100644 --- a/lib/src/test/java/growthbook/sdk/java/VariationMetaTest.java +++ b/lib/src/test/java/growthbook/sdk/java/VariationMetaTest.java @@ -1,18 +1,18 @@ package growthbook.sdk.java; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; class VariationMetaTest { @Test void canBeBuilt() { VariationMeta subject = VariationMeta - .builder() - .key("my-key") - .name("my-name") - .passThrough(true) - .build(); + .builder() + .key("my-key") + .name("my-name") + .passThrough(true) + .build(); assertEquals("my-key", subject.getKey()); assertEquals("my-name", subject.getName()); diff --git a/lib/src/test/java/growthbook/sdk/java/testhelpers/ITestCasesJsonHelper.java b/lib/src/test/java/growthbook/sdk/java/testhelpers/ITestCasesJsonHelper.java index f5cf7146..9ed3b4ec 100644 --- a/lib/src/test/java/growthbook/sdk/java/testhelpers/ITestCasesJsonHelper.java +++ b/lib/src/test/java/growthbook/sdk/java/testhelpers/ITestCasesJsonHelper.java @@ -5,18 +5,26 @@ interface ITestCasesJsonHelper { JsonObject getTestCases(); + JsonArray evalConditionTestCases(); + JsonArray getHNVTestCases(); + JsonArray getBucketRangeTestCases(); + JsonArray featureTestCases(); + JsonArray runTestCases(); + JsonArray getChooseVariationTestCases(); + JsonArray getQueryStringOverrideTestCases(); + JsonArray getInNamespaceTestCases(); + JsonArray getEqualWeightsTestCases(); + JsonArray decryptionTestCases(); - JsonArray versionCompareTestCases_eq(); - JsonArray versionCompareTestCases_lt(); - JsonArray versionCompareTestCases_gt(); + JsonArray getStickyBucketTestCases(); } diff --git a/lib/src/test/java/growthbook/sdk/java/testhelpers/SSETestServer.java b/lib/src/test/java/growthbook/sdk/java/testhelpers/SSETestServer.java index 79845c4e..4e1c8ced 100644 --- a/lib/src/test/java/growthbook/sdk/java/testhelpers/SSETestServer.java +++ b/lib/src/test/java/growthbook/sdk/java/testhelpers/SSETestServer.java @@ -3,8 +3,11 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; -import growthbook.sdk.java.*; - +import growthbook.sdk.java.FeatureFetchException; +import growthbook.sdk.java.FeatureRefreshStrategy; +import growthbook.sdk.java.GBContext; +import growthbook.sdk.java.GBFeaturesRepository; +import growthbook.sdk.java.GrowthBook; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; @@ -16,10 +19,10 @@ public class SSETestServer { public static void main(String[] args) throws IOException, FeatureFetchException { // Unencrypted GBFeaturesRepository featuresRepository = GBFeaturesRepository.builder() - .apiHost("https://cdn.growthbook.io") - .clientKey("sdk-pGmC6LrsiUoEUcpZ") - .refreshStrategy(FeatureRefreshStrategy.SERVER_SENT_EVENTS) - .build(); + .apiHost("https://cdn.growthbook.io") + .clientKey("sdk-pGmC6LrsiUoEUcpZ") + .refreshStrategy(FeatureRefreshStrategy.SERVER_SENT_EVENTS) + .build(); // Encrypted // GBFeaturesRepository featuresRepository = GBFeaturesRepository.builder() @@ -50,8 +53,8 @@ private static class TestServerHandler implements HttpHandler { public void handle(HttpExchange exchange) throws IOException { // Setup GrowthBook SDK GBContext context = GBContext.builder() - .featuresJson(featuresRepository.getFeaturesJson()) - .build(); + .featuresJson(featuresRepository.getFeaturesJson()) + .build(); GrowthBook growthBook = new GrowthBook(context); // Get a feature value diff --git a/lib/src/test/java/growthbook/sdk/java/testhelpers/TestCasesJsonHelper.java b/lib/src/test/java/growthbook/sdk/java/testhelpers/TestCasesJsonHelper.java index 9e99ddd4..b3950e2c 100644 --- a/lib/src/test/java/growthbook/sdk/java/testhelpers/TestCasesJsonHelper.java +++ b/lib/src/test/java/growthbook/sdk/java/testhelpers/TestCasesJsonHelper.java @@ -4,7 +4,6 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; - import java.io.FileNotFoundException; import java.io.FileReader; import java.nio.file.Path; @@ -65,21 +64,6 @@ public JsonArray decryptionTestCases() { return this.testCases.get("decrypt").getAsJsonArray(); } - @Override - public JsonArray versionCompareTestCases_eq() { - return this.testCases.get("versionCompare").getAsJsonObject().get("eq").getAsJsonArray(); - } - - @Override - public JsonArray versionCompareTestCases_lt() { - return this.testCases.get("versionCompare").getAsJsonObject().get("lt").getAsJsonArray(); - } - - @Override - public JsonArray versionCompareTestCases_gt() { - return this.testCases.get("versionCompare").getAsJsonObject().get("gt").getAsJsonArray(); - } - @Override public JsonArray getQueryStringOverrideTestCases() { return this.testCases.get("getQueryStringOverride").getAsJsonArray(); @@ -140,7 +124,7 @@ private String initializeDemoFeaturesFromFile() { } private String getResourceDirectoryPath() { - Path resourceDirectory = Paths.get("src","test","resources"); + Path resourceDirectory = Paths.get("src", "test", "resources"); String absolutePath = resourceDirectory.toFile().getAbsolutePath(); System.out.println(absolutePath); return absolutePath; diff --git a/lib/src/test/java/growthbook/sdk/java/testhelpers/TestCasesJsonHelperTest.java b/lib/src/test/java/growthbook/sdk/java/testhelpers/TestCasesJsonHelperTest.java index 60d7b97f..f9cfd18b 100644 --- a/lib/src/test/java/growthbook/sdk/java/testhelpers/TestCasesJsonHelperTest.java +++ b/lib/src/test/java/growthbook/sdk/java/testhelpers/TestCasesJsonHelperTest.java @@ -1,11 +1,11 @@ package growthbook.sdk.java.testhelpers; -import com.google.gson.JsonObject; -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import com.google.gson.JsonObject; +import org.junit.jupiter.api.Test; + class TestCasesJsonHelperTest { @Test @@ -21,6 +21,6 @@ void getTestCases_returnsTestCasesAsJson() { JsonObject testCases = TestCasesJsonHelper.getInstance().getTestCases(); assertNotNull(testCases); - assertEquals("0.5.4", testCases.get("specVersion").getAsString()); + assertEquals("0.6.0", testCases.get("specVersion").getAsString()); } } diff --git a/lib/src/test/resources/test-cases.json b/lib/src/test/resources/test-cases.json index 98ef1231..92bab9b8 100644 --- a/lib/src/test/resources/test-cases.json +++ b/lib/src/test/resources/test-cases.json @@ -1,5 +1,5 @@ { - "specVersion": "0.5.4", + "specVersion": "0.6.0", "evalCondition": [ [ "$not - pass", @@ -2019,135 +2019,781 @@ "v": "1.2.3-alpha" }, true - ] - ], - "versionCompare": { - "lt": [ - ["0.9.99", "1.0.0", true], - ["0.9.0", "0.10.0", true], - ["1.0.0-0.0", "1.0.0-0.0.0", true], - ["1.0.0-9999", "1.0.0--", true], - ["1.0.0-99", "1.0.0-100", true], - ["1.0.0-alpha", "1.0.0-alpha.1", true], - ["1.0.0-alpha.1", "1.0.0-alpha.beta", true], - ["1.0.0-alpha.beta", "1.0.0-beta", true], - ["1.0.0-beta", "1.0.0-beta.2", true], - ["1.0.0-beta.2", "1.0.0-beta.11", true], - ["1.0.0-beta.11", "1.0.0-rc.1", true], - ["1.0.0-rc.1", "1.0.0", true], - ["1.0.0-0", "1.0.0--1", true], - ["1.0.0-0", "1.0.0-1", true], - ["1.0.0-1.0", "1.0.0-1.-1", true] - ], - "gt": [ - ["0.0.0", "0.0.0-foo", true], - ["0.0.1", "0.0.0", true], - ["1.0.0", "0.9.9", true], - ["0.10.0", "0.9.0", true], - ["0.99.0", "0.10.0", true], - ["2.0.0", "1.2.3", true], - ["v0.0.0", "0.0.0-foo", true], - ["v0.0.1", "0.0.0", true], - ["v1.0.0", "0.9.9", true], - ["v0.10.0", "0.9.0", true], - ["v0.99.0", "0.10.0", true], - ["v2.0.0", "1.2.3", true], - ["0.0.0", "v0.0.0-foo", true], - ["0.0.1", "v0.0.0", true], - ["1.0.0", "v0.9.9", true], - ["0.10.0", "v0.9.0", true], - ["0.99.0", "v0.10.0", true], - ["2.0.0", "v1.2.3", true], - ["1.2.3", "1.2.3-asdf", true], - ["1.2.3", "1.2.3-4", true], - ["1.2.3", "1.2.3-4-foo", true], - ["1.2.3-5-foo", "1.2.3-5", true], - ["1.2.3-5", "1.2.3-4", true], - ["1.2.3-5-foo", "1.2.3-5-Foo", true], - ["3.0.0", "2.7.2+asdf", true], - ["1.2.3-a.10", "1.2.3-a.5", true], - ["1.2.3-a.b", "1.2.3-a.5", true], - ["1.2.3-a.b", "1.2.3-a", true], - ["1.2.3-a.b.c", "1.2.3-a.b.c.d", false], - ["1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100", true], - ["1.2.3-r2", "1.2.3-r100", true], - ["1.2.3-r100", "1.2.3-R2", true], - ["a.b.c.d.e.f", "1.2.3", true], - ["10.0.0", "9.0.0", true], - ["10000.0.0", "9999.0.0", true] - ], - "eq": [ - ["1.2.3", "1.2.3", true], - ["1.2.3", "v1.2.3", true], - ["1.2.3-0", "v1.2.3-0", true], - ["1.2.3-1", "1.2.3-1", true], - ["1.2.3-1", "v1.2.3-1", true], - ["1.2.3-beta", "1.2.3-beta", true], - ["1.2.3-beta", "v1.2.3-beta", true], - ["1.2.3-beta+build", "1.2.3-beta+otherbuild", true], - ["1.2.3-beta+build", "v1.2.3-beta+otherbuild", true], - ["1-2-3", "1.2.3", true], - ["1-2-3", "1-2.3+build99", true], - ["1-2-3", "v1.2.3", true], - ["1.2.3.4", "1.2.3-4", true] - ] - }, - "hash": [ + ], [ - "a", - 0.22, - 1, - "" + "version 0.9.99 < 1.0.0", + { + "version": { + "$vlt": "1.0.0" + } + }, + { + "version": "0.9.99" + }, + true ], [ - "b", - 0.077, - 1, - "" + "version 0.9.0 < 0.10.0", + { + "version": { + "$vlt": "0.10.0" + } + }, + { + "version": "0.9.0" + }, + true + ], + [ + "version 1.0.0-0.0 < 1.0.0-0.0.0", + { + "version": { + "$vlt": "1.0.0-0.0.0" + } + }, + { + "version": "1.0.0-0.0" + }, + true + ], + [ + "version 1.0.0-9999 < 1.0.0--", + { + "version": { + "$vlt": "1.0.0--" + } + }, + { + "version": "1.0.0-9999" + }, + true + ], + [ + "version 1.0.0-99 < 1.0.0-100", + { + "version": { + "$vlt": "1.0.0-100" + } + }, + { + "version": "1.0.0-99" + }, + true + ], + [ + "version 1.0.0-alpha < 1.0.0-alpha.1", + { + "version": { + "$vlt": "1.0.0-alpha.1" + } + }, + { + "version": "1.0.0-alpha" + }, + true + ], + [ + "version 1.0.0-alpha.1 < 1.0.0-alpha.beta", + { + "version": { + "$vlt": "1.0.0-alpha.beta" + } + }, + { + "version": "1.0.0-alpha.1" + }, + true + ], + [ + "version 1.0.0-alpha.beta < 1.0.0-beta", + { + "version": { + "$vlt": "1.0.0-beta" + } + }, + { + "version": "1.0.0-alpha.beta" + }, + true + ], + [ + "version 1.0.0-beta < 1.0.0-beta.2", + { + "version": { + "$vlt": "1.0.0-beta.2" + } + }, + { + "version": "1.0.0-beta" + }, + true + ], + [ + "version 1.0.0-beta.2 < 1.0.0-beta.11", + { + "version": { + "$vlt": "1.0.0-beta.11" + } + }, + { + "version": "1.0.0-beta.2" + }, + true + ], + [ + "version 1.0.0-beta.11 < 1.0.0-rc.1", + { + "version": { + "$vlt": "1.0.0-rc.1" + } + }, + { + "version": "1.0.0-beta.11" + }, + true + ], + [ + "version 1.0.0-rc.1 < 1.0.0", + { + "version": { + "$vlt": "1.0.0" + } + }, + { + "version": "1.0.0-rc.1" + }, + true + ], + [ + "version 1.0.0-0 < 1.0.0--1", + { + "version": { + "$vlt": "1.0.0--1" + } + }, + { + "version": "1.0.0-0" + }, + true + ], + [ + "version 1.0.0-0 < 1.0.0-1", + { + "version": { + "$vlt": "1.0.0-1" + } + }, + { + "version": "1.0.0-0" + }, + true + ], + [ + "version 1.0.0-1.0 < 1.0.0-1.-1", + { + "version": { + "$vlt": "1.0.0-1.-1" + } + }, + { + "version": "1.0.0-1.0" + }, + true + ], + [ + "version 1.2.3-a.b.c < 1.2.3-a.b.c.d", + { + "version": { + "$vlt": "1.2.3-a.b.c.d" + } + }, + { + "version": "1.2.3-a.b.c" + }, + true + ], + [ + "version 0.0.0 > 0.0.0-foo", + { + "version": { + "$vgt": "0.0.0-foo" + } + }, + { + "version": "0.0.0" + }, + true + ], + [ + "version 0.0.1 > 0.0.0", + { + "version": { + "$vgt": "0.0.0" + } + }, + { + "version": "0.0.1" + }, + true + ], + [ + "version 1.0.0 > 0.9.9", + { + "version": { + "$vgt": "0.9.9" + } + }, + { + "version": "1.0.0" + }, + true + ], + [ + "version 0.10.0 > 0.9.0", + { + "version": { + "$vgt": "0.9.0" + } + }, + { + "version": "0.10.0" + }, + true + ], + [ + "version 0.99.0 > 0.10.0", + { + "version": { + "$vgt": "0.10.0" + } + }, + { + "version": "0.99.0" + }, + true + ], + [ + "version 2.0.0 > 1.2.3", + { + "version": { + "$vgt": "1.2.3" + } + }, + { + "version": "2.0.0" + }, + true + ], + [ + "version v0.0.0 > 0.0.0-foo", + { + "version": { + "$vgt": "0.0.0-foo" + } + }, + { + "version": "v0.0.0" + }, + true + ], + [ + "version v0.0.1 > 0.0.0", + { + "version": { + "$vgt": "0.0.0" + } + }, + { + "version": "v0.0.1" + }, + true + ], + [ + "version v1.0.0 > 0.9.9", + { + "version": { + "$vgt": "0.9.9" + } + }, + { + "version": "v1.0.0" + }, + true + ], + [ + "version v0.10.0 > 0.9.0", + { + "version": { + "$vgt": "0.9.0" + } + }, + { + "version": "v0.10.0" + }, + true + ], + [ + "version v0.99.0 > 0.10.0", + { + "version": { + "$vgt": "0.10.0" + } + }, + { + "version": "v0.99.0" + }, + true + ], + [ + "version v2.0.0 > 1.2.3", + { + "version": { + "$vgt": "1.2.3" + } + }, + { + "version": "v2.0.0" + }, + true + ], + [ + "version 0.0.0 > v0.0.0-foo", + { + "version": { + "$vgt": "v0.0.0-foo" + } + }, + { + "version": "0.0.0" + }, + true + ], + [ + "version 0.0.1 > v0.0.0", + { + "version": { + "$vgt": "v0.0.0" + } + }, + { + "version": "0.0.1" + }, + true + ], + [ + "version 1.0.0 > v0.9.9", + { + "version": { + "$vgt": "v0.9.9" + } + }, + { + "version": "1.0.0" + }, + true + ], + [ + "version 0.10.0 > v0.9.0", + { + "version": { + "$vgt": "v0.9.0" + } + }, + { + "version": "0.10.0" + }, + true + ], + [ + "version 0.99.0 > v0.10.0", + { + "version": { + "$vgt": "v0.10.0" + } + }, + { + "version": "0.99.0" + }, + true + ], + [ + "version 2.0.0 > v1.2.3", + { + "version": { + "$vgt": "v1.2.3" + } + }, + { + "version": "2.0.0" + }, + true + ], + [ + "version 1.2.3 > 1.2.3-asdf", + { + "version": { + "$vgt": "1.2.3-asdf" + } + }, + { + "version": "1.2.3" + }, + true + ], + [ + "version 1.2.3 > 1.2.3-4", + { + "version": { + "$vgt": "1.2.3-4" + } + }, + { + "version": "1.2.3" + }, + true + ], + [ + "version 1.2.3 > 1.2.3-4-foo", + { + "version": { + "$vgt": "1.2.3-4-foo" + } + }, + { + "version": "1.2.3" + }, + true + ], + [ + "version 1.2.3-5-foo > 1.2.3-5", + { + "version": { + "$vgt": "1.2.3-5" + } + }, + { + "version": "1.2.3-5-foo" + }, + true + ], + [ + "version 1.2.3-5 > 1.2.3-4", + { + "version": { + "$vgt": "1.2.3-4" + } + }, + { + "version": "1.2.3-5" + }, + true + ], + [ + "version 1.2.3-5-foo > 1.2.3-5-Foo", + { + "version": { + "$vgt": "1.2.3-5-Foo" + } + }, + { + "version": "1.2.3-5-foo" + }, + true + ], + [ + "version 3.0.0 > 2.7.2+asdf", + { + "version": { + "$vgt": "2.7.2+asdf" + } + }, + { + "version": "3.0.0" + }, + true + ], + [ + "version 1.2.3-a.10 > 1.2.3-a.5", + { + "version": { + "$vgt": "1.2.3-a.5" + } + }, + { + "version": "1.2.3-a.10" + }, + true + ], + [ + "version 1.2.3-a.b > 1.2.3-a.5", + { + "version": { + "$vgt": "1.2.3-a.5" + } + }, + { + "version": "1.2.3-a.b" + }, + true + ], + [ + "version 1.2.3-a.b > 1.2.3-a", + { + "version": { + "$vgt": "1.2.3-a" + } + }, + { + "version": "1.2.3-a.b" + }, + true + ], + [ + "version 1.2.3-a.b.c.10.d.5 > 1.2.3-a.b.c.5.d.100", + { + "version": { + "$vgt": "1.2.3-a.b.c.5.d.100" + } + }, + { + "version": "1.2.3-a.b.c.10.d.5" + }, + true + ], + [ + "version 1.2.3-r2 > 1.2.3-r100", + { + "version": { + "$vgt": "1.2.3-r100" + } + }, + { + "version": "1.2.3-r2" + }, + true + ], + [ + "version 1.2.3-r100 > 1.2.3-R2", + { + "version": { + "$vgt": "1.2.3-R2" + } + }, + { + "version": "1.2.3-r100" + }, + true + ], + [ + "version a.b.c.d.e.f > 1.2.3", + { + "version": { + "$vgt": "1.2.3" + } + }, + { + "version": "a.b.c.d.e.f" + }, + true + ], + [ + "version 10.0.0 > 9.0.0", + { + "version": { + "$vgt": "9.0.0" + } + }, + { + "version": "10.0.0" + }, + true + ], + [ + "version 10000.0.0 > 9999.0.0", + { + "version": { + "$vgt": "9999.0.0" + } + }, + { + "version": "10000.0.0" + }, + true + ], + [ + "version 1.2.3 == 1.2.3", + { + "version": { + "$veq": "1.2.3" + } + }, + { + "version": "1.2.3" + }, + true + ], + [ + "version 1.2.3 == v1.2.3", + { + "version": { + "$veq": "v1.2.3" + } + }, + { + "version": "1.2.3" + }, + true + ], + [ + "version 1.2.3-0 == v1.2.3-0", + { + "version": { + "$veq": "v1.2.3-0" + } + }, + { + "version": "1.2.3-0" + }, + true + ], + [ + "version 1.2.3-1 == 1.2.3-1", + { + "version": { + "$veq": "1.2.3-1" + } + }, + { + "version": "1.2.3-1" + }, + true + ], + [ + "version 1.2.3-1 == v1.2.3-1", + { + "version": { + "$veq": "v1.2.3-1" + } + }, + { + "version": "1.2.3-1" + }, + true + ], + [ + "version 1.2.3-beta == 1.2.3-beta", + { + "version": { + "$veq": "1.2.3-beta" + } + }, + { + "version": "1.2.3-beta" + }, + true ], [ - "ab", - 0.946, - 1, - "" + "version 1.2.3-beta == v1.2.3-beta", + { + "version": { + "$veq": "v1.2.3-beta" + } + }, + { + "version": "1.2.3-beta" + }, + true ], [ - "def", - 0.652, - 1, - "" + "version 1.2.3-beta+build == 1.2.3-beta+otherbuild", + { + "version": { + "$veq": "1.2.3-beta+otherbuild" + } + }, + { + "version": "1.2.3-beta+build" + }, + true ], [ - "8952klfjas09ujkasdf", - 0.549, - 1, - "" + "version 1.2.3-beta+build == v1.2.3-beta+otherbuild", + { + "version": { + "$veq": "v1.2.3-beta+otherbuild" + } + }, + { + "version": "1.2.3-beta+build" + }, + true ], [ - "123", - 0.011, - 1, - "" + "version 1-2-3 == 1.2.3", + { + "version": { + "$veq": "1.2.3" + } + }, + { + "version": "1-2-3" + }, + true ], [ - "___)((*\":&", - 0.563, - 1, - "" + "version 1-2-3 == 1-2.3+build99", + { + "version": { + "$veq": "1-2.3+build99" + } + }, + { + "version": "1-2-3" + }, + true ], [ - "a", - 0.0505, - 2, - "seed" + "version 1-2-3 == v1.2.3", + { + "version": { + "$veq": "v1.2.3" + } + }, + { + "version": "1-2-3" + }, + true ], [ - "def", - null, - 99, - "abc" + "version 1.2.3.4 == 1.2.3-4", + { + "version": { + "$veq": "1.2.3-4" + } + }, + { + "version": "1.2.3.4" + }, + true ] ], + "hash": [ + ["", "a", 1, 0.22], + ["", "b", 1, 0.077], + ["b", "a", 1, 0.946], + ["ef", "d", 1, 0.652], + ["asdf", "8952klfjas09ujk", 1, 0.549], + ["", "123", 1, 0.011], + ["", "___)((*\":&", 1, 0.563], + ["seed", "a", 2, 0.0505], + ["seed", "b", 2, 0.2696], + ["foo", "ab", 2, 0.2575], + ["foo", "def", 2, 0.2019], + ["89123klj", "8952klfjas09ujkasdf", 2, 0.124], + ["90850943850283058242805", "123", 2, 0.7516], + ["()**(%$##$%#$#", "___)((*\":&", 2, 0.0128], + ["abc", "def", 99, null] + ], "getBucketRange": [ [ "normal 50/50", @@ -2451,6 +3097,33 @@ "source": "defaultValue" } ], + [ + "force rules - coverage 0", + { + "attributes": { + "id": "d0bc0a5a" + }, + "features": { + "8d156": { + "defaultValue": 0, + "rules": [ + { + "force": 1, + "coverage": 0, + "hashVersion": 2 + } + ] + } + } + }, + "8d156", + { + "value": 0, + "on": false, + "off": true, + "source": "defaultValue" + } + ], [ "force rules - condition pass", { @@ -3323,8 +3996,93 @@ } } ], - - + [ + "Prerequisite flag off, block dependent flag", + { + "attributes": { + "id": "123", + "memberType": "basic", + "country": "Canada" + }, + "features": { + "parentFlag": { + "defaultValue": "silver", + "rules": [ + { + "condition": { "country": "Canada" }, + "force": "red" + }, + { + "condition": { "country": { "$in": ["USA", "Mexico"] } }, + "force": "green" + } + ] + }, + "childFlag": { + "defaultValue": "default", + "rules": [ + { + "parentConditions": [ + { + "id": "parentFlag", + "condition": { "value": "green" }, + "gate": true + } + ] + }, + { + "condition": { "memberType": "basic" }, + "force": "success" + } + ] + } + } + }, + "childFlag", + { + "value": null, + "on": false, + "off": true, + "source": "prerequisite" + } + ], + [ + "Prerequisite flag missing, block dependent flag", + { + "attributes": { + "id": "123", + "memberType": "basic", + "country": "Canada" + }, + "features": { + "childFlag": { + "defaultValue": "default", + "rules": [ + { + "parentConditions": [ + { + "id": "parentFlag", + "condition": { "value": "green" }, + "gate": true + } + ] + }, + { + "condition": { "memberType": "basic" }, + "force": "success" + } + ] + } + } + }, + "childFlag", + { + "value": null, + "on": false, + "off": true, + "source": "prerequisite" + } + ], [ "Prerequisite flag on, evaluate dependent flag", { @@ -3562,6 +4320,51 @@ "off": false, "source": "force" } + ], + [ + "Prerequisite cycle detected, break", + { + "attributes": { + "id": "123" + }, + "features": { + "flag1": { + "defaultValue": true, + "rules": [ + { + "parentConditions": [ + { + "id": "flag2", + "condition": { "value": true }, + "gate": true + } + ] + } + ] + }, + "flag2": { + "defaultValue": true, + "rules": [ + { + "parentConditions": [ + { + "id": "flag1", + "condition": { "value": true }, + "gate": true + } + ] + } + ] + } + } + }, + "flag1", + { + "value": null, + "on": false, + "off": true, + "source": "cyclicPrerequisite" + } ] ], "run": [ @@ -3996,6 +4799,22 @@ false, false ], + [ + "querystring force", + { + "attributes": { + "id": "1" + }, + "url": "http://example.com?forced-test-qs=1#someanchor" + }, + { + "key": "forced-test-qs", + "variations": [0, 1] + }, + 1, + true, + false + ], [ "run active experiments", { @@ -4028,6 +4847,23 @@ false, false ], + [ + "querystring force with inactive", + { + "attributes": { + "id": "1" + }, + "url": "http://example.com/?my-test=1" + }, + { + "key": "my-test", + "active": false, + "variations": [0, 1] + }, + 1, + true, + false + ], [ "coverage take precendence over forced", { @@ -4385,6 +5221,32 @@ 1, true, true + ], + [ + "Prerequisite condition fails", + { + "attributes": { "id": "1" }, + "features": { + "parentFlag": { + "defaultValue": false + } + } + }, + { + "key": "my-test", + "variations": [0, 1], + "parentConditions": [ + { + "id": "parentFlag", + "condition": { + "value": true + } + } + ] + }, + 0, + false, + false ] ], "chooseVariation": [