From 9732dc6092faa7bab12f0796a7f077dfd13b4b64 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Tue, 13 Aug 2024 11:43:52 +0300 Subject: [PATCH 1/5] feat: add Prefab provider Signed-off-by: liran2000 --- .github/component_owners.yml | 3 + .release-please-manifest.json | 1 + pom.xml | 1 + providers/prefab/CHANGELOG.md | 1 + providers/prefab/README.md | 58 ++++++ providers/prefab/lombok.config | 5 + providers/prefab/pom.xml | 40 ++++ .../providers/prefab/ContextTransformer.java | 17 ++ .../providers/prefab/PrefabProvider.java | 172 +++++++++++++++++ .../prefab/PrefabProviderConfig.java | 18 ++ .../providers/prefab/PrefabProviderTest.java | 174 ++++++++++++++++++ .../resources/.prefab.default.config.yaml | 38 ++++ .../prefab/src/test/resources/log4j2-test.xml | 13 ++ providers/prefab/version.txt | 1 + 14 files changed, 542 insertions(+) create mode 100644 providers/prefab/CHANGELOG.md create mode 100644 providers/prefab/README.md create mode 100644 providers/prefab/lombok.config create mode 100644 providers/prefab/pom.xml create mode 100644 providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java create mode 100644 providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java create mode 100644 providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProviderConfig.java create mode 100644 providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java create mode 100644 providers/prefab/src/test/resources/.prefab.default.config.yaml create mode 100644 providers/prefab/src/test/resources/log4j2-test.xml create mode 100644 providers/prefab/version.txt diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 2b280a851..5dfa92327 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -35,6 +35,9 @@ components: - novalisdenahi providers/statsig: - liran2000 + providers/statsig: + - liran2000 + - jkebinger ignored-authors: - renovate-bot diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 78af2e8f6..b5677c997 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -9,5 +9,6 @@ "providers/flipt": "0.0.2", "providers/configcat": "0.0.3", "providers/statsig": "0.0.4", + "providers/prefab": "0.0.1", "tools/junit-openfeature": "0.0.2" } diff --git a/pom.xml b/pom.xml index 733318862..af575b52b 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,7 @@ providers/flipt providers/configcat providers/statsig + providers/prefab diff --git a/providers/prefab/CHANGELOG.md b/providers/prefab/CHANGELOG.md new file mode 100644 index 000000000..825c32f0d --- /dev/null +++ b/providers/prefab/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/providers/prefab/README.md b/providers/prefab/README.md new file mode 100644 index 000000000..e54d47d87 --- /dev/null +++ b/providers/prefab/README.md @@ -0,0 +1,58 @@ +# Unofficial Prefab OpenFeature Provider for Java + +[Prefab](https://www.prefab.cloud/) OpenFeature Provider can provide usage for Prefab via OpenFeature Java SDK. + +## Installation + + + +```xml + + + dev.openfeature.contrib.providers + prefab + 0.0.1 + +``` + + + +## Usage +Prefab OpenFeature Provider is using Prefab Java SDK. + +### Usage Example + +``` +PrefabProviderConfig prefabProviderConfig = PrefabProviderConfig.builder().sdkKey(sdkKey).build(); +prefabProvider = new PrefabProvider(prefabProviderConfig); +OpenFeatureAPI.getInstance().setProviderAndWait(prefabProvider); + + +Options options = new Options().setApikey(sdkKey); +PrefabProviderConfig prefabProviderConfig = PrefabProviderConfig.builder() + .options(options).build(); +PrefabProvider prefabProvider = new PrefabProvider(prefabProviderConfig); +OpenFeatureAPI.getInstance().setProviderAndWait(prefabProvider); + +boolean featureEnabled = client.getBooleanValue(FLAG_NAME, false); + +MutableContext evaluationContext = new MutableContext(); +evaluationContext.add("domain", "domain.com"); +featureEnabled = client.getBooleanValue(USERS_FLAG_NAME, false, evaluationContext); +``` + +See [PrefabProviderTest](./src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java) +for more information. + +## Notes +Some Prefab custom operations are supported from the provider client via: + +```java +prefabProvider.getPrefabCloudClient()... +``` + +## Prefab Provider Tests Strategies + +Unit test based on Prefab local features file. +See [PrefabProviderTest](./src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java) +for more information. diff --git a/providers/prefab/lombok.config b/providers/prefab/lombok.config new file mode 100644 index 000000000..bcd1afdae --- /dev/null +++ b/providers/prefab/lombok.config @@ -0,0 +1,5 @@ +# This file is needed to avoid errors throw by findbugs when working with lombok. +lombok.addSuppressWarnings = true +lombok.addLombokGeneratedAnnotation = true +config.stopBubbling = true +lombok.extern.findbugs.addSuppressFBWarnings = true diff --git a/providers/prefab/pom.xml b/providers/prefab/pom.xml new file mode 100644 index 000000000..ebb7bd279 --- /dev/null +++ b/providers/prefab/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + dev.openfeature.contrib + parent + 0.1.0 + ../../pom.xml + + dev.openfeature.contrib.providers + prefab + 0.0.1 + + prefab + Prefab provider for Java + https://www.prefab.cloud + + + + cloud.prefab + client + 0.3.20 + + + + org.slf4j + slf4j-api + 2.0.16 + + + + org.apache.logging.log4j + log4j-slf4j2-impl + 2.23.1 + test + + + + diff --git a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java new file mode 100644 index 000000000..3f3248bc1 --- /dev/null +++ b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java @@ -0,0 +1,17 @@ +package dev.openfeature.contrib.providers.prefab; + +import cloud.prefab.context.PrefabContext; +import dev.openfeature.sdk.EvaluationContext; + +/** + * Transformer from OpenFeature context to Prefab context. + */ +public class ContextTransformer { + + protected static PrefabContext transform(EvaluationContext ctx) { + PrefabContext.Builder contextBuilder = PrefabContext.newBuilder("User"); + ctx.asObjectMap().forEach((k, v) -> contextBuilder.put(k, String.valueOf(v))); + return contextBuilder.build(); + } + +} diff --git a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java new file mode 100644 index 000000000..edfc018d0 --- /dev/null +++ b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java @@ -0,0 +1,172 @@ +package dev.openfeature.contrib.providers.prefab; + +import cloud.prefab.client.PrefabCloudClient; +import cloud.prefab.context.PrefabContext; +import cloud.prefab.domain.Prefab; +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.Value; +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.exceptions.ProviderNotReadyError; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Provider implementation for Prefab. + */ +@Slf4j +public class PrefabProvider extends EventProvider { + + @Getter + private static final String NAME = "Prefab"; + + public static final String PROVIDER_NOT_YET_INITIALIZED = "provider not yet initialized"; + public static final String UNKNOWN_ERROR = "unknown error"; + + private final PrefabProviderConfig prefabProviderConfig; + + @Getter + private PrefabCloudClient prefabCloudClient; + + @Getter + private ProviderState state = ProviderState.NOT_READY; + + private final AtomicBoolean isInitialized = new AtomicBoolean(false); + + /** + * Constructor. + * @param prefabProviderConfig prefabProvider Config + */ + public PrefabProvider(PrefabProviderConfig prefabProviderConfig) { + this.prefabProviderConfig = prefabProviderConfig; + } + + /** + * Initialize the provider. + * @param evaluationContext evaluation context + * @throws Exception on error + */ + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + boolean initialized = isInitialized.getAndSet(true); + if (initialized) { + throw new GeneralError("already initialized"); + } + super.initialize(evaluationContext); + prefabCloudClient = new PrefabCloudClient(prefabProviderConfig.getOptions()); + prefabProviderConfig.postInit(); + state = ProviderState.READY; + log.info("finished initializing provider, state: {}", state); + + prefabProviderConfig.getOptions().addConfigChangeListener(changeEvent -> { + ProviderEventDetails providerEventDetails = ProviderEventDetails.builder() + .flagsChanged(Collections.singletonList(changeEvent.getKey())) + .message("config changed") + .build(); + emitProviderConfigurationChanged(providerEventDetails); + }); + } + + @Override + public Metadata getMetadata() { + return () -> NAME; + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + verifyEvaluation(); + PrefabContext context = ctx == null ? null : ContextTransformer.transform(ctx); + Boolean evaluatedValue = prefabCloudClient.featureFlagClient().featureIsOn(key, context); + return ProviderEvaluation.builder() + .value(evaluatedValue) + .build(); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + verifyEvaluation(); + PrefabContext context = ctx == null ? null : ContextTransformer.transform(ctx); + String evaluatedValue = defaultValue; + Optional opt = prefabCloudClient.featureFlagClient().get(key, context); + if (opt.isPresent() && Prefab.ConfigValue.TypeCase.STRING.equals(opt.get().getTypeCase())) { + evaluatedValue = opt.get().getString(); + } + return ProviderEvaluation.builder() + .value(evaluatedValue) + .build(); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + verifyEvaluation(); + PrefabContext context = ctx == null ? null : ContextTransformer.transform(ctx); + Integer evaluatedValue = defaultValue; + Optional opt = prefabCloudClient.featureFlagClient().get(key, context); + if (opt.isPresent() && Prefab.ConfigValue.TypeCase.INT.equals(opt.get().getTypeCase())) { + evaluatedValue = Math.toIntExact(opt.get().getInt()); + } + return ProviderEvaluation.builder() + .value(evaluatedValue) + .build(); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + verifyEvaluation(); + PrefabContext context = ctx == null ? null : ContextTransformer.transform(ctx); + Double evaluatedValue = defaultValue; + Optional opt = prefabCloudClient.featureFlagClient().get(key, context); + if (opt.isPresent() && Prefab.ConfigValue.TypeCase.DOUBLE.equals(opt.get().getTypeCase())) { + evaluatedValue = opt.get().getDouble(); + } + return ProviderEvaluation.builder() + .value(evaluatedValue) + .build(); + } + + @SneakyThrows + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + String defaultValueString = defaultValue == null ? null : defaultValue.asString(); + ProviderEvaluation stringEvaluation = getStringEvaluation(key, defaultValueString, ctx); + Value evaluatedValue = new Value(stringEvaluation.getValue()); + return ProviderEvaluation.builder() + .value(evaluatedValue) + .build(); + } + + private void verifyEvaluation() throws ProviderNotReadyError, GeneralError { + if (!ProviderState.READY.equals(state)) { + + /* + According to spec Requirement 2.4.5: + "The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready." + https://github.com/open-feature/spec/blob/main/specification/sections/02-providers.md#requirement-245 + */ + if (ProviderState.NOT_READY.equals(state)) { + throw new ProviderNotReadyError(PROVIDER_NOT_YET_INITIALIZED); + } + throw new GeneralError(UNKNOWN_ERROR); + } + } + + @SneakyThrows + @Override + public void shutdown() { + super.shutdown(); + log.info("shutdown"); + if (prefabCloudClient != null) { + prefabCloudClient.close(); + } + state = ProviderState.NOT_READY; + } +} diff --git a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProviderConfig.java b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProviderConfig.java new file mode 100644 index 000000000..8076805b7 --- /dev/null +++ b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProviderConfig.java @@ -0,0 +1,18 @@ +package dev.openfeature.contrib.providers.prefab; + +import cloud.prefab.client.Options; +import lombok.Builder; +import lombok.Getter; + +/** + * Options for initializing prefab provider. + */ +@Getter +@Builder +public class PrefabProviderConfig { + private Options options; + + public void postInit() { + + } +} diff --git a/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java b/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java new file mode 100644 index 000000000..0eacb94a9 --- /dev/null +++ b/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java @@ -0,0 +1,174 @@ +package dev.openfeature.contrib.providers.prefab; + +import cloud.prefab.client.Options; +import cloud.prefab.context.PrefabContext; +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.MutableContext; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.ProviderEventDetails; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.exceptions.ProviderNotReadyError; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * PrefabProvider test, based local defaul config file. + */ +@Slf4j +class PrefabProviderTest { + + public static final String FLAG_NAME = "sample_bool"; + public static final String VARIANT_FLAG_NAME = "sample"; + public static final String VARIANT_FLAG_VALUE = "test sample value"; + public static final String INT_FLAG_NAME = "sample_int"; + public static final Integer INT_FLAG_VALUE = 123; + public static final String DOUBLE_FLAG_NAME = "sample_double"; + public static final Double DOUBLE_FLAG_VALUE = 12.12; + public static final String USERS_FLAG_NAME = "just_my_domain"; + private static PrefabProvider prefabProvider; + private static Client client; + + @BeforeAll + static void setUp() { + String sdkKey = "prefab-test"; + Options options = new Options() + .setApikey(sdkKey) + .setPrefabDatasource(Options.Datasources.LOCAL_ONLY) + .setInitializationTimeoutSec(10); + PrefabProviderConfig prefabProviderConfig = PrefabProviderConfig.builder() + .options(options).build(); + prefabProvider = new PrefabProvider(prefabProviderConfig); + OpenFeatureAPI.getInstance().setProviderAndWait(prefabProvider); + client = OpenFeatureAPI.getInstance().getClient(); + } + + @AfterAll + static void shutdown() { + prefabProvider.shutdown(); + } + + @Test + void getBooleanEvaluation() { + assertEquals(true, prefabProvider.getBooleanEvaluation(FLAG_NAME, false, new ImmutableContext()).getValue()); + assertEquals(true, client.getBooleanValue(FLAG_NAME, false)); + assertEquals(false, prefabProvider.getBooleanEvaluation("non-existing", false, new ImmutableContext()).getValue()); + assertEquals(false, client.getBooleanValue("non-existing", false)); + } + + @Test + void getStringEvaluation() { + assertEquals(VARIANT_FLAG_VALUE, prefabProvider.getStringEvaluation(VARIANT_FLAG_NAME, "", + new ImmutableContext()).getValue()); + assertEquals(VARIANT_FLAG_VALUE, client.getStringValue(VARIANT_FLAG_NAME, "")); + assertEquals("fallback_str", prefabProvider.getStringEvaluation("non-existing", + "fallback_str", new ImmutableContext()).getValue()); + assertEquals("fallback_str", client.getStringValue("non-existing", "fallback_str")); + } + + @Test + void getObjectEvaluation() { + assertEquals(VARIANT_FLAG_VALUE, prefabProvider.getStringEvaluation(VARIANT_FLAG_NAME, "", + new ImmutableContext()).getValue()); + assertEquals(new Value(VARIANT_FLAG_VALUE), client.getObjectValue(VARIANT_FLAG_NAME, new Value(""))); + assertEquals(new Value("fallback_str"), prefabProvider.getObjectEvaluation("non-existing", + new Value("fallback_str"), new ImmutableContext()).getValue()); + assertEquals(new Value("fallback_str"), client.getObjectValue("non-existing", new Value("fallback_str"))); + } + + @Test + void getIntegerEvaluation() { + MutableContext evaluationContext = new MutableContext(); + assertEquals(INT_FLAG_VALUE, prefabProvider.getIntegerEvaluation(INT_FLAG_NAME, 1, + evaluationContext).getValue()); + assertEquals(INT_FLAG_VALUE, client.getIntegerValue(INT_FLAG_NAME, 1)); + assertEquals(1, client.getIntegerValue("non-existing", 1)); + + // non-number flag value + assertEquals(1, client.getIntegerValue(VARIANT_FLAG_NAME, 1)); + } + + @Test + void getDoubleEvaluation() { + MutableContext evaluationContext = new MutableContext(); + assertEquals(DOUBLE_FLAG_VALUE, prefabProvider.getDoubleEvaluation(DOUBLE_FLAG_NAME, 1.1, + evaluationContext).getValue()); + assertEquals(DOUBLE_FLAG_VALUE, client.getDoubleValue(DOUBLE_FLAG_NAME, 1.1)); + assertEquals(1.1, client.getDoubleValue("non-existing", 1.1)); + + // non-number flag value + assertEquals(1.1, client.getDoubleValue(VARIANT_FLAG_NAME, 1.1)); + } + +// @Test + void getBooleanEvaluationByUser() { + MutableContext evaluationContext = new MutableContext(); + evaluationContext.add("domain", "prefab.cloud"); + assertEquals(true, prefabProvider.getBooleanEvaluation(USERS_FLAG_NAME, false, evaluationContext).getValue()); + assertEquals(true, client.getBooleanValue(USERS_FLAG_NAME, false, evaluationContext)); + evaluationContext.add("domain", "other.com"); + assertEquals(false, prefabProvider.getBooleanEvaluation(USERS_FLAG_NAME, false, evaluationContext).getValue()); + assertEquals(false, client.getBooleanValue(USERS_FLAG_NAME, false, evaluationContext)); + } + + @SneakyThrows + @Test + void shouldThrowIfNotInitialized() { + Options options = new Options() + .setApikey("test-sdk-key") + .setPrefabDatasource(Options.Datasources.LOCAL_ONLY) + .setInitializationTimeoutSec(10); + PrefabProviderConfig prefabProviderConfig = PrefabProviderConfig.builder() + .options(options).build(); + PrefabProvider tempPrefabProvider = new PrefabProvider(prefabProviderConfig); + + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext())); + + OpenFeatureAPI.getInstance().setProviderAndWait("tempPrefabProvider", tempPrefabProvider); + + assertThrows(GeneralError.class, ()-> tempPrefabProvider.initialize(null)); + + tempPrefabProvider.shutdown(); + + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext())); + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getDoubleEvaluation("fail_not_initialized", 0.1, new ImmutableContext())); + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getIntegerEvaluation("fail_not_initialized", 3, new ImmutableContext())); + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getObjectEvaluation("fail_not_initialized", null, new ImmutableContext())); + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getStringEvaluation("fail_not_initialized", "", new ImmutableContext())); + } + + @Test + void eventsTest() { + prefabProvider.emitProviderReady(ProviderEventDetails.builder().build()); + prefabProvider.emitProviderError(ProviderEventDetails.builder().build()); + prefabProvider.emitProviderConfigurationChanged(ProviderEventDetails.builder().build()); + assertDoesNotThrow(() -> log.debug("provider state: {}", prefabProvider.getState())); + } + + @SneakyThrows + @Test + void contextTransformTest() { + String customPropertyValue = "customProperty_value"; + String customPropertyKey = "customProperty"; + + MutableContext evaluationContext = new MutableContext(); + evaluationContext.add(customPropertyKey, customPropertyValue); + + PrefabContext expectedContext = PrefabContext.newBuilder("User") + .put(customPropertyKey, customPropertyValue) + .build(); + PrefabContext transformedContext = ContextTransformer.transform(evaluationContext); + + // equals not implemented for User, using toString + assertEquals(expectedContext.toString(), transformedContext.toString()); + } + +} \ No newline at end of file diff --git a/providers/prefab/src/test/resources/.prefab.default.config.yaml b/providers/prefab/src/test/resources/.prefab.default.config.yaml new file mode 100644 index 000000000..deb9037d2 --- /dev/null +++ b/providers/prefab/src/test/resources/.prefab.default.config.yaml @@ -0,0 +1,38 @@ +sample_int: 123 +sample_double: 12.12 +sample_bool: true +false_value: false +zero_value: 0 +sample_to_override: Foo +prefab.log_level: debug +sample: test sample value +enabled_flag: true +disabled_flag: false +flag_with_a_value: { "feature_flag": "true", value: "all-features" } +in_lookup_key: { "feature_flag": "true", value: true, criteria: { operator: LOOKUP_KEY_IN, values: [ "abc123", "xyz987" ] } } +just_my_domain: { "feature_flag": "true", value: "new-version", criteria: { operator: PROP_IS_ONE_OF, property: "domain", values: [ "prefab.cloud", "example.com" ] } } +nested: + values: + _: top level + string: nested value + + +nested2: + _: the value + +log-level: + _: warn + cloud.prefab.client: warn + tests: + _: debug + capitalized: INFO + uncapitalized: info + nested: + _: warn + deeply: error + +example: + nested: + path: hello + +example2.nested.path: hello2 \ No newline at end of file diff --git a/providers/prefab/src/test/resources/log4j2-test.xml b/providers/prefab/src/test/resources/log4j2-test.xml new file mode 100644 index 000000000..aced30f8a --- /dev/null +++ b/providers/prefab/src/test/resources/log4j2-test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/providers/prefab/version.txt b/providers/prefab/version.txt new file mode 100644 index 000000000..8acdd82b7 --- /dev/null +++ b/providers/prefab/version.txt @@ -0,0 +1 @@ +0.0.1 From bd22d51beaf46deefc7354fced990932e95ee4f5 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Tue, 13 Aug 2024 17:59:01 +0300 Subject: [PATCH 2/5] context updates Signed-off-by: liran2000 --- .github/component_owners.yml | 2 +- providers/prefab/README.md | 3 +- .../providers/prefab/ContextTransformer.java | 28 ++- .../providers/prefab/PrefabProvider.java | 10 +- .../providers/prefab/PrefabProviderTest.java | 36 ++-- .../resources/.prefab.default.config.yaml | 38 ---- .../prefab/src/test/resources/features.json | 183 ++++++++++++++++++ 7 files changed, 236 insertions(+), 64 deletions(-) delete mode 100644 providers/prefab/src/test/resources/.prefab.default.config.yaml create mode 100644 providers/prefab/src/test/resources/features.json diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 5dfa92327..427d32535 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -35,7 +35,7 @@ components: - novalisdenahi providers/statsig: - liran2000 - providers/statsig: + providers/prefab: - liran2000 - jkebinger diff --git a/providers/prefab/README.md b/providers/prefab/README.md index e54d47d87..654f80a85 100644 --- a/providers/prefab/README.md +++ b/providers/prefab/README.md @@ -37,7 +37,8 @@ OpenFeatureAPI.getInstance().setProviderAndWait(prefabProvider); boolean featureEnabled = client.getBooleanValue(FLAG_NAME, false); MutableContext evaluationContext = new MutableContext(); -evaluationContext.add("domain", "domain.com"); +evaluationContext.add("user.key", "key1"); +evaluationContext.add("team.domain", "prefab.cloud"); featureEnabled = client.getBooleanValue(USERS_FLAG_NAME, false, evaluationContext); ``` diff --git a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java index 3f3248bc1..b04ff8001 100644 --- a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java +++ b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java @@ -1,17 +1,37 @@ package dev.openfeature.contrib.providers.prefab; import cloud.prefab.context.PrefabContext; +import cloud.prefab.context.PrefabContextSet; +import cloud.prefab.context.PrefabContextSetReadable; import dev.openfeature.sdk.EvaluationContext; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + /** * Transformer from OpenFeature context to Prefab context. */ public class ContextTransformer { - protected static PrefabContext transform(EvaluationContext ctx) { - PrefabContext.Builder contextBuilder = PrefabContext.newBuilder("User"); - ctx.asObjectMap().forEach((k, v) -> contextBuilder.put(k, String.valueOf(v))); - return contextBuilder.build(); + protected static PrefabContextSetReadable transform(EvaluationContext ctx) { + Map contextsMap = new HashMap<>(); + ctx.asObjectMap().forEach((k, v) -> { + String[] parts = k.split("\\.", 2); + if (parts.length < 2) { + throw new IllegalArgumentException("context key structure should be in the form of x.y: " + k); + } + contextsMap.putIfAbsent(parts[0], PrefabContext.newBuilder(parts[0])); + PrefabContext.Builder contextBuilder = contextsMap.get(parts[0]); + contextBuilder.put(parts[1], Objects.toString(v, null)); + }); + PrefabContextSet prefabContextSet = new PrefabContextSet(); + contextsMap.forEach((key, value) -> { + PrefabContext prefabContext = value.build(); + prefabContextSet.addContext(prefabContext); + }); + + return prefabContextSet; } } diff --git a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java index edfc018d0..d61efd430 100644 --- a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java +++ b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java @@ -1,7 +1,7 @@ package dev.openfeature.contrib.providers.prefab; import cloud.prefab.client.PrefabCloudClient; -import cloud.prefab.context.PrefabContext; +import cloud.prefab.context.PrefabContextSetReadable; import cloud.prefab.domain.Prefab; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; @@ -84,7 +84,7 @@ public Metadata getMetadata() { @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { verifyEvaluation(); - PrefabContext context = ctx == null ? null : ContextTransformer.transform(ctx); + PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx); Boolean evaluatedValue = prefabCloudClient.featureFlagClient().featureIsOn(key, context); return ProviderEvaluation.builder() .value(evaluatedValue) @@ -94,7 +94,7 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { verifyEvaluation(); - PrefabContext context = ctx == null ? null : ContextTransformer.transform(ctx); + PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx); String evaluatedValue = defaultValue; Optional opt = prefabCloudClient.featureFlagClient().get(key, context); if (opt.isPresent() && Prefab.ConfigValue.TypeCase.STRING.equals(opt.get().getTypeCase())) { @@ -108,7 +108,7 @@ public ProviderEvaluation getStringEvaluation(String key, String default @Override public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { verifyEvaluation(); - PrefabContext context = ctx == null ? null : ContextTransformer.transform(ctx); + PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx); Integer evaluatedValue = defaultValue; Optional opt = prefabCloudClient.featureFlagClient().get(key, context); if (opt.isPresent() && Prefab.ConfigValue.TypeCase.INT.equals(opt.get().getTypeCase())) { @@ -122,7 +122,7 @@ public ProviderEvaluation getIntegerEvaluation(String key, Integer defa @Override public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { verifyEvaluation(); - PrefabContext context = ctx == null ? null : ContextTransformer.transform(ctx); + PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx); Double evaluatedValue = defaultValue; Optional opt = prefabCloudClient.featureFlagClient().get(key, context); if (opt.isPresent() && Prefab.ConfigValue.TypeCase.DOUBLE.equals(opt.get().getTypeCase())) { diff --git a/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java b/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java index 0eacb94a9..883e45e9a 100644 --- a/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java +++ b/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java @@ -2,6 +2,8 @@ import cloud.prefab.client.Options; import cloud.prefab.context.PrefabContext; +import cloud.prefab.context.PrefabContextSet; +import cloud.prefab.context.PrefabContextSetReadable; import dev.openfeature.sdk.Client; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.MutableContext; @@ -16,6 +18,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.io.File; + import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -33,16 +37,16 @@ class PrefabProviderTest { public static final Integer INT_FLAG_VALUE = 123; public static final String DOUBLE_FLAG_NAME = "sample_double"; public static final Double DOUBLE_FLAG_VALUE = 12.12; - public static final String USERS_FLAG_NAME = "just_my_domain"; + public static final String USERS_FLAG_NAME = "test1"; private static PrefabProvider prefabProvider; private static Client client; @BeforeAll static void setUp() { - String sdkKey = "prefab-test"; + File localDataFile = new File("src/test/resources/features.json"); Options options = new Options() - .setApikey(sdkKey) - .setPrefabDatasource(Options.Datasources.LOCAL_ONLY) + .setPrefabDatasource(Options.Datasources.ALL) + .setLocalDatafile(localDataFile.toString()) .setInitializationTimeoutSec(10); PrefabProviderConfig prefabProviderConfig = PrefabProviderConfig.builder() .options(options).build(); @@ -108,13 +112,15 @@ void getDoubleEvaluation() { assertEquals(1.1, client.getDoubleValue(VARIANT_FLAG_NAME, 1.1)); } -// @Test + @Test void getBooleanEvaluationByUser() { MutableContext evaluationContext = new MutableContext(); - evaluationContext.add("domain", "prefab.cloud"); + evaluationContext.add("user.key", "key1"); + evaluationContext.add("team.domain", "prefab.cloud"); + assertEquals(true, prefabProvider.getBooleanEvaluation(USERS_FLAG_NAME, false, evaluationContext).getValue()); assertEquals(true, client.getBooleanValue(USERS_FLAG_NAME, false, evaluationContext)); - evaluationContext.add("domain", "other.com"); + evaluationContext.add("team.domain", "other.com"); assertEquals(false, prefabProvider.getBooleanEvaluation(USERS_FLAG_NAME, false, evaluationContext).getValue()); assertEquals(false, client.getBooleanValue(USERS_FLAG_NAME, false, evaluationContext)); } @@ -156,16 +162,16 @@ void eventsTest() { @SneakyThrows @Test void contextTransformTest() { - String customPropertyValue = "customProperty_value"; - String customPropertyKey = "customProperty"; MutableContext evaluationContext = new MutableContext(); - evaluationContext.add(customPropertyKey, customPropertyValue); - - PrefabContext expectedContext = PrefabContext.newBuilder("User") - .put(customPropertyKey, customPropertyValue) - .build(); - PrefabContext transformedContext = ContextTransformer.transform(evaluationContext); + evaluationContext.add("user.key", "key1"); + evaluationContext.add("team.domain", "prefab.cloud"); + + PrefabContextSet expectedContext = PrefabContextSet.from( + PrefabContext.newBuilder("user").put("key", "key1").build(), + PrefabContext.newBuilder("team").put("domain", "prefab.cloud").build() + ); + PrefabContextSetReadable transformedContext = ContextTransformer.transform(evaluationContext); // equals not implemented for User, using toString assertEquals(expectedContext.toString(), transformedContext.toString()); diff --git a/providers/prefab/src/test/resources/.prefab.default.config.yaml b/providers/prefab/src/test/resources/.prefab.default.config.yaml deleted file mode 100644 index deb9037d2..000000000 --- a/providers/prefab/src/test/resources/.prefab.default.config.yaml +++ /dev/null @@ -1,38 +0,0 @@ -sample_int: 123 -sample_double: 12.12 -sample_bool: true -false_value: false -zero_value: 0 -sample_to_override: Foo -prefab.log_level: debug -sample: test sample value -enabled_flag: true -disabled_flag: false -flag_with_a_value: { "feature_flag": "true", value: "all-features" } -in_lookup_key: { "feature_flag": "true", value: true, criteria: { operator: LOOKUP_KEY_IN, values: [ "abc123", "xyz987" ] } } -just_my_domain: { "feature_flag": "true", value: "new-version", criteria: { operator: PROP_IS_ONE_OF, property: "domain", values: [ "prefab.cloud", "example.com" ] } } -nested: - values: - _: top level - string: nested value - - -nested2: - _: the value - -log-level: - _: warn - cloud.prefab.client: warn - tests: - _: debug - capitalized: INFO - uncapitalized: info - nested: - _: warn - deeply: error - -example: - nested: - path: hello - -example2.nested.path: hello2 \ No newline at end of file diff --git a/providers/prefab/src/test/resources/features.json b/providers/prefab/src/test/resources/features.json new file mode 100644 index 000000000..5e6e29fcb --- /dev/null +++ b/providers/prefab/src/test/resources/features.json @@ -0,0 +1,183 @@ +{ + "configs": [ + { + "id": "17235540036203003", + "projectId": "453", + "key": "sample_int", + "changedBy": { + "userId": "878", + "email": "liran2000@gmail.com" + }, + "rows": [ + { + "projectEnvId": "962", + "values": [ + { + "value": { + "int": "123" + } + } + ] + } + ], + "allowableValues": [ + { + "int": "123" + } + ], + "configType": "FEATURE_FLAG", + "valueType": "INT" + }, + { + "id": "17235541126207669", + "projectId": "453", + "key": "sample_double", + "changedBy": { + "userId": "878", + "email": "liran2000@gmail.com" + }, + "rows": [ + { + "projectEnvId": "962", + "values": [ + { + "value": { + "double": 12.12 + } + } + ] + } + ], + "allowableValues": [ + { + "double": 12.12 + } + ], + "configType": "FEATURE_FLAG", + "valueType": "DOUBLE" + }, + { + "id": "17235541571344121", + "projectId": "453", + "key": "sample_bool", + "changedBy": { + "userId": "878", + "email": "liran2000@gmail.com" + }, + "rows": [ + { + "projectEnvId": "962", + "values": [ + { + "value": { + "bool": true + } + } + ] + } + ], + "allowableValues": [ + { + "bool": false + }, + { + "bool": true + } + ], + "configType": "FEATURE_FLAG", + "valueType": "BOOL" + }, + { + "id": "17235603983939168", + "projectId": "453", + "key": "test1", + "changedBy": { + "userId": "878", + "email": "liran2000@gmail.com" + }, + "rows": [ + { + "projectEnvId": "962", + "values": [ + { + "criteria": [ + { + "propertyName": "user.key", + "operator": "PROP_IS_ONE_OF", + "valueToMatch": { + "stringList": { + "values": [ + "key1" + ] + } + } + }, + { + "propertyName": "team.domain", + "operator": "PROP_IS_ONE_OF", + "valueToMatch": { + "stringList": { + "values": [ + "prefab.cloud" + ] + } + } + } + ], + "value": { + "bool": true + } + }, + { + "value": { + "bool": false + } + } + ] + } + ], + "allowableValues": [ + { + "bool": false + }, + { + "bool": true + } + ], + "configType": "FEATURE_FLAG", + "valueType": "BOOL" + }, + { + "id": "17235608162176898", + "projectId": "453", + "key": "sample", + "changedBy": { + "userId": "878", + "email": "liran2000@gmail.com" + }, + "rows": [ + { + "projectEnvId": "962", + "values": [ + { + "value": { + "string": "test sample value" + } + } + ] + } + ], + "allowableValues": [ + { + "string": "test sample value" + } + ], + "configType": "FEATURE_FLAG", + "valueType": "STRING" + } + ], + "configServicePointer": { + "projectId": "453", + "projectEnvId": "962" + } +} \ No newline at end of file From 64b016a0666413c0c7d46979c2368d20d1f52e83 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Thu, 24 Apr 2025 08:45:01 +0300 Subject: [PATCH 3/5] updates Signed-off-by: liran2000 --- .../providers/prefab/ContextTransformer.java | 4 ++++ .../contrib/providers/prefab/PrefabProvider.java | 7 +++---- .../providers/prefab/PrefabProviderConfig.java | 4 ---- .../providers/prefab/PrefabProviderTest.java | 15 ++++++++------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java index b04ff8001..90d6a74d8 100644 --- a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java +++ b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java @@ -14,6 +14,10 @@ */ public class ContextTransformer { + private ContextTransformer() { + + } + protected static PrefabContextSetReadable transform(EvaluationContext ctx) { Map contextsMap = new HashMap<>(); ctx.asObjectMap().forEach((k, v) -> { diff --git a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java index d61efd430..966134106 100644 --- a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java +++ b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java @@ -63,15 +63,14 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { } super.initialize(evaluationContext); prefabCloudClient = new PrefabCloudClient(prefabProviderConfig.getOptions()); - prefabProviderConfig.postInit(); state = ProviderState.READY; log.info("finished initializing provider, state: {}", state); prefabProviderConfig.getOptions().addConfigChangeListener(changeEvent -> { ProviderEventDetails providerEventDetails = ProviderEventDetails.builder() - .flagsChanged(Collections.singletonList(changeEvent.getKey())) - .message("config changed") - .build(); + .flagsChanged(Collections.singletonList(changeEvent.getKey())) + .message("config changed") + .build(); emitProviderConfigurationChanged(providerEventDetails); }); } diff --git a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProviderConfig.java b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProviderConfig.java index 8076805b7..6d3c9a977 100644 --- a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProviderConfig.java +++ b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProviderConfig.java @@ -11,8 +11,4 @@ @Builder public class PrefabProviderConfig { private Options options; - - public void postInit() { - - } } diff --git a/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java b/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java index 883e45e9a..f80364245 100644 --- a/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java +++ b/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java @@ -25,7 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; /** - * PrefabProvider test, based local defaul config file. + * PrefabProvider test, based local default config file. */ @Slf4j class PrefabProviderTest { @@ -136,7 +136,8 @@ void shouldThrowIfNotInitialized() { .options(options).build(); PrefabProvider tempPrefabProvider = new PrefabProvider(prefabProviderConfig); - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext())); + ImmutableContext context = new ImmutableContext(); + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getBooleanEvaluation("fail_not_initialized", false, context)); OpenFeatureAPI.getInstance().setProviderAndWait("tempPrefabProvider", tempPrefabProvider); @@ -144,11 +145,11 @@ void shouldThrowIfNotInitialized() { tempPrefabProvider.shutdown(); - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext())); - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getDoubleEvaluation("fail_not_initialized", 0.1, new ImmutableContext())); - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getIntegerEvaluation("fail_not_initialized", 3, new ImmutableContext())); - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getObjectEvaluation("fail_not_initialized", null, new ImmutableContext())); - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getStringEvaluation("fail_not_initialized", "", new ImmutableContext())); + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getBooleanEvaluation("fail_not_initialized", false, context)); + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getDoubleEvaluation("fail_not_initialized", 0.1, context)); + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getIntegerEvaluation("fail_not_initialized", 3, context)); + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getObjectEvaluation("fail_not_initialized", null, context)); + assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getStringEvaluation("fail_not_initialized", "", context)); } @Test From 1a5094db0d7faaf4b50b011a3ee58c18771eea65 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Thu, 24 Apr 2025 09:04:09 +0300 Subject: [PATCH 4/5] updates Signed-off-by: liran2000 --- .../providers/prefab/PrefabProvider.java | 28 +------------------ .../providers/prefab/PrefabProviderTest.java | 24 ++++------------ 2 files changed, 7 insertions(+), 45 deletions(-) diff --git a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java index 966134106..ef300b838 100644 --- a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java +++ b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java @@ -8,10 +8,8 @@ import dev.openfeature.sdk.Metadata; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.ProviderEventDetails; -import dev.openfeature.sdk.ProviderState; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -37,9 +35,6 @@ public class PrefabProvider extends EventProvider { @Getter private PrefabCloudClient prefabCloudClient; - @Getter - private ProviderState state = ProviderState.NOT_READY; - private final AtomicBoolean isInitialized = new AtomicBoolean(false); /** @@ -63,8 +58,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { } super.initialize(evaluationContext); prefabCloudClient = new PrefabCloudClient(prefabProviderConfig.getOptions()); - state = ProviderState.READY; - log.info("finished initializing provider, state: {}", state); + log.info("finished initializing provider"); prefabProviderConfig.getOptions().addConfigChangeListener(changeEvent -> { ProviderEventDetails providerEventDetails = ProviderEventDetails.builder() @@ -82,7 +76,6 @@ public Metadata getMetadata() { @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { - verifyEvaluation(); PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx); Boolean evaluatedValue = prefabCloudClient.featureFlagClient().featureIsOn(key, context); return ProviderEvaluation.builder() @@ -92,7 +85,6 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { - verifyEvaluation(); PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx); String evaluatedValue = defaultValue; Optional opt = prefabCloudClient.featureFlagClient().get(key, context); @@ -106,7 +98,6 @@ public ProviderEvaluation getStringEvaluation(String key, String default @Override public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { - verifyEvaluation(); PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx); Integer evaluatedValue = defaultValue; Optional opt = prefabCloudClient.featureFlagClient().get(key, context); @@ -120,7 +111,6 @@ public ProviderEvaluation getIntegerEvaluation(String key, Integer defa @Override public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { - verifyEvaluation(); PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx); Double evaluatedValue = defaultValue; Optional opt = prefabCloudClient.featureFlagClient().get(key, context); @@ -143,21 +133,6 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa .build(); } - private void verifyEvaluation() throws ProviderNotReadyError, GeneralError { - if (!ProviderState.READY.equals(state)) { - - /* - According to spec Requirement 2.4.5: - "The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready." - https://github.com/open-feature/spec/blob/main/specification/sections/02-providers.md#requirement-245 - */ - if (ProviderState.NOT_READY.equals(state)) { - throw new ProviderNotReadyError(PROVIDER_NOT_YET_INITIALIZED); - } - throw new GeneralError(UNKNOWN_ERROR); - } - } - @SneakyThrows @Override public void shutdown() { @@ -166,6 +141,5 @@ public void shutdown() { if (prefabCloudClient != null) { prefabCloudClient.close(); } - state = ProviderState.NOT_READY; } } diff --git a/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java b/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java index f80364245..848fdefc9 100644 --- a/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java +++ b/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java @@ -1,5 +1,9 @@ package dev.openfeature.contrib.providers.prefab; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + import cloud.prefab.client.Options; import cloud.prefab.context.PrefabContext; import cloud.prefab.context.PrefabContextSet; @@ -11,19 +15,13 @@ import dev.openfeature.sdk.ProviderEventDetails; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.GeneralError; -import dev.openfeature.sdk.exceptions.ProviderNotReadyError; +import java.io.File; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import java.io.File; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - /** * PrefabProvider test, based local default config file. */ @@ -136,20 +134,11 @@ void shouldThrowIfNotInitialized() { .options(options).build(); PrefabProvider tempPrefabProvider = new PrefabProvider(prefabProviderConfig); - ImmutableContext context = new ImmutableContext(); - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getBooleanEvaluation("fail_not_initialized", false, context)); - OpenFeatureAPI.getInstance().setProviderAndWait("tempPrefabProvider", tempPrefabProvider); assertThrows(GeneralError.class, ()-> tempPrefabProvider.initialize(null)); tempPrefabProvider.shutdown(); - - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getBooleanEvaluation("fail_not_initialized", false, context)); - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getDoubleEvaluation("fail_not_initialized", 0.1, context)); - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getIntegerEvaluation("fail_not_initialized", 3, context)); - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getObjectEvaluation("fail_not_initialized", null, context)); - assertThrows(ProviderNotReadyError.class, ()-> tempPrefabProvider.getStringEvaluation("fail_not_initialized", "", context)); } @Test @@ -157,7 +146,6 @@ void eventsTest() { prefabProvider.emitProviderReady(ProviderEventDetails.builder().build()); prefabProvider.emitProviderError(ProviderEventDetails.builder().build()); prefabProvider.emitProviderConfigurationChanged(ProviderEventDetails.builder().build()); - assertDoesNotThrow(() -> log.debug("provider state: {}", prefabProvider.getState())); } @SneakyThrows @@ -178,4 +166,4 @@ void contextTransformTest() { assertEquals(expectedContext.toString(), transformedContext.toString()); } -} \ No newline at end of file +} From 4c3d9b0e1c746fba2c05f96515b383e68132e9df Mon Sep 17 00:00:00 2001 From: liran2000 Date: Thu, 24 Apr 2025 09:46:12 +0300 Subject: [PATCH 5/5] updates Signed-off-by: liran2000 --- .../providers/prefab/ContextTransformer.java | 6 +- .../providers/prefab/PrefabProvider.java | 41 ++++---- .../providers/prefab/PrefabProviderTest.java | 97 +++++++++++++------ 3 files changed, 83 insertions(+), 61 deletions(-) diff --git a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java index 90d6a74d8..ebc7dafa7 100644 --- a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java +++ b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/ContextTransformer.java @@ -4,7 +4,6 @@ import cloud.prefab.context.PrefabContextSet; import cloud.prefab.context.PrefabContextSetReadable; import dev.openfeature.sdk.EvaluationContext; - import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -14,9 +13,7 @@ */ public class ContextTransformer { - private ContextTransformer() { - - } + private ContextTransformer() {} protected static PrefabContextSetReadable transform(EvaluationContext ctx) { Map contextsMap = new HashMap<>(); @@ -37,5 +34,4 @@ protected static PrefabContextSetReadable transform(EvaluationContext ctx) { return prefabContextSet; } - } diff --git a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java index ef300b838..d795c1294 100644 --- a/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java +++ b/providers/prefab/src/main/java/dev/openfeature/contrib/providers/prefab/PrefabProvider.java @@ -10,13 +10,12 @@ import dev.openfeature.sdk.ProviderEventDetails; import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.GeneralError; -import lombok.Getter; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; - import java.util.Collections; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; /** * Provider implementation for Prefab. @@ -39,6 +38,7 @@ public class PrefabProvider extends EventProvider { /** * Constructor. + * * @param prefabProviderConfig prefabProvider Config */ public PrefabProvider(PrefabProviderConfig prefabProviderConfig) { @@ -47,6 +47,7 @@ public PrefabProvider(PrefabProviderConfig prefabProviderConfig) { /** * Initialize the provider. + * * @param evaluationContext evaluation context * @throws Exception on error */ @@ -62,9 +63,9 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { prefabProviderConfig.getOptions().addConfigChangeListener(changeEvent -> { ProviderEventDetails providerEventDetails = ProviderEventDetails.builder() - .flagsChanged(Collections.singletonList(changeEvent.getKey())) - .message("config changed") - .build(); + .flagsChanged(Collections.singletonList(changeEvent.getKey())) + .message("config changed") + .build(); emitProviderConfigurationChanged(providerEventDetails); }); } @@ -78,9 +79,7 @@ public Metadata getMetadata() { public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx); Boolean evaluatedValue = prefabCloudClient.featureFlagClient().featureIsOn(key, context); - return ProviderEvaluation.builder() - .value(evaluatedValue) - .build(); + return ProviderEvaluation.builder().value(evaluatedValue).build(); } @Override @@ -88,12 +87,11 @@ public ProviderEvaluation getStringEvaluation(String key, String default PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx); String evaluatedValue = defaultValue; Optional opt = prefabCloudClient.featureFlagClient().get(key, context); - if (opt.isPresent() && Prefab.ConfigValue.TypeCase.STRING.equals(opt.get().getTypeCase())) { + if (opt.isPresent() + && Prefab.ConfigValue.TypeCase.STRING.equals(opt.get().getTypeCase())) { evaluatedValue = opt.get().getString(); } - return ProviderEvaluation.builder() - .value(evaluatedValue) - .build(); + return ProviderEvaluation.builder().value(evaluatedValue).build(); } @Override @@ -104,9 +102,7 @@ public ProviderEvaluation getIntegerEvaluation(String key, Integer defa if (opt.isPresent() && Prefab.ConfigValue.TypeCase.INT.equals(opt.get().getTypeCase())) { evaluatedValue = Math.toIntExact(opt.get().getInt()); } - return ProviderEvaluation.builder() - .value(evaluatedValue) - .build(); + return ProviderEvaluation.builder().value(evaluatedValue).build(); } @Override @@ -114,12 +110,11 @@ public ProviderEvaluation getDoubleEvaluation(String key, Double default PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx); Double evaluatedValue = defaultValue; Optional opt = prefabCloudClient.featureFlagClient().get(key, context); - if (opt.isPresent() && Prefab.ConfigValue.TypeCase.DOUBLE.equals(opt.get().getTypeCase())) { + if (opt.isPresent() + && Prefab.ConfigValue.TypeCase.DOUBLE.equals(opt.get().getTypeCase())) { evaluatedValue = opt.get().getDouble(); } - return ProviderEvaluation.builder() - .value(evaluatedValue) - .build(); + return ProviderEvaluation.builder().value(evaluatedValue).build(); } @SneakyThrows @@ -128,9 +123,7 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa String defaultValueString = defaultValue == null ? null : defaultValue.asString(); ProviderEvaluation stringEvaluation = getStringEvaluation(key, defaultValueString, ctx); Value evaluatedValue = new Value(stringEvaluation.getValue()); - return ProviderEvaluation.builder() - .value(evaluatedValue) - .build(); + return ProviderEvaluation.builder().value(evaluatedValue).build(); } @SneakyThrows diff --git a/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java b/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java index 848fdefc9..f9ae9351c 100644 --- a/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java +++ b/providers/prefab/src/test/java/dev/openfeature/contrib/providers/prefab/PrefabProviderTest.java @@ -43,11 +43,11 @@ class PrefabProviderTest { static void setUp() { File localDataFile = new File("src/test/resources/features.json"); Options options = new Options() - .setPrefabDatasource(Options.Datasources.ALL) - .setLocalDatafile(localDataFile.toString()) - .setInitializationTimeoutSec(10); - PrefabProviderConfig prefabProviderConfig = PrefabProviderConfig.builder() - .options(options).build(); + .setPrefabDatasource(Options.Datasources.ALL) + .setLocalDatafile(localDataFile.toString()) + .setInitializationTimeoutSec(10); + PrefabProviderConfig prefabProviderConfig = + PrefabProviderConfig.builder().options(options).build(); prefabProvider = new PrefabProvider(prefabProviderConfig); OpenFeatureAPI.getInstance().setProviderAndWait(prefabProvider); client = OpenFeatureAPI.getInstance().getClient(); @@ -60,37 +60,60 @@ static void shutdown() { @Test void getBooleanEvaluation() { - assertEquals(true, prefabProvider.getBooleanEvaluation(FLAG_NAME, false, new ImmutableContext()).getValue()); + assertEquals( + true, + prefabProvider + .getBooleanEvaluation(FLAG_NAME, false, new ImmutableContext()) + .getValue()); assertEquals(true, client.getBooleanValue(FLAG_NAME, false)); - assertEquals(false, prefabProvider.getBooleanEvaluation("non-existing", false, new ImmutableContext()).getValue()); + assertEquals( + false, + prefabProvider + .getBooleanEvaluation("non-existing", false, new ImmutableContext()) + .getValue()); assertEquals(false, client.getBooleanValue("non-existing", false)); } @Test void getStringEvaluation() { - assertEquals(VARIANT_FLAG_VALUE, prefabProvider.getStringEvaluation(VARIANT_FLAG_NAME, "", - new ImmutableContext()).getValue()); + assertEquals( + VARIANT_FLAG_VALUE, + prefabProvider + .getStringEvaluation(VARIANT_FLAG_NAME, "", new ImmutableContext()) + .getValue()); assertEquals(VARIANT_FLAG_VALUE, client.getStringValue(VARIANT_FLAG_NAME, "")); - assertEquals("fallback_str", prefabProvider.getStringEvaluation("non-existing", - "fallback_str", new ImmutableContext()).getValue()); + assertEquals( + "fallback_str", + prefabProvider + .getStringEvaluation("non-existing", "fallback_str", new ImmutableContext()) + .getValue()); assertEquals("fallback_str", client.getStringValue("non-existing", "fallback_str")); } @Test void getObjectEvaluation() { - assertEquals(VARIANT_FLAG_VALUE, prefabProvider.getStringEvaluation(VARIANT_FLAG_NAME, "", - new ImmutableContext()).getValue()); + assertEquals( + VARIANT_FLAG_VALUE, + prefabProvider + .getStringEvaluation(VARIANT_FLAG_NAME, "", new ImmutableContext()) + .getValue()); assertEquals(new Value(VARIANT_FLAG_VALUE), client.getObjectValue(VARIANT_FLAG_NAME, new Value(""))); - assertEquals(new Value("fallback_str"), prefabProvider.getObjectEvaluation("non-existing", - new Value("fallback_str"), new ImmutableContext()).getValue()); + assertEquals( + new Value("fallback_str"), + prefabProvider + .getObjectEvaluation("non-existing", new Value("fallback_str"), new ImmutableContext()) + .getValue()); assertEquals(new Value("fallback_str"), client.getObjectValue("non-existing", new Value("fallback_str"))); } @Test void getIntegerEvaluation() { MutableContext evaluationContext = new MutableContext(); - assertEquals(INT_FLAG_VALUE, prefabProvider.getIntegerEvaluation(INT_FLAG_NAME, 1, - evaluationContext).getValue()); + assertEquals( + INT_FLAG_VALUE, + prefabProvider + .getIntegerEvaluation(INT_FLAG_NAME, 1, evaluationContext) + .getValue()); assertEquals(INT_FLAG_VALUE, client.getIntegerValue(INT_FLAG_NAME, 1)); assertEquals(1, client.getIntegerValue("non-existing", 1)); @@ -101,8 +124,11 @@ void getIntegerEvaluation() { @Test void getDoubleEvaluation() { MutableContext evaluationContext = new MutableContext(); - assertEquals(DOUBLE_FLAG_VALUE, prefabProvider.getDoubleEvaluation(DOUBLE_FLAG_NAME, 1.1, - evaluationContext).getValue()); + assertEquals( + DOUBLE_FLAG_VALUE, + prefabProvider + .getDoubleEvaluation(DOUBLE_FLAG_NAME, 1.1, evaluationContext) + .getValue()); assertEquals(DOUBLE_FLAG_VALUE, client.getDoubleValue(DOUBLE_FLAG_NAME, 1.1)); assertEquals(1.1, client.getDoubleValue("non-existing", 1.1)); @@ -116,10 +142,18 @@ void getBooleanEvaluationByUser() { evaluationContext.add("user.key", "key1"); evaluationContext.add("team.domain", "prefab.cloud"); - assertEquals(true, prefabProvider.getBooleanEvaluation(USERS_FLAG_NAME, false, evaluationContext).getValue()); + assertEquals( + true, + prefabProvider + .getBooleanEvaluation(USERS_FLAG_NAME, false, evaluationContext) + .getValue()); assertEquals(true, client.getBooleanValue(USERS_FLAG_NAME, false, evaluationContext)); evaluationContext.add("team.domain", "other.com"); - assertEquals(false, prefabProvider.getBooleanEvaluation(USERS_FLAG_NAME, false, evaluationContext).getValue()); + assertEquals( + false, + prefabProvider + .getBooleanEvaluation(USERS_FLAG_NAME, false, evaluationContext) + .getValue()); assertEquals(false, client.getBooleanValue(USERS_FLAG_NAME, false, evaluationContext)); } @@ -127,16 +161,16 @@ void getBooleanEvaluationByUser() { @Test void shouldThrowIfNotInitialized() { Options options = new Options() - .setApikey("test-sdk-key") - .setPrefabDatasource(Options.Datasources.LOCAL_ONLY) - .setInitializationTimeoutSec(10); - PrefabProviderConfig prefabProviderConfig = PrefabProviderConfig.builder() - .options(options).build(); + .setApikey("test-sdk-key") + .setPrefabDatasource(Options.Datasources.LOCAL_ONLY) + .setInitializationTimeoutSec(10); + PrefabProviderConfig prefabProviderConfig = + PrefabProviderConfig.builder().options(options).build(); PrefabProvider tempPrefabProvider = new PrefabProvider(prefabProviderConfig); OpenFeatureAPI.getInstance().setProviderAndWait("tempPrefabProvider", tempPrefabProvider); - assertThrows(GeneralError.class, ()-> tempPrefabProvider.initialize(null)); + assertThrows(GeneralError.class, () -> tempPrefabProvider.initialize(null)); tempPrefabProvider.shutdown(); } @@ -145,7 +179,8 @@ void shouldThrowIfNotInitialized() { void eventsTest() { prefabProvider.emitProviderReady(ProviderEventDetails.builder().build()); prefabProvider.emitProviderError(ProviderEventDetails.builder().build()); - prefabProvider.emitProviderConfigurationChanged(ProviderEventDetails.builder().build()); + assertDoesNotThrow(() -> prefabProvider.emitProviderConfigurationChanged( + ProviderEventDetails.builder().build())); } @SneakyThrows @@ -157,13 +192,11 @@ void contextTransformTest() { evaluationContext.add("team.domain", "prefab.cloud"); PrefabContextSet expectedContext = PrefabContextSet.from( - PrefabContext.newBuilder("user").put("key", "key1").build(), - PrefabContext.newBuilder("team").put("domain", "prefab.cloud").build() - ); + PrefabContext.newBuilder("user").put("key", "key1").build(), + PrefabContext.newBuilder("team").put("domain", "prefab.cloud").build()); PrefabContextSetReadable transformedContext = ContextTransformer.transform(evaluationContext); // equals not implemented for User, using toString assertEquals(expectedContext.toString(), transformedContext.toString()); } - }