From 350196c6882aadba1c037132ff61296c8212d871 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:46:39 +0000 Subject: [PATCH 1/8] chore(deps): update github/codeql-action digest to 3ecf990 (#557) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pullrequest.yml | 4 ++-- .github/workflows/static-code-scanning.yaml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 4e705114..0e8e34b1 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -27,7 +27,7 @@ jobs: cache: maven - name: Initialize CodeQL - uses: github/codeql-action/init@2160dd3b55fb3a72bfc85e989cad8aa785d2e8d5 + uses: github/codeql-action/init@3ecf990cd2aa6b9aba1927f4af47605ce8c9fe79 with: languages: java @@ -52,4 +52,4 @@ jobs: verbose: true # optional (default = false) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@2160dd3b55fb3a72bfc85e989cad8aa785d2e8d5 + uses: github/codeql-action/analyze@3ecf990cd2aa6b9aba1927f4af47605ce8c9fe79 diff --git a/.github/workflows/static-code-scanning.yaml b/.github/workflows/static-code-scanning.yaml index 00f33d2b..719cf96f 100644 --- a/.github/workflows/static-code-scanning.yaml +++ b/.github/workflows/static-code-scanning.yaml @@ -33,12 +33,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@2160dd3b55fb3a72bfc85e989cad8aa785d2e8d5 + uses: github/codeql-action/init@3ecf990cd2aa6b9aba1927f4af47605ce8c9fe79 with: languages: java - name: Autobuild - uses: github/codeql-action/autobuild@2160dd3b55fb3a72bfc85e989cad8aa785d2e8d5 + uses: github/codeql-action/autobuild@3ecf990cd2aa6b9aba1927f4af47605ce8c9fe79 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@2160dd3b55fb3a72bfc85e989cad8aa785d2e8d5 + uses: github/codeql-action/analyze@3ecf990cd2aa6b9aba1927f4af47605ce8c9fe79 From 6c52ee464fdb5d5733cb5a31e9cfea1644840177 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 01:33:46 +0000 Subject: [PATCH 2/8] chore(deps): update github/codeql-action digest to e683046 (#559) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pullrequest.yml | 4 ++-- .github/workflows/static-code-scanning.yaml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 0e8e34b1..1ebcb4b6 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -27,7 +27,7 @@ jobs: cache: maven - name: Initialize CodeQL - uses: github/codeql-action/init@3ecf990cd2aa6b9aba1927f4af47605ce8c9fe79 + uses: github/codeql-action/init@e683046da183a09174d531cc43713853e27debb3 with: languages: java @@ -52,4 +52,4 @@ jobs: verbose: true # optional (default = false) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3ecf990cd2aa6b9aba1927f4af47605ce8c9fe79 + uses: github/codeql-action/analyze@e683046da183a09174d531cc43713853e27debb3 diff --git a/.github/workflows/static-code-scanning.yaml b/.github/workflows/static-code-scanning.yaml index 719cf96f..e4a66ac5 100644 --- a/.github/workflows/static-code-scanning.yaml +++ b/.github/workflows/static-code-scanning.yaml @@ -33,12 +33,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@3ecf990cd2aa6b9aba1927f4af47605ce8c9fe79 + uses: github/codeql-action/init@e683046da183a09174d531cc43713853e27debb3 with: languages: java - name: Autobuild - uses: github/codeql-action/autobuild@3ecf990cd2aa6b9aba1927f4af47605ce8c9fe79 + uses: github/codeql-action/autobuild@e683046da183a09174d531cc43713853e27debb3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@3ecf990cd2aa6b9aba1927f4af47605ce8c9fe79 + uses: github/codeql-action/analyze@e683046da183a09174d531cc43713853e27debb3 From a741568762b96091bb0e50a32a8e00e863268429 Mon Sep 17 00:00:00 2001 From: Liran M <77168114+liran2000@users.noreply.github.com> Date: Tue, 15 Aug 2023 22:04:55 +0300 Subject: [PATCH 3/8] feat: In-memory provider for e2e testing and minimal usage (#546) * Adds InMemoryProvider to enable simple testing and basic usage Signed-off-by: liran2000 Co-authored-by: Todd Baert --- .github/workflows/pullrequest.yml | 7 - CONTRIBUTING.md | 9 +- pom.xml | 67 +++----- .../sdk/FlagEvaluationDetails.java | 1 + .../java/dev/openfeature/sdk/Structure.java | 14 ++ src/main/java/dev/openfeature/sdk/Value.java | 37 ++++ .../providers/memory/ContextEvaluator.java | 12 ++ .../sdk/providers/memory/Flag.java | 21 +++ .../providers/memory/InMemoryProvider.java | 162 ++++++++++++++++++ .../dev/openfeature/sdk/StructureTest.java | 42 ++++- .../openfeature/sdk/e2e/StepDefinitions.java | 29 ++-- .../memory/InMemoryProviderTest.java | 84 +++++++++ .../sdk/testutils/TestFlagsUtils.java | 73 ++++++++ 13 files changed, 489 insertions(+), 69 deletions(-) create mode 100644 src/main/java/dev/openfeature/sdk/providers/memory/ContextEvaluator.java create mode 100644 src/main/java/dev/openfeature/sdk/providers/memory/Flag.java create mode 100644 src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java create mode 100644 src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java create mode 100644 src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 1ebcb4b6..364ef65a 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -8,13 +8,6 @@ permissions: jobs: build: runs-on: ubuntu-latest - # TODO: this can be removed with https://github.com/open-feature/java-sdk/issues/523 - services: - flagd: - image: ghcr.io/open-feature/flagd-testbed:latest - ports: - - 8013:8013 - steps: - name: Check out the code uses: actions/checkout@96f53100ba2a5449eb71d2e6604bbcd94b9449b5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63c9d533..7c0b0383 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,14 +18,9 @@ If you're adding tests to cover something in the spec, use the `@Specification` ## End-to-End Tests - +The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) using `InMemoryProvider`. -The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) using [`flagd`](https://github.com/open-feature/flagd). These tests do not run with the default maven profile. If you'd like to run them locally, you can start the flagd testbed with - -``` -docker run -p 8013:8013 ghcr.io/open-feature/flagd-testbed:latest -``` -and then run +to run alone: ``` mvn test -P e2e-test ``` diff --git a/pom.xml b/pom.xml index 098d6eb0..09c52f91 100644 --- a/pom.xml +++ b/pom.xml @@ -11,8 +11,6 @@ 1.8 ${maven.compiler.source} 5.10.0 - - **/e2e/*.java ${groupId}.${artifactId} @@ -21,10 +19,10 @@ https://openfeature.dev - abrahms - Justin Abrahms - eBay - https://justin.abrah.ms/ + abrahms + Justin Abrahms + eBay + https://justin.abrah.ms/ @@ -120,9 +118,9 @@ - io.cucumber - cucumber-junit-platform-engine - test + io.cucumber + cucumber-junit-platform-engine + test @@ -139,39 +137,33 @@ test - - dev.openfeature.contrib.providers - flagd - 0.5.10 - test - - org.awaitility awaitility 4.2.0 test + - - io.cucumber - cucumber-bom - 7.13.0 - pom - import - - - - org.junit - junit-bom - 5.10.0 - pom - import - + + io.cucumber + cucumber-bom + 7.13.0 + pom + import + + + + org.junit + junit-bom + 5.10.0 + pom + import + @@ -203,7 +195,7 @@ - + maven-dependency-plugin 3.6.0 @@ -249,7 +241,7 @@ ${testExclusions} - + @@ -271,7 +263,7 @@ - prepare-agent + prepare-agent prepare-agent @@ -319,7 +311,7 @@ - + @@ -496,14 +488,11 @@ - - - e2e-test - + diff --git a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java index 78e04c71..b324c07c 100644 --- a/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java +++ b/src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java @@ -40,6 +40,7 @@ public static FlagEvaluationDetails from(ProviderEvaluation providerEv .value(providerEval.getValue()) .variant(providerEval.getVariant()) .reason(providerEval.getReason()) + .errorMessage(providerEval.getErrorMessage()) .errorCode(providerEval.getErrorCode()) .flagMetadata(providerEval.getFlagMetadata()) .build(); diff --git a/src/main/java/dev/openfeature/sdk/Structure.java b/src/main/java/dev/openfeature/sdk/Structure.java index f8b55212..46274e70 100644 --- a/src/main/java/dev/openfeature/sdk/Structure.java +++ b/src/main/java/dev/openfeature/sdk/Structure.java @@ -9,6 +9,8 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static dev.openfeature.sdk.Value.objectToValue; + /** * {@link Structure} represents a potentially nested object type which is used to represent * structured data. @@ -123,4 +125,16 @@ default Map merge(Function map) { + return new MutableStructure(map.entrySet().stream() + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap(Map.Entry::getKey, e -> objectToValue(e.getValue())))); + } } diff --git a/src/main/java/dev/openfeature/sdk/Value.java b/src/main/java/dev/openfeature/sdk/Value.java index 672b65c1..8be50179 100644 --- a/src/main/java/dev/openfeature/sdk/Value.java +++ b/src/main/java/dev/openfeature/sdk/Value.java @@ -5,10 +5,13 @@ import java.util.Map; import java.util.stream.Collectors; +import dev.openfeature.sdk.exceptions.TypeMismatchError; import lombok.EqualsAndHashCode; import lombok.SneakyThrows; import lombok.ToString; +import static dev.openfeature.sdk.Structure.mapToStructure; + /** * Values serve as a generic return type for structure data from providers. * Providers may deal in JSON, protobuf, XML or some other data-interchange format. @@ -280,4 +283,38 @@ protected Value clone() { } return new Value(this.asObject()); } + + /** + * Wrap an object into a Value. + * + * @param object the object to wrap + * @return the wrapped object + */ + public static Value objectToValue(Object object) { + if (object instanceof Value) { + return (Value) object; + } else if (object == null) { + return null; + } else if (object instanceof String) { + 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) { + return new Value(((List) object).stream() + .map(o -> objectToValue(o)) + .collect(Collectors.toList())); + } else if (object instanceof Instant) { + return new Value((Instant) object); + } else if (object instanceof Map) { + return new Value(mapToStructure((Map) object)); + } else { + throw new TypeMismatchError("Flag value " + object + " had unexpected type " + object.getClass() + "."); + } + } } diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/ContextEvaluator.java b/src/main/java/dev/openfeature/sdk/providers/memory/ContextEvaluator.java new file mode 100644 index 00000000..02fa323c --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/providers/memory/ContextEvaluator.java @@ -0,0 +1,12 @@ +package dev.openfeature.sdk.providers.memory; + +import dev.openfeature.sdk.EvaluationContext; + +/** + * Context evaluator - use for resolving flag according to evaluation context, for handling targeting. + * @param expected value type + */ +public interface ContextEvaluator { + + T evaluate(Flag flag, EvaluationContext evaluationContext); +} diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java new file mode 100644 index 00000000..8cfe85c9 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/providers/memory/Flag.java @@ -0,0 +1,21 @@ +package dev.openfeature.sdk.providers.memory; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; +import lombok.ToString; + +import java.util.Map; + +/** + * Flag representation for the in-memory provider. + */ +@ToString +@Builder +@Getter +public class Flag { + @Singular + private Map variants; + private String defaultVariant; + private ContextEvaluator contextEvaluator; +} diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java new file mode 100644 index 00000000..1006d88f --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java @@ -0,0 +1,162 @@ +package dev.openfeature.sdk.providers.memory; + +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.EventProvider; +import dev.openfeature.sdk.ProviderState; +import dev.openfeature.sdk.ProviderEventDetails; +import dev.openfeature.sdk.ErrorCode; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Reason; +import dev.openfeature.sdk.exceptions.OpenFeatureError; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.HashSet; +import java.util.Arrays; +import java.util.ArrayList; + +/** + * In-memory provider. + */ +@Slf4j +public class InMemoryProvider extends EventProvider { + + @Getter + private static final String NAME = "InMemoryProvider"; + + private Map> flags; + + @Getter + private ProviderState state = ProviderState.NOT_READY; + + @Override + public Metadata getMetadata() { + return () -> NAME; + } + + public InMemoryProvider(Map> flags) { + this.flags = new HashMap<>(flags); + } + + /** + * Initialize the provider. + * @param evaluationContext evaluation context + * @throws Exception on error + */ + public void initialize(EvaluationContext evaluationContext) throws Exception { + super.initialize(evaluationContext); + state = ProviderState.READY; + log.debug("finished initializing provider, state: {}", state); + } + + /** + * Updating provider flags configuration, replacing existing flags. + * @param flags the flags to use instead of the previous flags. + */ + public void updateFlags(Map> flags) { + Set flagsChanged = new HashSet<>(); + flagsChanged.addAll(this.flags.keySet()); + flagsChanged.addAll(flags.keySet()); + this.flags = new HashMap<>(flags); + ProviderEventDetails details = ProviderEventDetails.builder() + .flagsChanged(new ArrayList<>(flagsChanged)) + .message("flags changed") + .build(); + emitProviderConfigurationChanged(details); + } + + /** + * Updating provider flags configuration with adding or updating a flag. + * @param flag the flag to update. If a flag with this key already exists, new flag replaces it. + */ + public void updateFlag(String flagKey, Flag flag) { + this.flags.put(flagKey, flag); + ProviderEventDetails details = ProviderEventDetails.builder() + .flagsChanged(Arrays.asList(flagKey)) + .message("flag added/updated") + .build(); + emitProviderConfigurationChanged(details); + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, + EvaluationContext evaluationContext) { + return getEvaluation(key, defaultValue, evaluationContext, Boolean.class); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, + EvaluationContext evaluationContext) { + return getEvaluation(key, defaultValue, evaluationContext, String.class); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, + EvaluationContext evaluationContext) { + return getEvaluation(key, defaultValue, evaluationContext, Integer.class); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, + EvaluationContext evaluationContext) { + return getEvaluation(key, defaultValue, evaluationContext, Double.class); + } + + @SneakyThrows + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, + EvaluationContext evaluationContext) { + return getEvaluation(key, defaultValue, evaluationContext, Value.class); + } + + private ProviderEvaluation getEvaluation( + String key, T defaultValue, EvaluationContext evaluationContext, Class expectedType + ) throws OpenFeatureError { + if (!ProviderState.READY.equals(state)) { + ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; + if (ProviderState.ERROR.equals(state)) { + errorCode = ErrorCode.GENERAL; + } + return ProviderEvaluation.builder() + .errorCode(errorCode) + .reason(errorCode.name()) + .value(defaultValue) + .build(); + } + Flag flag = flags.get(key); + if (flag == null) { + return ProviderEvaluation.builder() + .value(defaultValue) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .build(); + } + T value; + if (flag.getContextEvaluator() != null) { + value = (T) flag.getContextEvaluator().evaluate(flag, evaluationContext); + } else if (!expectedType.isInstance(flag.getVariants().get(flag.getDefaultVariant()))) { + return ProviderEvaluation.builder() + .value(defaultValue) + .variant(flag.getDefaultVariant()) + .reason(Reason.ERROR.toString()) + .errorMessage(ErrorCode.TYPE_MISMATCH.name()) + .errorCode(ErrorCode.TYPE_MISMATCH) + .build(); + } else { + value = (T) flag.getVariants().get(flag.getDefaultVariant()); + } + return ProviderEvaluation.builder() + .value(value) + .variant(flag.getDefaultVariant()) + .reason(Reason.STATIC.toString()) + .build(); + } + +} diff --git a/src/test/java/dev/openfeature/sdk/StructureTest.java b/src/test/java/dev/openfeature/sdk/StructureTest.java index f05f9302..8f188911 100644 --- a/src/test/java/dev/openfeature/sdk/StructureTest.java +++ b/src/test/java/dev/openfeature/sdk/StructureTest.java @@ -1,16 +1,20 @@ package dev.openfeature.sdk; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertTrue; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.junit.jupiter.api.Test; +import static dev.openfeature.sdk.Structure.mapToStructure; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class StructureTest { @Test public void noArgShouldContainEmptyAttributes() { @@ -46,7 +50,7 @@ public class StructureTest { double DOUBLE_VAL = .5; Instant DATE_VAL = Instant.now(); MutableStructure STRUCT_VAL = new MutableStructure(); - List LIST_VAL = new ArrayList(); + List LIST_VAL = new ArrayList<>(); Value VALUE_VAL = new Value(); MutableStructure structure = new MutableStructure(); @@ -68,4 +72,32 @@ public class StructureTest { assertEquals(LIST_VAL, structure.getValue(LIST_KEY).asList()); assertTrue(structure.getValue(VALUE_KEY).isNull()); } + + @SneakyThrows + @Test + void mapToStructureTest() { + Map map = new HashMap<>(); + map.put("String", "str"); + map.put("Boolean", true); + map.put("Integer", 1); + map.put("Double", 1.1); + map.put("List", Collections.singletonList(new Value(1))); + map.put("Value", new Value((true))); + map.put("Instant", Instant.ofEpochSecond(0)); + map.put("Map", new HashMap<>()); + map.put("nullKey", null); + ImmutableContext immutableContext = new ImmutableContext(); + map.put("ImmutableContext", immutableContext); + Structure res = mapToStructure(map); + assertEquals(new Value("str"), res.getValue("String")); + assertEquals(new Value(true), res.getValue("Boolean")); + assertEquals(new Value(1), res.getValue("Integer")); + assertEquals(new Value(1.1), res.getValue("Double")); + assertEquals(new Value(Collections.singletonList(new Value(1))), res.getValue("List")); + assertEquals(new Value(true), res.getValue("Value")); + assertEquals(new Value(Instant.ofEpochSecond(0)), res.getValue("Instant")); + assertEquals(new HashMap<>(), res.getValue("Map").asStructure().asMap()); + assertEquals(new Value(immutableContext), res.getValue("ImmutableContext")); + assertNull(res.getValue("nullKey")); + } } diff --git a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java index 7048fc0b..650fa242 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java @@ -1,22 +1,25 @@ package dev.openfeature.sdk.e2e; -import dev.openfeature.contrib.providers.flagd.FlagdProvider; -import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.Value; import dev.openfeature.sdk.EvaluationContext; -import dev.openfeature.sdk.FlagEvaluationDetails; -import dev.openfeature.sdk.ImmutableContext; -import dev.openfeature.sdk.OpenFeatureAPI; import dev.openfeature.sdk.Reason; +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.OpenFeatureAPI; import dev.openfeature.sdk.Structure; -import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.providers.memory.Flag; +import dev.openfeature.sdk.providers.memory.InMemoryProvider; import io.cucumber.java.BeforeAll; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; +import lombok.SneakyThrows; import java.util.HashMap; import java.util.Map; +import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -47,13 +50,17 @@ public class StepDefinitions { private int typeErrorDefaultValue; private FlagEvaluationDetails typeErrorDetails; + @SneakyThrows @BeforeAll() @Given("an openfeature client is registered with cache disabled") public static void setup() { - // TODO: when the FlagdProvider is updated to support caching, we might need to disable it here for this test to work as expected. - FlagdProvider provider = new FlagdProvider(); - provider.setDeadline(3000); // set a generous deadline, to prevent timeouts in actions + Map> flags = buildFlags(); + InMemoryProvider provider = new InMemoryProvider(flags); OpenFeatureAPI.getInstance().setProvider(provider); + + // TODO: setProvider with wait for init, pending https://github.com/open-feature/ofep/pull/80 + Thread.sleep(500); + client = OpenFeatureAPI.getInstance().getClient(); } @@ -265,7 +272,7 @@ public void then_the_default_string_value_should_be_returned() { public void the_reason_should_indicate_an_error_and_the_error_code_should_be_flag_not_found(String errorCode) { assertEquals(Reason.ERROR.toString(), notFoundDetails.getReason()); assertTrue(notFoundDetails.getErrorMessage().contains(errorCode)); - // TODO: add errorCode assertion once flagd provider is updated. + assertTrue(notFoundDetails.getErrorCode().name().equals(errorCode)); } // type mismatch @@ -286,7 +293,7 @@ public void then_the_default_integer_value_should_be_returned() { public void the_reason_should_indicate_an_error_and_the_error_code_should_be_type_mismatch(String errorCode) { assertEquals(Reason.ERROR.toString(), typeErrorDetails.getReason()); assertTrue(typeErrorDetails.getErrorMessage().contains(errorCode)); - // TODO: add errorCode assertion once flagd provider is updated. + assertTrue(typeErrorDetails.getErrorCode().name().equals(errorCode)); } } diff --git a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java new file mode 100644 index 00000000..f05a6b79 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -0,0 +1,84 @@ +package dev.openfeature.sdk.providers.memory; + +import com.google.common.collect.ImmutableMap; +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.Value; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static dev.openfeature.sdk.Structure.mapToStructure; +import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; +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.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class InMemoryProviderTest { + + private static Client client; + + private static InMemoryProvider provider; + + @SneakyThrows + @BeforeAll + static void beforeAll() { + Map> flags = buildFlags(); + provider = spy(new InMemoryProvider(flags)); + OpenFeatureAPI.getInstance().onProviderConfigurationChanged(eventDetails -> {}); + OpenFeatureAPI.getInstance().setProvider(provider); + + // TODO: setProvider with wait for init, pending https://github.com/open-feature/ofep/pull/80 + Thread.sleep(500); + + client = OpenFeatureAPI.getInstance().getClient(); + provider.updateFlags(flags); + provider.updateFlag("addedFlag", Flag.builder() + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .build()); + } + + @SneakyThrows + @Test + void eventsTest() { + verify(provider, times(2)).emitProviderConfigurationChanged(any()); + } + + @Test + void getBooleanEvaluation() { + assertTrue(client.getBooleanValue("boolean-flag", false)); + } + + @Test + void getStringEvaluation() { + assertEquals("hi", client.getStringValue("string-flag", "dummy")); + } + + @Test + void getIntegerEvaluation() { + assertEquals(10, client.getIntegerValue("integer-flag", 999)); + } + + @Test + void getDoubleEvaluation() { + assertEquals(0.5, client.getDoubleValue("float-flag", 9.99)); + } + + @Test + void getObjectEvaluation() { + Value expectedObject = new Value(mapToStructure(ImmutableMap.of( + "showImages", new Value(true), + "title", new Value("Check out these pics!"), + "imagesPerPage", new Value(100) + ))); + assertEquals(expectedObject, client.getObjectValue("object-flag", new Value(true))); + } + +} \ No newline at end of file diff --git a/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java new file mode 100644 index 00000000..d9035929 --- /dev/null +++ b/src/test/java/dev/openfeature/sdk/testutils/TestFlagsUtils.java @@ -0,0 +1,73 @@ +package dev.openfeature.sdk.testutils; + +import com.google.common.collect.ImmutableMap; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.providers.memory.Flag; +import lombok.experimental.UtilityClass; + +import java.util.HashMap; +import java.util.Map; + +import static dev.openfeature.sdk.Structure.mapToStructure; + +/** + * Test flags utils. + */ +@UtilityClass +public class TestFlagsUtils { + + /** + * Building flags for testing purposes. + * @return map of flags + */ + public static Map> buildFlags() { + Map> flags = new HashMap<>(); + flags.put("boolean-flag", Flag.builder() + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .build()); + flags.put("string-flag", Flag.builder() + .variant("greeting", "hi") + .variant("parting", "bye") + .defaultVariant("greeting") + .build()); + flags.put("integer-flag", Flag.builder() + .variant("one", 1) + .variant("ten", 10) + .defaultVariant("ten") + .build()); + flags.put("float-flag", Flag.builder() + .variant("tenth", 0.1) + .variant("half", 0.5) + .defaultVariant("half") + .build()); + flags.put("object-flag", Flag.builder() + .variant("empty", new HashMap<>()) + .variant("template", new Value(mapToStructure(ImmutableMap.of( + "showImages", new Value(true), + "title", new Value("Check out these pics!"), + "imagesPerPage", new Value(100) + )))) + .defaultVariant("template") + .build()); + flags.put("context-aware", Flag.builder() + .variant("internal", "INTERNAL") + .variant("external", "EXTERNAL") + .defaultVariant("external") + .contextEvaluator((flag, evaluationContext) -> { + if (new Value(false).equals(evaluationContext.getValue("customer"))) { + return (String) flag.getVariants().get("internal"); + } else { + return (String) flag.getVariants().get(flag.getDefaultVariant()); + } + }) + .build()); + flags.put("wrong-flag", Flag.builder() + .variant("one", "uno") + .variant("two", "dos") + .defaultVariant("one") + .build()); + return flags; + } +} From 94a5a869d8f2d41adf632f60db1eb0f7e9b530ec Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Tue, 15 Aug 2023 15:20:22 -0400 Subject: [PATCH 4/8] chore: fix e2e profile Signed-off-by: Todd Baert --- .github/workflows/pullrequest.yml | 2 +- CONTRIBUTING.md | 2 +- pom.xml | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 364ef65a..56431667 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -33,7 +33,7 @@ jobs: ${{ runner.os }}-maven- - name: Verify with Maven - run: mvn --batch-mode --update-snapshots --activate-profiles e2e-test verify + run: mvn --batch-mode --update-snapshots --activate-profiles e2e verify - name: Upload coverage to Codecov uses: codecov/codecov-action@e1dd05cde2ed37d100f658b34ea423728ba1812e diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c0b0383..8acd316a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ The continuous integration runs a set of [gherkin e2e tests](https://github.com/ to run alone: ``` -mvn test -P e2e-test +mvn test -P e2e ``` ## Releasing diff --git a/pom.xml b/pom.xml index 09c52f91..8444030b 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,8 @@ 1.8 ${maven.compiler.source} 5.10.0 + + **/e2e/*.java ${groupId}.${artifactId} @@ -488,7 +490,7 @@ - e2e-test + e2e From 1f28921fec2cece6d492fb77160bb97dd05c3e08 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 16 Aug 2023 12:37:21 -0400 Subject: [PATCH 5/8] chore: fix jacoco coverage minimum, throw in memory provider (#561) chore: fix codecov, throw in memory provider Signed-off-by: Todd Baert --- .../sdk/exceptions/ProviderNotReadyError.java | 12 ++++ .../providers/memory/InMemoryProvider.java | 67 ++++++++----------- .../openfeature/sdk/e2e/StepDefinitions.java | 2 - .../memory/InMemoryProviderTest.java | 18 +++++ 4 files changed, 57 insertions(+), 42 deletions(-) create mode 100644 src/main/java/dev/openfeature/sdk/exceptions/ProviderNotReadyError.java diff --git a/src/main/java/dev/openfeature/sdk/exceptions/ProviderNotReadyError.java b/src/main/java/dev/openfeature/sdk/exceptions/ProviderNotReadyError.java new file mode 100644 index 00000000..21807344 --- /dev/null +++ b/src/main/java/dev/openfeature/sdk/exceptions/ProviderNotReadyError.java @@ -0,0 +1,12 @@ +package dev.openfeature.sdk.exceptions; + +import dev.openfeature.sdk.ErrorCode; +import lombok.Getter; +import lombok.experimental.StandardException; + +@SuppressWarnings("checkstyle:MissingJavadocType") +@StandardException +public class ProviderNotReadyError extends OpenFeatureError { + private static final long serialVersionUID = 1L; + @Getter private final ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; +} diff --git a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java index 1006d88f..f71e9e36 100644 --- a/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java +++ b/src/main/java/dev/openfeature/sdk/providers/memory/InMemoryProvider.java @@ -1,26 +1,29 @@ package dev.openfeature.sdk.providers.memory; -import dev.openfeature.sdk.Value; -import dev.openfeature.sdk.Metadata; -import dev.openfeature.sdk.EventProvider; -import dev.openfeature.sdk.ProviderState; -import dev.openfeature.sdk.ProviderEventDetails; -import dev.openfeature.sdk.ErrorCode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.EventProvider; +import dev.openfeature.sdk.Metadata; import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.ProviderEventDetails; +import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Reason; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.exceptions.OpenFeatureError; +import dev.openfeature.sdk.exceptions.ProviderNotReadyError; +import dev.openfeature.sdk.exceptions.TypeMismatchError; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import java.util.Map; -import java.util.HashMap; -import java.util.Set; -import java.util.HashSet; -import java.util.Arrays; -import java.util.ArrayList; - /** * In-memory provider. */ @@ -87,68 +90,52 @@ public void updateFlag(String flagKey, Flag flag) { @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext evaluationContext) { - return getEvaluation(key, defaultValue, evaluationContext, Boolean.class); + return getEvaluation(key, evaluationContext, Boolean.class); } @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext evaluationContext) { - return getEvaluation(key, defaultValue, evaluationContext, String.class); + return getEvaluation(key, evaluationContext, String.class); } @Override public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext evaluationContext) { - return getEvaluation(key, defaultValue, evaluationContext, Integer.class); + return getEvaluation(key, evaluationContext, Integer.class); } @Override public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext evaluationContext) { - return getEvaluation(key, defaultValue, evaluationContext, Double.class); + return getEvaluation(key, evaluationContext, Double.class); } @SneakyThrows @Override public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext evaluationContext) { - return getEvaluation(key, defaultValue, evaluationContext, Value.class); + return getEvaluation(key, evaluationContext, Value.class); } private ProviderEvaluation getEvaluation( - String key, T defaultValue, EvaluationContext evaluationContext, Class expectedType + String key, EvaluationContext evaluationContext, Class expectedType ) throws OpenFeatureError { if (!ProviderState.READY.equals(state)) { - ErrorCode errorCode = ErrorCode.PROVIDER_NOT_READY; - if (ProviderState.ERROR.equals(state)) { - errorCode = ErrorCode.GENERAL; + if (ProviderState.NOT_READY.equals(state)) { + throw new ProviderNotReadyError("provider not yet initialized"); } - return ProviderEvaluation.builder() - .errorCode(errorCode) - .reason(errorCode.name()) - .value(defaultValue) - .build(); + throw new GeneralError("unknown error"); } Flag flag = flags.get(key); if (flag == null) { - return ProviderEvaluation.builder() - .value(defaultValue) - .reason(Reason.ERROR.toString()) - .errorMessage(ErrorCode.FLAG_NOT_FOUND.name()) - .errorCode(ErrorCode.FLAG_NOT_FOUND) - .build(); + throw new FlagNotFoundError("flag " + key + "not found"); } T value; if (flag.getContextEvaluator() != null) { value = (T) flag.getContextEvaluator().evaluate(flag, evaluationContext); } else if (!expectedType.isInstance(flag.getVariants().get(flag.getDefaultVariant()))) { - return ProviderEvaluation.builder() - .value(defaultValue) - .variant(flag.getDefaultVariant()) - .reason(Reason.ERROR.toString()) - .errorMessage(ErrorCode.TYPE_MISMATCH.name()) - .errorCode(ErrorCode.TYPE_MISMATCH) - .build(); + throw new TypeMismatchError("flag " + key + "is not of expected type"); } else { value = (T) flag.getVariants().get(flag.getDefaultVariant()); } diff --git a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java index 650fa242..903f0bf8 100644 --- a/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java +++ b/src/test/java/dev/openfeature/sdk/e2e/StepDefinitions.java @@ -271,7 +271,6 @@ public void then_the_default_string_value_should_be_returned() { @Then("the reason should indicate an error and the error code should indicate a missing flag with {string}") public void the_reason_should_indicate_an_error_and_the_error_code_should_be_flag_not_found(String errorCode) { assertEquals(Reason.ERROR.toString(), notFoundDetails.getReason()); - assertTrue(notFoundDetails.getErrorMessage().contains(errorCode)); assertTrue(notFoundDetails.getErrorCode().name().equals(errorCode)); } @@ -292,7 +291,6 @@ public void then_the_default_integer_value_should_be_returned() { @Then("the reason should indicate an error and the error code should indicate a type mismatch with {string}") public void the_reason_should_indicate_an_error_and_the_error_code_should_be_type_mismatch(String errorCode) { assertEquals(Reason.ERROR.toString(), typeErrorDetails.getReason()); - assertTrue(typeErrorDetails.getErrorMessage().contains(errorCode)); assertTrue(typeErrorDetails.getErrorCode().name().equals(errorCode)); } diff --git a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java index f05a6b79..4cfbb812 100644 --- a/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java +++ b/src/test/java/dev/openfeature/sdk/providers/memory/InMemoryProviderTest.java @@ -2,17 +2,22 @@ import com.google.common.collect.ImmutableMap; import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.OpenFeatureAPI; import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import dev.openfeature.sdk.exceptions.TypeMismatchError; import lombok.SneakyThrows; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.omg.CORBA.DynAnyPackage.TypeMismatch; import java.util.Map; import static dev.openfeature.sdk.Structure.mapToStructure; import static dev.openfeature.sdk.testutils.TestFlagsUtils.buildFlags; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; @@ -81,4 +86,17 @@ void getObjectEvaluation() { assertEquals(expectedObject, client.getObjectValue("object-flag", new Value(true))); } + @Test + void notFound() { + assertThrows(FlagNotFoundError.class, () -> { + provider.getBooleanEvaluation("not-found-flag", false, new ImmutableContext()); + }); + } + + @Test + void typeMismatch() { + assertThrows(TypeMismatchError.class, () -> { + provider.getBooleanEvaluation("string-flag", false, new ImmutableContext()); + }); + } } \ No newline at end of file From 3496366ae880e85c08e0b5925e9220b55a67c8eb Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 16 Aug 2023 12:44:39 -0400 Subject: [PATCH 6/8] docs: update README to be auto-included in openfeature.dev (#560) * chore: update README to be auto-included in openfeatre.dev Signed-off-by: Todd Baert Co-authored-by: Michael Beemer Co-authored-by: Kavindu Dodanduwa --- README.md | 347 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 241 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index b453b426..131d316b 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,58 @@ +

- - + + OpenFeature Logo

OpenFeature Java SDK

-[![Specification](https://img.shields.io/static/v1?label=Specification&message=v0.6.0&color=yellow)](https://github.com/open-feature/spec/tree/v0.6.0) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.openfeature/sdk/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.openfeature/sdk) -[![javadoc](https://javadoc.io/badge2/dev.openfeature/sdk/javadoc.svg)](https://javadoc.io/doc/dev.openfeature/sdk) -[![Project Status: Active โ€“ The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) -[![Known Vulnerabilities](https://snyk.io/test/github/open-feature/java-sdk/badge.svg)](https://snyk.io/test/github/open-feature/java-sdk) -[![on-merge](https://github.com/open-feature/java-sdk/actions/workflows/merge.yml/badge.svg)](https://github.com/open-feature/java-sdk/actions/workflows/merge.yml) -[![codecov](https://codecov.io/gh/open-feature/java-sdk/branch/main/graph/badge.svg?token=XMS9L7PBY1)](https://codecov.io/gh/open-feature/java-sdk) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6241/badge)](https://bestpractices.coreinfrastructure.org/projects/6241) - -## ๐Ÿ‘‹ Hey there! Thanks for checking out the OpenFeature Java SDK - -### What is OpenFeature? - -[OpenFeature][openfeature-website] is an open standard that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. + + +

+ + Specification + + + + + Release + + + +
+ + Javadoc + + + Maven Central + + + Codecov + + + CII Best Practices + +

+ -### Why standardize feature flags? +[OpenFeature](https://openfeature.dev) is an open standard that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. -Standardizing feature flags unifies tools and vendors behind a common interface which avoids vendor lock-in at the code level. Additionally, it offers a framework for building extensions and integrations and allows providers to focus on their unique value proposition. + +## ๐Ÿš€ Quick start -## ๐Ÿ” Requirements +### Requirements - Java 8+ (compiler target is 1.8) Note that this library is intended to be used in server-side contexts and has not been evaluated for use in mobile devices. -## ๐Ÿ“ฆ Installation +### Install -### Maven +#### Maven ```xml @@ -63,7 +79,8 @@ If you would like snapshot builds, this is the relevant repository information: ``` -### Gradle +#### Gradle + ```groovy dependencies { @@ -72,68 +89,153 @@ dependencies { ``` -### Software Bill of Materials (SBOM) - -We publish SBOMs with all of our releases as of 0.3.0. You can find them in Maven Central alongside the artifacts. - -## ๐ŸŒŸ Features - -- support for various backend [providers](https://openfeature.dev/docs/reference/concepts/provider) -- easy integration and extension via [hooks](https://openfeature.dev/docs/reference/concepts/hooks) -- bool, string, numeric, and object flag types -- [context-aware](https://openfeature.dev/docs/reference/concepts/evaluation-context) evaluation - -## ๐Ÿš€ Usage +### Usage ```java public void example(){ + // flags defined in memory + Map> myFlags = new HashMap<>(); + flags.put("v2_enabled", Flag.builder() + .variant("on", true) + .variant("off", false) + .defaultVariant("on") + .build()); + // configure a provider OpenFeatureAPI api = OpenFeatureAPI.getInstance(); - api.setProvider(new MyProviderOfChoice()); + api.setProvider(new InMemoryProvider(myFlags)); // create a client Client client = api.getClient(); // get a bool flag value - boolean flagValue = client.getBooleanValue("boolFlag", false); + boolean flagValue = client.getBooleanValue("v2_enabled", false); } ``` -### Context-aware evaluation +### API Reference + +See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs. + +## ๐ŸŒŸ Features + +| Status | Features | Description | +| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| โœ… | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| โœ… | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| โœ… | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| โœ… | [Logging](#logging) | Integrate with popular logging packages. | +| โœ… | [Named clients](#named-clients) | Utilize multiple providers in a single application. | +| โœ… | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| โœ… | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| โœ… | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | + +Implemented: โœ… | In-progress: โš ๏ธ | Not implemented yet: โŒ -Sometimes the value of a flag must take into account some dynamic criteria about the application or user, such as the user location, IP, email address, or the location of the server. -In OpenFeature, we refer to this as [`targeting`](https://openfeature.dev/specification/glossary#targeting). -If the flag system you're using supports targeting, you can provide the input data using the `EvaluationContext`. +### Providers + +[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK. +Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Java) for a complete list of available providers. +If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself. + +Once you've added a provider as a dependency, it can be registered with OpenFeature like this: + +```java + OpenFeatureAPI.getInstance().setProvider(new MyProvider()); +``` + +In some situations, it may be beneficial to register multiple providers in the same application. +This is possible using [named clients](#named-clients), which is covered in more details below. + +### Targeting + +Sometimes, the value of a flag must consider some dynamic criteria about the application or user, such as the user's location, IP, email address, or the server's location. +In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting). +If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). ```java -// global context for static data +// set a value to the global context OpenFeatureAPI api = OpenFeatureAPI.getInstance(); -Map attributes = new HashMap<>(); -attributes.put("appVersion", new Value(System.getEnv("APP_VERSION"))); -EvaluationContext apiCtx = new ImmutableContext(attributes); +Map apiAttrs = new HashMap<>(); +apiAttrs.put("region", new Value(System.getEnv("us-east-1"))); +EvaluationContext apiCtx = new ImmutableContext(apiAttrs); api.setEvaluationContext(apiCtx); -// request context -Map attributes = new HashMap<>(); -attributes.put("email", new Value(session.getAttribute("email"))); -attributes.put("product", new Value(productId)); +// set a value to the client context +Map clientAttrs = new HashMap<>(); +clientAttrs.put("region", new Value(System.getEnv("us-east-1"))); +EvaluationContext clientCtx = new ImmutableContext(clientAttrs); +Client client = api.getInstance().getClient(); +client.setEvaluationContext(clientCtx); + +// set a value to the invocation context +Map requestAttrs = new HashMap<>(); +requestAttrs.put("email", new Value(session.getAttribute("email"))); +requestAttrs.put("product", new Value("productId")); String targetingKey = session.getId(); -EvaluationContext reqCtx = new ImmutableContext(targetingKey, attributes); +EvaluationContext reqCtx = new ImmutableContext(targetingKey, requestAttrs); -// use merged contextual data to determine a flag value boolean flagValue = client.getBooleanValue("some-flag", false, reqCtx); ``` -### Events +### Hooks + +[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle +Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Java) for a complete list of available hooks. +If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself. + +Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level. + +```java + // add a hook globally, to run on all evaluations + OpenFeatureAPI api = OpenFeatureAPI.getInstance(); + api.addHooks(new ExampleHook()); + + // add a hook on this client, to run on all evaluations made by this client + Client client = api.getClient(); + client.addHooks(new ExampleHook()); + + // add a hook for this evaluation only + Boolean retval = client.getBooleanValue(flagKey, false, null, + FlagEvaluationOptions.builder().hook(new ExampleHook()).build()); +``` + +### Logging + +The Java SDK uses SLF4J. See the [SLF4J manual](https://slf4j.org/manual.html) for complete documentation. + +### Named clients + +Clients can be given a name. +A name is a logical identifier which can be used to associate clients with a particular provider. +If a name has no associated provider, the global provider is used. + +```java +FeatureProvider scopedProvider = new MyProvider(); + +// registering the default provider +OpenFeatureAPI.getInstance().setProvider(LocalProvider()); +// registering a named provider +OpenFeatureAPI.getInstance().setProvider("clientForCache", new CachedProvider()); + +// a client backed by default provider +Client clientDefault = OpenFeatureAPI.getInstance().getClient(); +// a client backed by CachedProvider +Client clientNamed = OpenFeatureAPI.getInstance().getClient("clientForCache"); +``` + +### Eventing Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions. Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider. Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`. + Please refer to the documentation of the provider you're using to see what events are supported. ```java // add an event handler to a client +Client client = OpenFeatureAPI.getInstance().getClient(); client.onProviderConfigurationChanged((EventDetails eventDetails) -> { // do something when the provider's flag settings change }); @@ -144,62 +246,46 @@ OpenFeatureAPI.getInstance().onProviderStale((EventDetails eventDetails) -> { }); ``` -### Hooks +### Shutdown -A hook is a mechanism that allows for adding arbitrary behavior at well-defined points of the flag evaluation life-cycle. -Use cases include validating the resolved flag value, modifying or adding data to the evaluation context, logging, telemetry, and tracking. +The OpenFeature API provides a close function to perform a cleanup of all registered providers. +This should only be called when your application is in the process of shutting down. ```java -public class MyHook implements Hook { - /** - * - * @param ctx Information about the particular flag evaluation - * @param details Information about how the flag was resolved, including any resolved values. - * @param hints An immutable mapping of data for users to communicate to the hooks. - */ - @Override - public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) { - System.out.println("After evaluation!"); - } -} +// shut down all providers +OpenFeatureAPI.getInstance().shutdown(); ``` -See [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Java) for a catalog of available hooks. +## Extending -### Logging: - -The Java SDK uses SLF4J. See the [SLF4J manual](https://slf4j.org/manual.html) for complete documentation. - -### Named clients - -Clients can be given a name. -A name is a logical identifier which can be used to associate clients with a particular provider. -If a name has no associated provider, clients with that name use the global provider. - -```java -FeatureProvider scopedProvider = new MyProvider(); - -// set this provider for clients named "my-name" -OpenFeatureAPI.getInstance().setProvider("my-name", provider); - -// create a client bound to the provider above -Client client = OpenFeatureAPI.getInstance().getClient("my-name"); -``` - -### Providers: +### Develop a provider To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency. This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/java-sdk-contrib) available under the OpenFeature organization. -Finally, youโ€™ll then need to write the provider itself. -This can be accomplished by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. +Youโ€™ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK. ```java public class MyProvider implements FeatureProvider { -@Override + @Override public Metadata getMetadata() { return () -> "My Provider"; } + @Override + public ProviderState getState() { + // optionally indicate your provider's state (assumed to be READY if not implemented) + } + + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + // start up your provider + } + + @Override + public void shutdown() { + // shut down your provider + } + @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { // resolve a boolean flag value @@ -227,22 +313,76 @@ public class MyProvider implements FeatureProvider { } ``` -See [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Java) for a catalog of available providers. +If you'd like your provider to support firing events, such as events for when flags are changed in the flag management system, extend `EventProvider`. -### Shutdown +```java +class MyEventProvider extends EventProvider { + @Override + public Metadata getMetadata() { + return () -> "My Event Provider"; + } -The OpenFeature API provides a close function to perform a cleanup of all registered providers. -This should only be called when your application is in the process of shutting down. + @Override + public ProviderState getState() { + // indicate your provider's state (required for EventProviders) + } -```java -// shut down all providers -OpenFeatureAPI.getInstance().shutdown(); + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + // emit events when flags are changed in a hypothetical REST API + this.restApiClient.onFlagsChanged(() -> { + ProviderEventDetails details = ProviderEventDetails.builder().message("flags changed in API!").build(); + this.emitProviderConfigurationChanged(details); + }); + } + + @Override + public void shutdown() { + // shut down your provider + } + + // remaining provider methods... +} ``` -### Complete API documentation: +> Built a new provider? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&projects=&template=document-provider.yaml&title=%5BProvider%5D%3A+) so we can add it to the docs! + +### Develop a hook + +To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency. +This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/java-sdk-contrib) available under the OpenFeature organization. +Implement your own hook by conforming to the `Hook interface`. +To satisfy the interface, all methods (`Before`/`After`/`Finally`/`Error`) need to be defined. +To avoid defining empty functions make use of the `UnimplementedHook` struct (which already implements all the empty functions). + +```java +class MyHook implements Hook { + + @Override + public Optional before(HookContext ctx, Map hints) { + // code that runs before the flag evaluation + } + + @Override + public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) { + // code that runs after the flag evaluation succeeds + } -See [here](https://www.javadoc.io/doc/dev.openfeature/sdk/latest/index.html) for the complete API documentation. + @Override + public void error(HookContext ctx, Exception error, Map hints) { + // code that runs when there's an error during a flag evaluation + } + @Override + public void finallyAfter(HookContext ctx, Map hints) { + // code that runs regardless of success or error + } +}; +``` + +> Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs! + + ## โญ๏ธ Support the project - Give this repo a โญ๏ธ! @@ -263,9 +403,4 @@ Interested in contributing? Great, we'd love your help! To get started, take a l Made with [contrib.rocks](https://contrib.rocks). - -## ๐Ÿ“œ License - -[Apache License 2.0](LICENSE) - -[openfeature-website]: https://openfeature.dev + From 4a4d81367b50ab957d99371ffe368c0977768329 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 19:15:57 +0000 Subject: [PATCH 7/8] chore(deps): update actions/setup-java digest to 5b86b67 (#562) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/merge.yml | 2 +- .github/workflows/pullrequest.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index d82baeb3..153fd0a5 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@96f53100ba2a5449eb71d2e6604bbcd94b9449b5 - name: Set up JDK 8 - uses: actions/setup-java@b943a4ed876ec91f6d0cde2181d88d24e8e348de + uses: actions/setup-java@5b86b67f5bb794ee4de9464f70b700b9445b03a8 with: java-version: '8' distribution: 'temurin' diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 56431667..dd5d05b2 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@96f53100ba2a5449eb71d2e6604bbcd94b9449b5 - name: Set up JDK 8 - uses: actions/setup-java@b943a4ed876ec91f6d0cde2181d88d24e8e348de + uses: actions/setup-java@5b86b67f5bb794ee4de9464f70b700b9445b03a8 with: java-version: '8' distribution: 'temurin' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8b9d79d..6a77578c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@96f53100ba2a5449eb71d2e6604bbcd94b9449b5 - name: Set up JDK 8 if: ${{ steps.release.outputs.releases_created }} - uses: actions/setup-java@b943a4ed876ec91f6d0cde2181d88d24e8e348de + uses: actions/setup-java@5b86b67f5bb794ee4de9464f70b700b9445b03a8 with: java-version: '8' distribution: 'temurin' From 703551f54ce81ba43a4dad0fceb73be6ade52977 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 16:19:01 -0400 Subject: [PATCH 8/8] chore(main): release 1.5.0 (#558) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 22 ++++++++++++++++++++++ README.md | 8 ++++---- pom.xml | 2 +- version.txt | 2 +- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 54824065..287b4c71 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{".":"1.4.3"} \ No newline at end of file +{".":"1.5.0"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed96d88..51917159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [1.5.0](https://github.com/open-feature/java-sdk/compare/v1.4.3...v1.5.0) (2023-08-16) + + +### โœจ New Features + +* In-memory provider for e2e testing and minimal usage ([#546](https://github.com/open-feature/java-sdk/issues/546)) ([a741568](https://github.com/open-feature/java-sdk/commit/a741568762b96091bb0e50a32a8e00e863268429)) + + +### ๐Ÿงน Chore + +* **deps:** update actions/setup-java digest to 5b86b67 ([#562](https://github.com/open-feature/java-sdk/issues/562)) ([4a4d813](https://github.com/open-feature/java-sdk/commit/4a4d81367b50ab957d99371ffe368c0977768329)) +* **deps:** update github/codeql-action digest to 3ecf990 ([#557](https://github.com/open-feature/java-sdk/issues/557)) ([350196c](https://github.com/open-feature/java-sdk/commit/350196c6882aadba1c037132ff61296c8212d871)) +* **deps:** update github/codeql-action digest to e683046 ([#559](https://github.com/open-feature/java-sdk/issues/559)) ([6c52ee4](https://github.com/open-feature/java-sdk/commit/6c52ee464fdb5d5733cb5a31e9cfea1644840177)) +* fix codecov, throw in memory provider ([1f28921](https://github.com/open-feature/java-sdk/commit/1f28921fec2cece6d492fb77160bb97dd05c3e08)) +* fix e2e profile ([94a5a86](https://github.com/open-feature/java-sdk/commit/94a5a869d8f2d41adf632f60db1eb0f7e9b530ec)) +* fix jacoco coverage minimum, throw in memory provider ([#561](https://github.com/open-feature/java-sdk/issues/561)) ([1f28921](https://github.com/open-feature/java-sdk/commit/1f28921fec2cece6d492fb77160bb97dd05c3e08)) + + +### ๐Ÿ“š Documentation + +* update README to be auto-included in openfeature.dev ([#560](https://github.com/open-feature/java-sdk/issues/560)) ([3496366](https://github.com/open-feature/java-sdk/commit/3496366ae880e85c08e0b5925e9220b55a67c8eb)) + ## [1.4.3](https://github.com/open-feature/java-sdk/compare/v1.4.2...v1.4.3) (2023-08-11) diff --git a/README.md b/README.md index 131d316b..34f5ee44 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ - - Release + + Release @@ -59,7 +59,7 @@ Note that this library is intended to be used in server-side contexts and has no dev.openfeature sdk - 1.4.3 + 1.5.0 ``` @@ -84,7 +84,7 @@ If you would like snapshot builds, this is the relevant repository information: ```groovy dependencies { - implementation 'dev.openfeature:sdk:1.4.3' + implementation 'dev.openfeature:sdk:1.5.0' } ``` diff --git a/pom.xml b/pom.xml index 8444030b..f0133467 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ dev.openfeature sdk - 1.4.3 + 1.5.0 UTF-8 diff --git a/version.txt b/version.txt index 428b770e..bc80560f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.4.3 +1.5.0