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 @@ - + - - - - - - - - - - - - + - + +