diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f130b89d7..4cf2d508c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,6 +24,8 @@ jobs:
id: release
with:
token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}}
+ prerelease: ${{ github.ref == 'refs/heads/beta' }}
+ prerelease-type: "beta"
outputs:
release_created: ${{ fromJSON(steps.release.outputs.paths_released)[0] != null }} # if we have a single release path, do the release
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index b0c190560..6ae2a9275 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1 +1,4 @@
-{".":"1.18.0"}
+{
+ "./openfeature-api": "0.0.0-beta",
+ "./openfeature-sdk": "2.0.0-beta"
+}
diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md
new file mode 100644
index 000000000..f89559249
--- /dev/null
+++ b/BREAKING_CHANGES.md
@@ -0,0 +1,649 @@
+# Breaking Changes - OpenFeature Java SDK v2.0.0
+
+This document outlines all breaking changes introduced in the `feat/split-api-and-sdk` branch compared to the `main` branch (v1.18.0). These changes represent a major version bump to v2.0.0.
+
+## 📊 Change Summary
+- **32 commits** with comprehensive refactoring starting from v1.18.0
+- **Complete architectural transformation** from single-module to multi-module Maven project
+- **76 Java files** in original `src/main/java/dev/openfeature/sdk/` → **Split into 2 modules**
+- **API module**: 84 files in `dev.openfeature.api` package structure
+- **SDK module**: Implementation + compatibility layer + providers
+- **Lombok completely removed** - replaced with hand-written builders
+- **Full immutability transformation** - all POJOs now immutable with builders
+- **Package reorganization** - interfaces moved from `dev.openfeature.sdk.*` to `dev.openfeature.api.*`
+- **Comprehensive compatibility layer** for gradual migration (deprecated wrappers)
+- **ServiceLoader integration** for API provider discovery
+
+## 🏗️ Architecture Changes
+
+### Module Structure & Maven Coordinates
+**Breaking**: The monolithic SDK has been split into separate API and SDK modules with new Maven coordinates.
+
+**Before (v1.18.0)**:
+```xml
+
+
+ dev.openfeature
+ sdk
+ 1.18.0
+
+
+
+
+```
+
+**After (v2.0.0)**:
+```xml
+
+
+ dev.openfeature
+ api
+ 0.0.1
+
+
+
+
+ dev.openfeature
+ sdk
+ 2.0.0
+
+
+
+
+
+```
+
+### Maven Project Structure
+**Breaking**: Project structure changed from single module to multi-module Maven project.
+
+**Before**: Single `pom.xml` with `artifactId=sdk`
+
+**After**: Multi-module structure:
+```
+java-sdk/
+├── pom.xml # Parent aggregator POM (artifactId: openfeature-java)
+├── openfeature-api/
+│ └── pom.xml # API module POM (artifactId: api)
+└── openfeature-sdk/
+ └── pom.xml # SDK module POM (artifactId: sdk)
+```
+
+**Parent POM Changes**:
+- `artifactId` changed from `sdk` to `openfeature-java`
+- `packaging` changed from `jar` to `pom`
+- Added `` section with `openfeature-api` and `openfeature-sdk`
+- Maintains all shared configuration (plugins, dependencies, etc.)
+
+**Module Dependencies**:
+- **API module** (`artifactId: api`): Standalone with minimal dependencies (SLF4J, SpotBugs annotations)
+- **SDK module** (`artifactId: sdk`): Depends on API module and includes full implementation
+
+**Migration**:
+- **Library Authors**: Switch to `dev.openfeature:api` for minimal dependencies
+- **Application Developers**: Switch to `dev.openfeature:sdk` for full functionality (note: same `artifactId` but different structure)
+- **Build Systems**: Update to reference new parent POM structure
+- **CI/CD**: May need updates to handle multi-module Maven builds
+
+---
+
+## 🔒 POJO Immutability Changes
+
+### ProviderEvaluation
+**Breaking**: `ProviderEvaluation` transformed from Lombok `@Data` to immutable with builders.
+
+**Before (v1.18.0 with Lombok)**:
+```java
+// Lombok @Data, @Builder, @NoArgsConstructor, @AllArgsConstructor
+ProviderEvaluation eval = new ProviderEvaluation<>();
+eval.setValue("test"); // Lombok-generated setter
+eval.setVariant("variant1"); // Lombok-generated setter
+eval.setReason("DEFAULT"); // Lombok-generated setter
+
+// Or Lombok builder
+ProviderEvaluation eval = ProviderEvaluation.builder()
+ .value("test")
+ .variant("variant1")
+ .reason("DEFAULT")
+ .build();
+```
+
+**After (v2.0.0 with hand-written builders)**:
+```java
+// Hand-written builder pattern only - no more Lombok
+ProviderEvaluation eval = ProviderEvaluation.builder()
+ .value("test")
+ .variant("variant1")
+ .reason("DEFAULT")
+ .errorCode(ErrorCode.NONE)
+ .flagMetadata(metadata)
+ .build();
+
+// Object is immutable - no setters available
+// eval.setValue("new"); // ❌ Compilation error - no Lombok setters
+// Moved from dev.openfeature.sdk.ProviderEvaluation → dev.openfeature.api.evaluation.ProviderEvaluation
+```
+
+**Migration**: Replace constructor calls and setter usage with builder pattern.
+
+### FlagEvaluationDetails
+**Breaking**: `FlagEvaluationDetails` is now immutable with private constructors.
+
+**Before**:
+```java
+// Public constructors
+FlagEvaluationDetails details = new FlagEvaluationDetails<>();
+details.setFlagKey("my-flag");
+details.setValue("test");
+
+// Or constructor with parameters
+FlagEvaluationDetails details = new FlagEvaluationDetails<>(
+ "my-flag", "test", "variant1", "DEFAULT", ErrorCode.NONE, null, metadata);
+```
+
+**After**:
+```java
+// Builder pattern only
+FlagEvaluationDetails details = FlagEvaluationDetails.builder()
+ .flagKey("my-flag")
+ .value("test")
+ .variant("variant1")
+ .reason("DEFAULT")
+ .build();
+```
+
+### EventDetails & ProviderEventDetails
+**Breaking**: Constructor access removed, builder pattern required.
+
+**Before**:
+```java
+ProviderEventDetails details = new ProviderEventDetails();
+EventDetails event = new EventDetails("provider", "domain", details);
+```
+
+**After**:
+```java
+ProviderEventDetails details = ProviderEventDetails.builder()
+ .message("Configuration changed")
+ .flagsChanged(Arrays.asList("flag1", "flag2"))
+ .build();
+
+EventDetails event = EventDetails.builder()
+ .providerName("provider")
+ .domain("domain")
+ .providerEventDetails(details)
+ .build();
+```
+
+---
+
+## 🏗️ Builder Pattern Changes
+
+### Builder Class Names
+**Breaking**: All builder class names standardized to `Builder`.
+
+**Before**:
+```java
+ImmutableMetadata.ImmutableMetadataBuilder builder = ImmutableMetadata.builder();
+FlagEvaluationDetails.FlagEvaluationDetailsBuilder builder =
+ FlagEvaluationDetails.builder();
+ProviderEvaluation.ProviderEvaluationBuilder builder =
+ ProviderEvaluation.builder();
+```
+
+**After**:
+```java
+ImmutableMetadata.Builder builder = ImmutableMetadata.builder();
+FlagEvaluationDetails.Builder builder = FlagEvaluationDetails.builder();
+ProviderEvaluation.Builder builder = ProviderEvaluation.builder();
+```
+
+**Migration**: Update any explicit builder type references (rare in typical usage).
+
+### Removed Convenience Methods
+**Breaking**: Convenience methods removed in favor of consistent builder patterns.
+
+**Before**:
+```java
+// Convenience methods
+EventDetails details = EventDetails.fromProviderEventDetails(providerDetails);
+HookContext context = HookContext.from(otherContext);
+FlagEvaluationDetails details = FlagEvaluationDetails.from(evaluation);
+```
+
+**After**:
+```java
+// Builder pattern only
+EventDetails details = EventDetails.builder()
+ .providerEventDetails(providerDetails)
+ .providerName(providerName)
+ .build();
+
+HookContext context = HookContext.builder()
+ .flagKey(flagKey)
+ .type(FlagValueType.STRING)
+ .defaultValue(defaultValue)
+ .build();
+```
+
+**Migration**: Replace convenience method calls with explicit builder usage.
+
+---
+
+## 📦 Package and Class Changes
+
+### DefaultOpenFeatureAPI Encapsulation
+**Breaking**: `DefaultOpenFeatureAPI` constructor is now package-private.
+
+**Before**:
+```java
+// Direct instantiation possible (not recommended)
+DefaultOpenFeatureAPI api = new DefaultOpenFeatureAPI();
+```
+
+**After**:
+```java
+// Package-private constructor - use factory methods
+OpenFeatureAPI api = OpenFeature.getApi(); // Recommended approach
+```
+
+**Migration**: Use `OpenFeature.getApi()` instead of direct instantiation.
+
+### Interface Reorganization & Package Changes
+**Breaking**: Major package reorganization with new interface names and locations.
+
+**Original Structure (v1.18.0)**:
+```
+src/main/java/dev/openfeature/sdk/
+├── FeatureProvider.java # Main provider interface
+├── Features.java # Client interface
+├── OpenFeatureAPI.java # API singleton
+├── ProviderEvaluation.java # Evaluation result (Lombok @Data)
+├── EvaluationContext.java # Context interface
+├── Value.java # Value type
+├── ErrorCode.java # Error enum
+├── Hook.java # Hook interface
+└── exceptions/
+ ├── OpenFeatureError.java # Base exception
+ └── ...
+```
+
+**New Structure (v2.0.0)**:
+```
+openfeature-api/src/main/java/dev/openfeature/api/
+├── Provider.java # Renamed from FeatureProvider
+├── evaluation/
+│ ├── EvaluationClient.java # Renamed from Features
+│ ├── ProviderEvaluation.java # Moved here, now immutable
+│ └── EvaluationContext.java # Moved here
+├── types/
+│ └── Value.java # Moved here
+├── ErrorCode.java # Moved here
+├── lifecycle/
+│ └── Hook.java # Moved here
+└── exceptions/
+ └── OpenFeatureError.java # Moved here
+
+openfeature-sdk/src/main/java/dev/openfeature/sdk/
+├── FeatureProvider.java # Deprecated wrapper → extends Provider
+├── Features.java # Deprecated wrapper → extends EvaluationClient
+├── OpenFeatureClient.java # Implementation
+├── compat/
+│ └── CompatibilityGuide.java # Migration helper
+└── providers/memory/ # Concrete providers
+```
+
+**Interface Migration**:
+- `dev.openfeature.sdk.FeatureProvider` → `dev.openfeature.api.Provider`
+- `dev.openfeature.sdk.Features` → `dev.openfeature.api.evaluation.EvaluationClient`
+- `dev.openfeature.sdk.OpenFeatureAPI` → `dev.openfeature.api.OpenFeatureAPI`
+
+**EvaluationClient Optimization**:
+- **Reduced from 30 methods to 10 abstract methods** + 20 default methods
+- Default methods handle parameter delegation (empty context, default options)
+- `get{Type}Value` methods now delegate to `get{Type}Details().getValue()`
+- Massive reduction in boilerplate for implementers
+
+**Migration**: Update import statements and leverage new default method implementations.
+
+### ServiceLoader Integration
+**Breaking**: New ServiceLoader pattern for provider discovery.
+
+**New File**: `openfeature-sdk/src/main/resources/META-INF/services/dev.openfeature.api.OpenFeatureAPIProvider`
+
+**Impact**: Enables automatic discovery of OpenFeature API implementations.
+
+### Internal Class Movement
+**Breaking**: Internal utility classes moved from API to SDK module.
+
+**Moved Classes**:
+- `AutoCloseableLock` → SDK module
+- `AutoCloseableReentrantReadWriteLock` → SDK module
+- `ObjectUtils` → SDK module
+- `TriConsumer` → SDK module (kept in API for internal use)
+
+**Migration**: These were internal classes - external usage should be minimal. If used, switch to SDK dependency.
+
+---
+
+## 🔧 API Consistency Changes
+
+### Event Details Architecture
+**Breaking**: Event details now use composition over inheritance.
+
+**Before**:
+```java
+// EventDetails extended ProviderEventDetails
+EventDetails details = new EventDetails(...);
+details.getFlagsChanged(); // Inherited method
+```
+
+**After**:
+```java
+// EventDetails composes ProviderEventDetails
+EventDetails details = EventDetails.builder()...build();
+details.getFlagsChanged(); // Delegates to composed object
+```
+
+**Impact**: Behavioral compatibility maintained, but inheritance relationship removed.
+
+### Required Provider Names
+**Breaking**: Provider names now required for EventDetails per OpenFeature spec.
+
+**Before**:
+```java
+// Provider name could be null
+EventDetails details = EventDetails.builder()
+ .domain("domain")
+ .build();
+```
+
+**After**:
+```java
+// Provider name is required
+EventDetails details = EventDetails.builder()
+ .providerName("my-provider") // Required
+ .domain("domain")
+ .build(); // Will throw if providerName is null
+```
+
+**Migration**: Always provide provider names when creating EventDetails.
+
+---
+
+## 📦 Lombok Dependency Removal
+**Breaking**: Complete removal of Lombok dependency from both API and SDK modules.
+
+**Original State (v1.18.0)**:
+```java
+// Heavy Lombok usage throughout codebase
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProviderEvaluation implements BaseEvaluation {
+ T value;
+ String variant;
+ private String reason;
+ ErrorCode errorCode;
+ private String errorMessage;
+ @Builder.Default
+ private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
+}
+
+// Similar @Data pattern used in:
+// - FlagEvaluationDetails, EventDetails, ProviderEventDetails
+// - ImmutableContext, ImmutableMetadata, Value
+// - Many other POJOs
+```
+
+**Transformed State (v2.0.0)**:
+```java
+// Hand-written immutable classes with custom builders
+public final class ProviderEvaluation implements BaseEvaluation {
+ private final T value;
+ private final String variant;
+ private final String reason;
+ private final ErrorCode errorCode;
+ private final String errorMessage;
+ private final ImmutableMetadata flagMetadata;
+
+ private ProviderEvaluation(Builder builder) {
+ this.value = builder.value;
+ this.variant = builder.variant;
+ // ... (all fields set from builder)
+ }
+
+ public static Builder builder() {
+ return new Builder<>();
+ }
+
+ public static final class Builder {
+ // Hand-written builder implementation
+ }
+
+ // Hand-written getters (no setters - immutable)
+ public T getValue() { return value; }
+ // ...
+}
+```
+
+**Impact**:
+- **No more Lombok annotations**: `@Data`, `@Builder`, `@Value`, `@Slf4j` completely removed
+- **All builder patterns now hand-written** with consistent naming (`Builder` instead of `ClassNameBuilder`)
+- **Improved IDE compatibility** - no more IDE plugins required for Lombok
+- **Better debugging experience** - actual source code instead of generated methods
+- **Cleaner bytecode** - no Lombok magic
+- **Explicit control** over builder behavior and validation
+
+**Migration**: No user action required - all Lombok-generated functionality replaced with equivalent hand-written code. Builder patterns remain the same from user perspective.
+
+---
+
+## 🔧 Recent Interface Optimizations (Latest Changes)
+
+### EvaluationClient Default Method Implementation
+**Enhancement**: Major reduction in implementation burden for interface implementers.
+
+**Before**: Implementers had to override all 30 methods manually
+```java
+public class MyClient implements EvaluationClient {
+ @Override
+ public Boolean getBooleanValue(String key, Boolean defaultValue) {
+ return getBooleanValue(key, defaultValue, EvaluationContext.EMPTY);
+ }
+
+ @Override
+ public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx) {
+ return getBooleanValue(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
+ }
+
+ // ... 28 more similar delegating methods
+}
+```
+
+**After**: Only core methods need implementation, defaults handle delegation
+```java
+public class MyClient implements EvaluationClient {
+ // Only need to implement these 10 core methods:
+ // - get{Type}Details(key, defaultValue, ctx, options) - 5 methods
+ // All other 25 methods provided as defaults that delegate properly
+}
+```
+
+**Impact**:
+- **75% reduction** in required method implementations
+- **NoOpClient reduced from ~200 lines to ~50 lines**
+- Consistent delegation logic across all implementations
+- Future-proof: changes to delegation only need to happen in interface
+
+---
+
+## 🚫 Removed Public APIs
+
+### Public Setters
+**Breaking**: All public setters removed from immutable POJOs.
+
+**Removed Methods**:
+- `ProviderEvaluation.setValue(T)`
+- `ProviderEvaluation.setVariant(String)`
+- `ProviderEvaluation.setReason(String)`
+- `ProviderEvaluation.setErrorCode(ErrorCode)`
+- `ProviderEvaluation.setErrorMessage(String)`
+- `ProviderEvaluation.setFlagMetadata(ImmutableMetadata)`
+- `FlagEvaluationDetails.setFlagKey(String)`
+- `FlagEvaluationDetails.setValue(T)`
+- `FlagEvaluationDetails.setVariant(String)`
+- `FlagEvaluationDetails.setReason(String)`
+- `FlagEvaluationDetails.setErrorCode(ErrorCode)`
+- `FlagEvaluationDetails.setErrorMessage(String)`
+- `FlagEvaluationDetails.setFlagMetadata(ImmutableMetadata)`
+
+**Migration**: Use builders to create objects with desired state instead of mutation.
+
+### Public Constructors
+**Breaking**: Public constructors removed from POJOs.
+
+**Removed Constructors**:
+- `ProviderEvaluation()`
+- `ProviderEvaluation(T, String, String, ErrorCode, String, ImmutableMetadata)`
+- `FlagEvaluationDetails()`
+- `FlagEvaluationDetails(String, T, String, String, ErrorCode, String, ImmutableMetadata)`
+- `EventDetails(String, String, ProviderEventDetails)`
+- `ProviderEventDetails()` (deprecated, now private)
+- `ProviderEventDetails(List, String, ImmutableMetadata, ErrorCode)`
+
+**Migration**: Use builder patterns exclusively for object creation.
+
+---
+
+## 🔄 Migration Summary
+
+### For Library Authors (Feature Flag Provider Implementers)
+1. **Update Dependencies**: Change from old monolithic `sdk` to new `api` module
+ ```xml
+
+
+ dev.openfeature
+ sdk
+ 1.18.0
+
+
+
+
+ dev.openfeature
+ api
+ 0.0.1
+
+ ```
+
+2. **Update Package Imports**: Change all package references
+ ```java
+ // OLD imports (v1.18.0)
+ import dev.openfeature.sdk.FeatureProvider;
+ import dev.openfeature.sdk.ProviderEvaluation;
+ import dev.openfeature.sdk.EvaluationContext;
+ import dev.openfeature.sdk.ErrorCode;
+
+ // NEW imports (v2.0.0)
+ import dev.openfeature.api.Provider; // FeatureProvider → Provider
+ import dev.openfeature.api.evaluation.ProviderEvaluation;
+ import dev.openfeature.api.evaluation.EvaluationContext;
+ import dev.openfeature.api.ErrorCode;
+ ```
+2. **Review Package Access**: Ensure no usage of moved internal classes
+3. **Update Documentation**: Reference new module structure
+4. **Verify Scope**: API module contains only interfaces and POJOs needed for provider implementation
+
+### For SDK Users (Application Developers)
+1. **Update Dependencies**: Update `sdk` dependency version (same artifactId, major refactor)
+ ```xml
+
+
+ dev.openfeature
+ sdk
+ 1.18.0
+
+
+
+
+ dev.openfeature
+ sdk
+ 2.0.0
+
+ ```
+
+2. **Gradual Migration Strategy**: v2.0.0 includes compatibility layer
+ ```java
+ // IMMEDIATE: These still work but show deprecation warnings
+ import dev.openfeature.sdk.FeatureProvider; // @Deprecated, extends Provider
+ import dev.openfeature.sdk.Features; // @Deprecated, extends EvaluationClient
+
+ // FUTURE: Migrate imports gradually (before v2.1.0 when compatibility layer is removed)
+ import dev.openfeature.api.Provider;
+ import dev.openfeature.api.evaluation.EvaluationClient;
+ ```
+2. **Replace Constructors**: Use builders for all POJO creation
+3. **Remove Setter Usage**: Objects are now immutable
+4. **Update Convenience Methods**: Use builders instead of `from()` methods
+5. **Ensure Provider Names**: Always specify provider names in events
+
+### For Build Systems & CI/CD
+1. **Multi-module Builds**: Update build scripts to handle Maven multi-module structure
+2. **Artifact Publishing**: Both API and SDK modules are now published separately
+3. **Version Management**: Parent POM manages versions for both modules
+4. **Testing**: Tests are distributed across both modules
+
+### Quick Migration Checklist
+
+#### Maven/Gradle Dependencies
+- [ ] **Library Authors**: Update from `dev.openfeature:sdk` → `dev.openfeature:api`
+- [ ] **App Developers**: Keep `dev.openfeature:sdk` but update version to `2.0.0`
+- [ ] Update `groupId` (remains `dev.openfeature`)
+- [ ] Update version to `2.0.0`
+- [ ] Note: Parent POM is now `dev.openfeature:openfeature-java`
+
+#### Build System Changes
+- [ ] Update CI/CD scripts for multi-module Maven structure
+- [ ] Verify artifact publishing handles both API and SDK modules
+- [ ] Update documentation references to new artifact names
+
+#### Code Changes
+- [ ] Replace `new ProviderEvaluation<>()` with `ProviderEvaluation.builder().build()`
+- [ ] Replace `new FlagEvaluationDetails<>()` with `FlagEvaluationDetails.builder().build()`
+- [ ] Replace `new EventDetails()` with `EventDetails.builder().build()`
+- [ ] Remove all setter method calls on POJOs
+- [ ] Replace convenience methods with builder patterns
+- [ ] Add provider names to all EventDetails creation
+- [ ] Update any explicit builder type references
+
+## 💡 Benefits of These Changes
+
+### Thread Safety
+- All POJOs are now immutable and thread-safe by default
+- No risk of concurrent modification
+
+### API Consistency
+- Unified builder patterns across all POJOs
+- Predictable object creation patterns
+- Clear separation between API contracts and implementation
+
+### OpenFeature Compliance
+- Event details architecture now complies with OpenFeature specification
+- Required fields are enforced at build time
+
+### Module Separation & Dependency Management
+- **Clean Architecture**: Clear separation between API contracts (`openfeature-api`) and SDK implementation (`openfeature-sdk`)
+- **Smaller Dependencies**: Library authors can depend on API-only module (lighter footprint)
+- **Better Dependency Management**: Applications can choose between API-only or full SDK
+- **Multi-module Maven Structure**: Better organization and build management
+- **Independent Versioning**: Modules can evolve independently (though currently versioned together)
+
+### Build & Deployment Benefits
+- **Parallel Builds**: Maven can build modules in parallel
+- **Selective Deployment**: Can deploy API and SDK modules independently
+- **Better Testing**: Test isolation between API contracts and implementation
+- **Cleaner Artifacts**: API module contains only interfaces, POJOs, and exceptions
+
+---
+
+**Note**: This is a major version release (v2.0.0) due to the breaking nature of these changes. All changes improve API consistency, thread safety, and OpenFeature specification compliance while maintaining the same core functionality.
\ No newline at end of file
diff --git a/README.md b/README.md
index 39f558f8f..464e11b12 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,11 @@ Note that this library is intended to be used in server-side contexts and has no
### Install
-#### Maven
+OpenFeature Java is now structured as a multi-module project with separate API and SDK artifacts. In most cases, you'll want to use the full SDK, but you can also use just the API if you're building a provider or need a lighter dependency.
+
+#### Complete SDK (Recommended)
+
+For full OpenFeature functionality, use the SDK module:
```xml
@@ -64,6 +68,20 @@ Note that this library is intended to be used in server-side contexts and has no
```
+#### API Only
+
+For provider development or minimal dependencies, use the API module:
+
+
+```xml
+
+ dev.openfeature
+ api
+ 1.16.0
+
+```
+
+
If you would like snapshot builds, this is the relevant repository information:
```xml
@@ -81,6 +99,7 @@ If you would like snapshot builds, this is the relevant repository information:
#### Gradle
+Complete SDK:
```groovy
dependencies {
@@ -89,6 +108,15 @@ dependencies {
```
+API only:
+
+```groovy
+dependencies {
+ implementation 'dev.openfeature:api:1.16.0'
+}
+```
+
+
### Usage
```java
@@ -123,6 +151,19 @@ public void example(){
See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs.
+## 🏗️ Architecture
+
+OpenFeature Java SDK is structured as a multi-module project:
+
+- **`openfeature-api`**: Core interfaces, data types, and contracts. Use this if you're building providers or hooks, or if you need minimal dependencies.
+- **`openfeature-sdk`**: Full implementation with all OpenFeature functionality. This is what most applications should use.
+
+This separation allows for:
+- **Cleaner dependencies**: Provider and hook developers only need the lightweight API module
+- **Better modularity**: Clear separation between contracts (API) and implementation (SDK)
+- **Easier testing**: Components can be tested against the API contracts
+- **Reduced coupling**: Implementation details are isolated in the SDK module
+
## 🌟 Features
| Status | Features | Description |
@@ -327,9 +368,17 @@ Additionally, you can develop a custom transaction context propagator by impleme
### Develop a provider
-To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency.
+To develop a provider, you need to create a new project and include the OpenFeature API as a dependency (you only need the API module for provider development).
This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/java-sdk-contrib) available under the OpenFeature organization.
-You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK.
+You'll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature API.
+
+```xml
+
+ dev.openfeature
+ api
+ 1.16.0
+
+```
```java
public class MyProvider implements FeatureProvider {
@@ -413,10 +462,18 @@ OpenFeatureAPI.getInstance().getClient().getProviderState();
### Develop a hook
-To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency.
+To develop a hook, you need to create a new project and include the OpenFeature API as a dependency (you only need the API module for hook development).
This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/java-sdk-contrib) available under the OpenFeature organization.
Implement your own hook by conforming to the `Hook interface`.
+```xml
+
+ dev.openfeature
+ api
+ 1.16.0
+
+```
+
```java
class MyHook implements Hook {
diff --git a/benchmark.txt b/benchmark.txt
index e43e684d0..065a2c564 100644
--- a/benchmark.txt
+++ b/benchmark.txt
@@ -1,5 +1,5 @@
[INFO] Scanning for projects...
-[INFO]
+[INFO]
[INFO] ------------------------< dev.openfeature:sdk >-------------------------
[INFO] Building OpenFeature Java SDK 1.12.1
[INFO] from pom.xml
@@ -7,21 +7,21 @@
[WARNING] Parameter 'encoding' is unknown for plugin 'maven-checkstyle-plugin:3.5.0:check (validate)'
[WARNING] Parameter 'encoding' is unknown for plugin 'maven-checkstyle-plugin:3.5.0:check (validate)'
[WARNING] Parameter 'encoding' is unknown for plugin 'maven-checkstyle-plugin:3.5.0:check (validate)'
-[INFO]
+[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ sdk ---
[INFO] Deleting /home/todd/git/java-sdk/target
-[INFO]
+[INFO]
[INFO] --- checkstyle:3.5.0:check (validate) @ sdk ---
[INFO] Starting audit...
Audit done.
[INFO] You have 0 Checkstyle violations.
-[INFO]
+[INFO]
[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ sdk ---
[INFO] surefireArgLine set to -javaagent:/home/todd/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/home/todd/git/java-sdk/target/coverage-reports/jacoco-ut.exec
-[INFO]
+[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ sdk ---
[INFO] skip non existing resourceDirectory /home/todd/git/java-sdk/src/main/resources
-[INFO]
+[INFO]
[INFO] --- compiler:3.13.0:compile (default-compile) @ sdk ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 65 source files with javac [debug target 1.8] to target/classes
@@ -44,24 +44,24 @@ Audit done.
[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/NoOpProvider.java: Recompile with -Xlint:deprecation for details.
[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/Value.java: Some input files use unchecked or unsafe operations.
[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/Value.java: Recompile with -Xlint:unchecked for details.
-[INFO]
+[INFO]
[INFO] --- checkstyle:3.5.0:check (validate) @ sdk ---
[INFO] Starting audit...
Audit done.
[INFO] You have 0 Checkstyle violations.
-[INFO]
+[INFO]
[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ sdk ---
[INFO] surefireArgLine set to -javaagent:/home/todd/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/home/todd/git/java-sdk/target/coverage-reports/jacoco-ut.exec
-[INFO]
+[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ sdk ---
[INFO] skip non existing resourceDirectory /home/todd/git/java-sdk/src/main/resources
-[INFO]
+[INFO]
[INFO] --- compiler:3.13.0:compile (default-compile) @ sdk ---
[INFO] Nothing to compile - all classes are up to date.
-[INFO]
+[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ sdk ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
-[INFO]
+[INFO]
[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ sdk ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 52 source files with javac [debug target 1.8] to target/test-classes
@@ -80,29 +80,29 @@ Audit done.
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/EventsTest.java: Recompile with -Xlint:deprecation for details.
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/HookSpecTest.java: Some input files use unchecked or unsafe operations.
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/HookSpecTest.java: Recompile with -Xlint:unchecked for details.
-[INFO]
+[INFO]
[INFO] >>> jmh:0.2.2:benchmark (default-cli) > process-test-resources @ sdk >>>
-[INFO]
+[INFO]
[INFO] --- checkstyle:3.5.0:check (validate) @ sdk ---
[INFO] Starting audit...
Audit done.
[INFO] You have 0 Checkstyle violations.
-[INFO]
+[INFO]
[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ sdk ---
[INFO] surefireArgLine set to -javaagent:/home/todd/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/home/todd/git/java-sdk/target/coverage-reports/jacoco-ut.exec
-[INFO]
+[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ sdk ---
[INFO] skip non existing resourceDirectory /home/todd/git/java-sdk/src/main/resources
-[INFO]
+[INFO]
[INFO] --- compiler:3.13.0:compile (default-compile) @ sdk ---
[INFO] Nothing to compile - all classes are up to date.
-[INFO]
+[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ sdk ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
-[INFO]
+[INFO]
[INFO] <<< jmh:0.2.2:benchmark (default-cli) < process-test-resources @ sdk <<<
-[INFO]
-[INFO]
+[INFO]
+[INFO]
[INFO] --- jmh:0.2.2:benchmark (default-cli) @ sdk ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 52 source files to /home/todd/git/java-sdk/target/test-classes
@@ -150,7 +150,7 @@ Iteration 1: num #instances #bytes class name (module)
19: 149 1884376 [Ljdk.internal.vm.FillerElement; (java.base@21.0.4)
20: 56476 1807232 java.util.ArrayList$Itr (java.base@21.0.4)
21: 37481 1799088 dev.openfeature.sdk.FlagEvaluationDetails$FlagEvaluationDetailsBuilder
- 22: 100001 1600016 dev.openfeature.sdk.NoOpProvider$$Lambda/0x000076e79c02fa78
+ 22: 100001 1600016 dev.openfeature.api.NoOpProvider$$Lambda/0x000076e79c02fa78
23: 50000 1600000 [Ldev.openfeature.sdk.EvaluationContext;
24: 50000 1600000 [Ljava.util.List; (java.base@21.0.4)
25: 100000 1600000 dev.openfeature.sdk.OpenFeatureClient$$Lambda/0x000076e79c082800
diff --git a/mvnw b/mvnw
old mode 100644
new mode 100755
diff --git a/openfeature-api/pom.xml b/openfeature-api/pom.xml
new file mode 100644
index 000000000..c03264279
--- /dev/null
+++ b/openfeature-api/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+
+
+ dev.openfeature
+ openfeature-java
+ 2.0.0
+
+
+ api
+
+ OpenFeature Java API
+ OpenFeature Java API - Core contracts and interfaces for feature flag evaluation
+
+ 0.0.1
+
+
+ dev.openfeature.api
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.17
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs
+ 4.8.6
+ provided
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.11.4
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.26.3
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.4.2
+
+
+
+ ${module-name}
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.13
+
+
+ jacoco-check
+
+ check
+
+
+ ${project.build.directory}/coverage-reports/jacoco-ut.exec
+
+ dev/openfeature/api/exceptions/**
+ dev/openfeature/api/internal/**
+
+
+
+
+
+
+
+
+
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/AbstractEventProvider.java b/openfeature-api/src/main/java/dev/openfeature/api/AbstractEventProvider.java
new file mode 100644
index 000000000..1b20b25f8
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/AbstractEventProvider.java
@@ -0,0 +1,104 @@
+package dev.openfeature.api;
+
+import dev.openfeature.api.evaluation.EvaluationContext;
+import dev.openfeature.api.events.EventEmitter;
+import dev.openfeature.api.events.EventProvider;
+import dev.openfeature.api.events.ProviderEventDetails;
+import dev.openfeature.api.internal.TriConsumer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Abstract EventProvider. Providers must extend this class to support events.
+ * Emit events with {@link #emit(ProviderEvent, ProviderEventDetails)}. Please
+ * note that the SDK will automatically emit
+ * {@link ProviderEvent#PROVIDER_READY } or
+ * {@link ProviderEvent#PROVIDER_ERROR } accordingly when
+ * {@link Provider#initialize(EvaluationContext)} completes successfully
+ * or with error, so these events need not be emitted manually during
+ * initialization.
+ *
+ * @see Provider
+ */
+public abstract class AbstractEventProvider implements EventProvider {
+ private EventEmitter eventEmitter;
+ private List> hooks;
+
+ public void setEventEmitter(EventEmitter eventEmitter) {
+ this.eventEmitter = eventEmitter;
+ }
+
+ /**
+ * "Attach" this EventProvider to an SDK, which allows events to propagate from this provider.
+ * No-op if the same onEmit is already attached.
+ *
+ * @param onEmit the function to run when a provider emits events.
+ * @throws IllegalStateException if attempted to bind a new emitter for already bound provider
+ */
+ public void attach(TriConsumer onEmit) {
+ if (eventEmitter == null) {
+ return;
+ }
+ eventEmitter.attach(onEmit);
+ }
+
+ /**
+ * "Detach" this EventProvider from an SDK, stopping propagation of all events.
+ */
+ public void detach() {
+ if (eventEmitter == null) {
+ return;
+ }
+ eventEmitter.detach();
+ }
+
+ /**
+ * Stop the event emitter executor and block until either termination has completed
+ * or timeout period has elapsed.
+ */
+ @Override
+ public void shutdown() {
+ if (eventEmitter == null) {
+ return;
+ }
+ eventEmitter.shutdown();
+ }
+
+ /**
+ * Emit the specified {@link ProviderEvent}.
+ *
+ * @param event The event type
+ * @param details The details of the event
+ */
+ public Awaitable emit(final ProviderEvent event, final ProviderEventDetails details) {
+ if (eventEmitter == null) {
+ return null;
+ }
+ return eventEmitter.emit(event, details);
+ }
+
+ @Override
+ public Provider addHooks(Hook>... hooks) {
+ if (this.hooks == null) {
+ this.hooks = new ArrayList<>();
+ }
+ this.hooks.addAll(List.of(hooks));
+ return this;
+ }
+
+ @Override
+ public List> getHooks() {
+ if (hooks == null) {
+ return List.of();
+ }
+ return List.copyOf(hooks);
+ }
+
+ @Override
+ public void clearHooks() {
+ if (hooks == null) {
+ return;
+ }
+ hooks.clear();
+ }
+}
diff --git a/src/main/java/dev/openfeature/sdk/Awaitable.java b/openfeature-api/src/main/java/dev/openfeature/api/Awaitable.java
similarity index 97%
rename from src/main/java/dev/openfeature/sdk/Awaitable.java
rename to openfeature-api/src/main/java/dev/openfeature/api/Awaitable.java
index 7d5f477dc..ad2a1094e 100644
--- a/src/main/java/dev/openfeature/sdk/Awaitable.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/Awaitable.java
@@ -1,4 +1,4 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api;
/**
* A class to help with synchronization by allowing the optional awaiting of the associated action.
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/Client.java b/openfeature-api/src/main/java/dev/openfeature/api/Client.java
new file mode 100644
index 000000000..b7b1055c9
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/Client.java
@@ -0,0 +1,23 @@
+package dev.openfeature.api;
+
+import dev.openfeature.api.evaluation.EvaluationClient;
+import dev.openfeature.api.evaluation.EvaluationContextHolder;
+import dev.openfeature.api.events.EventBus;
+import dev.openfeature.api.lifecycle.Hookable;
+import dev.openfeature.api.tracking.Tracking;
+import dev.openfeature.api.types.ClientMetadata;
+
+/**
+ * Interface used to resolve flags of varying types.
+ */
+public interface Client
+ extends EvaluationClient, Tracking, EventBus, Hookable, EvaluationContextHolder {
+ ClientMetadata getMetadata();
+
+ /**
+ * Returns the current state of the associated provider.
+ *
+ * @return the provider state
+ */
+ ProviderState getProviderState();
+}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/DefaultEvaluationEvent.java b/openfeature-api/src/main/java/dev/openfeature/api/DefaultEvaluationEvent.java
new file mode 100644
index 000000000..a1f7726ae
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/DefaultEvaluationEvent.java
@@ -0,0 +1,96 @@
+package dev.openfeature.api;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Represents an evaluation event.
+ * This class is immutable and thread-safe.
+ */
+class DefaultEvaluationEvent implements EvaluationEvent {
+
+ private final String name;
+ private final Map attributes;
+
+ /**
+ * Private constructor - use builder() to create instances.
+ */
+ private DefaultEvaluationEvent(String name, Map attributes) {
+ this.name = name;
+ this.attributes = attributes != null ? new HashMap<>(attributes) : new HashMap<>();
+ }
+
+ /**
+ * Gets the name of the evaluation event.
+ *
+ * @return the event name
+ */
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Gets a copy of the event attributes.
+ *
+ * @return a new map containing the event attributes
+ */
+ @Override
+ public Map getAttributes() {
+ return new HashMap<>(attributes);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ DefaultEvaluationEvent that = (DefaultEvaluationEvent) obj;
+ return Objects.equals(name, that.name) && Objects.equals(attributes, that.attributes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, attributes);
+ }
+
+ @Override
+ public String toString() {
+ return "EvaluationEvent{" + "name='" + name + '\'' + ", attributes=" + attributes + '}';
+ }
+
+ /**
+ * Builder class for creating instances of EvaluationEvent.
+ */
+ public static class Builder {
+ private String name;
+ private Map attributes = new HashMap<>();
+
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder attributes(Map attributes) {
+ this.attributes = attributes != null ? new HashMap<>(attributes) : new HashMap<>();
+ return this;
+ }
+
+ public Builder attribute(String key, Object value) {
+ this.attributes.put(key, value);
+ return this;
+ }
+
+ public EvaluationEvent build() {
+ return new DefaultEvaluationEvent(name, attributes);
+ }
+ }
+}
diff --git a/src/main/java/dev/openfeature/sdk/ErrorCode.java b/openfeature-api/src/main/java/dev/openfeature/api/ErrorCode.java
similarity index 89%
rename from src/main/java/dev/openfeature/sdk/ErrorCode.java
rename to openfeature-api/src/main/java/dev/openfeature/api/ErrorCode.java
index cb5798f31..3a2c2a829 100644
--- a/src/main/java/dev/openfeature/sdk/ErrorCode.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/ErrorCode.java
@@ -1,4 +1,4 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api;
@SuppressWarnings("checkstyle:MissingJavadocType")
public enum ErrorCode {
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/EvaluationEvent.java b/openfeature-api/src/main/java/dev/openfeature/api/EvaluationEvent.java
new file mode 100644
index 000000000..f8d90f978
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/EvaluationEvent.java
@@ -0,0 +1,13 @@
+package dev.openfeature.api;
+
+import java.util.Map;
+
+/**
+ * Represents an evaluation event.
+ * This class is immutable and thread-safe.
+ */
+public interface EvaluationEvent {
+ String getName();
+
+ Map getAttributes();
+}
diff --git a/src/main/java/dev/openfeature/sdk/FlagValueType.java b/openfeature-api/src/main/java/dev/openfeature/api/FlagValueType.java
similarity index 83%
rename from src/main/java/dev/openfeature/sdk/FlagValueType.java
rename to openfeature-api/src/main/java/dev/openfeature/api/FlagValueType.java
index a8938d454..531490342 100644
--- a/src/main/java/dev/openfeature/sdk/FlagValueType.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/FlagValueType.java
@@ -1,4 +1,4 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api;
@SuppressWarnings("checkstyle:MissingJavadocType")
public enum FlagValueType {
diff --git a/src/main/java/dev/openfeature/sdk/Hook.java b/openfeature-api/src/main/java/dev/openfeature/api/Hook.java
similarity index 91%
rename from src/main/java/dev/openfeature/sdk/Hook.java
rename to openfeature-api/src/main/java/dev/openfeature/api/Hook.java
index 08aa18314..84162f7d8 100644
--- a/src/main/java/dev/openfeature/sdk/Hook.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/Hook.java
@@ -1,5 +1,8 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api;
+import dev.openfeature.api.evaluation.EvaluationContext;
+import dev.openfeature.api.evaluation.FlagEvaluationDetails;
+import dev.openfeature.api.lifecycle.HookContext;
import java.util.Map;
import java.util.Optional;
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/OpenFeatureAPI.java b/openfeature-api/src/main/java/dev/openfeature/api/OpenFeatureAPI.java
new file mode 100644
index 000000000..381365309
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/OpenFeatureAPI.java
@@ -0,0 +1,100 @@
+package dev.openfeature.api;
+
+import dev.openfeature.api.evaluation.EvaluationContextHolder;
+import dev.openfeature.api.events.EventBus;
+import dev.openfeature.api.internal.noop.NoOpOpenFeatureAPI;
+import dev.openfeature.api.lifecycle.Hookable;
+import dev.openfeature.api.lifecycle.Lifecycle;
+import java.util.ServiceLoader;
+
+/**
+ * Main abstract class that combines all OpenFeature interfaces.
+ * Uses ServiceLoader pattern to automatically discover and load implementations.
+ * This allows for multiple SDK implementations with priority-based selection.
+ *
+ *
Implements all OpenFeature interface facets:
+ * - Core operations (client management, provider configuration)
+ * - Hook management (global hook configuration)
+ * - Context management (global evaluation context)
+ * - Event handling (provider event registration and management)
+ * - Transaction context (transaction-scoped context propagation)
+ * - Lifecycle management (cleanup and shutdown)
+ */
+public abstract class OpenFeatureAPI
+ implements OpenFeatureCore,
+ Hookable,
+ EvaluationContextHolder,
+ EventBus,
+ Transactional,
+ Lifecycle {
+ // package-private multi-read/single-write lock
+ private static volatile OpenFeatureAPI instance;
+ private static final Object instanceLock = new Object();
+
+ /**
+ * Gets the singleton OpenFeature API instance.
+ * Uses ServiceLoader to automatically discover and load the best available implementation.
+ *
+ * @return The singleton instance
+ */
+ public static OpenFeatureAPI getInstance() {
+ if (instance == null) {
+ synchronized (instanceLock) {
+ if (instance == null) {
+ instance = loadImplementation();
+ }
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * Load the best available OpenFeature implementation using ServiceLoader.
+ * Implementations are selected based on priority, with higher priorities taking precedence.
+ * If no implementation is available, returns a no-op implementation.
+ *
+ * @return the loaded OpenFeature API implementation
+ */
+ private static OpenFeatureAPI loadImplementation() {
+ ServiceLoader loader = ServiceLoader.load(OpenFeatureAPIProvider.class);
+
+ OpenFeatureAPIProvider bestProvider = null;
+ int highestPriority = Integer.MIN_VALUE;
+
+ for (OpenFeatureAPIProvider provider : loader) {
+ try {
+ int priority = provider.getPriority();
+ if (priority > highestPriority) {
+ bestProvider = provider;
+ highestPriority = priority;
+ }
+ } catch (Exception e) {
+ // Log but continue - don't let one bad provider break everything
+ System.err.println("Failed to get priority from provider "
+ + provider.getClass().getName() + ": " + e.getMessage());
+ }
+ }
+
+ if (bestProvider != null) {
+ try {
+ return bestProvider.createAPI();
+ } catch (Exception e) {
+ System.err.println("Failed to create API from provider "
+ + bestProvider.getClass().getName() + ": " + e.getMessage());
+ // Fall through to no-op
+ }
+ }
+
+ return new NoOpOpenFeatureAPI();
+ }
+
+ /**
+ * Reset the singleton instance. This method is primarily for testing purposes
+ * and should be used with caution in production environments.
+ */
+ protected static void resetInstance() {
+ synchronized (instanceLock) {
+ instance = null;
+ }
+ }
+}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/OpenFeatureAPIProvider.java b/openfeature-api/src/main/java/dev/openfeature/api/OpenFeatureAPIProvider.java
new file mode 100644
index 000000000..99442e74c
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/OpenFeatureAPIProvider.java
@@ -0,0 +1,25 @@
+package dev.openfeature.api;
+
+/**
+ * ServiceLoader interface for OpenFeature API implementations.
+ * Implementations of this interface can provide OpenFeature API instances
+ * with different capabilities and priorities.
+ */
+public interface OpenFeatureAPIProvider {
+ /**
+ * Create an OpenFeature API implementation.
+ *
+ * @return the API implementation
+ */
+ OpenFeatureAPI createAPI();
+
+ /**
+ * Priority for this provider. Higher values take precedence.
+ * This allows multiple implementations to coexist with clear precedence rules.
+ *
+ * @return priority value (default: 0)
+ */
+ default int getPriority() {
+ return 0;
+ }
+}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/OpenFeatureCore.java b/openfeature-api/src/main/java/dev/openfeature/api/OpenFeatureCore.java
new file mode 100644
index 000000000..dbd39d21c
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/OpenFeatureCore.java
@@ -0,0 +1,112 @@
+package dev.openfeature.api;
+
+import dev.openfeature.api.exceptions.OpenFeatureError;
+import dev.openfeature.api.types.ProviderMetadata;
+
+/**
+ * Core interface for basic OpenFeature operations.
+ * Provides client management and provider configuration.
+ */
+public interface OpenFeatureCore {
+ /**
+ * A factory function for creating new, OpenFeature client.
+ * Clients can contain their own state (e.g. logger, hook, context).
+ * Multiple clients can be used to segment feature flag configuration.
+ * All un-named or unbound clients use the default provider.
+ *
+ * @return a new client instance
+ */
+ Client getClient();
+
+ /**
+ * A factory function for creating new domainless OpenFeature client.
+ * Clients can contain their own state (e.g. logger, hook, context).
+ * Multiple clients can be used to segment feature flag configuration.
+ * If there is already a provider bound to this domain, this provider will be used.
+ * Otherwise, the default provider is used until a provider is assigned to that domain.
+ *
+ * @param domain an identifier which logically binds clients with providers
+ * @return a new client instance
+ */
+ Client getClient(String domain);
+
+ /**
+ * A factory function for creating new domainless OpenFeature client.
+ * Clients can contain their own state (e.g. logger, hook, context).
+ * Multiple clients can be used to segment feature flag configuration.
+ * If there is already a provider bound to this domain, this provider will be used.
+ * Otherwise, the default provider is used until a provider is assigned to that domain.
+ *
+ * @param domain a identifier which logically binds clients with providers
+ * @param version a version identifier
+ * @return a new client instance
+ */
+ Client getClient(String domain, String version);
+
+ /**
+ * Set the default provider.
+ *
+ * @param provider the provider to set as default
+ */
+ void setProvider(Provider provider);
+
+ /**
+ * Add a provider for a domain.
+ *
+ * @param domain The domain to bind the provider to.
+ * @param provider The provider to set.
+ */
+ void setProvider(String domain, Provider provider);
+
+ /**
+ * Sets the default provider and waits for its initialization to complete.
+ *
+ *
Note: If the provider fails during initialization, an {@link OpenFeatureError} will be thrown.
+ * It is recommended to wrap this call in a try-catch block to handle potential initialization failures gracefully.
+ *
+ * @param provider the {@link Provider} to set as the default.
+ * @throws OpenFeatureError if the provider fails during initialization.
+ */
+ void setProviderAndWait(Provider provider) throws OpenFeatureError;
+
+ /**
+ * Add a provider for a domain and wait for initialization to finish.
+ *
+ *
Note: If the provider fails during initialization, an {@link OpenFeatureError} will be thrown.
+ * It is recommended to wrap this call in a try-catch block to handle potential initialization failures gracefully.
+ *
+ * @param domain The domain to bind the provider to.
+ * @param provider The provider to set.
+ * @throws OpenFeatureError if the provider fails during initialization.
+ */
+ void setProviderAndWait(String domain, Provider provider) throws OpenFeatureError;
+
+ /**
+ * Return the default provider.
+ */
+ Provider getProvider();
+
+ /**
+ * Fetch a provider for a domain. If not found, return the default.
+ *
+ * @param domain The domain to look for.
+ * @return A named {@link Provider}
+ */
+ Provider getProvider(String domain);
+
+ /**
+ * Get metadata about the default provider.
+ *
+ * @return the provider metadata
+ */
+ ProviderMetadata getProviderMetadata();
+
+ /**
+ * Get metadata about a registered provider using the client name.
+ * An unbound or empty client name will return metadata from the default provider.
+ *
+ * @param domain an identifier which logically binds clients with providers
+ * @return the provider metadata
+ */
+ ProviderMetadata getProviderMetadata(String domain);
+}
diff --git a/src/main/java/dev/openfeature/sdk/FeatureProvider.java b/openfeature-api/src/main/java/dev/openfeature/api/Provider.java
similarity index 55%
rename from src/main/java/dev/openfeature/sdk/FeatureProvider.java
rename to openfeature-api/src/main/java/dev/openfeature/api/Provider.java
index 22819ef10..ea30cb54a 100644
--- a/src/main/java/dev/openfeature/sdk/FeatureProvider.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/Provider.java
@@ -1,19 +1,22 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api;
-import java.util.ArrayList;
+import dev.openfeature.api.evaluation.EvaluationContext;
+import dev.openfeature.api.evaluation.ProviderEvaluation;
+import dev.openfeature.api.events.EventProvider;
+import dev.openfeature.api.lifecycle.Hookable;
+import dev.openfeature.api.lifecycle.Lifecycle;
+import dev.openfeature.api.tracking.TrackingProvider;
+import dev.openfeature.api.types.ProviderMetadata;
+import dev.openfeature.api.types.Value;
import java.util.List;
/**
* The interface implemented by upstream flag providers to resolve flags for
* their service. If you want to support realtime events with your provider, you
- * should extend {@link EventProvider}
+ * should implement {@link EventProvider}
*/
-public interface FeatureProvider {
- Metadata getMetadata();
-
- default List getProviderHooks() {
- return new ArrayList<>();
- }
+public interface Provider extends Hookable, Lifecycle, TrackingProvider {
+ ProviderMetadata getMetadata();
ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx);
@@ -57,28 +60,13 @@ default void shutdown() {
// Intentionally left blank
}
- /**
- * Returns a representation of the current readiness of the provider.
- * If the provider needs to be initialized, it should return {@link ProviderState#NOT_READY}.
- * If the provider is in an error state, it should return {@link ProviderState#ERROR}.
- * If the provider is functioning normally, it should return {@link ProviderState#READY}.
- *
- *
Providers which do not implement this method are assumed to be ready immediately.
- *
- * @return ProviderState
- * @deprecated The state is handled by the SDK internally. Query the state from the {@link Client} instead.
- */
- @Deprecated
- default ProviderState getState() {
- return ProviderState.READY;
+ @Override
+ default List> getHooks() {
+ return List.of();
}
- /**
- * Feature provider implementations can opt in for to support Tracking by implementing this method.
- *
- * @param eventName The name of the tracking event
- * @param context Evaluation context used in flag evaluation (Optional)
- * @param details Data pertinent to a particular tracking event (Optional)
- */
- default void track(String eventName, EvaluationContext context, TrackingEventDetails details) {}
+ @Override
+ default Provider addHooks(Hook>... hooks) {
+ return this;
+ }
}
diff --git a/src/main/java/dev/openfeature/sdk/ProviderEvent.java b/openfeature-api/src/main/java/dev/openfeature/api/ProviderEvent.java
similarity index 84%
rename from src/main/java/dev/openfeature/sdk/ProviderEvent.java
rename to openfeature-api/src/main/java/dev/openfeature/api/ProviderEvent.java
index 47ac8c952..55fdae6a5 100644
--- a/src/main/java/dev/openfeature/sdk/ProviderEvent.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/ProviderEvent.java
@@ -1,4 +1,4 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api;
/**
* Provider event types.
diff --git a/src/main/java/dev/openfeature/sdk/ProviderState.java b/openfeature-api/src/main/java/dev/openfeature/api/ProviderState.java
similarity index 86%
rename from src/main/java/dev/openfeature/sdk/ProviderState.java
rename to openfeature-api/src/main/java/dev/openfeature/api/ProviderState.java
index 42747e986..d7d240f6f 100644
--- a/src/main/java/dev/openfeature/sdk/ProviderState.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/ProviderState.java
@@ -1,4 +1,4 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api;
/**
* Indicates the state of the provider.
@@ -16,7 +16,7 @@ public enum ProviderState {
* @param event event to compare
* @return boolean if matches.
*/
- boolean matchesEvent(ProviderEvent event) {
+ public boolean matchesEvent(ProviderEvent event) {
return this == READY && event == ProviderEvent.PROVIDER_READY
|| this == STALE && event == ProviderEvent.PROVIDER_STALE
|| this == ERROR && event == ProviderEvent.PROVIDER_ERROR;
diff --git a/src/main/java/dev/openfeature/sdk/Reason.java b/openfeature-api/src/main/java/dev/openfeature/api/Reason.java
similarity index 85%
rename from src/main/java/dev/openfeature/sdk/Reason.java
rename to openfeature-api/src/main/java/dev/openfeature/api/Reason.java
index 23fca82d2..4962f416f 100644
--- a/src/main/java/dev/openfeature/sdk/Reason.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/Reason.java
@@ -1,4 +1,4 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api;
/**
* Predefined resolution reasons.
diff --git a/src/main/java/dev/openfeature/sdk/Telemetry.java b/openfeature-api/src/main/java/dev/openfeature/api/Telemetry.java
similarity index 94%
rename from src/main/java/dev/openfeature/sdk/Telemetry.java
rename to openfeature-api/src/main/java/dev/openfeature/api/Telemetry.java
index 7e94983ee..411e16995 100644
--- a/src/main/java/dev/openfeature/sdk/Telemetry.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/Telemetry.java
@@ -1,4 +1,7 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api;
+
+import dev.openfeature.api.evaluation.FlagEvaluationDetails;
+import dev.openfeature.api.lifecycle.HookContext;
/**
* The Telemetry class provides constants and methods for creating OpenTelemetry compliant
@@ -41,7 +44,7 @@ private Telemetry() {}
*/
public static EvaluationEvent createEvaluationEvent(
HookContext> hookContext, FlagEvaluationDetails> evaluationDetails) {
- EvaluationEvent.EvaluationEventBuilder evaluationEventBuilder = EvaluationEvent.builder()
+ DefaultEvaluationEvent.Builder evaluationEventBuilder = DefaultEvaluationEvent.builder()
.name(FLAG_EVALUATION_EVENT_NAME)
.attribute(TELEMETRY_KEY, hookContext.getFlagKey())
.attribute(TELEMETRY_PROVIDER, hookContext.getProviderMetadata().getName());
diff --git a/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java b/openfeature-api/src/main/java/dev/openfeature/api/TransactionContextPropagator.java
similarity index 52%
rename from src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java
rename to openfeature-api/src/main/java/dev/openfeature/api/TransactionContextPropagator.java
index 9e2718787..d1c82dfa1 100644
--- a/src/main/java/dev/openfeature/sdk/TransactionContextPropagator.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/TransactionContextPropagator.java
@@ -1,4 +1,6 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api;
+
+import dev.openfeature.api.evaluation.EvaluationContextHolder;
/**
* {@link TransactionContextPropagator} is responsible for persisting a transactional context
@@ -11,18 +13,4 @@
* the specification.
*
*/
-public interface TransactionContextPropagator {
-
- /**
- * Returns the currently defined transaction context using the registered transaction
- * context propagator.
- *
- * @return {@link EvaluationContext} The current transaction context
- */
- EvaluationContext getTransactionContext();
-
- /**
- * Sets the transaction context.
- */
- void setTransactionContext(EvaluationContext evaluationContext);
-}
+public interface TransactionContextPropagator extends EvaluationContextHolder {}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/Transactional.java b/openfeature-api/src/main/java/dev/openfeature/api/Transactional.java
new file mode 100644
index 000000000..f8aa4d46d
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/Transactional.java
@@ -0,0 +1,33 @@
+package dev.openfeature.api;
+
+import dev.openfeature.api.evaluation.EvaluationContext;
+
+/**
+ * Interface for transaction context management operations.
+ * Provides transaction-scoped context propagation and management,
+ * allowing for context to be passed across multiple operations
+ * within the same transaction or thread boundary.
+ */
+public interface Transactional {
+ /**
+ * Return the transaction context propagator.
+ *
+ * @return the current transaction context propagator
+ */
+ TransactionContextPropagator getTransactionContextPropagator();
+
+ /**
+ * Sets the transaction context propagator.
+ *
+ * @param transactionContextPropagator the transaction context propagator to use
+ * @throws IllegalArgumentException if {@code transactionContextPropagator} is null
+ */
+ void setTransactionContextPropagator(TransactionContextPropagator transactionContextPropagator);
+
+ /**
+ * Sets the transaction context using the registered transaction context propagator.
+ *
+ * @param evaluationContext the evaluation context to set for the current transaction
+ */
+ void setTransactionContext(EvaluationContext evaluationContext);
+}
diff --git a/src/main/java/dev/openfeature/sdk/BaseEvaluation.java b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/BaseEvaluation.java
similarity index 86%
rename from src/main/java/dev/openfeature/sdk/BaseEvaluation.java
rename to openfeature-api/src/main/java/dev/openfeature/api/evaluation/BaseEvaluation.java
index d4209d9b2..96117b696 100644
--- a/src/main/java/dev/openfeature/sdk/BaseEvaluation.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/BaseEvaluation.java
@@ -1,4 +1,7 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api.evaluation;
+
+import dev.openfeature.api.ErrorCode;
+import dev.openfeature.api.types.Metadata;
/**
* This is a common interface between the evaluation results that providers return and what is given to the end users.
@@ -41,4 +44,6 @@ public interface BaseEvaluation {
* @return {String}
*/
String getErrorMessage();
+
+ Metadata getFlagMetadata();
}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/evaluation/DefaultFlagEvaluationDetails.java b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/DefaultFlagEvaluationDetails.java
new file mode 100644
index 000000000..25f8a91cd
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/DefaultFlagEvaluationDetails.java
@@ -0,0 +1,120 @@
+package dev.openfeature.api.evaluation;
+
+import dev.openfeature.api.ErrorCode;
+import dev.openfeature.api.types.Metadata;
+import java.util.Objects;
+
+/**
+ * Contains information about how the provider resolved a flag, including the
+ * resolved value.
+ *
+ * @param the type of the flag being evaluated.
+ */
+class DefaultFlagEvaluationDetails implements FlagEvaluationDetails {
+
+ private final String flagKey;
+ private final T value;
+ private final String variant;
+ private final String reason;
+ private final ErrorCode errorCode;
+ private final String errorMessage;
+ private final Metadata flagMetadata;
+
+ /**
+ * Private constructor for builder pattern only.
+ */
+ DefaultFlagEvaluationDetails() {
+ this(null, null, null, null, null, null, null);
+ }
+
+ /**
+ * Private constructor for immutable FlagEvaluationDetails.
+ *
+ * @param flagKey the flag key
+ * @param value the resolved value
+ * @param variant the variant identifier
+ * @param reason the reason for the evaluation result
+ * @param errorCode the error code if applicable
+ * @param errorMessage the error message if applicable
+ * @param flagMetadata metadata associated with the flag
+ */
+ DefaultFlagEvaluationDetails(
+ String flagKey,
+ T value,
+ String variant,
+ String reason,
+ ErrorCode errorCode,
+ String errorMessage,
+ Metadata flagMetadata) {
+ this.flagKey = flagKey;
+ this.value = value;
+ this.variant = variant;
+ this.reason = reason;
+ this.errorCode = errorCode;
+ this.errorMessage = errorMessage;
+ this.flagMetadata = flagMetadata != null ? flagMetadata : Metadata.EMPTY;
+ }
+
+ public String getFlagKey() {
+ return flagKey;
+ }
+
+ public T getValue() {
+ return value;
+ }
+
+ public String getVariant() {
+ return variant;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public ErrorCode getErrorCode() {
+ return errorCode;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public Metadata getFlagMetadata() {
+ return flagMetadata;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ FlagEvaluationDetails> that = (FlagEvaluationDetails>) obj;
+ return Objects.equals(flagKey, that.getFlagKey())
+ && Objects.equals(value, that.getValue())
+ && Objects.equals(variant, that.getVariant())
+ && Objects.equals(reason, that.getReason())
+ && errorCode == that.getErrorCode()
+ && Objects.equals(errorMessage, that.getErrorMessage())
+ && Objects.equals(flagMetadata, that.getFlagMetadata());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(flagKey, value, variant, reason, errorCode, errorMessage, flagMetadata);
+ }
+
+ @Override
+ public String toString() {
+ return "FlagEvaluationDetails{" + "flagKey='"
+ + flagKey + '\'' + ", value="
+ + value + ", variant='"
+ + variant + '\'' + ", reason='"
+ + reason + '\'' + ", errorCode="
+ + errorCode + ", errorMessage='"
+ + errorMessage + '\'' + ", flagMetadata="
+ + flagMetadata + '}';
+ }
+}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/evaluation/DefaultProviderEvaluation.java b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/DefaultProviderEvaluation.java
new file mode 100644
index 000000000..06e3632e2
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/DefaultProviderEvaluation.java
@@ -0,0 +1,103 @@
+package dev.openfeature.api.evaluation;
+
+import dev.openfeature.api.ErrorCode;
+import dev.openfeature.api.types.Metadata;
+import java.util.Objects;
+
+/**
+ * Contains information about how the a flag was evaluated, including the resolved value.
+ *
+ * @param the type of the flag being evaluated.
+ */
+class DefaultProviderEvaluation implements ProviderEvaluation {
+ private final T value;
+ private final String variant;
+ private final String reason;
+ private final ErrorCode errorCode;
+ private final String errorMessage;
+ private final Metadata flagMetadata;
+
+ /**
+ * Private constructor for builder pattern only.
+ */
+ DefaultProviderEvaluation() {
+ this(null, null, null, null, null, null);
+ }
+
+ /**
+ * Private constructor for immutable ProviderEvaluation.
+ *
+ * @param value the resolved value
+ * @param variant the variant identifier
+ * @param reason the reason for the evaluation result
+ * @param errorCode the error code if applicable
+ * @param errorMessage the error message if applicable
+ * @param flagMetadata metadata associated with the flag
+ */
+ DefaultProviderEvaluation(
+ T value, String variant, String reason, ErrorCode errorCode, String errorMessage, Metadata flagMetadata) {
+ this.value = value;
+ this.variant = variant;
+ this.reason = reason;
+ this.errorCode = errorCode;
+ this.errorMessage = errorMessage;
+ this.flagMetadata = flagMetadata != null ? flagMetadata : Metadata.EMPTY;
+ }
+
+ public T getValue() {
+ return value;
+ }
+
+ public String getVariant() {
+ return variant;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public ErrorCode getErrorCode() {
+ return errorCode;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public Metadata getFlagMetadata() {
+ return flagMetadata;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ProviderEvaluation> that = (ProviderEvaluation>) obj;
+ return Objects.equals(value, that.getValue())
+ && Objects.equals(variant, that.getVariant())
+ && Objects.equals(reason, that.getReason())
+ && errorCode == that.getErrorCode()
+ && Objects.equals(errorMessage, that.getErrorMessage())
+ && Objects.equals(flagMetadata, that.getFlagMetadata());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(value, variant, reason, errorCode, errorMessage, flagMetadata);
+ }
+
+ @Override
+ public String toString() {
+ return "ProviderEvaluation{" + "value="
+ + value + ", variant='"
+ + variant + '\'' + ", reason='"
+ + reason + '\'' + ", errorCode="
+ + errorCode + ", errorMessage='"
+ + errorMessage + '\'' + ", flagMetadata="
+ + flagMetadata + '}';
+ }
+}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/evaluation/EvaluationClient.java b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/EvaluationClient.java
new file mode 100644
index 000000000..3ce99027b
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/EvaluationClient.java
@@ -0,0 +1,124 @@
+package dev.openfeature.api.evaluation;
+
+import dev.openfeature.api.types.Value;
+
+/**
+ * An API for the type-specific fetch methods offered to users.
+ */
+public interface EvaluationClient {
+
+ default Boolean getBooleanValue(String key, Boolean defaultValue) {
+ return getBooleanValue(key, defaultValue, EvaluationContext.EMPTY);
+ }
+
+ default Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx) {
+ return getBooleanValue(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
+ }
+
+ default Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
+ return getBooleanDetails(key, defaultValue, ctx, options).getValue();
+ }
+
+ default FlagEvaluationDetails getBooleanDetails(String key, Boolean defaultValue) {
+ return getBooleanDetails(key, defaultValue, EvaluationContext.EMPTY);
+ }
+
+ default FlagEvaluationDetails getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx) {
+ return getBooleanDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
+ }
+
+ FlagEvaluationDetails getBooleanDetails(
+ String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
+
+ default String getStringValue(String key, String defaultValue) {
+ return getStringValue(key, defaultValue, EvaluationContext.EMPTY);
+ }
+
+ default String getStringValue(String key, String defaultValue, EvaluationContext ctx) {
+ return getStringValue(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
+ }
+
+ default String getStringValue(String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
+ return getStringDetails(key, defaultValue, ctx, options).getValue();
+ }
+
+ default FlagEvaluationDetails getStringDetails(String key, String defaultValue) {
+ return getStringDetails(key, defaultValue, EvaluationContext.EMPTY);
+ }
+
+ default FlagEvaluationDetails getStringDetails(String key, String defaultValue, EvaluationContext ctx) {
+ return getStringDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
+ }
+
+ FlagEvaluationDetails getStringDetails(
+ String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
+
+ default Integer getIntegerValue(String key, Integer defaultValue) {
+ return getIntegerValue(key, defaultValue, EvaluationContext.EMPTY);
+ }
+
+ default Integer getIntegerValue(String key, Integer defaultValue, EvaluationContext ctx) {
+ return getIntegerValue(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
+ }
+
+ default Integer getIntegerValue(String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
+ return getIntegerDetails(key, defaultValue, ctx, options).getValue();
+ }
+
+ default FlagEvaluationDetails getIntegerDetails(String key, Integer defaultValue) {
+ return getIntegerDetails(key, defaultValue, EvaluationContext.EMPTY);
+ }
+
+ default FlagEvaluationDetails getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx) {
+ return getIntegerDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
+ }
+
+ FlagEvaluationDetails getIntegerDetails(
+ String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
+
+ default Double getDoubleValue(String key, Double defaultValue) {
+ return getDoubleValue(key, defaultValue, EvaluationContext.EMPTY);
+ }
+
+ default Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx) {
+ return getDoubleValue(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
+ }
+
+ default Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
+ return getDoubleDetails(key, defaultValue, ctx, options).getValue();
+ }
+
+ default FlagEvaluationDetails getDoubleDetails(String key, Double defaultValue) {
+ return getDoubleDetails(key, defaultValue, EvaluationContext.EMPTY);
+ }
+
+ default FlagEvaluationDetails getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx) {
+ return getDoubleDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
+ }
+
+ FlagEvaluationDetails getDoubleDetails(
+ String key, Double defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
+
+ default Value getObjectValue(String key, Value defaultValue) {
+ return getObjectValue(key, defaultValue, EvaluationContext.EMPTY);
+ }
+
+ default Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx) {
+ return getObjectValue(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
+ }
+
+ default Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
+ return getObjectDetails(key, defaultValue, ctx, options).getValue();
+ }
+
+ default FlagEvaluationDetails getObjectDetails(String key, Value defaultValue) {
+ return getObjectDetails(key, defaultValue, EvaluationContext.EMPTY);
+ }
+
+ default FlagEvaluationDetails getObjectDetails(String key, Value defaultValue, EvaluationContext ctx) {
+ return getObjectDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
+ }
+
+ FlagEvaluationDetails getObjectDetails(
+ String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
+}
diff --git a/src/main/java/dev/openfeature/sdk/EvaluationContext.java b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/EvaluationContext.java
similarity index 70%
rename from src/main/java/dev/openfeature/sdk/EvaluationContext.java
rename to openfeature-api/src/main/java/dev/openfeature/api/evaluation/EvaluationContext.java
index 84760c0d9..74a030809 100644
--- a/src/main/java/dev/openfeature/sdk/EvaluationContext.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/EvaluationContext.java
@@ -1,5 +1,7 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api.evaluation;
+import dev.openfeature.api.types.Structure;
+import dev.openfeature.api.types.Value;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
@@ -13,6 +15,27 @@ public interface EvaluationContext extends Structure {
String TARGETING_KEY = "targetingKey";
+ /**
+ * Empty evaluation context for use as a default.
+ */
+ EvaluationContext EMPTY = new ImmutableContext();
+
+ static EvaluationContext immutableOf(Map attributes) {
+ return new ImmutableContext(attributes);
+ }
+
+ static EvaluationContext immutableOf(String targetingKey, Map attributes) {
+ return new ImmutableContext(targetingKey, attributes);
+ }
+
+ static ImmutableContextBuilder immutableBuilder() {
+ return new ImmutableContext.Builder();
+ }
+
+ static ImmutableContextBuilder immutableBuilder(EvaluationContext original) {
+ return new ImmutableContext.Builder().attributes(original.asMap()).targetingKey(original.getTargetingKey());
+ }
+
String getTargetingKey();
/**
@@ -60,4 +83,6 @@ static void mergeMaps(
}
}
}
+
+ boolean isEmpty();
}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/evaluation/EvaluationContextHolder.java b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/EvaluationContextHolder.java
new file mode 100644
index 000000000..0a5b355ab
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/EvaluationContextHolder.java
@@ -0,0 +1,20 @@
+package dev.openfeature.api.evaluation;
+
+/**
+ * TBD.
+ */
+public interface EvaluationContextHolder {
+ /**
+ * Return an optional client-level evaluation context.
+ *
+ * @return {@link EvaluationContext}
+ */
+ EvaluationContext getEvaluationContext();
+
+ /**
+ * Set the client-level evaluation context.
+ *
+ * @param ctx Client level context.
+ */
+ T setEvaluationContext(EvaluationContext ctx);
+}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/evaluation/FlagEvaluationDetails.java b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/FlagEvaluationDetails.java
new file mode 100644
index 000000000..7a76a4312
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/FlagEvaluationDetails.java
@@ -0,0 +1,48 @@
+package dev.openfeature.api.evaluation;
+
+import dev.openfeature.api.ErrorCode;
+import dev.openfeature.api.Reason;
+import dev.openfeature.api.types.Metadata;
+
+/**
+ * Contains information about how the provider resolved a flag, including the
+ * resolved value.
+ *
+ * @param the type of the flag being evaluated.
+ */
+public interface FlagEvaluationDetails extends BaseEvaluation {
+
+ FlagEvaluationDetails> EMPTY = new DefaultFlagEvaluationDetails<>();
+
+ String getFlagKey();
+
+ static FlagEvaluationDetails of(String key, T value, Reason reason) {
+ return of(key, value, null, reason);
+ }
+
+ static FlagEvaluationDetails of(String key, T value, String variant, Reason reason) {
+ return of(key, value, variant, reason, null, null, null);
+ }
+
+ static FlagEvaluationDetails of(
+ String key,
+ T value,
+ String variant,
+ Reason reason,
+ ErrorCode errorCode,
+ String errorMessage,
+ Metadata flagMetadata) {
+ return of(key, value, variant, reason.toString(), errorCode, errorMessage, flagMetadata);
+ }
+
+ static FlagEvaluationDetails of(
+ String key,
+ T value,
+ String variant,
+ String reason,
+ ErrorCode errorCode,
+ String errorMessage,
+ Metadata flagMetadata) {
+ return new DefaultFlagEvaluationDetails<>(key, value, variant, reason, errorCode, errorMessage, flagMetadata);
+ }
+}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/evaluation/FlagEvaluationOptions.java b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/FlagEvaluationOptions.java
new file mode 100644
index 000000000..4ba5c5cb7
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/FlagEvaluationOptions.java
@@ -0,0 +1,82 @@
+package dev.openfeature.api.evaluation;
+
+import dev.openfeature.api.Hook;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@SuppressWarnings("checkstyle:MissingJavadocType")
+public final class FlagEvaluationOptions {
+ private final List> hooks;
+ private final Map hookHints;
+
+ public FlagEvaluationOptions() {
+ this.hooks = new ArrayList<>();
+ this.hookHints = new HashMap<>();
+ }
+
+ public FlagEvaluationOptions(List> hooks, Map hookHints) {
+ this.hooks = hooks != null ? new ArrayList<>(hooks) : new ArrayList<>();
+ this.hookHints = hookHints != null ? new HashMap<>(hookHints) : new HashMap<>();
+ }
+
+ public List> getHooks() {
+ return new ArrayList<>(hooks);
+ }
+
+ public Map getHookHints() {
+ return new HashMap<>(hookHints);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ FlagEvaluationOptions that = (FlagEvaluationOptions) obj;
+ return Objects.equals(hooks, that.hooks) && Objects.equals(hookHints, that.hookHints);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(hooks, hookHints);
+ }
+
+ @Override
+ public String toString() {
+ return "FlagEvaluationOptions{" + "hooks=" + hooks + ", hookHints=" + hookHints + '}';
+ }
+
+ public static class Builder {
+ private List> hooks = new ArrayList<>();
+ private Map hookHints = new HashMap<>();
+
+ public Builder hooks(List> hooks) {
+ this.hooks = hooks != null ? new ArrayList<>(hooks) : new ArrayList<>();
+ return this;
+ }
+
+ public Builder hook(Hook> hook) {
+ this.hooks.add(hook);
+ return this;
+ }
+
+ public Builder hookHints(Map hookHints) {
+ this.hookHints = hookHints != null ? new HashMap<>(hookHints) : new HashMap<>();
+ return this;
+ }
+
+ public FlagEvaluationOptions build() {
+ return new FlagEvaluationOptions(hooks, hookHints);
+ }
+ }
+}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/evaluation/ImmutableContext.java b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/ImmutableContext.java
new file mode 100644
index 000000000..7e2399618
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/ImmutableContext.java
@@ -0,0 +1,307 @@
+package dev.openfeature.api.evaluation;
+
+import dev.openfeature.api.types.ImmutableStructure;
+import dev.openfeature.api.types.Structure;
+import dev.openfeature.api.types.Value;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * The EvaluationContext is a container for arbitrary contextual data
+ * that can be used as a basis for dynamic evaluation.
+ * The ImmutableContext is an EvaluationContext implementation which is
+ * threadsafe, and whose attributes can
+ * not be modified after instantiation.
+ */
+@SuppressWarnings("PMD.BeanMembersShouldSerialize")
+final class ImmutableContext implements EvaluationContext {
+
+ private final ImmutableStructure structure;
+
+ /**
+ * Create an immutable context with an empty targeting_key and attributes
+ * provided.
+ */
+ ImmutableContext() {
+ this(new HashMap<>());
+ }
+
+ /**
+ * Create an immutable context with given targeting_key provided.
+ *
+ * @param targetingKey targeting key
+ */
+ ImmutableContext(String targetingKey) {
+ this(targetingKey, new HashMap<>());
+ }
+
+ /**
+ * Create an immutable context with an attributes provided.
+ *
+ * @param attributes evaluation context attributes
+ */
+ ImmutableContext(Map attributes) {
+ this(null, attributes);
+ }
+
+ /**
+ * Create an immutable context with given targetingKey and attributes provided.
+ *
+ * @param targetingKey targeting key
+ * @param attributes evaluation context attributes
+ */
+ ImmutableContext(String targetingKey, Map attributes) {
+ if (targetingKey != null && !targetingKey.trim().isEmpty()) {
+ this.structure = new ImmutableStructure(targetingKey, attributes);
+ } else {
+ this.structure = new ImmutableStructure(attributes);
+ }
+ }
+
+ /**
+ * Retrieve targetingKey from the context.
+ */
+ @Override
+ public String getTargetingKey() {
+ Value value = this.getValue(TARGETING_KEY);
+ return value == null ? null : value.asString();
+ }
+
+ // Delegated methods from ImmutableStructure
+ @Override
+ public boolean isEmpty() {
+ return structure.isEmpty();
+ }
+
+ @Override
+ public Set keySet() {
+ return structure.keySet();
+ }
+
+ @Override
+ public Value getValue(String key) {
+ return structure.getValue(key);
+ }
+
+ @Override
+ public Map asMap() {
+ return structure.asMap();
+ }
+
+ @Override
+ public Map asUnmodifiableMap() {
+ return structure.asUnmodifiableMap();
+ }
+
+ @Override
+ public Map asObjectMap() {
+ return structure.asObjectMap();
+ }
+
+ /**
+ * Merges this EvaluationContext object with the passed EvaluationContext,
+ * overriding in case of conflict.
+ *
+ * @param overridingContext overriding context
+ * @return new, resulting merged context
+ */
+ @Override
+ public EvaluationContext merge(EvaluationContext overridingContext) {
+ if (overridingContext == null || overridingContext.isEmpty()) {
+ return new ImmutableContext(this.asUnmodifiableMap());
+ }
+ if (this.isEmpty()) {
+ return new ImmutableContext(overridingContext.asUnmodifiableMap());
+ }
+
+ Map attributes = this.asMap();
+ EvaluationContext.mergeMaps(ImmutableStructure::new, attributes, overridingContext.asUnmodifiableMap());
+ return new ImmutableContext(attributes);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ImmutableContext that = (ImmutableContext) obj;
+ return Objects.equals(structure, that.structure);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(structure);
+ }
+
+ @Override
+ public String toString() {
+ return "ImmutableContext{" + "structure=" + structure + '}';
+ }
+
+ /**
+ * Returns a builder initialized with the current state of this object.
+ *
+ * @return a builder for ImmutableContext
+ */
+ public ImmutableContextBuilder toBuilder() {
+ return new Builder().targetingKey(this.getTargetingKey()).attributes(this.structure.asMap());
+ }
+
+ /**
+ * Builder class for creating instances of ImmutableContext.
+ */
+ static class Builder implements ImmutableContextBuilder {
+ private String targetingKey;
+ private final Map attributes;
+
+ Builder() {
+ this.attributes = new HashMap<>();
+ }
+
+ /**
+ * Sets the targeting key for the evaluation context.
+ *
+ * @param targetingKey the targeting key
+ * @return this builder
+ */
+ @Override
+ public ImmutableContextBuilder targetingKey(String targetingKey) {
+ this.targetingKey = targetingKey;
+ return this;
+ }
+
+ /**
+ * Sets the attributes from a map.
+ *
+ * @param attributes map of attributes
+ * @return this builder
+ */
+ @Override
+ public ImmutableContextBuilder attributes(Map attributes) {
+ if (attributes != null) {
+ this.attributes.clear();
+ this.attributes.putAll(attributes);
+ }
+ return this;
+ }
+
+ /**
+ * Add String value to the evaluation context.
+ *
+ * @param key attribute key
+ * @param value attribute value
+ * @return this builder
+ */
+ @Override
+ public ImmutableContextBuilder add(final String key, final String value) {
+ attributes.put(key, Value.objectToValue(value));
+ return this;
+ }
+
+ /**
+ * Add Integer value to the evaluation context.
+ *
+ * @param key attribute key
+ * @param value attribute value
+ * @return this builder
+ */
+ @Override
+ public ImmutableContextBuilder add(final String key, final Integer value) {
+ attributes.put(key, Value.objectToValue(value));
+ return this;
+ }
+
+ /**
+ * Add Long value to the evaluation context.
+ *
+ * @param key attribute key
+ * @param value attribute value
+ * @return this builder
+ */
+ @Override
+ public ImmutableContextBuilder add(final String key, final Long value) {
+ attributes.put(key, Value.objectToValue(value));
+ return this;
+ }
+
+ /**
+ * Add Float value to the evaluation context.
+ *
+ * @param key attribute key
+ * @param value attribute value
+ * @return this builder
+ */
+ @Override
+ public ImmutableContextBuilder add(final String key, final Float value) {
+ attributes.put(key, Value.objectToValue(value));
+ return this;
+ }
+
+ /**
+ * Add Double value to the evaluation context.
+ *
+ * @param key attribute key
+ * @param value attribute value
+ * @return this builder
+ */
+ @Override
+ public ImmutableContextBuilder add(final String key, final Double value) {
+ attributes.put(key, Value.objectToValue(value));
+ return this;
+ }
+
+ /**
+ * Add Boolean value to the evaluation context.
+ *
+ * @param key attribute key
+ * @param value attribute value
+ * @return this builder
+ */
+ @Override
+ public ImmutableContextBuilder add(final String key, final Boolean value) {
+ attributes.put(key, Value.objectToValue(value));
+ return this;
+ }
+
+ /**
+ * Add Structure value to the evaluation context.
+ *
+ * @param key attribute key
+ * @param value attribute value
+ * @return this builder
+ */
+ @Override
+ public ImmutableContextBuilder add(final String key, final Structure value) {
+ attributes.put(key, Value.objectToValue(value));
+ return this;
+ }
+
+ /**
+ * Add Value to the evaluation context.
+ *
+ * @param key attribute key
+ * @param value attribute value
+ * @return this builder
+ */
+ @Override
+ public ImmutableContextBuilder add(final String key, final Value value) {
+ attributes.put(key, value);
+ return this;
+ }
+
+ /**
+ * Build the ImmutableContext with the provided values.
+ *
+ * @return a new ImmutableContext instance
+ */
+ @Override
+ public ImmutableContext build() {
+ return new ImmutableContext(targetingKey, new HashMap<>(attributes));
+ }
+ }
+}
diff --git a/openfeature-api/src/main/java/dev/openfeature/api/evaluation/ImmutableContextBuilder.java b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/ImmutableContextBuilder.java
new file mode 100644
index 000000000..c7365d2ef
--- /dev/null
+++ b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/ImmutableContextBuilder.java
@@ -0,0 +1,32 @@
+package dev.openfeature.api.evaluation;
+
+import dev.openfeature.api.types.Structure;
+import dev.openfeature.api.types.Value;
+import java.util.Map;
+
+/**
+ * Builder class for creating instances of ImmutableContext.
+ */
+public interface ImmutableContextBuilder {
+ ImmutableContextBuilder targetingKey(String targetingKey);
+
+ ImmutableContextBuilder attributes(Map attributes);
+
+ ImmutableContextBuilder add(String key, String value);
+
+ ImmutableContextBuilder add(String key, Integer value);
+
+ ImmutableContextBuilder add(String key, Long value);
+
+ ImmutableContextBuilder add(String key, Float value);
+
+ ImmutableContextBuilder add(String key, Double value);
+
+ ImmutableContextBuilder add(String key, Boolean value);
+
+ ImmutableContextBuilder add(String key, Structure value);
+
+ ImmutableContextBuilder add(String key, Value value);
+
+ EvaluationContext build();
+}
diff --git a/src/main/java/dev/openfeature/sdk/MutableContext.java b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/MutableContext.java
similarity index 67%
rename from src/main/java/dev/openfeature/sdk/MutableContext.java
rename to openfeature-api/src/main/java/dev/openfeature/api/evaluation/MutableContext.java
index 7fda58065..48d41b929 100644
--- a/src/main/java/dev/openfeature/sdk/MutableContext.java
+++ b/openfeature-api/src/main/java/dev/openfeature/api/evaluation/MutableContext.java
@@ -1,14 +1,14 @@
-package dev.openfeature.sdk;
+package dev.openfeature.api.evaluation;
-import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
+import dev.openfeature.api.types.MutableStructure;
+import dev.openfeature.api.types.Structure;
+import dev.openfeature.api.types.Value;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.function.Function;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-import lombok.experimental.Delegate;
+import java.util.Objects;
+import java.util.Set;
/**
* The EvaluationContext is a container for arbitrary contextual data
@@ -16,12 +16,9 @@
* The MutableContext is an EvaluationContext implementation which is not threadsafe, and whose attributes can
* be modified after instantiation.
*/
-@ToString
-@EqualsAndHashCode
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
public class MutableContext implements EvaluationContext {
- @Delegate(excludes = DelegateExclusions.class)
private final MutableStructure structure;
public MutableContext() {
@@ -46,7 +43,7 @@ public MutableContext(Map attributes) {
public MutableContext(String targetingKey, Map attributes) {
this.structure = new MutableStructure(new HashMap<>(attributes));
if (targetingKey != null && !targetingKey.trim().isEmpty()) {
- this.structure.attributes.put(TARGETING_KEY, new Value(targetingKey));
+ this.structure.add(TARGETING_KEY, targetingKey);
}
}
@@ -96,6 +93,37 @@ public MutableContext setTargetingKey(String targetingKey) {
return this;
}
+ // Delegated methods from MutableStructure
+ @Override
+ public boolean isEmpty() {
+ return structure.isEmpty();
+ }
+
+ @Override
+ public Set keySet() {
+ return structure.keySet();
+ }
+
+ @Override
+ public Value getValue(String key) {
+ return structure.getValue(key);
+ }
+
+ @Override
+ public Map asMap() {
+ return structure.asMap();
+ }
+
+ @Override
+ public Map asUnmodifiableMap() {
+ return structure.asUnmodifiableMap();
+ }
+
+ @Override
+ public Map asObjectMap() {
+ return structure.asObjectMap();
+ }
+
/**
* Retrieve targetingKey from the context.
*/
@@ -125,51 +153,25 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
return new MutableContext(attributes);
}
- /**
- * Hidden class to tell Lombok not to copy these methods over via delegation.
- */
- @SuppressWarnings("all")
- private static class DelegateExclusions {
-
- @ExcludeFromGeneratedCoverageReport
- public Map merge(
- Function