Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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