diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 4372c0a2a..b2aef428d 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -19,6 +19,9 @@ components: - thomaspoignant providers/jsonlogic-eval-provider: - justinabrahms + providers/togglz: + - liran2000 + - bennetelli ignored-authors: - renovate-bot \ No newline at end of file diff --git a/pom.xml b/pom.xml index 50d38c56f..ebbec1753 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ providers/go-feature-flag providers/jsonlogic-eval-provider providers/env-var + providers/togglz diff --git a/providers/togglz/CHANGELOG.md b/providers/togglz/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/providers/togglz/README.md b/providers/togglz/README.md new file mode 100644 index 000000000..e7735f63e --- /dev/null +++ b/providers/togglz/README.md @@ -0,0 +1,42 @@ +# Unofficial Togglz OpenFeature Provider for Java + +Togglz OpenFeature Provider can provide usage for Togglz via OpenFeature Java SDK. + +## Installation + + + +```xml + + + dev.openfeature.contrib.providers + togglz + 0.0.1 + +``` + + + +## Usage +Togglz OpenFeature Provider is using Togglz Java SDK. + +### Usage Example + +``` +TogglzOptions togglzOptions = TogglzOptions.builder().features(features).build(); +FeatureProvider featureProvider = new TogglzProvider(togglzOptions); +api.setProviderAndWait(featureProvider); +client = api.getClient(); +boolean featureEnabled = client.getBooleanValue(TestFeatures.FEATURE_ONE.name(), false); + +``` + +See [TogglzProviderTest.java](./src/test/java/dev/openfeature/contrib/providers/togglz/TogglzProviderTest.java) for more information. + +## Caveats / Limitations + +* Togglz OpenFeature Provider only supports boolean feature flags. +* Evaluation does not treat evaluation context, but relies on Togglz functionalities. + +## References +* [Togglz](https://www.togglz.org) diff --git a/providers/togglz/lombok.config b/providers/togglz/lombok.config new file mode 100644 index 000000000..bcd1afdae --- /dev/null +++ b/providers/togglz/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/togglz/pom.xml b/providers/togglz/pom.xml new file mode 100644 index 000000000..804eefdc0 --- /dev/null +++ b/providers/togglz/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + dev.openfeature.contrib + parent + 0.1.0 + ../../pom.xml + + dev.openfeature.contrib.providers + togglz + 0.0.1 + + togglz + togglz provider for Java + https://www.togglz.org/ + + + + org.togglz + togglz-core + 3.3.0 + + + + org.slf4j + slf4j-api + 2.0.7 + + + + org.apache.logging.log4j + log4j-slf4j2-impl + 2.20.0 + test + + + + diff --git a/providers/togglz/src/main/java/dev/openfeature/contrib/providers/togglz/TogglzOptions.java b/providers/togglz/src/main/java/dev/openfeature/contrib/providers/togglz/TogglzOptions.java new file mode 100644 index 000000000..413262d4c --- /dev/null +++ b/providers/togglz/src/main/java/dev/openfeature/contrib/providers/togglz/TogglzOptions.java @@ -0,0 +1,16 @@ +package dev.openfeature.contrib.providers.togglz; + +import lombok.Builder; +import lombok.Data; +import org.togglz.core.Feature; + +import java.util.Collection; + +/** + * Togglz Options for initializing Togglz provider. + */ +@Data +@Builder +public class TogglzOptions { + Collection features; +} diff --git a/providers/togglz/src/main/java/dev/openfeature/contrib/providers/togglz/TogglzProvider.java b/providers/togglz/src/main/java/dev/openfeature/contrib/providers/togglz/TogglzProvider.java new file mode 100644 index 000000000..54bb1545a --- /dev/null +++ b/providers/togglz/src/main/java/dev/openfeature/contrib/providers/togglz/TogglzProvider.java @@ -0,0 +1,98 @@ +package dev.openfeature.contrib.providers.togglz; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.EventProvider; +import dev.openfeature.sdk.Metadata; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.ProviderState; +import dev.openfeature.sdk.Reason; +import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import dev.openfeature.sdk.exceptions.GeneralError; +import dev.openfeature.sdk.exceptions.ProviderNotReadyError; +import dev.openfeature.sdk.exceptions.TypeMismatchError; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.togglz.core.Feature; + +import java.util.HashMap; +import java.util.Map; + +/** + * Provider implementation for Togglz. + */ +@AllArgsConstructor +@Slf4j +public class TogglzProvider extends EventProvider { + + @Getter + private static final String NAME = "Togglz Provider"; + public static final String NOT_IMPLEMENTED = + "Not implemented - provider does not support this type. Only boolean is supported."; + + private Map features = new HashMap<>(); + + @Getter + private ProviderState state = ProviderState.NOT_READY; + + public TogglzProvider(TogglzOptions togglzOptions) { + togglzOptions.features.forEach(feature -> features.put(feature.name(), feature)); + } + + /** + * Initialize the provider. + * @param evaluationContext evaluation context + * @throws Exception on error + */ + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + super.initialize(evaluationContext); + state = ProviderState.READY; + log.debug("finished initializing provider, state: {}", state); + } + + @Override + public Metadata getMetadata() { + return () -> NAME; + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + if (!ProviderState.READY.equals(state)) { + if (ProviderState.NOT_READY.equals(state)) { + throw new ProviderNotReadyError("provider not yet initialized"); + } + throw new GeneralError("unknown error"); + } + Feature feature = features.get(key); + if (feature == null) { + throw new FlagNotFoundError("flag " + key + " not found"); + } + boolean featureBooleanValue = feature.isActive(); + return ProviderEvaluation.builder() + .value(featureBooleanValue) + .reason(Reason.TARGETING_MATCH.name()) + .build(); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + throw new TypeMismatchError(NOT_IMPLEMENTED); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + throw new TypeMismatchError(NOT_IMPLEMENTED); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + throw new TypeMismatchError(NOT_IMPLEMENTED); + } + + @Override + public ProviderEvaluation getObjectEvaluation(String s, Value value, EvaluationContext evaluationContext) { + throw new TypeMismatchError(NOT_IMPLEMENTED); + } +} diff --git a/providers/togglz/src/test/java/dev/openfeature/contrib/providers/togglz/TestFeatures.java b/providers/togglz/src/test/java/dev/openfeature/contrib/providers/togglz/TestFeatures.java new file mode 100644 index 000000000..a3f862ac0 --- /dev/null +++ b/providers/togglz/src/test/java/dev/openfeature/contrib/providers/togglz/TestFeatures.java @@ -0,0 +1,18 @@ +package dev.openfeature.contrib.providers.togglz; + +import org.togglz.core.Feature; +import org.togglz.core.annotation.Label; +import org.togglz.core.context.FeatureContext; + +public enum TestFeatures implements Feature { + + @Label("First Feature") + FEATURE_ONE, + + @Label("Second Feature") + FEATURE_TWO; + + public boolean isActive() { + return FeatureContext.getFeatureManager().isActive(this); + } +} diff --git a/providers/togglz/src/test/java/dev/openfeature/contrib/providers/togglz/TogglzProviderTest.java b/providers/togglz/src/test/java/dev/openfeature/contrib/providers/togglz/TogglzProviderTest.java new file mode 100644 index 000000000..2250b2333 --- /dev/null +++ b/providers/togglz/src/test/java/dev/openfeature/contrib/providers/togglz/TogglzProviderTest.java @@ -0,0 +1,82 @@ +package dev.openfeature.contrib.providers.togglz; + +import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.FeatureProvider; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.exceptions.FlagNotFoundError; +import dev.openfeature.sdk.exceptions.ProviderNotReadyError; +import dev.openfeature.sdk.exceptions.TypeMismatchError; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.togglz.core.context.StaticFeatureManagerProvider; +import org.togglz.core.manager.FeatureManager; +import org.togglz.core.manager.FeatureManagerBuilder; +import org.togglz.core.repository.FeatureState; +import org.togglz.core.repository.StateRepository; +import org.togglz.core.repository.mem.InMemoryStateRepository; +import org.togglz.core.user.NoOpUserProvider; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TogglzProviderTest { + + private FeatureProvider featureProvider; + + private Client client; + + @BeforeEach + void setUp() throws Exception { + StateRepository stateRepository = new InMemoryStateRepository(); + stateRepository.setFeatureState(new FeatureState(TestFeatures.FEATURE_ONE, true)); + stateRepository.setFeatureState(new FeatureState(TestFeatures.FEATURE_TWO, false)); + + FeatureManager featureManager = new FeatureManagerBuilder() + .featureEnums(TestFeatures.class) + .stateRepository(stateRepository) + .userProvider(new NoOpUserProvider()) + .build(); + StaticFeatureManagerProvider.setFeatureManager(featureManager); + + TogglzOptions togglzOptions = TogglzOptions.builder().features(Arrays.asList(TestFeatures.values())).build(); + featureProvider = new TogglzProvider(togglzOptions); + OpenFeatureAPI.getInstance().setProviderAndWait(featureProvider); + client = OpenFeatureAPI.getInstance().getClient(); + } + + @Test + void getBooleanEvaluation() { + assertEquals(true, featureProvider.getBooleanEvaluation(TestFeatures.FEATURE_ONE.name(), false, new ImmutableContext()).getValue()); + assertEquals(true, client.getBooleanValue(TestFeatures.FEATURE_ONE.name(), false)); + assertEquals(false, featureProvider.getBooleanEvaluation(TestFeatures.FEATURE_TWO.name(), false, new ImmutableContext()).getValue()); + assertEquals(false, client.getBooleanValue(TestFeatures.FEATURE_TWO.name(), false)); + } + + @Test + void notFound() { + assertThrows(FlagNotFoundError.class, () -> { + featureProvider.getBooleanEvaluation("not-found-flag", false, new ImmutableContext()); + }); + } + + @Test + void typeMismatch() { + assertThrows(TypeMismatchError.class, () -> { + featureProvider.getStringEvaluation(TestFeatures.FEATURE_ONE.name(), "default_value", new ImmutableContext()); + }); + } + + @SneakyThrows + @Test + void shouldThrowIfNotInitialized() { + TogglzOptions togglzOptions = TogglzOptions.builder().features(Arrays.asList(TestFeatures.values())).build(); + FeatureProvider togglzProvider = new TogglzProvider(togglzOptions); + + // ErrorCode.PROVIDER_NOT_READY should be returned when evaluated via the client + assertThrows(ProviderNotReadyError.class, ()-> togglzProvider.getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext())); + } +} \ No newline at end of file diff --git a/providers/togglz/src/test/resources/log4j2-test.xml b/providers/togglz/src/test/resources/log4j2-test.xml new file mode 100644 index 000000000..223d21a89 --- /dev/null +++ b/providers/togglz/src/test/resources/log4j2-test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/providers/togglz/version.txt b/providers/togglz/version.txt new file mode 100644 index 000000000..8acdd82b7 --- /dev/null +++ b/providers/togglz/version.txt @@ -0,0 +1 @@ +0.0.1