Skip to content
Closed
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

ignored-authors:
- renovate-bot
1 change: 1 addition & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
"providers/flipt": "0.0.2",
"providers/configcat": "0.0.3",
"providers/statsig": "0.0.4",
"providers/prefab": "0.0.1",
"tools/junit-openfeature": "0.0.2"
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<module>providers/flipt</module>
<module>providers/configcat</module>
<module>providers/statsig</module>
<module>providers/prefab</module>
</modules>

<scm>
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>
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

hi @jkebinger,
OpenFeature SDK built with Java 8, so it will require Java 8 version.
Is it possible to release Java 8 Prefab client ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It appears the primary change to get to java8 compatibility will be removing the use of the java 11 HttpClient, so not too bad. Can't make any promises on when we may be able to prioritize that work; I'll probably aim to implement it such that there's a java8 compatibility add on so java 11+ users don't have to get an additional set of dependencies (probably okhttp). We'll see

</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 {

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,172 @@
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.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.Collections;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

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

@Getter
private ProviderState state = ProviderState.NOT_READY;

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());
prefabProviderConfig.postInit();
state = ProviderState.READY;
log.info("finished initializing provider, state: {}", state);

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) {
verifyEvaluation();
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) {
verifyEvaluation();
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) {
verifyEvaluation();
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) {
verifyEvaluation();
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();
}

private void verifyEvaluation() throws ProviderNotReadyError, GeneralError {
if (!ProviderState.READY.equals(state)) {

/*
According to spec Requirement 2.4.5:
"The provider SHOULD indicate an error if flag resolution is attempted before the provider is ready."
https://github.com/open-feature/spec/blob/main/specification/sections/02-providers.md#requirement-245
*/
if (ProviderState.NOT_READY.equals(state)) {
throw new ProviderNotReadyError(PROVIDER_NOT_YET_INITIALIZED);
}
throw new GeneralError(UNKNOWN_ERROR);
}
}

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

public void postInit() {

}
}
Loading