Skip to content

feat: add Prefab provider #1325

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ components:
- novalisdenahi
providers/statsig:
- liran2000
providers/prefab:
- liran2000
- jkebinger
providers/multiprovider:
- liran2000

Expand Down
1 change: 1 addition & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"providers/configcat": "0.1.0",
"providers/statsig": "0.1.0",
"providers/multiprovider": "0.0.1",
"providers/prefab": "0.0.1",
"tools/junit-openfeature": "0.1.2"
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<module>providers/flipt</module>
<module>providers/configcat</module>
<module>providers/statsig</module>
<module>providers/prefab</module>
<module>providers/multiprovider</module>
</modules>

Expand Down
1 change: 1 addition & 0 deletions providers/prefab/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Changelog
59 changes: 59 additions & 0 deletions providers/prefab/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Unofficial Prefab OpenFeature Provider for Java

[Prefab](https://www.prefab.cloud/) OpenFeature Provider can provide usage for Prefab via OpenFeature Java SDK.

## Installation

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

```xml

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

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

## 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("user.key", "key1");
evaluationContext.add("team.domain", "prefab.cloud");
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.
5 changes: 5 additions & 0 deletions providers/prefab/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/prefab/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>prefab</artifactId>
<version>0.0.1</version> <!--x-release-please-version -->

<name>prefab</name>
<description>Prefab provider for Java</description>
<url>https://www.prefab.cloud</url>

<dependencies>
<dependency>
<groupId>cloud.prefab</groupId>
<artifactId>client</artifactId>
<version>0.3.20</version>
</dependency>

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

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

</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +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 {

private ContextTransformer() {}

protected static PrefabContextSetReadable transform(EvaluationContext ctx) {
Map<String, PrefabContext.Builder> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package dev.openfeature.contrib.providers.prefab;

import cloud.prefab.client.PrefabCloudClient;
import cloud.prefab.context.PrefabContextSetReadable;
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.Value;
import dev.openfeature.sdk.exceptions.GeneralError;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

/**
* 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;

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());
log.info("finished initializing provider");

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<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx);
Boolean evaluatedValue = prefabCloudClient.featureFlagClient().featureIsOn(key, context);
return ProviderEvaluation.<Boolean>builder().value(evaluatedValue).build();
}

@Override
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx);
String evaluatedValue = defaultValue;
Optional<Prefab.ConfigValue> opt = prefabCloudClient.featureFlagClient().get(key, context);
if (opt.isPresent()
&& Prefab.ConfigValue.TypeCase.STRING.equals(opt.get().getTypeCase())) {
evaluatedValue = opt.get().getString();
}
return ProviderEvaluation.<String>builder().value(evaluatedValue).build();
}

@Override
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx);
Integer evaluatedValue = defaultValue;
Optional<Prefab.ConfigValue> 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.<Integer>builder().value(evaluatedValue).build();
}

@Override
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
PrefabContextSetReadable context = ctx == null ? null : ContextTransformer.transform(ctx);
Double evaluatedValue = defaultValue;
Optional<Prefab.ConfigValue> opt = prefabCloudClient.featureFlagClient().get(key, context);
if (opt.isPresent()
&& Prefab.ConfigValue.TypeCase.DOUBLE.equals(opt.get().getTypeCase())) {
evaluatedValue = opt.get().getDouble();
}
return ProviderEvaluation.<Double>builder().value(evaluatedValue).build();
}

@SneakyThrows
@Override
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
String defaultValueString = defaultValue == null ? null : defaultValue.asString();
ProviderEvaluation<String> stringEvaluation = getStringEvaluation(key, defaultValueString, ctx);
Value evaluatedValue = new Value(stringEvaluation.getValue());
return ProviderEvaluation.<Value>builder().value(evaluatedValue).build();
}

@SneakyThrows
@Override
public void shutdown() {
super.shutdown();
log.info("shutdown");
if (prefabCloudClient != null) {
prefabCloudClient.close();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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;
}
Loading