Skip to content

feat: ofrep provider #1429

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

Merged
merged 13 commits into from
Jul 17, 2025
Merged
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
@@ -37,6 +37,9 @@ components:
- liran2000
providers/multiprovider:
- liran2000
providers/ofrep-provider:
- Rahul-Baradol
- toddbaert
tools/flagd-http-connector:
- liran2000

1 change: 1 addition & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
"providers/configcat": "0.2.1",
"providers/statsig": "0.2.1",
"providers/multiprovider": "0.0.3",
"providers/ofrep": "0.0.1",
"tools/junit-openfeature": "0.2.1",
"tools/flagd-http-connector": "0.0.4",
".": "1.0.0"
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@
<module>providers/configcat</module>
<module>providers/statsig</module>
<module>providers/multiprovider</module>
<module>providers/ofrep</module>
<module>tools/flagd-http-connector</module>
</modules>

74 changes: 74 additions & 0 deletions providers/ofrep/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# OFREP Provider for OpenFeature

This provider allows to connect to any feature flag management system that supports OFREP.

## Installation
For Maven
<!-- x-release-please-start-version -->
```xml
<dependency>
<groupId>dev.openfeature.contrib.providers</groupId>
<artifactId>ofrep</artifactId>
<version>0.0.1</version>
</dependency>
```

For Gradle
```groovy
implementation 'dev.openfeature.contrib.providers:ofrep:0.0.1'
```
<!-- x-release-please-end-version -->

## Configuration and Usage

### Usage
```java
OfrepProviderOptions options = OfrepProviderOptions.builder().build();
OfrepProvider ofrepProvider = OfrepProvider.constructProvider(options);
```
### Example
```java
import dev.openfeature.contrib.providers.ofrep.OfrepProvider;
import dev.openfeature.contrib.providers.ofrep.OfrepProviderOptions;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.FlagEvaluationDetails;
import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.OpenFeatureAPI;

public class App {
public static void main(String[] args) {
OpenFeatureAPI openFeatureAPI = OpenFeatureAPI.getInstance();

OfrepProviderOptions options = OfrepProviderOptions.builder().build();
OfrepProvider ofrepProvider = OfrepProvider.constructProvider(options);

openFeatureAPI.setProvider(ofrepProvider);

Client client = openFeatureAPI.getClient();

MutableContext context = new MutableContext();
context.setTargetingKey("my-identify-id");

FlagEvaluationDetails<Boolean> details = client.getBooleanDetails("my-boolean-flag", false, context);
System.out.println("Flag value: " + details.getValue());

openFeatureAPI.shutdown();
}
}
```

### Configuration options

Options are passed via `OfrepProviderOptions`, using which default values can be overridden.

Given below are the supported configurations:


| Option name | Type | Default | Description
| ----------- | ------- | --------- | ---------
| baseUrl | String | http://localhost:8016 | Override the default OFREP API URL.
| headers | ImmutableMap | Empty Map | Add custom headers which will be sent with each network request to the OFREP API.
| timeout | Duration | 10 Seconds | The timeout duration to establishing the connection.
| proxySelector | ProxySelector | ProxySelector.getDefault() | The proxy selector used by HTTP Client.
| executor | Executor | Thread Pool of size 5 | The executor used by HTTP Client.

85 changes: 85 additions & 0 deletions providers/ofrep/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.openfeature.contrib</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<groupId>dev.openfeature.contrib.providers</groupId>
<artifactId>ofrep</artifactId>
<version>0.0.1</version> <!--x-release-please-version -->

<name>ofrep</name>
<description>OFREP Provider</description>
<url>https://openfeature.dev</url>

<developers>
<developer>
<id>Rahul-Baradol</id>
<name>Rahul Baradol</name>
<organization>OpenFeature</organization>
<url>https://openfeature.dev/</url>
</developer>
</developers>

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.19.1</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.19.1</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.19.1</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.19.1</version>
</dependency>

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

<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.7</version>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.4.0-jre</version>
</dependency>

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.12.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package dev.openfeature.contrib.providers.ofrep;

import dev.openfeature.contrib.providers.ofrep.internal.Resolver;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Value;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.validator.routines.UrlValidator;

/**
* OpenFeature provider for OFREP.
*/
@Slf4j
public final class OfrepProvider implements FeatureProvider {

private static final String OFREP_PROVIDER = "ofrep";
private static final long DEFAULT_EXECUTOR_SHUTDOWN_TIMEOUT = 1000;

private final Resolver ofrepResolver;
private Executor executor;

public static OfrepProvider constructProvider() {
return new OfrepProvider();
}

/**
* Constructs an OfrepProvider with the specified options.
*
* @param options The options for configuring the provider.
* @return An instance of OfrepProvider configured with the provided options.
* @throws IllegalArgumentException if any of the options are invalid.
*/
public static OfrepProvider constructProvider(OfrepProviderOptions options) {
if (!isValidUrl(options.getBaseUrl())) {
throw new IllegalArgumentException("Invalid base URL: " + options.getBaseUrl());
}

if (options.getHeaders() == null) {
throw new IllegalArgumentException("Headers cannot be null");
}

if (options.getRequestTimeout() == null
|| options.getRequestTimeout().isNegative()
|| options.getRequestTimeout().isZero()) {
throw new IllegalArgumentException("Request timeout must be a positive duration");
}

if (options.getConnectTimeout() == null
|| options.getConnectTimeout().isNegative()
|| options.getConnectTimeout().isZero()) {
throw new IllegalArgumentException("Connect timeout must be a positive duration");
}

if (options.getProxySelector() == null) {
throw new IllegalArgumentException("ProxySelector cannot be null");
}

if (options.getExecutor() == null) {
throw new IllegalArgumentException("Executor cannot be null");
}

return new OfrepProvider(options);
}

private OfrepProvider() {
this(new OfrepProviderOptions.Builder().build());
}

private OfrepProvider(OfrepProviderOptions options) {
this.executor = options.getExecutor();
this.ofrepResolver = new Resolver(
options.getBaseUrl(),
options.getHeaders(),
options.getRequestTimeout(),
options.getConnectTimeout(),
options.getProxySelector(),
options.getExecutor());
}

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

@Override
public void shutdown() {
if (executor instanceof ExecutorService) {
ExecutorService executorService = (ExecutorService) executor;
try {
executorService.shutdown();

if (!executorService.awaitTermination(DEFAULT_EXECUTOR_SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS)) {
executorService.shutdownNow();
if (!executorService.awaitTermination(DEFAULT_EXECUTOR_SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS)) {
log.error("Provider couldn't shutdown gracefully.");
}
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

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

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

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

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

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

private static boolean isValidUrl(String url) {
UrlValidator validator = new UrlValidator(new String[] {"http", "https"}, UrlValidator.ALLOW_LOCAL_URLS);
return validator.isValid(url);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package dev.openfeature.contrib.providers.ofrep;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.net.ProxySelector;
import java.time.Duration;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import lombok.Builder;
import lombok.Getter;

/**
* Options for configuring the OFREP provider.
*/
@Getter
@Builder(builderClassName = "Builder", buildMethodName = "build")
public class OfrepProviderOptions {

private static final int DEFAULT_THREAD_POOL_SIZE = 5;

@Builder.Default
private final String baseUrl = "http://localhost:8016";

@Builder.Default
private final ProxySelector proxySelector = ProxySelector.getDefault();

@Builder.Default
private final Executor executor = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE);

@Builder.Default
private final Duration requestTimeout = Duration.ofSeconds(10);

@Builder.Default
private final Duration connectTimeout = Duration.ofSeconds(10);

@Builder.Default
private final ImmutableMap<String, ImmutableList<String>> headers = ImmutableMap.of();
}
Loading