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 @@ <module>providers/go-feature-flag</module> <module>providers/jsonlogic-eval-provider</module> <module>providers/env-var</module> + <module>providers/togglz</module> </modules> <scm> 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 + +<!-- x-release-please-start-version --> + +```xml + +<dependency> + <groupId>dev.openfeature.contrib.providers</groupId> + <artifactId>togglz</artifactId> + <version>0.0.1</version> +</dependency> +``` + +<!-- x-release-please-end-version --> + +## 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>dev.openfeature.contrib</groupId> + <artifactId>parent</artifactId> + <version>0.1.0</version> + <relativePath>../../pom.xml</relativePath> + </parent> + <groupId>dev.openfeature.contrib.providers</groupId> + <artifactId>togglz</artifactId> + <version>0.0.1</version> <!--x-release-please-version --> + + <name>togglz</name> + <description>togglz provider for Java</description> + <url>https://www.togglz.org/</url> + + <dependencies> + <dependency> + <groupId>org.togglz</groupId> + <artifactId>togglz-core</artifactId> + <version>3.3.0</version> + </dependency> + + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>2.0.7</version> + </dependency> + + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-slf4j2-impl</artifactId> + <version>2.20.0</version> + <scope>test</scope> + </dependency> + + </dependencies> +</project> 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<Feature> 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<String, Feature> 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<Boolean> 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.<Boolean>builder() + .value(featureBooleanValue) + .reason(Reason.TARGETING_MATCH.name()) + .build(); + } + + @Override + public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + throw new TypeMismatchError(NOT_IMPLEMENTED); + } + + @Override + public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + throw new TypeMismatchError(NOT_IMPLEMENTED); + } + + @Override + public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + throw new TypeMismatchError(NOT_IMPLEMENTED); + } + + @Override + public ProviderEvaluation<Value> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Configuration status="DEBUG"> + <Appenders> + <Console name="consoleLogger" target="SYSTEM_OUT"> + <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> + </Console> + </Appenders> + <Loggers> + <Root level="trace"> + <AppenderRef ref="consoleLogger"/> + </Root> + </Loggers> +</Configuration> \ 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