Skip to content

Commit bae4f4f

Browse files
committed
feat: ofrep provider
Signed-off-by: Rahul Baradol <[email protected]>
1 parent d53cac7 commit bae4f4f

File tree

15 files changed

+733
-0
lines changed

15 files changed

+733
-0
lines changed

.github/component_owners.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ components:
3737
- liran2000
3838
providers/multiprovider:
3939
- liran2000
40+
providers/ofrep-provider:
41+
- Rahul-Baradol
4042
tools/flagd-http-connector:
4143
- liran2000
4244

.release-please-manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"providers/configcat": "0.1.0",
1111
"providers/statsig": "0.1.0",
1212
"providers/multiprovider": "0.0.1",
13+
"providers/ofrep": "0.0.1",
1314
"tools/junit-openfeature": "0.1.2",
1415
"tools/flagd-http-connector": "0.0.2",
1516
".": "0.2.2"

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<module>providers/configcat</module>
4141
<module>providers/statsig</module>
4242
<module>providers/multiprovider</module>
43+
<module>providers/ofrep</module>
4344
<module>tools/flagd-http-connector</module>
4445
</modules>
4546

providers/ofrep/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
### Todo

providers/ofrep/pom.xml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?xml version="1.0"?>
2+
<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"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4+
<modelVersion>4.0.0</modelVersion>
5+
<parent>
6+
<groupId>dev.openfeature.contrib</groupId>
7+
<artifactId>parent</artifactId>
8+
<version>0.2.2</version>
9+
<relativePath>../../pom.xml</relativePath>
10+
</parent>
11+
12+
<groupId>dev.openfeature.contrib.providers</groupId>
13+
<artifactId>ofrep</artifactId>
14+
<version>0.0.1</version> <!--x-release-please-version -->
15+
16+
<name>ofrep</name>
17+
<description>OFREP Provider</description>
18+
<url>https://openfeature.dev</url>
19+
20+
<developers>
21+
<developer>
22+
<id>Rahul-Baradol</id>
23+
<name>Rahul Baradol</name>
24+
<organization>OpenFeature</organization>
25+
<url>https://openfeature.dev/</url>
26+
</developer>
27+
</developers>
28+
29+
<dependencies>
30+
<dependency>
31+
<groupId>junit</groupId>
32+
<artifactId>junit</artifactId>
33+
<version>3.8.1</version>
34+
<scope>test</scope>
35+
</dependency>
36+
37+
<dependency>
38+
<groupId>com.fasterxml.jackson.datatype</groupId>
39+
<artifactId>jackson-datatype-jsr310</artifactId>
40+
<version>2.19.1</version>
41+
</dependency>
42+
43+
<dependency>
44+
<groupId>com.fasterxml.jackson.core</groupId>
45+
<artifactId>jackson-core</artifactId>
46+
<version>2.19.1</version>
47+
</dependency>
48+
49+
<dependency>
50+
<groupId>com.fasterxml.jackson.core</groupId>
51+
<artifactId>jackson-databind</artifactId>
52+
<version>2.19.1</version>
53+
</dependency>
54+
55+
<dependency>
56+
<groupId>com.fasterxml.jackson.core</groupId>
57+
<artifactId>jackson-annotations</artifactId>
58+
<version>2.19.1</version>
59+
</dependency>
60+
61+
<dependency>
62+
<groupId>org.slf4j</groupId>
63+
<artifactId>slf4j-api</artifactId>
64+
<version>2.0.17</version>
65+
</dependency>
66+
67+
<dependency>
68+
<groupId>commons-validator</groupId>
69+
<artifactId>commons-validator</artifactId>
70+
<version>1.7</version>
71+
</dependency>
72+
73+
<dependency>
74+
<groupId>com.google.guava</groupId>
75+
<artifactId>guava</artifactId>
76+
<version>33.4.0-jre</version>
77+
</dependency>
78+
</dependencies>
79+
</project>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package dev.openfeature.contrib.providers.ofrep;
2+
3+
import dev.openfeature.contrib.providers.ofrep.internal.Resolver;
4+
import dev.openfeature.sdk.EvaluationContext;
5+
import dev.openfeature.sdk.FeatureProvider;
6+
import dev.openfeature.sdk.Metadata;
7+
import dev.openfeature.sdk.ProviderEvaluation;
8+
import dev.openfeature.sdk.Value;
9+
import org.apache.commons.validator.routines.UrlValidator;
10+
11+
/**
12+
* OpenFeature provider for OFREP.
13+
*/
14+
public final class OfrepProvider implements FeatureProvider {
15+
16+
private static final String OFREP_PROVIDER = "ofrep";
17+
private final Resolver ofrepResolver;
18+
19+
public static OfrepProvider constructProvider() {
20+
return new OfrepProvider();
21+
}
22+
23+
/**
24+
* Constructs an OfrepProvider with the specified options.
25+
*
26+
* @param options The options for configuring the provider.
27+
* @return An instance of OfrepProvider configured with the provided options.
28+
* @throws IllegalArgumentException if any of the options are invalid.
29+
*/
30+
public static OfrepProvider constructProvider(OfrepProviderOptions options) {
31+
if (!isValidUrl(options.getBaseUrl())) {
32+
throw new IllegalArgumentException("Invalid base URL: " + options.getBaseUrl());
33+
}
34+
35+
if (options.getHeaders() == null) {
36+
throw new IllegalArgumentException("Headers cannot be null");
37+
}
38+
39+
if (options.getTimeout() == null
40+
|| options.getTimeout().isNegative()
41+
|| options.getTimeout().isZero()) {
42+
throw new IllegalArgumentException("Timeout must be a positive duration");
43+
}
44+
45+
if (options.getProxySelector() == null) {
46+
throw new IllegalArgumentException("ProxySelector cannot be null");
47+
}
48+
49+
if (options.getExecutor() == null) {
50+
throw new IllegalArgumentException("Executor cannot be null");
51+
}
52+
53+
return new OfrepProvider(options);
54+
}
55+
56+
private OfrepProvider() {
57+
this(new OfrepProviderOptions.Builder().build());
58+
}
59+
60+
private OfrepProvider(OfrepProviderOptions options) {
61+
this.ofrepResolver = new Resolver(
62+
options.getBaseUrl(),
63+
options.getHeaders(),
64+
options.getTimeout(),
65+
options.getProxySelector(),
66+
options.getExecutor());
67+
}
68+
69+
@Override
70+
public Metadata getMetadata() {
71+
return () -> OFREP_PROVIDER;
72+
}
73+
74+
@Override
75+
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
76+
return ofrepResolver.resolveBoolean(key, defaultValue, ctx);
77+
}
78+
79+
@Override
80+
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
81+
return ofrepResolver.resolveString(key, defaultValue, ctx);
82+
}
83+
84+
@Override
85+
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
86+
return ofrepResolver.resolveInteger(key, defaultValue, ctx);
87+
}
88+
89+
@Override
90+
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
91+
return ofrepResolver.resolveDouble(key, defaultValue, ctx);
92+
}
93+
94+
@Override
95+
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
96+
return ofrepResolver.resolveObject(key, defaultValue, ctx);
97+
}
98+
99+
private static boolean isValidUrl(String url) {
100+
UrlValidator validator = new UrlValidator(new String[] {"http", "https"}, UrlValidator.ALLOW_LOCAL_URLS);
101+
return validator.isValid(url);
102+
}
103+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dev.openfeature.contrib.providers.ofrep;
2+
3+
import com.google.common.collect.ImmutableList;
4+
import com.google.common.collect.ImmutableMap;
5+
import java.net.ProxySelector;
6+
import java.time.Duration;
7+
import java.util.concurrent.Executor;
8+
import lombok.Builder;
9+
import lombok.Getter;
10+
11+
/**
12+
* Options for configuring the OFREP provider.
13+
*/
14+
@Getter
15+
@Builder(builderClassName = "Builder", buildMethodName = "build")
16+
public class OfrepProviderOptions {
17+
18+
@Builder.Default
19+
private final String baseUrl = "http://localhost:8016";
20+
21+
@Builder.Default
22+
private final ProxySelector proxySelector = ProxySelector.getDefault();
23+
24+
@Builder.Default
25+
private final Executor executor = Runnable::run;
26+
27+
@Builder.Default
28+
private final Duration timeout = Duration.ofSeconds(10);
29+
30+
@Builder.Default
31+
private final ImmutableMap<String, ImmutableList<String>> headers = ImmutableMap.of();
32+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package dev.openfeature.contrib.providers.ofrep.internal;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.DeserializationFeature;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.google.common.collect.ImmutableList;
7+
import com.google.common.collect.ImmutableMap;
8+
import dev.openfeature.sdk.exceptions.GeneralError;
9+
import java.io.IOException;
10+
import java.net.ProxySelector;
11+
import java.net.URI;
12+
import java.net.http.HttpClient;
13+
import java.net.http.HttpRequest;
14+
import java.net.http.HttpResponse;
15+
import java.time.Duration;
16+
import java.time.Instant;
17+
import java.util.concurrent.Executor;
18+
import lombok.extern.slf4j.Slf4j;
19+
20+
/**
21+
* The OfrepApi class is responsible for communicating with the OFREP server to evaluate flags.
22+
*/
23+
@Slf4j
24+
public class OfrepApi {
25+
26+
private static final String path = "/ofrep/v1/evaluate/flags/";
27+
private final ObjectMapper serializer;
28+
private final ObjectMapper deserializer;
29+
private final HttpClient httpClient;
30+
private Instant nextAllowedRequestTime = Instant.now();
31+
32+
/**
33+
* Constructs an OfrepApi instance with a HTTP client and JSON serializers.
34+
*
35+
* @param timeout - The timeout duration for establishing HTTP connection.
36+
* @param proxySelector - The ProxySelector to use for HTTP requests.
37+
* @param executor - The Executor to use for operations.
38+
*/
39+
public OfrepApi(Duration timeout, ProxySelector proxySelector, Executor executor) {
40+
httpClient = HttpClient.newBuilder()
41+
.connectTimeout(timeout)
42+
.proxy(proxySelector)
43+
.executor(executor)
44+
.build();
45+
serializer = new ObjectMapper();
46+
deserializer = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
47+
}
48+
49+
/**
50+
* prepareHttpRequest is preparing the request to be sent to the OFREP Server.
51+
*
52+
* @param url - url of the request
53+
* @param headers - headers to be included in the request
54+
* @param requestBody - body of the request
55+
*
56+
* @return HttpRequest ready to be sent
57+
* @throws JsonProcessingException - if an error occurred while processing the json.
58+
*/
59+
private <T> HttpRequest prepareHttpRequest(
60+
final URI url, ImmutableMap<String, ImmutableList<String>> headers, final T requestBody)
61+
throws JsonProcessingException {
62+
63+
HttpRequest.Builder reqBuilder = HttpRequest.newBuilder()
64+
.uri(url)
65+
.header("Content-Type", "application/json; charset=utf-8")
66+
.POST(HttpRequest.BodyPublishers.ofByteArray(serializer.writeValueAsBytes(requestBody)));
67+
68+
for (ImmutableMap.Entry<String, ImmutableList<String>> entry : headers.entrySet()) {
69+
String key = entry.getKey();
70+
for (String value : entry.getValue()) {
71+
reqBuilder.header(key, value);
72+
}
73+
}
74+
75+
return reqBuilder.build();
76+
}
77+
78+
/**
79+
* resolve is the method that interacts with the OFREP server to evaluate flags.
80+
*
81+
* @param baseUrl - The base URL of the OFREP server.
82+
* @param headers - headers to include in the request.
83+
* @param key - The flag key to evaluate.
84+
* @param requestBody - The evaluation context as a map of key-value pairs.
85+
*
86+
* @return Resolution object, containing the response status, headers, and body.
87+
*/
88+
public Resolution resolve(
89+
String baseUrl,
90+
ImmutableMap<String, ImmutableList<String>> headers,
91+
String key,
92+
final OfrepRequest requestBody) {
93+
if (nextAllowedRequestTime.isAfter(Instant.now())) {
94+
throw new GeneralError("Rate limit exceeded. Please wait before making another request.");
95+
}
96+
97+
try {
98+
String fullPath = baseUrl + path + key;
99+
URI uri = URI.create(fullPath);
100+
101+
HttpRequest request = prepareHttpRequest(uri, headers, requestBody);
102+
103+
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
104+
OfrepResponse responseBody = deserializer.readValue(response.body(), OfrepResponse.class);
105+
106+
return new Resolution(response.statusCode(), response.headers(), responseBody);
107+
} catch (JsonProcessingException e) {
108+
throw new GeneralError("Error processing JSON: " + e.getMessage());
109+
} catch (IOException e) {
110+
throw new GeneralError("IO error: " + e.getMessage());
111+
} catch (InterruptedException e) {
112+
throw new GeneralError("Request interrupted: " + e.getMessage());
113+
}
114+
}
115+
116+
/**
117+
* Sets the next allowed request time based on the Retry-After header.
118+
* If the provided time is later than the current next allowed request time, it updates it.
119+
*
120+
* @param retryAfter The value of the Retry-After header, which can be a number of seconds or a date string.
121+
*/
122+
public void setNextAllowedRequestTime(Instant retryAfter) {
123+
if (retryAfter.isAfter(nextAllowedRequestTime)) {
124+
nextAllowedRequestTime = retryAfter;
125+
}
126+
}
127+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package dev.openfeature.contrib.providers.ofrep.internal;
2+
3+
import com.google.common.collect.ImmutableMap;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import lombok.ToString;
7+
8+
/**
9+
* Represents the request body for the OFREP API request.
10+
*/
11+
@ToString
12+
@Getter
13+
@AllArgsConstructor
14+
public class OfrepRequest {
15+
private final ImmutableMap<String, Object> context;
16+
}

0 commit comments

Comments
 (0)