From 9732dc6092faa7bab12f0796a7f077dfd13b4b64 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Tue, 13 Aug 2024 11:43:52 +0300 Subject: [PATCH 1/2] 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/2] 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