Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Togglz provider #415

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Empty file added providers/togglz/CHANGELOG.md
Empty file.
7 changes: 7 additions & 0 deletions providers/togglz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Unofficial Togglz OpenFeature Provider for Java

## Usage
See [TogglzProviderTest.java](./src/test/java/dev/openfeature/contrib/providers/togglz/TogglzProviderTest.java)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be addressed in a follow-up PR, but please add more information to the readme. Please include information such as install instructions, basic usage, limitations (only booleans are supported), etc.


### References
* [Togglz](https://www.togglz.org)
5 changes: 5 additions & 0 deletions providers/togglz/lombok.config
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions providers/togglz/pom.xml
Original file line number Diff line number Diff line change
@@ -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>4.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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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.Collection;
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(Collection<Feature> featuresCollection) {
featuresCollection.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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to use the reason STATIC since it doesn't look like evaluation context is being used.

https://openfeature.dev/specification/types#resolution-details

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure that STATIC is correct here, since Activation can be done by user even without passing context if I understand correctly.
@bennetelli, @hennr can share their thoughts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't you need to include FeatureUser when calling isActive?

Copy link
Member

@toddbaert toddbaert Sep 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@beeme1mr I don't think so. It seems like Togglz uses pre-configured features/beans to get these values contextually. For example, to get the user name you configure one of these: https://www.togglz.org/documentation/authentication, which take the user-name from the servlet context, etc.

This is actually configured in the feature (see the constructor) if I understand correctly.

I'm honestly not sure how to properly determine the reason with this provider.

.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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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);

OpenFeatureAPI api = OpenFeatureAPI.getInstance();
featureProvider = new TogglzProvider(Arrays.asList(TestFeatures.values()));
api.setProviderAndWait(featureProvider);
client = api.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() {
TogglzProvider togglzProvider = new TogglzProvider(Arrays.asList(TestFeatures.values()));

// ErrorCode.PROVIDER_NOT_READY should be returned when evaluated via the client
assertThrows(ProviderNotReadyError.class, ()-> togglzProvider.getBooleanEvaluation("fail_not_initialized", false, new ImmutableContext()));
}
}
13 changes: 13 additions & 0 deletions providers/togglz/src/test/resources/log4j2-test.xml
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions providers/togglz/version.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.0.1
Loading