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
4 changes: 3 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
1.0.0
- First release. Up to date with spec v0.1.0
- First release. Up to date with java sdk v0.1.0
1.1.0
- Up tp date with spec v0.5.0 and java sdk v0.3.1
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,29 @@ This Provider is designed to allow the use of OpenFeature with Split, the platfo
This SDK is compatible with Java 11 and higher.

## Getting started
### Add it to your maven build
```java
<dependency>
<groupId>io.split.openfeature</groupId>
<artifactId>split-openfeature-provider</artifactId>
<version>1.1.0</version>
</dependency>
```
### Configure it
Below is a simple example that describes the instantiation of the Split Provider. Please see the [OpenFeature Documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api) for details on how to use the OpenFeature SDK.

```java
import dev.openfeature.javasdk.OpenFeatureAPI;
import io.split.openfeature.SplitProvider
import dev.openfeature.sdk.OpenFeatureAPI;
import io.split.openfeature.SplitProvider;

OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(new SplitProvider("YOUR_API_KEY"));
```

If you are more familiar with Split or want access to other initialization options, you can provide a `SplitClient` to the constructor. See the [Split Java SDK Documentation](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) for more information.
```java
import dev.openfeature.javasdk.OpenFeatureAPI;
import io.split.openfeature.SplitProvider
import dev.openfeature.sdk.OpenFeatureAPI;
import io.split.openfeature.SplitProvider;
import io.split.client.SplitClient;
import io.split.client.SplitClientConfig;
import io.split.client.SplitFactoryBuilder;
Expand All @@ -43,18 +52,18 @@ One important note is that the Split Provider **requires a targeting key** to be
```java
Client client = api.getClient("CLIENT_NAME");

EvaluationContext context = new EvaluationContext("TARGETING_KEY");
EvaluationContext context = new MutableContext("TARGETING_KEY");
Boolean boolValue = client.getBooleanValue("boolFlag", false, context);
```
If the same targeting key is used repeatedly, the evaluation context may be set at the client level
```java
EvaluationContext context = new EvaluationContext("TARGETING_KEY");
EvaluationContext context = new MutableContext("TARGETING_KEY");
client.setEvaluationContext(context)
```
or at the OpenFeatureAPI level
```java
EvaluationContext context = new EvaluationContext("TARGETING_KEY");
OpenFeatureAPI.getInstance().setCtx(context)
EvaluationContext context = new MutableContext("TARGETING_KEY");
OpenFeatureAPI.getInstance().setEvaluationContext(context)
````
If the context was set at the client or api level, it is not required to provide it during flag evaluation.

Expand Down
7 changes: 3 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.split.openfeature</groupId>
<artifactId>split-openfeature-provider</artifactId>
<version>1.0.0</version>
<version>1.1.0</version>

<name>split-openfeature-provider-java</name>
<description>Split OpenFeature Java Provider</description>
Expand Down Expand Up @@ -74,9 +74,8 @@
</dependency>
<dependency>
<groupId>dev.openfeature</groupId>
<artifactId>javasdk</artifactId>
<!--TODO: use stable version once released-->
<version>0.1.0</version>
<artifactId>sdk</artifactId>
<version>0.3.1</version>
</dependency>
</dependencies>

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/split/openfeature/SplitModule.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.split.openfeature;

import dev.openfeature.javasdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.GeneralError;
import io.split.client.SplitClient;
import io.split.client.SplitClientConfig;
import io.split.client.SplitFactory;
Expand Down
132 changes: 46 additions & 86 deletions src/main/java/io/split/openfeature/SplitProvider.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package io.split.openfeature;

import dev.openfeature.javasdk.ErrorCode;
import dev.openfeature.javasdk.EvaluationContext;
import dev.openfeature.javasdk.FeatureProvider;
import dev.openfeature.javasdk.Metadata;
import dev.openfeature.javasdk.ProviderEvaluation;
import dev.openfeature.javasdk.Reason;
import dev.openfeature.javasdk.Structure;
import dev.openfeature.javasdk.Value;
import dev.openfeature.javasdk.exceptions.GeneralError;
import dev.openfeature.javasdk.exceptions.OpenFeatureError;
import dev.openfeature.javasdk.exceptions.ParseError;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.MutableStructure;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.OpenFeatureError;
import dev.openfeature.sdk.exceptions.ParseError;
import dev.openfeature.sdk.exceptions.TargetingKeyMissingError;
import io.split.client.SplitClient;
import io.split.openfeature.utils.Serialization;

import java.time.ZonedDateTime;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -48,7 +48,7 @@ public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defa
try {
String evaluated = evaluateTreatment(key, evaluationContext);
if (noTreatment(evaluated)) {
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND.name());
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND);
}
// if treatment is "on" or "true" we treat that as true
// if it is "off" or "false" we treat it as false
Expand All @@ -59,7 +59,7 @@ public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defa
} else if (evaluated.equalsIgnoreCase("false") || evaluated.equals("off")) {
value = false;
} else {
throw new ParseError(ErrorCode.PARSE_ERROR.name());
throw new ParseError();
}
return constructProviderEvaluation(value, evaluated);
} catch (OpenFeatureError e) {
Expand All @@ -75,7 +75,7 @@ public ProviderEvaluation<String> getStringEvaluation(String key, String default
try {
String evaluated = evaluateTreatment(key, evaluationContext);
if (noTreatment(evaluated)) {
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND.name());
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND);
}
return constructProviderEvaluation(evaluated, evaluated);
} catch (OpenFeatureError e) {
Expand All @@ -90,14 +90,14 @@ public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defa
try {
String evaluated = evaluateTreatment(key, evaluationContext);
if (noTreatment(evaluated)) {
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND.name());
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND);
}
Integer value = Integer.valueOf(evaluated);
return constructProviderEvaluation(value, evaluated);
} catch (OpenFeatureError e) {
throw e;
} catch (NumberFormatException e) {
throw new ParseError(ErrorCode.PARSE_ERROR.name());
throw new ParseError();
} catch (Exception e) {
throw new GeneralError("Error getting Integer evaluation", e);
}
Expand All @@ -108,29 +108,29 @@ public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double default
try {
String evaluated = evaluateTreatment(key, evaluationContext);
if (noTreatment(evaluated)) {
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND.name());
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND);
}
Double value = Double.valueOf(evaluated);
return constructProviderEvaluation(value, evaluated);
} catch (OpenFeatureError e) {
throw e;
} catch (NumberFormatException e) {
throw new ParseError(ErrorCode.PARSE_ERROR.name());
throw new ParseError();
} catch (Exception e) {
throw new GeneralError("Error getting Double evaluation", e);
}
}

@Override
public ProviderEvaluation<Structure> getObjectEvaluation(String key, Structure defaultTreatment, EvaluationContext evaluationContext) {
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultTreatment, EvaluationContext evaluationContext) {
try {
String evaluated = evaluateTreatment(key, evaluationContext);
if (noTreatment(evaluated)) {
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND.name());
return constructProviderEvaluation(defaultTreatment, evaluated, Reason.DEFAULT, ErrorCode.FLAG_NOT_FOUND);
}
Map<String, Object> rawMap = Serialization.stringToMap(evaluated);
Structure structure = mapToStructure(rawMap);
return constructProviderEvaluation(structure, evaluated);
Value value = mapToValue(rawMap);
return constructProviderEvaluation(value, evaluated);
} catch (OpenFeatureError e) {
throw e;
} catch (Exception e) {
Expand All @@ -139,14 +139,14 @@ public ProviderEvaluation<Structure> getObjectEvaluation(String key, Structure d
}

public Map<String, Object> transformContext(EvaluationContext context) {
return getMapFromStructMap(context.asMap());
return context.asObjectMap();
}

private String evaluateTreatment(String key, EvaluationContext evaluationContext) {
String id = evaluationContext.getTargetingKey();
if (id == null || id.isEmpty()) {
// targeting key is always required
throw new GeneralError("TARGETING_KEY_MISSING");
throw new TargetingKeyMissingError();
}
Map<String, Object> attributes = transformContext(evaluationContext);
return client.getTreatment(id, key, attributes);
Expand All @@ -160,87 +160,47 @@ private <T> ProviderEvaluation<T> constructProviderEvaluation(T value, String va
return constructProviderEvaluation(value, variant, Reason.TARGETING_MATCH, null);
}

private <T> ProviderEvaluation<T> constructProviderEvaluation(T value, String variant, Reason reason, String errorCode) {
private <T> ProviderEvaluation<T> constructProviderEvaluation(T value, String variant, Reason reason, ErrorCode errorCode) {
ProviderEvaluation.ProviderEvaluationBuilder<T> builder = ProviderEvaluation.builder();
return builder
.value(value)
.reason(reason)
.reason(reason.name())
.variant(variant)
.errorCode(errorCode)
.build();
}

private Map<String, Object> getMapFromStructMap(Map<String, Value> structMap) {
return structMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> getInnerValue(e.getValue())));
}

private Structure mapToStructure(Map<String, Object> map) {
return new Structure(
map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue()))));
/**
* Turn map String->Object into a Value.
* @param map a Map String->Object, where object is NOT Value or Structure
* @return Value representing the map passed in
*/
private Value mapToValue(Map<String, Object> map) {
return new Value(
new MutableStructure(
map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue())))));
}

private Object getInnerValue(Value value) {
Object object = value.asBoolean();
if (object != null) {
return object;
}
object = value.asDouble();
if (object != null) {
return object;
}
object = value.asInteger();
if (object != null) {
return object;
}
object = value.asString();
if (object != null) {
return object;
}
object = value.asZonedDateTime();
if (object != null) {
return object;
}
object = value.asStructure();
if (object != null) {
// must return a map
return getMapFromStructMap(((Structure) object).asMap());
}
object = value.asList();
if (object != null) {
// must return a list of inner objects
List<Value> values = (List<Value>) object;
return values.stream().map(this::getInnerValue).collect(Collectors.toList());
}
throw new ClassCastException("Could not get inner value from Value object.");
}

private Value objectToValue(Object object) {
if (object instanceof Value) {
return (Value) object;
} else if (object instanceof String) {
// try to parse to zoned date time, otherwise use as string
if (object instanceof String) {
// try to parse as instant, otherwise use as string
try {
return new Value(ZonedDateTime.parse((String) object));
return new Value(Instant.parse((String) object));
} catch (DateTimeParseException e) {
return new Value((String) object);
}
} else if (object instanceof Boolean) {
return new Value((Boolean) object);
} else if (object instanceof Integer) {
return new Value((Integer) object);
} else if (object instanceof Double) {
return new Value((Double) object);
} else if (object instanceof Structure) {
return new Value((Structure) object);
} else if (object instanceof List) {
// need to translate each elem in list to a value
return new Value(((List<Object>) object).stream().map(this::objectToValue).collect(Collectors.toList()));
} else if (object instanceof ZonedDateTime) {
return new Value((ZonedDateTime) object);
} else if (object instanceof Map) {
return new Value(mapToStructure((Map<String, Object>) object));
return mapToValue((Map<String, Object>) object);
} else {
throw new ClassCastException("Could not cast Object to Value");
try {
return new Value(object);
} catch (InstantiationException e) {
throw new ClassCastException("Could not cast Object to Value");
}
}
}
}
4 changes: 2 additions & 2 deletions src/main/java/io/split/openfeature/utils/Serialization.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.openfeature.javasdk.ErrorCode;
import dev.openfeature.javasdk.exceptions.ParseError;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.exceptions.ParseError;

import java.util.Map;

Expand Down
Loading
Loading