Skip to content

Commit

Permalink
feat: Add ConfigCat provider (#521)
Browse files Browse the repository at this point in the history
Signed-off-by: liran2000 <[email protected]>
  • Loading branch information
liran2000 authored Nov 16, 2023
1 parent e97124a commit 879cc9d
Show file tree
Hide file tree
Showing 15 changed files with 668 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ components:
providers/flipt:
- liran2000
- markphelps
providers/configcat:
- liran2000
- z4kn4fein
- laliconfigcat
- novalisdenahi

ignored-authors:
- renovate-bot
3 changes: 2 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"providers/env-var": "0.0.4",
"providers/jsonlogic-eval-provider": "1.0.1",
"providers/unleash": "0.0.2-alpha",
"providers/flipt": "0.0.2"
"providers/flipt": "0.0.2",
"providers/configcat": "0.0.1"
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<module>providers/env-var</module>
<module>providers/unleash</module>
<module>providers/flipt</module>
<module>providers/configcat</module>
</modules>

<scm>
Expand Down
Empty file.
52 changes: 52 additions & 0 deletions providers/configcat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Unofficial ConfigCat OpenFeature Provider for Java

[ConfigCat](https://configcat.com/) OpenFeature Provider can provide usage for ConfigCat via OpenFeature Java SDK.

## Installation

<!-- x-release-please-start-version -->

```xml

<dependency>
<groupId>dev.openfeature.contrib.providers</groupId>
<artifactId>configcat</artifactId>
<version>0.0.1</version>
</dependency>
```

<!-- x-release-please-end-version -->

## Usage
ConfigCat OpenFeature Provider is using ConfigCat Java SDK.

### Usage Example

```
ConfigCatProviderConfig configCatProviderConfig = ConfigCatProviderConfig.builder().sdkKey(sdkKey).build();
configCatProvider = new ConfigCatProvider(configCatProviderConfig);
OpenFeatureAPI.getInstance().setProviderAndWait(configCatProvider);
boolean featureEnabled = client.getBooleanValue(FLAG_NAME, false);
MutableContext evaluationContext = new MutableContext();
evaluationContext.setTargetingKey("[email protected]");
evaluationContext.add("Email", "[email protected]");
evaluationContext.add("Country", "someCountry");
featureEnabled = client.getBooleanValue(USERS_FLAG_NAME, false, evaluationContext);
```

See [ConfigCatProviderTest.java](./src/test/java/dev/openfeature/contrib/providers/configcat/ConfigCatProviderTest.java)
for more information.

## Notes
Some ConfigCat custom operations are supported from the provider client via:

```java
configCatProvider.getConfigCatClient()...
```

## ConfigCat Provider Tests Strategies

Unit test based on ConfigCat local features file.
See [ConfigCatProviderTest.java](./src/test/java/dev/openfeature/contrib/providers/configcat/ConfigCatProviderTest.java)
for more information.
5 changes: 5 additions & 0 deletions providers/configcat/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/configcat/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>configcat</artifactId>
<version>0.0.1</version> <!--x-release-please-version -->

<name>configcat</name>
<description>configcat provider for Java</description>
<url>https://configcat.com/</url>

<dependencies>
<dependency>
<groupId>com.configcat</groupId>
<artifactId>configcat-java-client</artifactId>
<version>8.3.0</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.21.0</version>
<scope>test</scope>
</dependency>

</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package dev.openfeature.contrib.providers.configcat;

import com.configcat.ConfigCatClient;
import com.configcat.EvaluationDetails;
import com.configcat.User;
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.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* Provider implementation for ConfigCat.
*/
@Slf4j
public class ConfigCatProvider extends EventProvider {

@Getter
private static final String NAME = "ConfigCat";

public static final String PROVIDER_NOT_YET_INITIALIZED = "provider not yet initialized";
public static final String UNKNOWN_ERROR = "unknown error";

private ConfigCatProviderConfig configCatProviderConfig;

@Getter
private ConfigCatClient configCatClient;

@Getter
private ProviderState state = ProviderState.NOT_READY;

private AtomicBoolean isInitialized = new AtomicBoolean(false);

/**
* Constructor.
* @param configCatProviderConfig configCatProvider Config
*/
public ConfigCatProvider(ConfigCatProviderConfig configCatProviderConfig) {
this.configCatProviderConfig = configCatProviderConfig;
}

/**
* 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);
configCatClient = ConfigCatClient.get(configCatProviderConfig.getSdkKey(),
configCatProviderConfig.getOptions());
configCatProviderConfig.postInit();
state = ProviderState.READY;
log.info("finished initializing provider, state: {}", state);

configCatClient.getHooks().addOnConfigChanged(map -> {
ProviderEventDetails providerEventDetails = ProviderEventDetails.builder()
.flagsChanged(new ArrayList<>(map.keySet()))
.message("config changed")
.build();
emitProviderReady(providerEventDetails);
});

configCatClient.getHooks().addOnError(errorMessage -> {
ProviderEventDetails providerEventDetails = ProviderEventDetails.builder()
.message(errorMessage)
.build();
emitProviderError(providerEventDetails);
});
}

@Override
public Metadata getMetadata() {
return () -> NAME;
}

@Override
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
return getEvaluation(Boolean.class, key, defaultValue, ctx);
}

@Override
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
return getEvaluation(String.class, key, defaultValue, ctx);
}

@Override
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
return getEvaluation(Integer.class, key, defaultValue, ctx);
}

@Override
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
return getEvaluation(Double.class, key, defaultValue, ctx);
}

@SneakyThrows
@Override
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
return getEvaluation(Value.class, key, defaultValue, ctx);
}

private <T> ProviderEvaluation<T> getEvaluation(Class<T> classOfT, String key, T 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);
}
User user = ctx == null ? null : ContextTransformer.transform(ctx);
EvaluationDetails<T> evaluationDetails;
T evaluatedValue;
if (classOfT.isAssignableFrom(Value.class)) {
String defaultValueStr = defaultValue == null ? null : ((Value)defaultValue).asString();
evaluationDetails = (EvaluationDetails<T>) configCatClient
.getValueDetails(String.class, key, user, defaultValueStr);
evaluatedValue = evaluationDetails.getValue() == null ? null :
(T) Value.objectToValue(evaluationDetails.getValue());
} else {
evaluationDetails = configCatClient
.getValueDetails(classOfT, key, user, defaultValue);
evaluatedValue = evaluationDetails.getValue();
}
return ProviderEvaluation.<T>builder()
.value(evaluatedValue)
.variant(evaluationDetails.getVariationId())
.build();
}

@SneakyThrows
@Override
public void shutdown() {
super.shutdown();
log.info("shutdown");
if (configCatClient != null) {
configCatClient.close();
}
state = ProviderState.NOT_READY;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.openfeature.contrib.providers.configcat;

import com.configcat.ConfigCatClient;
import lombok.Builder;
import lombok.Getter;

import java.util.function.Consumer;


/**
* Options for initializing ConfigCat provider.
*/
@Getter
@Builder
public class ConfigCatProviderConfig {
private Consumer<ConfigCatClient.Options> options;

// Only holding temporary for initialization
private String sdkKey;

public void postInit() {
sdkKey = null; // for security, not holding key in memory for long-term
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package dev.openfeature.contrib.providers.configcat;

import com.configcat.User;
import dev.openfeature.sdk.EvaluationContext;

import java.util.HashMap;
import java.util.Map;

/**
* Transformer from OpenFeature context to ConfigCat User.
*/
public class ContextTransformer {

public static final String CONTEXT_EMAIL = "Email";
public static final String CONTEXT_COUNTRY = "Country";

protected static User transform(EvaluationContext ctx) {
User.Builder userBuilder = User.newBuilder();
Map<String, String> customMap = new HashMap<>();
ctx.asObjectMap().forEach((k, v) -> {
switch (k) {
case CONTEXT_COUNTRY:
userBuilder.country(String.valueOf(v));
break;
case CONTEXT_EMAIL:
userBuilder.email(String.valueOf(v));
break;
default:
customMap.put(k, String.valueOf(v));
break;
}
});
userBuilder.custom(customMap);
return userBuilder.build(ctx.getTargetingKey());
}

}
Loading

0 comments on commit 879cc9d

Please sign in to comment.