Skip to content

Commit 4961478

Browse files
aepfliclaude
andcommitted
feat: Create OpenFeature API module with ServiceLoader pattern
- Implement interface segregation (Core, Hooks, Context, Advanced) - Add ServiceLoader singleton with priority-based provider selection - Create no-op fallback implementation for API-only consumers - Move core interfaces and data types from SDK to API package - Support multiple implementations with clean API contracts - Enable backward compatibility through abstraction layer Key components: - OpenFeatureAPI: Main abstract class combining all interfaces - OpenFeatureAPIProvider: ServiceLoader interface for implementations - NoOpOpenFeatureAPI/NoOpClient: Safe fallback implementations - Core interfaces: Client, FeatureProvider, Hook, Metadata - Data types: Value, Structure, EvaluationContext, exceptions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 0b1aa7a commit 4961478

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+3263
-0
lines changed

openfeature-api/pom.xml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>dev.openfeature</groupId>
9+
<artifactId>openfeature-java</artifactId>
10+
<version>2.0.0</version>
11+
</parent>
12+
13+
<artifactId>openfeature-api</artifactId>
14+
15+
<name>OpenFeature Java API</name>
16+
<description>OpenFeature Java API - Core contracts and interfaces for feature flag evaluation</description>
17+
18+
<properties>
19+
<module-name>dev.openfeature.api</module-name>
20+
</properties>
21+
22+
<dependencies>
23+
<!-- Minimal dependencies for API -->
24+
<dependency>
25+
<groupId>org.slf4j</groupId>
26+
<artifactId>slf4j-api</artifactId>
27+
<version>2.0.17</version>
28+
</dependency>
29+
30+
<!-- Lombok for clean code generation -->
31+
<dependency>
32+
<groupId>org.projectlombok</groupId>
33+
<artifactId>lombok</artifactId>
34+
<version>1.18.38</version>
35+
<scope>provided</scope>
36+
</dependency>
37+
38+
<!-- Spotbugs for annotations -->
39+
<dependency>
40+
<groupId>com.github.spotbugs</groupId>
41+
<artifactId>spotbugs</artifactId>
42+
<version>4.8.6</version>
43+
<scope>provided</scope>
44+
</dependency>
45+
</dependencies>
46+
47+
<build>
48+
<plugins>
49+
<plugin>
50+
<groupId>org.apache.maven.plugins</groupId>
51+
<artifactId>maven-compiler-plugin</artifactId>
52+
</plugin>
53+
54+
<plugin>
55+
<groupId>org.apache.maven.plugins</groupId>
56+
<artifactId>maven-jar-plugin</artifactId>
57+
<version>3.4.2</version>
58+
<configuration>
59+
<archive>
60+
<manifestEntries>
61+
<Automatic-Module-Name>${module-name}</Automatic-Module-Name>
62+
</manifestEntries>
63+
</archive>
64+
</configuration>
65+
</plugin>
66+
</plugins>
67+
</build>
68+
69+
</project>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package dev.openfeature.api;
2+
3+
import java.util.Collections;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
import lombok.EqualsAndHashCode;
7+
8+
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
9+
@EqualsAndHashCode
10+
abstract class AbstractStructure implements Structure {
11+
12+
protected final Map<String, Value> attributes;
13+
14+
@Override
15+
public boolean isEmpty() {
16+
return attributes == null || attributes.isEmpty();
17+
}
18+
19+
AbstractStructure() {
20+
this.attributes = new HashMap<>();
21+
}
22+
23+
AbstractStructure(Map<String, Value> attributes) {
24+
this.attributes = attributes;
25+
}
26+
27+
/**
28+
* Returns an unmodifiable representation of the internal attribute map.
29+
*
30+
* @return immutable map
31+
*/
32+
public Map<String, Value> asUnmodifiableMap() {
33+
return Collections.unmodifiableMap(attributes);
34+
}
35+
36+
/**
37+
* Get all values as their underlying primitives types.
38+
*
39+
* @return all attributes on the structure into a Map
40+
*/
41+
@Override
42+
public Map<String, Object> asObjectMap() {
43+
return attributes.entrySet().stream()
44+
// custom collector, workaround for Collectors.toMap in JDK8
45+
// https://bugs.openjdk.org/browse/JDK-8148463
46+
.collect(
47+
HashMap::new,
48+
(accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())),
49+
HashMap::putAll);
50+
}
51+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package dev.openfeature.api;
2+
3+
/**
4+
* This is a common interface between the evaluation results that providers return and what is given to the end users.
5+
*
6+
* @param <T> The type of flag being evaluated.
7+
*/
8+
public interface BaseEvaluation<T> {
9+
/**
10+
* Returns the resolved value of the evaluation.
11+
*
12+
* @return {T} the resolve value
13+
*/
14+
T getValue();
15+
16+
/**
17+
* Returns an identifier for this value, if applicable.
18+
*
19+
* @return {String} value identifier
20+
*/
21+
String getVariant();
22+
23+
/**
24+
* Describes how we came to the value that we're returning.
25+
*
26+
* @return {Reason}
27+
*/
28+
String getReason();
29+
30+
/**
31+
* The error code, if applicable. Should only be set when the Reason is ERROR.
32+
*
33+
* @return {ErrorCode}
34+
*/
35+
ErrorCode getErrorCode();
36+
37+
/**
38+
* The error message (usually from exception.getMessage()), if applicable.
39+
* Should only be set when the Reason is ERROR.
40+
*
41+
* @return {String}
42+
*/
43+
String getErrorMessage();
44+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dev.openfeature.api;
2+
3+
/**
4+
* An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic
5+
* to the lifecycle of flag evaluation.
6+
*
7+
* @see Hook
8+
*/
9+
public interface BooleanHook extends Hook<Boolean> {
10+
11+
@Override
12+
default boolean supportsFlagValueType(FlagValueType flagValueType) {
13+
return FlagValueType.BOOLEAN == flagValueType;
14+
}
15+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package dev.openfeature.api;
2+
3+
import java.util.List;
4+
5+
/**
6+
* Interface used to resolve flags of varying types.
7+
*/
8+
public interface Client extends Features, Tracking, EventBus<Client> {
9+
ClientMetadata getMetadata();
10+
11+
/**
12+
* Return an optional client-level evaluation context.
13+
*
14+
* @return {@link EvaluationContext}
15+
*/
16+
EvaluationContext getEvaluationContext();
17+
18+
/**
19+
* Set the client-level evaluation context.
20+
*
21+
* @param ctx Client level context.
22+
*/
23+
Client setEvaluationContext(EvaluationContext ctx);
24+
25+
/**
26+
* Adds hooks for evaluation.
27+
* Hooks are run in the order they're added in the before stage. They are run in reverse order for all other stages.
28+
*
29+
* @param hooks The hook to add.
30+
*/
31+
Client addHooks(Hook... hooks);
32+
33+
/**
34+
* Fetch the hooks associated to this client.
35+
*
36+
* @return A list of {@link Hook}s.
37+
*/
38+
List<Hook> getHooks();
39+
40+
/**
41+
* Returns the current state of the associated provider.
42+
*
43+
* @return the provider state
44+
*/
45+
ProviderState getProviderState();
46+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package dev.openfeature.api;
2+
3+
/**
4+
* Metadata specific to an OpenFeature {@code Client}.
5+
*/
6+
public interface ClientMetadata {
7+
String getDomain();
8+
9+
@Deprecated
10+
// this is here for compatibility with getName() exposed from {@link Metadata}
11+
default String getName() {
12+
return getDomain();
13+
}
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dev.openfeature.api;
2+
3+
/**
4+
* An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic
5+
* to the lifecycle of flag evaluation.
6+
*
7+
* @see Hook
8+
*/
9+
public interface DoubleHook extends Hook<Double> {
10+
11+
@Override
12+
default boolean supportsFlagValueType(FlagValueType flagValueType) {
13+
return FlagValueType.DOUBLE == flagValueType;
14+
}
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package dev.openfeature.api;
2+
3+
@SuppressWarnings("checkstyle:MissingJavadocType")
4+
public enum ErrorCode {
5+
PROVIDER_NOT_READY,
6+
FLAG_NOT_FOUND,
7+
PARSE_ERROR,
8+
TYPE_MISMATCH,
9+
TARGETING_KEY_MISSING,
10+
INVALID_CONTEXT,
11+
GENERAL,
12+
PROVIDER_FATAL
13+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package dev.openfeature.api;
2+
3+
import java.util.Map;
4+
import java.util.Map.Entry;
5+
import java.util.function.Function;
6+
7+
/**
8+
* The EvaluationContext is a container for arbitrary contextual data
9+
* that can be used as a basis for dynamic evaluation.
10+
*/
11+
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
12+
public interface EvaluationContext extends Structure {
13+
14+
String TARGETING_KEY = "targetingKey";
15+
16+
String getTargetingKey();
17+
18+
/**
19+
* Merges this EvaluationContext object with the second overriding the this in
20+
* case of conflict.
21+
*
22+
* @param overridingContext overriding context
23+
* @return resulting merged context
24+
*/
25+
EvaluationContext merge(EvaluationContext overridingContext);
26+
27+
/**
28+
* Recursively merges the overriding map into the base Value map.
29+
* The base map is mutated, the overriding map is not.
30+
* Null maps will cause no-op.
31+
*
32+
* @param newStructure function to create the right structure(s) for Values
33+
* @param base base map to merge
34+
* @param overriding overriding map to merge
35+
*/
36+
static void mergeMaps(
37+
Function<Map<String, Value>, Structure> newStructure,
38+
Map<String, Value> base,
39+
Map<String, Value> overriding) {
40+
41+
if (base == null) {
42+
return;
43+
}
44+
if (overriding == null || overriding.isEmpty()) {
45+
return;
46+
}
47+
48+
for (Entry<String, Value> overridingEntry : overriding.entrySet()) {
49+
String key = overridingEntry.getKey();
50+
if (overridingEntry.getValue().isStructure()
51+
&& base.containsKey(key)
52+
&& base.get(key).isStructure()) {
53+
Structure mergedValue = base.get(key).asStructure();
54+
Structure overridingValue = overridingEntry.getValue().asStructure();
55+
Map<String, Value> newMap = mergedValue.asMap();
56+
mergeMaps(newStructure, newMap, overridingValue.asUnmodifiableMap());
57+
base.put(key, new Value(newStructure.apply(newMap)));
58+
} else {
59+
base.put(key, overridingEntry.getValue());
60+
}
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)