Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
package com.devcycle.sdk.server.openfeature;

import java.math.BigDecimal;
import java.util.Map;
import java.util.Optional;

import com.devcycle.sdk.server.common.api.IDevCycleClient;
import com.devcycle.sdk.server.common.exception.DevCycleException;
import com.devcycle.sdk.server.common.model.DevCycleEvent;
import com.devcycle.sdk.server.common.model.DevCycleUser;
import com.devcycle.sdk.server.common.model.EvalReason;
import com.devcycle.sdk.server.common.model.Variable;
import dev.openfeature.sdk.*;
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;

import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.ImmutableMetadata.ImmutableMetadataBuilder;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.TrackingEventDetails;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;

import java.math.BigDecimal;
import java.util.Map;
import java.util.Optional;

public class DevCycleProvider implements FeatureProvider {
private static final String PROVIDER_NAME = "DevCycle";

Expand Down Expand Up @@ -97,19 +109,36 @@ public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultVa
DevCycleUser user = DevCycleUser.fromEvaluationContext(ctx);

Variable<Object> variable = devcycleClient.variable(user, key, defaultValue.asStructure().asObjectMap());


if (variable == null || variable.getIsDefaulted()) {
ImmutableMetadata flagMetadata = null;
if (variable != null && variable.getEval() != null) {
EvalReason eval = variable.getEval();
flagMetadata = getFlagMetadata(eval);
}
return ProviderEvaluation.<Value>builder()
.value(defaultValue)
.reason(Reason.DEFAULT.toString())
.flagMetadata(flagMetadata)
.build();
} else {
if (variable.getValue() instanceof Map) {
// JSON objects are managed as Map implementations and must be converted to an OpenFeature structure
Value objectValue = new Value(Structure.mapToStructure((Map) variable.getValue()));

ImmutableMetadata flagMetadata = null;
String evalReason = Reason.TARGETING_MATCH.toString();
if (variable.getEval() != null) {
EvalReason eval = variable.getEval();
evalReason = eval.getReason();
flagMetadata = getFlagMetadata(eval);
}

return ProviderEvaluation.<Value>builder()
.value(objectValue)
.reason(Reason.TARGETING_MATCH.toString())
.reason(evalReason)
.flagMetadata(flagMetadata)
.build();
} else {
throw new TypeMismatchError("DevCycle variable for key " + key + " is not a JSON object");
Expand All @@ -136,9 +165,15 @@ <T> ProviderEvaluation<T> resolvePrimitiveVariable(String key, T defaultValue, E
Variable<T> variable = devcycleClient.variable(user, key, defaultValue);

if (variable == null || variable.getIsDefaulted()) {
ImmutableMetadata flagMetadata = null;
if (variable != null && variable.getEval() != null) {
EvalReason eval = variable.getEval();
flagMetadata = getFlagMetadata(eval);
}
return ProviderEvaluation.<T>builder()
.value(defaultValue)
.reason(Reason.DEFAULT.toString())
.flagMetadata(flagMetadata)
.build();
} else {
T value = variable.getValue();
Expand All @@ -149,9 +184,18 @@ <T> ProviderEvaluation<T> resolvePrimitiveVariable(String key, T defaultValue, E
value = (T) Integer.valueOf(numVal.intValue());
}

ImmutableMetadata flagMetadata = null;
String evalReason = Reason.TARGETING_MATCH.toString();
if (variable.getEval() != null) {
EvalReason eval = variable.getEval();
evalReason = eval.getReason();
flagMetadata = getFlagMetadata(eval);
}

return ProviderEvaluation.<T>builder()
.value(value)
.reason(Reason.TARGETING_MATCH.toString())
.reason(evalReason)
.flagMetadata(flagMetadata)
.build();
}
} catch (IllegalArgumentException e) {
Expand Down Expand Up @@ -206,4 +250,17 @@ private Map<String, Object> getMetadataWithoutValue(TrackingEventDetails details
metaData.remove("value");
return metaData;
}

private ImmutableMetadata getFlagMetadata(EvalReason evalReason) {
ImmutableMetadataBuilder flagMetadataBuilder = ImmutableMetadata.builder();

if (evalReason.getDetails() != null) {
flagMetadataBuilder.addString("evalReasonDetails", evalReason.getDetails());
}

if (evalReason.getTargetId() != null) {
flagMetadataBuilder.addString("evalReasonTargetId", evalReason.getTargetId());
}
return flagMetadataBuilder.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
package com.devcycle.sdk.server.openfeature;

import com.devcycle.sdk.server.common.api.IDevCycleClient;
import com.devcycle.sdk.server.common.model.Variable;
import dev.openfeature.sdk.*;
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
import dev.openfeature.sdk.exceptions.TargetingKeyMissingError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.*;
import com.devcycle.sdk.server.common.api.IDevCycleClient;
import com.devcycle.sdk.server.common.model.EvalReason;
import com.devcycle.sdk.server.common.model.Variable;

import static org.mockito.Mockito.*;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
import dev.openfeature.sdk.exceptions.TargetingKeyMissingError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;

@RunWith(MockitoJUnitRunner.class)
public class DevCycleProviderTest {
Expand Down Expand Up @@ -90,7 +104,7 @@ public void testResolveVariableDefaulted() {
IDevCycleClient dvcClient = mock(IDevCycleClient.class);
when(dvcClient.isInitialized()).thenReturn(true);

when(dvcClient.variable(any(), any(), any())).thenReturn(Variable.builder().key("some-flag").value("unused value 1").defaultValue("default value").isDefaulted(true).type(Variable.TypeEnum.STRING).build());
when(dvcClient.variable(any(), any(), any())).thenReturn(Variable.builder().key("some-flag").value("unused value 1").defaultValue("default value").isDefaulted(true).type(Variable.TypeEnum.STRING).eval(EvalReason.defaultReason(EvalReason.DefaultReasonDetailsEnum.USER_NOT_TARGETED)).build());

DevCycleProvider provider = new DevCycleProvider(dvcClient);

Expand All @@ -99,6 +113,8 @@ public void testResolveVariableDefaulted() {
Assert.assertEquals(result.getValue(), "default value");
Assert.assertEquals(result.getReason(), Reason.DEFAULT.toString());
Assert.assertNull(result.getErrorCode());
Assert.assertNotNull(result.getFlagMetadata());
Assert.assertEquals(result.getFlagMetadata().getString("evalReasonDetails"), EvalReason.DefaultReasonDetailsEnum.USER_NOT_TARGETED.getValue());
}

@Test
Expand All @@ -117,6 +133,25 @@ public void testResolveBooleanVariable() {
Assert.assertNull(result.getErrorCode());
}

@Test
public void testResolveBooleanVariableWithDevCycleEvalReason() {
IDevCycleClient dvcClient = mock(IDevCycleClient.class);
when(dvcClient.isInitialized()).thenReturn(true);

when(dvcClient.variable(any(), any(), any())).thenReturn(Variable.builder().key("some-flag").value(true).defaultValue(false).type(Variable.TypeEnum.BOOLEAN).eval(new EvalReason("SPLIT", "User ID", "bool_target_id")).build());

DevCycleProvider provider = new DevCycleProvider(dvcClient);

ProviderEvaluation<Boolean> result = provider.resolvePrimitiveVariable("some-flag", false, new ImmutableContext("user-1234"));
Assert.assertNotNull(result);
Assert.assertEquals(result.getValue(), true);
Assert.assertEquals(result.getReason(), "SPLIT");
Assert.assertNull(result.getErrorCode());
Assert.assertNotNull(result.getFlagMetadata());
Assert.assertEquals(result.getFlagMetadata().getString("evalReasonDetails"), "User ID");
Assert.assertEquals(result.getFlagMetadata().getString("evalReasonTargetId"), "bool_target_id");
}

@Test
public void testResolveIntegerVariable() {
IDevCycleClient dvcClient = mock(IDevCycleClient.class);
Expand Down Expand Up @@ -249,4 +284,38 @@ public void testGetObjectEvaluation() {
Assert.assertEquals(result.getReason(), Reason.TARGETING_MATCH.toString());
Assert.assertNull(result.getErrorCode());
}

@Test
public void testGetObjectEvaluationWithDevCycleEvalReason() {
Map<String, Object> jsonData = new LinkedHashMap<>();
jsonData.put("strVal", "some string");
jsonData.put("boolVal", true);
jsonData.put("numVal", 123);

Map<String, Object> defaultJsonData = new LinkedHashMap<>();

IDevCycleClient dvcClient = mock(IDevCycleClient.class);
when(dvcClient.isInitialized()).thenReturn(true);
when(dvcClient.variable(any(), any(), any())).thenReturn(Variable.builder().key("some-flag").value(jsonData).defaultValue(defaultJsonData).type(Variable.TypeEnum.JSON).eval(new EvalReason("SPLIT", "User ID", "json_target_id")).build());

DevCycleProvider provider = new DevCycleProvider(dvcClient);

Value defaultValue = new Value(Structure.mapToStructure(defaultJsonData));

ProviderEvaluation<Value> result = provider.getObjectEvaluation("some-flag", defaultValue, new ImmutableContext("user-1234"));
Assert.assertNotNull(result);
Assert.assertNotNull(result.getValue());
Assert.assertTrue(result.getValue().isStructure());

result.getValue().asStructure().asObjectMap().forEach((k, v) -> {
Assert.assertTrue(jsonData.containsKey(k));
Assert.assertEquals(jsonData.get(k), v);
});

Assert.assertEquals(result.getReason(), "SPLIT");
Assert.assertNull(result.getErrorCode());
Assert.assertNotNull(result.getFlagMetadata());
Assert.assertEquals(result.getFlagMetadata().getString("evalReasonDetails"), "User ID");
Assert.assertEquals(result.getFlagMetadata().getString("evalReasonTargetId"), "json_target_id");
}
}
Loading