Skip to content

Commit 5475ca3

Browse files
feat: Add OpenFeature provider (#111)
* initial commit * fixing tests * formatting and code cleanup * tweaked unit test * add docs * More code cleanup * more docs * Changed to the new pattern Jamie came up with * unit test fix * More readme tweaks * Add openfeature example to GHA test * updated some class references and ran a formatter * refactored out the user factory * renamed method * Fixed issues with JSON object translations and added a new integration unit tests with the local bucketing client * switched to better exception assertions * Fixed issue with Integer values not being converted properly * removed a null check * Fixing some null data issues and differentiating between Cloud and Local in the provider * Adding nulls as allowable custom data propery * I don't think an exception is necessary here, in the docs we say we just ignore the bad value * docs cleanup * Added some more examples * formatter * feedback * feedback bug * fix some unit tests * Applying some OpenFeature feedback * added null value to doc
1 parent dd0ff0f commit 5475ca3

File tree

17 files changed

+1487
-306
lines changed

17 files changed

+1487
-306
lines changed

.github/workflows/test-examples.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,9 @@ jobs:
2121
DEVCYCLE_SERVER_SDK_KEY: "${{ secrets.DEVCYCLE_SERVER_SDK_KEY }}"
2222
- name: Run cloud bucketing example
2323
run: ./gradlew runCloudExample
24+
env:
25+
DEVCYCLE_SERVER_SDK_KEY: "${{ secrets.DEVCYCLE_SERVER_SDK_KEY }}"
26+
- name: Run OpenFeature example
27+
run: ./gradlew runOpenFeatureExample
2428
env:
2529
DEVCYCLE_SERVER_SDK_KEY: "${{ secrets.DEVCYCLE_SERVER_SDK_KEY }}"

OpenFeature.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# DevCycle Java SDK OpenFeature Provider
2+
3+
This SDK provides a Java implementation of the [OpenFeature](https://openfeature.dev/) Provider interface.
4+
5+
## Example App
6+
7+
See the [example app](src/examples/java/com/devcycle/examples/OpenFeatureExample.java) for a working example of the DevCycle Java SDK OpenFeature Provider.
8+
9+
## Usage
10+
11+
Start by creating the appropriate DevCycle SDK client (`DevCycleLocalClient` or `DevCycleCloudClient`).
12+
13+
See our [Java Cloud Bucketing SDK](https://docs.devcycle.com/sdk/server-side-sdks/java-cloud) and [Java Local Bucketing SDK](https://docs.devcycle.com/sdk/server-side-sdks/java-local) documentation for more information on how to configure the SDK.
14+
15+
```java
16+
// Initialize DevCycle Client
17+
DevCycleLocalOptions options = DevCycleLocalOptions.builder().build();
18+
DevCycleLocalClient devCycleClient = new DevCycleLocalClient("DEVCYCLE_SERVER_SDK_KEY", options);
19+
20+
// Set the initialzed DevCycle client as the provider for OpenFeature
21+
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
22+
api.setProvider(devCycleClient.getOpenFeatureProvider());
23+
24+
// Get the OpenFeature client
25+
Client openFeatureClient = api.getClient();
26+
27+
// Create the evaluation context to use for fetching variable values
28+
EvaluationContext context = new MutableContext("user-1234");
29+
30+
// Retrieve a boolean flag from the OpenFeature client
31+
Boolean variableValue = openFeatureClient.getBooleanValue(VARIABLE_KEY, false, context);
32+
```
33+
34+
### Required Targeting Key
35+
36+
For DevCycle SDK to work we require either a `targeting key` or `user_id` attribute to be set on the OpenFeature context.
37+
This value is used to identify the user as the `user_id` property for a `DevCycleUser` in DevCycle.
38+
39+
### Mapping Context Properties to DevCycleUser
40+
41+
The provider will automatically translate known `DevCycleUser` properties from the OpenFeature context to the `DevCycleUser` object.
42+
[DevCycleUser Java Interface](https://github.com/DevCycleHQ/java-server-sdk/blob/main/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java)
43+
44+
For example all these properties will be set on the `DevCycleUser`:
45+
```java
46+
MutableContext context = new MutableContext("test-1234");
47+
context.add("email", "email@devcycle.com");
48+
context.add("name", "name");
49+
context.add("country", "CA");
50+
context.add("language", "en");
51+
context.add("appVersion", "1.0.11");
52+
context.add("appBuild", 1000);
53+
54+
Map<String,Object> customData = new LinkedHashMap<>();
55+
customData.put("custom", "value");
56+
context.add("customData", Structure.mapToStructure(customData));
57+
58+
Map<String,Object> privateCustomData = new LinkedHashMap<>();
59+
privateCustomData.put("private", "data");
60+
context.add("privateCustomData", Structure.mapToStructure(privateCustomData));
61+
```
62+
63+
Context properties that are not known `DevCycleUser` properties will be automatically
64+
added to the `customData` property of the `DevCycleUser`.
65+
66+
DevCycle allows the following data types for custom data values: **boolean**, **integer**, **double**, **float**, and **String**. Other data types will be ignored
67+
68+
### JSON Flag Limitations
69+
70+
The OpenFeature spec for JSON flags allows for any type of valid JSON value to be set as the flag value.
71+
72+
For example the following are all valid default value types to use with OpenFeature:
73+
```java
74+
// Invalid JSON values for the DevCycle SDK, will return defaults
75+
openFeatureClient.getObjectValue("json-flag", new Value(new ArrayList<String>(Arrays.asList("value1", "value2"))));
76+
openFeatureClient.getObjectValue("json-flag", new Value(610));
77+
openFeatureClient.getObjectValue("json-flag", new Value(false));
78+
openFeatureClient.getObjectValue("json-flag", new Value("string"));
79+
openFeatureClient.getObjectValue("json-flag", new Value());
80+
```
81+
82+
However, these are not valid types for the DevCycle SDK, the DevCycle SDK only supports JSON Objects:
83+
```java
84+
85+
Map<String,Object> defaultJsonData = new LinkedHashMap<>();
86+
defaultJsonData.put("default", "value");
87+
openFeatureClient.getObjectValue("json-flag", new Value(Structure.mapToStructure(defaultJsonData)));
88+
```

README.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ ldd --version
2727

2828
## Installation
2929

30+
### Gradle
31+
You can use the SDK in your Gradle project by adding the following to *build.gradle*:
32+
33+
```yaml
34+
implementation("com.devcycle:java-server-sdk:2.0.1")
35+
```
36+
3037
### Maven
3138

3239
You can use the SDK in your Maven project by adding the following to your *pom.xml*:
@@ -40,13 +47,6 @@ You can use the SDK in your Maven project by adding the following to your *pom.x
4047
</dependency>
4148
```
4249

43-
### Gradle
44-
Alternatively you can use the SDK in your Gradle project by adding the following to *build.gradle*:
45-
46-
```yaml
47-
implementation("com.devcycle:java-server-sdk:2.0.1")
48-
```
49-
5050
## DNS Caching
5151
The JVM, by default, caches DNS for infinity. DevCycle servers are load balanced and dynamic. To address this concern,
5252
setting the DNS cache TTL to a short duration is recommended. The TTL is controlled by this security setting `networkaddress.cache.ttl`.
@@ -84,9 +84,22 @@ public class MyClass {
8484
}
8585
```
8686

87+
## OpenFeature Support
88+
89+
This SDK provides an implementation of the [OpenFeature](https://openfeature.dev/) Provider interface. Use the `getOpenFeatureProvider()` method on the DevCycle SDK client to obtain a provider for OpenFeature.
90+
91+
```java
92+
DevCycleLocalClient devCycleClient = new DevCycleLocalClient("DEVCYCLE_SERVER_SDK_KEY", options);
93+
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
94+
api.setProvider(devCycleClient.getOpenFeatureProvider());
95+
```
96+
97+
You can find instructions on how to use it here: [DevCycle Java SDK OpenFeature Provider](OpenFeature.md)
98+
99+
87100
## Usage
88101

89-
To find usage documentation, visit our docs for [Local Bucketing](https://docs.devcycle.com/docs/sdk/server-side-sdks/java-local).
102+
To find usage documentation, visit our docs for [Local Bucketing](https://docs.devcycle.com/docs/sdk/server-side-sdks/java-local) and [Cloud Bucketing](https://docs.devcycle.com/docs/sdk/server-side-sdks/java-cloud)
90103

91104
## Logging
92105

build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ ext {
141141
junit_version = "4.13.2"
142142
mockito_core_version = "5.6.0"
143143
protobuf_version = "3.24.4"
144+
openfeature_version = "1.6.1"
144145
}
145146

146147
dependencies {
@@ -161,6 +162,8 @@ dependencies {
161162

162163
implementation("com.google.protobuf:protobuf-java:$protobuf_version")
163164

165+
implementation("dev.openfeature:sdk:$openfeature_version")
166+
164167
compileOnly("org.projectlombok:lombok:$lombok_version")
165168

166169
testAnnotationProcessor("org.projectlombok:lombok:$lombok_version")
@@ -196,3 +199,9 @@ task runCloudExample(type: JavaExec) {
196199
classpath = sourceSets.examples.runtimeClasspath
197200
main = 'com.devcycle.examples.CloudExample'
198201
}
202+
203+
task runOpenFeatureExample(type: JavaExec) {
204+
description = "Run the OpenFeature example"
205+
classpath = sourceSets.examples.runtimeClasspath
206+
main = 'com.devcycle.examples.OpenFeatureExample'
207+
}

src/examples/java/com/devcycle/examples/CloudExample.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.devcycle.sdk.server.cloud.api.DevCycleCloudClient;
44
import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions;
5-
import com.devcycle.sdk.server.common.exception.DevCycleException;
65
import com.devcycle.sdk.server.common.model.DevCycleUser;
76

87
public class CloudExample {
@@ -34,16 +33,16 @@ public static void main(String[] args) throws InterruptedException {
3433
Boolean variableValue = false;
3534
try {
3635
variableValue = client.variableValue(user, VARIABLE_KEY, defaultValue);
37-
} catch(DevCycleException e) {
36+
} catch (IllegalArgumentException e) {
3837
System.err.println("Error fetching variable value: " + e.getMessage());
3938
System.exit(1);
4039
}
4140

4241
// Use variable value
4342
if (variableValue) {
44-
System.err.println("feature is enabled");
43+
System.out.println("feature is enabled");
4544
} else {
46-
System.err.println("feature is NOT enabled");
45+
System.out.println("feature is NOT enabled");
4746
}
4847
}
4948
}

src/examples/java/com/devcycle/examples/LocalExample.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.devcycle.examples;
22

3+
import com.devcycle.sdk.server.common.model.DevCycleUser;
34
import com.devcycle.sdk.server.local.api.DevCycleLocalClient;
45
import com.devcycle.sdk.server.local.model.DevCycleLocalOptions;
5-
import com.devcycle.sdk.server.common.model.DevCycleUser;
66

77
public class LocalExample {
88
public static String VARIABLE_KEY = "test-boolean-variable";
@@ -29,7 +29,7 @@ public static void main(String[] args) throws InterruptedException {
2929
DevCycleLocalClient client = new DevCycleLocalClient(server_sdk_key, options);
3030

3131
for (int i = 0; i < 10; i++) {
32-
if(client.isInitialized()) {
32+
if (client.isInitialized()) {
3333
break;
3434
}
3535
Thread.sleep(500);
@@ -42,9 +42,9 @@ public static void main(String[] args) throws InterruptedException {
4242

4343
// Use variable value
4444
if (variableValue) {
45-
System.err.println("feature is enabled");
45+
System.out.println("feature is enabled");
4646
} else {
47-
System.err.println("feature is NOT enabled");
47+
System.out.println("feature is NOT enabled");
4848
}
4949
}
5050
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.devcycle.examples;
2+
3+
import com.devcycle.sdk.server.local.api.DevCycleLocalClient;
4+
import com.devcycle.sdk.server.local.model.DevCycleLocalOptions;
5+
import dev.openfeature.sdk.*;
6+
7+
import java.util.LinkedHashMap;
8+
import java.util.Map;
9+
10+
public class OpenFeatureExample {
11+
public static void main(String[] args) throws InterruptedException {
12+
String server_sdk_key = System.getenv("DEVCYCLE_SERVER_SDK_KEY");
13+
if (server_sdk_key == null) {
14+
System.err.println("Please set the DEVCYCLE_SERVER_SDK_KEY environment variable");
15+
System.exit(1);
16+
}
17+
18+
DevCycleLocalOptions options = DevCycleLocalOptions.builder().configPollingIntervalMS(60000)
19+
.disableAutomaticEventLogging(false).disableCustomEventLogging(false).build();
20+
21+
// Initialize DevCycle Client
22+
DevCycleLocalClient devCycleClient = new DevCycleLocalClient(server_sdk_key, options);
23+
24+
for (int i = 0; i < 10; i++) {
25+
if (devCycleClient.isInitialized()) {
26+
break;
27+
}
28+
Thread.sleep(500);
29+
}
30+
31+
// Setup OpenFeature with the DevCycle Provider
32+
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
33+
api.setProvider(devCycleClient.getOpenFeatureProvider());
34+
35+
Client openFeatureClient = api.getClient();
36+
37+
// Create the evaluation context to use for fetching variable values
38+
MutableContext context = new MutableContext("test-1234");
39+
context.add("email", "test-user@domain.com");
40+
context.add("name", "Test User");
41+
context.add("language", "en");
42+
context.add("country", "CA");
43+
context.add("appVersion", "1.0.0");
44+
context.add("appBuild", "1");
45+
context.add("deviceModel", "Macbook");
46+
47+
// Add Devcycle Custom Data values
48+
Map<String,Object> customData = new LinkedHashMap<>();
49+
customData.put("custom", "value");
50+
context.add("customData", Structure.mapToStructure(customData));
51+
52+
// Add Devcycle Private Custom Data values
53+
Map<String,Object> privateCustomData = new LinkedHashMap<>();
54+
privateCustomData.put("private", "data");
55+
context.add("privateCustomData", Structure.mapToStructure(privateCustomData));
56+
57+
// The default value can be of type string, boolean, number, or JSON
58+
Boolean defaultValue = false;
59+
60+
// Fetch variable values using the identifier key, with a default value and user
61+
// object. The default value can be of type string, boolean, number, or JSON
62+
Boolean variableValue = openFeatureClient.getBooleanValue("test-boolean-variable", defaultValue, context);
63+
64+
// Use variable value
65+
if (variableValue) {
66+
System.out.println("feature is enabled");
67+
} else {
68+
System.out.println("feature is NOT enabled");
69+
}
70+
71+
// Default JSON objects must be a map of string to primitive values
72+
Map<String, Object> defaultJsonData = new LinkedHashMap<>();
73+
defaultJsonData.put("default", "value");
74+
75+
// Fetch a JSON object variable
76+
Value jsonObject = openFeatureClient.getObjectValue("test-json-variable", new Value(Structure.mapToStructure(defaultJsonData)), context);
77+
System.out.println(jsonObject.toString());
78+
79+
// Retrieving a string variable along with the resolution details
80+
FlagEvaluationDetails<String> details = openFeatureClient.getStringDetails("doesnt-exist", "default", context);
81+
System.out.println("Value: " + details.getValue());
82+
System.out.println("Reason: " + details.getReason());
83+
84+
}
85+
}

0 commit comments

Comments
 (0)