diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..492e41f11
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "providers/flagd/schemas"]
+ path = providers/flagd/schemas
+ url = https://github.com/open-feature/schemas.git
diff --git a/README.md b/README.md
index 84e3992c9..50e6e69fb 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,82 @@ The project includes:
This repo uses _Release Please_ to release packages. Release Please sets up a running PR that tracks all changes for the library components, and maintains the versions according to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), generated when [PRs are merged](https://github.com/amannn/action-semantic-pull-request). When Release Please's running PR is merged, any changed artifacts are published.
+## Developing
+
+### Requirements
+
+Though we target Java 8, Java 18 is recommended for the tooling, plugins, etc. Maven 3.8+ is recommended.
+
+### Testing
+
+Run `mvn verify` to test, generate javadoc, and check style. If this passes locally, the CI will generally pass.
+
+### Adding a module
+
+1. Create a [standard directory structure](https://maven.apache.org/guides/introduction/introduction-to-the-standard-directory-layout.html) in the appropriate folder (`hooks/`, `providers/`).
+1. Create a new `pom.xml` in the root of your new module. It must inherit from the parent POM, which implements the javadoc, testing, publishing, and other boilerplate. Be sure to add `` on the line specifying the module version, so our release tooling can update it (see sample pom below).
+1. Add the new package to `release-please-config.json`.
+1. Add the new module to the ... section in the parent `pom.xml`.
+
+Sample pom.xml:
+```xml
+
+
+ 4.0.0
+ 4.0.0
+
+ dev.openfeature.contrib
+ java-sdk-contrib
+ 0.0.0
+ ../../pom.xml
+
+ dev.openfeature.contrib.${providers | hooks | etc}
+ module
+ 0.0.1
+
+ module
+ Your module description
+ https://openfeature.dev
+
+
+
+ Your GitHub ID
+ Your Name
+ OpenFeature
+ https://openfeature.dev/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### VS Code config
+
+To use vscode, install the standard [Java language support extension by Red Hat](https://marketplace.visualstudio.com/items?itemName=redhat.java).
+
+The following vscode settings are recommended (create a workspace settings file at .vscode/settings.json):
+
+```json
+{
+ "java.configuration.updateBuildConfiguration": "interactive",
+ "java.autobuild.enabled": false,
+ "java.checkstyle.configuration": "${workspaceFolder}/checkstyle.xml",
+ "java.checkstyle.version": "10.3.2",
+ "java.format.settings.url": "${workspaceFolder}/eclipse-java-google-style.xml",
+ "java.format.enabled": false
+}
+```
+
## License
Apache 2.0 - See [LICENSE](./LICENSE) for more information.
diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml
new file mode 100644
index 000000000..3d374f555
--- /dev/null
+++ b/checkstyle-suppressions.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/checkstyle.xml b/checkstyle.xml
index 9e524577a..4faa7abf4 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -1,7 +1,5 @@
-
+
-
-
+
+
-
+
-
+
+
+
-
+
+
-
-
+
-
+
-
+
-
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
-
+
+
-
+
-
-
+
+
-
+
-
+ OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF" />
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+ LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
+ NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
+ SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND" />
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
-
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
-
-
+
+
-
+ METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA,
+ RECORD_DEF" />
-
-
+
+
-
-
-
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
+
-
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
-
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
\ No newline at end of file
diff --git a/hooks/open-telemetry/src/main/java/dev/openfeature/contrib/hooks/otel/OpenTelemetryHook.java b/hooks/open-telemetry/src/main/java/dev/openfeature/contrib/hooks/otel/OpenTelemetryHook.java
index 24100d424..bd659993c 100644
--- a/hooks/open-telemetry/src/main/java/dev/openfeature/contrib/hooks/otel/OpenTelemetryHook.java
+++ b/hooks/open-telemetry/src/main/java/dev/openfeature/contrib/hooks/otel/OpenTelemetryHook.java
@@ -17,6 +17,7 @@ public OpenTelemetryHook() {
/**
* A test method...
+ *
* @return {boolean}
*/
public static boolean test() {
diff --git a/pom.xml b/pom.xml
index 484056139..6a23b2752 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,11 +26,23 @@
+
dev.openfeature
javasdk
- 0.0.3
+
+ [0.1,)
+
+ provided
+
+ org.projectlombok
+ lombok
+ 1.18.24
+ provided
+
+
+
org.mockito
@@ -86,6 +98,7 @@
1.8.1
test
+
@@ -119,13 +132,13 @@
com.puppycrawl.tools
checkstyle
- 8.31
+ 10.3.2
validate
- validate
+ verify
check
@@ -136,7 +149,9 @@
org.apache.maven.plugins
maven-pmd-plugin
- 3.13.0
+
+ ${basedir}/target/generated-sources/
+
run-pmd
@@ -221,6 +236,7 @@
3.4.0
true
+ dev.openfeature.flagd.grpc
diff --git a/providers/flagd/pom.xml b/providers/flagd/pom.xml
index 56e508e3f..d3f274129 100644
--- a/providers/flagd/pom.xml
+++ b/providers/flagd/pom.xml
@@ -26,6 +26,102 @@
+
+
+ io.grpc
+ grpc-netty-shaded
+ 1.48.1
+ runtime
+
+
+ io.grpc
+ grpc-protobuf
+ 1.48.1
+
+
+ io.grpc
+ grpc-stub
+ 1.48.1
+
+
+
+ org.apache.tomcat
+ annotations-api
+ 6.0.53
+ provided
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.6.2
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+
+ update-schemas-submodule
+ validate
+
+ exec
+
+
+
+ git
+
+ submodule
+ update
+ --init
+ --recursive
+
+
+
+
+ copy-protobuf-definition
+ validate
+
+ exec
+
+
+
+ cp
+
+ schemas/protobuf/schema/v1/schema.proto
+ src/main/proto/
+
+
+
+
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ 0.6.1
+
+ com.google.protobuf:protoc:3.21.1:exe:${os.detected.classifier}
+ grpc-java
+ io.grpc:protoc-gen-grpc-java:1.48.1:exe:${os.detected.classifier}
+
+
+
+
+ compile
+ compile-custom
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/providers/flagd/schemas b/providers/flagd/schemas
new file mode 160000
index 000000000..910fa3391
--- /dev/null
+++ b/providers/flagd/schemas
@@ -0,0 +1 @@
+Subproject commit 910fa3391adcddbbd8056879c3d7e1b465686bf6
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java
index 6c0a169fe..0cf34028c 100644
--- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProvider.java
@@ -1,29 +1,297 @@
package dev.openfeature.contrib.providers.flagd;
-import dev.openfeature.javasdk.Client;
-import dev.openfeature.javasdk.NoOpProvider;
-import dev.openfeature.javasdk.OpenFeatureAPI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
-/**
- * A placeholder.
+import org.apache.commons.lang3.EnumUtils;
+
+import dev.openfeature.flagd.grpc.Schema.ResolveBooleanRequest;
+import dev.openfeature.flagd.grpc.Schema.ResolveBooleanResponse;
+import dev.openfeature.flagd.grpc.Schema.ResolveFloatRequest;
+import dev.openfeature.flagd.grpc.Schema.ResolveFloatResponse;
+import dev.openfeature.flagd.grpc.Schema.ResolveIntRequest;
+import dev.openfeature.flagd.grpc.Schema.ResolveIntResponse;
+import dev.openfeature.flagd.grpc.Schema.ResolveObjectRequest;
+import dev.openfeature.flagd.grpc.Schema.ResolveObjectResponse;
+import dev.openfeature.flagd.grpc.Schema.ResolveStringRequest;
+import dev.openfeature.flagd.grpc.Schema.ResolveStringResponse;
+import dev.openfeature.flagd.grpc.ServiceGrpc;
+import dev.openfeature.flagd.grpc.ServiceGrpc.ServiceBlockingStub;
+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 io.grpc.ManagedChannelBuilder;
+import lombok.extern.slf4j.Slf4j;
+
+
+/**
+ * OpenFeature provider for flagd.
*/
-public class FlagdProvider {
+@Slf4j
+public class FlagdProvider implements FeatureProvider {
+
+ private ServiceBlockingStub serviceStub;
+ static final String PROVIDER_NAME = "flagD Provider";
+ static final int DEFAULT_PORT = 8013;
+ static final String DEFAULT_HOST = "localhost";
+
+ /**
+ * Create a new FlagdProvider instance.
+ *
+ * @param protocol transport protocol, "http" or "https"
+ * @param host flagd host, defaults to localhost
+ * @param port flagd port, defaults to 8013
+ */
+ public FlagdProvider(Protocol protocol, String host, int port) {
+
+ this(Protocol.HTTPS == protocol
+ ? ServiceGrpc.newBlockingStub(ManagedChannelBuilder.forAddress(host, port)
+ .useTransportSecurity()
+ .build()) :
+ ServiceGrpc.newBlockingStub(ManagedChannelBuilder.forAddress(host, port)
+ .usePlaintext()
+ .build()));
+ }
- /**
+ /**
* Create a new FlagdProvider instance.
*/
public FlagdProvider() {
+ this(Protocol.HTTP, DEFAULT_HOST, DEFAULT_PORT);
+ }
+
+ /**
+ * Create a new FlagdProvider instance.
+ *
+ * @param serviceStub service stub instance to use
+ */
+ public FlagdProvider(ServiceBlockingStub serviceStub) {
+ this.serviceStub = serviceStub;
+ }
+
+ @Override
+ public Metadata getMetadata() {
+ return new Metadata() {
+ @Override
+ public String getName() {
+ return PROVIDER_NAME;
+ }
+ };
+ }
+
+ @Override
+ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue,
+ EvaluationContext ctx) {
+
+ ResolveBooleanRequest request = ResolveBooleanRequest.newBuilder()
+ .setFlagKey(key)
+ .setContext(this.convertContext(ctx))
+ .build();
+ ResolveBooleanResponse r = this.serviceStub.resolveBoolean(request);
+ return ProviderEvaluation.builder()
+ .value(r.getValue())
+ .variant(r.getVariant())
+ .reason(this.mapReason(r.getReason()))
+ .build();
+ }
+
+ @Override
+ public ProviderEvaluation getStringEvaluation(String key, String defaultValue,
+ EvaluationContext ctx) {
+ ResolveStringRequest request = ResolveStringRequest.newBuilder()
+ .setFlagKey(key)
+ .setContext(this.convertContext(ctx)).build();
+ ResolveStringResponse r = this.serviceStub.resolveString(request);
+ return ProviderEvaluation.builder().value(r.getValue())
+ .variant(r.getVariant())
+ .reason(this.mapReason(r.getReason()))
+ .build();
+ }
+
+ @Override
+ public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue,
+ EvaluationContext ctx) {
+ ResolveFloatRequest request = ResolveFloatRequest.newBuilder()
+ .setFlagKey(key)
+ .setContext(this.convertContext(ctx))
+ .build();
+ ResolveFloatResponse r = this.serviceStub.resolveFloat(request);
+ return ProviderEvaluation.builder()
+ .value(r.getValue())
+ .variant(r.getVariant())
+ .reason(this.mapReason(r.getReason()))
+ .build();
+ }
+
+ @Override
+ public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue,
+ EvaluationContext ctx) {
+ ResolveIntRequest request = ResolveIntRequest.newBuilder()
+ .setFlagKey(key)
+ .setContext(this.convertContext(ctx))
+ .build();
+ ResolveIntResponse r = this.serviceStub.resolveInt(request);
+ return ProviderEvaluation.builder()
+ .value((int) r.getValue())
+ .variant(r.getVariant())
+ .reason(this.mapReason(r.getReason()))
+ .build();
+ }
+
+ @Override
+ public ProviderEvaluation getObjectEvaluation(String key, Structure defaultValue,
+ EvaluationContext ctx) {
+ ResolveObjectRequest request = ResolveObjectRequest.newBuilder()
+ .setFlagKey(key)
+ .setContext(this.convertContext(ctx))
+ .build();
+ ResolveObjectResponse r = this.serviceStub.resolveObject(request);
+ return ProviderEvaluation.builder()
+ .value(this.convertObjectResponse(r.getValue()))
+ .variant(r.getVariant())
+ .reason(this.mapReason(r.getReason()))
+ .build();
}
- /**
- * A test method.
- *
- * @return {boolean}
+ // Map FlagD reasons to Java SDK reasons.
+ private Reason mapReason(String flagDreason) {
+ if (!EnumUtils.isValidEnum(Reason.class, flagDreason)) {
+ // until we have "STATIC" in the spec and SDK, we map STATIC to DEFAULT
+ if ("STATIC".equals(flagDreason)) {
+ return Reason.DEFAULT;
+ } else {
+ return Reason.UNKNOWN;
+ }
+ } else {
+ return Reason.valueOf(flagDreason);
+ }
+ }
+
+ /**
+ * Recursively convert protobuf structure to openfeature structure.
*/
- public static boolean test() {
- OpenFeatureAPI.getInstance().setProvider(new NoOpProvider());
- Client client = OpenFeatureAPI.getInstance().getClient();
- return client.getBooleanValue("test", true);
+ private Structure convertObjectResponse(com.google.protobuf.Struct protobuf) {
+ return new Structure(this.convertProtobufMap(protobuf.getFieldsMap()).asStructure().asMap());
}
+ /**
+ * Recursively convert the Evaluation context to a protobuf structure.
+ */
+ private com.google.protobuf.Struct convertContext(EvaluationContext ctx) {
+ return this.convertMap(ctx.asMap()).getStructValue();
+ }
+
+ /**
+ * Convert any openfeature value to a protobuf value.
+ */
+ private com.google.protobuf.Value convertAny(Value value) {
+ if (value.isList()) {
+ return this.convertList(value.asList());
+ } else if (value.isStructure()) {
+ return this.convertMap(value.asStructure().asMap());
+ } else {
+ return this.convertPrimitive(value);
+ }
+ }
+
+ /**
+ * Convert any protobuf value to an openfeature value.
+ */
+ private Value convertAny(com.google.protobuf.Value protobuf) {
+ if (protobuf.hasListValue()) {
+ return this.convertList(protobuf.getListValue());
+ } else if (protobuf.hasStructValue()) {
+ return this.convertProtobufMap(protobuf.getStructValue().getFieldsMap());
+ } else {
+ return this.convertPrimitive(protobuf);
+ }
+ }
+
+ /**
+ * Convert openfeature map to protobuf map.
+ */
+ private com.google.protobuf.Value convertMap(Map map) {
+ Map values = new HashMap<>();
+
+ map.keySet().stream().forEach((String key) -> {
+ Value value = map.get(key);
+ values.put(key, this.convertAny(value));
+ });
+ com.google.protobuf.Struct struct = com.google.protobuf.Struct.newBuilder()
+ .putAllFields(values).build();
+ return com.google.protobuf.Value.newBuilder().setStructValue(struct).build();
+ }
+
+ /**
+ * Convert protobuf map to openfeature map.
+ */
+ private Value convertProtobufMap(Map map) {
+ Map values = new HashMap<>();
+
+ map.keySet().stream().forEach((String key) -> {
+ com.google.protobuf.Value value = map.get(key);
+ values.put(key, this.convertAny(value));
+ });
+ return new Value(new Structure(values));
+ }
+
+ /**
+ * Convert openfeature list to protobuf list.
+ */
+ private com.google.protobuf.Value convertList(List values) {
+ com.google.protobuf.ListValue list = com.google.protobuf.ListValue.newBuilder()
+ .addAllValues(values.stream()
+ .map(v -> this.convertAny(v)).collect(Collectors.toList())).build();
+ return com.google.protobuf.Value.newBuilder().setListValue(list).build();
+ }
+
+ /**
+ * Convert protobuf list to openfeature list.
+ */
+ private Value convertList(com.google.protobuf.ListValue protobuf) {
+ return new Value(protobuf.getValuesList().stream().map(p -> this.convertAny(p)).collect(Collectors.toList()));
+ }
+
+ /**
+ * Convert openfeature value to protobuf value.
+ */
+ private com.google.protobuf.Value convertPrimitive(Value value) {
+ com.google.protobuf.Value.Builder builder = com.google.protobuf.Value.newBuilder();
+
+ if (value.isBoolean()) {
+ builder.setBoolValue(value.asBoolean());
+ } else if (value.isString()) {
+ builder.setStringValue(value.asString());
+ } else if (value.isInteger()) {
+ builder.setNumberValue(Double.valueOf(value.asInteger()));
+ } else if (value.isDouble()) {
+ builder.setNumberValue(value.asDouble());
+ } else {
+ builder.setNullValue(null);
+ }
+ return builder.build();
+ }
+
+ /**
+ * Convert protobuf value openfeature value.
+ */
+ private Value convertPrimitive(com.google.protobuf.Value protobuf) {
+ Value value;
+ if (protobuf.hasBoolValue()) {
+ value = new Value(protobuf.getBoolValue());
+ } else if (protobuf.hasStringValue()) {
+ value = new Value(protobuf.getStringValue());
+ } else if (protobuf.hasNumberValue()) {
+ value = new Value(protobuf.getNumberValue());
+ } else {
+ value = new Value((Boolean) null);
+ }
+ return value;
+ }
}
diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Protocol.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Protocol.java
new file mode 100644
index 000000000..fe81aac57
--- /dev/null
+++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/Protocol.java
@@ -0,0 +1,6 @@
+package dev.openfeature.contrib.providers.flagd;
+
+enum Protocol {
+ HTTP,
+ HTTPS
+}
\ No newline at end of file
diff --git a/providers/flagd/src/main/proto/.gitignore b/providers/flagd/src/main/proto/.gitignore
new file mode 100644
index 000000000..b4db259ba
--- /dev/null
+++ b/providers/flagd/src/main/proto/.gitignore
@@ -0,0 +1 @@
+schema.proto
\ No newline at end of file
diff --git a/providers/flagd/src/main/proto/.gitkeep b/providers/flagd/src/main/proto/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java
index f7f45fa17..101c03603 100644
--- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java
+++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java
@@ -1,15 +1,202 @@
package dev.openfeature.contrib.providers.flagd;
-import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
-import org.junit.jupiter.api.DisplayName;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import dev.openfeature.flagd.grpc.Schema.ResolveBooleanRequest;
+import dev.openfeature.flagd.grpc.Schema.ResolveBooleanResponse;
+import dev.openfeature.flagd.grpc.Schema.ResolveFloatResponse;
+import dev.openfeature.flagd.grpc.Schema.ResolveIntResponse;
+import dev.openfeature.flagd.grpc.Schema.ResolveObjectResponse;
+import dev.openfeature.flagd.grpc.Schema.ResolveStringResponse;
+import dev.openfeature.flagd.grpc.ServiceGrpc.ServiceBlockingStub;
+import dev.openfeature.javasdk.EvaluationContext;
+import dev.openfeature.javasdk.FlagEvaluationDetails;
+import dev.openfeature.javasdk.OpenFeatureAPI;
+import dev.openfeature.javasdk.Reason;
+import dev.openfeature.javasdk.Structure;
+import dev.openfeature.javasdk.Value;
+
class FlagdProviderTest {
+ static final String FLAG_KEY = "some-key";
+ static final String BOOL_VARIANT = "on";
+ static final String DOUBLE_VARIANT = "half";
+ static final String INT_VARIANT = "one-hundred";
+ static final String STRING_VARIANT = "greeting";
+ static final String OBJECT_VARIANT = "obj";
+ static final Reason DEFAULT = Reason.DEFAULT;
+ static final Integer INT_VALUE = 100;
+ static final Double DOUBLE_VALUE = .5d;
+ static final String INNER_STRUCT_KEY = "inner_key";
+ static final String INNER_STRUCT_VALUE = "inner_value";
+ static final Structure OBJECT_VALUE = new Structure() {{
+ add(INNER_STRUCT_KEY, INNER_STRUCT_VALUE);
+ }};
+ static final com.google.protobuf.Struct PROTOBUF_STRUCTURE_VALUE = com.google.protobuf.Struct.newBuilder()
+ .putFields(INNER_STRUCT_KEY, com.google.protobuf.Value.newBuilder().setStringValue(INNER_STRUCT_VALUE).build())
+ .build();
+ static final String STRING_VALUE = "hi!";
+
+ static OpenFeatureAPI api;
+
+ @BeforeAll
+ public static void init() {
+ api = OpenFeatureAPI.getInstance();
+ }
+
+ @Test
+ void resolvers_call_grpc_service_and_return_details() {
+ ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
+ .setValue(true)
+ .setVariant(BOOL_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveStringResponse stringResponse = ResolveStringResponse.newBuilder()
+ .setValue(STRING_VALUE)
+ .setVariant(STRING_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveIntResponse intResponse = ResolveIntResponse.newBuilder()
+ .setValue(INT_VALUE)
+ .setVariant(INT_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveFloatResponse floatResponse = ResolveFloatResponse.newBuilder()
+ .setValue(DOUBLE_VALUE)
+ .setVariant(DOUBLE_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ResolveObjectResponse objectResponse = ResolveObjectResponse.newBuilder()
+ .setValue(PROTOBUF_STRUCTURE_VALUE)
+ .setVariant(OBJECT_VARIANT)
+ .setReason(DEFAULT.toString())
+ .build();
+
+ ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+ when(serviceBlockingStubMock
+ .resolveBoolean(argThat(x -> FLAG_KEY.equals(x.getFlagKey())))).thenReturn(booleanResponse);
+ when(serviceBlockingStubMock
+ .resolveFloat(argThat(x -> FLAG_KEY.equals(x.getFlagKey())))).thenReturn(floatResponse);
+ when(serviceBlockingStubMock
+ .resolveInt(argThat(x -> FLAG_KEY.equals(x.getFlagKey())))).thenReturn(intResponse);
+ when(serviceBlockingStubMock
+ .resolveString(argThat(x -> FLAG_KEY.equals(x.getFlagKey())))).thenReturn(stringResponse);
+ when(serviceBlockingStubMock
+ .resolveObject(argThat(x -> FLAG_KEY.equals(x.getFlagKey())))).thenReturn(objectResponse);
+
+ OpenFeatureAPI.getInstance().setProvider(new FlagdProvider(serviceBlockingStubMock));
+
+ FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY, false);
+ assertTrue(booleanDetails.getValue());
+ assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
+ assertEquals(DEFAULT, booleanDetails.getReason());
+
+ FlagEvaluationDetails stringDetails = api.getClient().getStringDetails(FLAG_KEY, "wrong");
+ assertEquals(STRING_VALUE, stringDetails.getValue());
+ assertEquals(STRING_VARIANT, stringDetails.getVariant());
+ assertEquals(DEFAULT, stringDetails.getReason());
+
+ FlagEvaluationDetails intDetails = api.getClient().getIntegerDetails(FLAG_KEY, 0);
+ assertEquals(INT_VALUE, intDetails.getValue());
+ assertEquals(INT_VARIANT, intDetails.getVariant());
+ assertEquals(DEFAULT, intDetails.getReason());
+
+ FlagEvaluationDetails floatDetails = api.getClient().getDoubleDetails(FLAG_KEY, 0.1);
+ assertEquals(DOUBLE_VALUE, floatDetails.getValue());
+ assertEquals(DOUBLE_VARIANT, floatDetails.getVariant());
+ assertEquals(DEFAULT, floatDetails.getReason());
+
+ FlagEvaluationDetails objectDetails = api.getClient().getObjectDetails(FLAG_KEY, new Structure());
+ assertEquals(INNER_STRUCT_VALUE, objectDetails.getValue().asMap().get(INNER_STRUCT_KEY).asString());
+ assertEquals(OBJECT_VARIANT, objectDetails.getVariant());
+ assertEquals(DEFAULT, objectDetails.getReason());
+ }
+
@Test
- @DisplayName("a simple test")
- void test() {
- assertThat(FlagdProvider.test()).isEqualTo(true);
+ void context_is_parsed_and_passed_to_grpc_service() {
+
+ final String BOOLEAN_ATTR_KEY = "bool-attr";
+ final String INT_ATTR_KEY = "int-attr";
+ final String STRING_ATTR_KEY = "string-attr";
+ final String STRUCT_ATTR_KEY = "struct-attr";
+ final String DOUBLE_ATTR_KEY = "double-attr";
+ final String LIST_ATTR_KEY = "list-attr";
+ final String STRUCT_ATTR_INNER_KEY = "struct-inner-key";
+
+ final Boolean BOOLEAN_ATTR_VALUE = true;
+ final Integer INT_ATTR_VALUE = 1;
+ final String STRING_ATTR_VALUE = "str";
+ final Double DOUBLE_ATTR_VALUE = 0.5d;
+ final List LIST_ATTR_VALUE = new ArrayList() {{
+ add(new Value(1));
+ }};
+ final String STRUCT_ATTR_INNER_VALUE = "struct-inner-value";
+ final Structure STRUCT_ATTR_VALUE = new Structure().add(STRUCT_ATTR_INNER_KEY, STRUCT_ATTR_INNER_VALUE);
+ final String STATIC = "STATIC";
+
+ ResolveBooleanResponse booleanResponse = ResolveBooleanResponse.newBuilder()
+ .setValue(true)
+ .setVariant(BOOL_VARIANT)
+ .setReason(STATIC.toString())
+ .build();
+
+ ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+ when(serviceBlockingStubMock.resolveBoolean(argThat(x ->
+ STRING_ATTR_VALUE.equals(x.getContext().getFieldsMap().get(STRING_ATTR_KEY).getStringValue())
+ && INT_ATTR_VALUE == x.getContext().getFieldsMap().get(INT_ATTR_KEY).getNumberValue()
+ && DOUBLE_ATTR_VALUE == x.getContext().getFieldsMap().get(DOUBLE_ATTR_KEY).getNumberValue()
+ && LIST_ATTR_VALUE.get(0).asInteger() == x.getContext().getFieldsMap()
+ .get(LIST_ATTR_KEY).getListValue().getValuesList().get(0).getNumberValue()
+ && x.getContext().getFieldsMap().get(BOOLEAN_ATTR_KEY).getBoolValue()
+ && STRUCT_ATTR_INNER_VALUE.equals(x.getContext().getFieldsMap()
+ .get(STRUCT_ATTR_KEY).getStructValue().getFieldsMap().get(STRUCT_ATTR_INNER_KEY).getStringValue())
+ ))).thenReturn(booleanResponse);
+
+ OpenFeatureAPI.getInstance().setProvider(new FlagdProvider(serviceBlockingStubMock));
+
+ EvaluationContext context = new EvaluationContext();
+ context.add(BOOLEAN_ATTR_KEY, BOOLEAN_ATTR_VALUE);
+ context.add(INT_ATTR_KEY, INT_ATTR_VALUE);
+ context.add(DOUBLE_ATTR_KEY, DOUBLE_ATTR_VALUE);
+ context.add(LIST_ATTR_KEY, LIST_ATTR_VALUE);
+ context.add(STRING_ATTR_KEY, STRING_ATTR_VALUE);
+ context.add(STRUCT_ATTR_KEY, STRUCT_ATTR_VALUE);
+
+ FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY, false, context);
+ assertTrue(booleanDetails.getValue());
+ assertEquals(BOOL_VARIANT, booleanDetails.getVariant());
+ assertEquals(DEFAULT, booleanDetails.getReason()); // reason should be converted from STATIC -> DEFAULT
+ }
+
+ @Test
+ void reason_mapped_correctly_if_unknown() {
+ ResolveBooleanResponse badReasonResponse = ResolveBooleanResponse.newBuilder()
+ .setValue(true)
+ .setVariant(BOOL_VARIANT)
+ .setReason("NOT_A_REAL_REASON") // set an invalid reason string
+ .build();
+
+ ServiceBlockingStub serviceBlockingStubMock = mock(ServiceBlockingStub.class);
+ when(serviceBlockingStubMock.resolveBoolean(any(ResolveBooleanRequest.class))).thenReturn(badReasonResponse);
+
+ OpenFeatureAPI.getInstance().setProvider(new FlagdProvider(serviceBlockingStubMock));
+
+ FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY, false, new EvaluationContext());
+ assertEquals(Reason.UNKNOWN, booleanDetails.getReason()); // reason should be converted to UNKNOWN
}
-}
+}
\ No newline at end of file
diff --git a/providers/flagd/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/providers/flagd/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 000000000..ca6ee9cea
--- /dev/null
+++ b/providers/flagd/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file
diff --git a/spotbugs-exclusions.xml b/spotbugs-exclusions.xml
index 59e92ca80..0aad72fd8 100644
--- a/spotbugs-exclusions.xml
+++ b/spotbugs-exclusions.xml
@@ -1,24 +1,11 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+