Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
269dfe4
feat: add hook data support
alexandraoberaigner Sep 23, 2025
3b113c1
feat: gemini suggestions
alexandraoberaigner Sep 23, 2025
5fb278b
feat: hook executor impl (WIP)
alexandraoberaigner Sep 23, 2025
ceedffd
Use shared hook context
guidobrei Sep 23, 2025
2843298
Split HookData interface and implementation
guidobrei Sep 23, 2025
235abba
Atopted tests
guidobrei Sep 23, 2025
e0935f3
Remove obsolete test
guidobrei Sep 23, 2025
dbfcab6
HookSupport improvements: rename back to old name, move code from sta…
alexandraoberaigner Sep 24, 2025
a3ed93e
PR suggestion: use concrete hooks
alexandraoberaigner Sep 24, 2025
3ea9894
PR suggestions: DefaultHookData access modifier, no star imports
alexandraoberaigner Sep 24, 2025
27f3e2a
feat: separate hook support data from logic, PR suggestions
alexandraoberaigner Sep 25, 2025
4ab14bf
Update DefaultHookDataTest.java spotless
alexandraoberaigner Sep 25, 2025
1d8f758
fix tests, spotless apply
alexandraoberaigner Sep 25, 2025
8a9738f
exclude lombok generated functions from codecov
alexandraoberaigner Sep 25, 2025
b6cd0e2
replace init function with setters
alexandraoberaigner Sep 29, 2025
1a7e5af
pr suggestion: replace Generated annotation with more descriptive Exc…
alexandraoberaigner Sep 30, 2025
7fe0675
PR suggestion: make HookSupportData a real POJO
alexandraoberaigner Sep 30, 2025
92b6dc4
gemini suggestions
alexandraoberaigner Sep 30, 2025
0e897f2
PR suggestion: call hooks as early as possible
alexandraoberaigner Sep 30, 2025
8b6aba9
PR suggestions: integration test hook data usage in client, set pair …
alexandraoberaigner Oct 2, 2025
3417827
add hook data spec test
alexandraoberaigner Oct 2, 2025
db6cc86
Merge branch 'main' into feat/hook-data-support
alexandraoberaigner Oct 2, 2025
519c303
Merge branch 'main' into feat/hook-data-support
chrfwow Oct 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 240 additions & 14 deletions src/main/java/dev/openfeature/sdk/HookContext.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
package dev.openfeature.sdk;

import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.With;

/**
* A data class to hold immutable context that {@link Hook} instances use.
*
* @param <T> the type for the flag being evaluated
*/
@Value
@Builder
@With
public class HookContext<T> {
@NonNull String flagKey;
public final class HookContext<T> {
@NonNull
private final String flagKey;

@NonNull FlagValueType type;
@NonNull
private final FlagValueType type;

@NonNull T defaultValue;
@NonNull
private final T defaultValue;

@NonNull EvaluationContext ctx;
@NonNull
private EvaluationContext ctx;

ClientMetadata clientMetadata;
Metadata providerMetadata;
private final ClientMetadata clientMetadata;
private final Metadata providerMetadata;

private final HookData hookData;

HookContext(@NonNull String flagKey, @NonNull FlagValueType type, @NonNull T defaultValue,
@NonNull EvaluationContext ctx, ClientMetadata clientMetadata, Metadata providerMetadata,
HookData hookData) {
this.flagKey = flagKey;
this.type = type;
this.defaultValue = defaultValue;
this.ctx = ctx;
this.clientMetadata = clientMetadata;
this.providerMetadata = providerMetadata;
this.hookData = hookData;
}

/**
* Builds a {@link HookContext} instances from request data.
* Builds {@link HookContext} instances from request data.
*
* @param key feature flag key
* @param type flag value type
Expand All @@ -51,6 +63,220 @@ public static <T> HookContext<T> from(
.providerMetadata(providerMetadata)
.ctx(ctx)
.defaultValue(defaultValue)
.hookData(null)
.build();
}

public static <T> HookContextBuilder<T> builder() {return new HookContextBuilder<T>();}

public @NonNull String getFlagKey() {
return this.flagKey;
}

public @NonNull FlagValueType getType() {
return this.type;
}

public @NonNull T getDefaultValue() {
return this.defaultValue;
}

public @NonNull EvaluationContext getCtx() {
return this.ctx;
}

public ClientMetadata getClientMetadata() {
return this.clientMetadata;
}

public Metadata getProviderMetadata() {
return this.providerMetadata;
}

public HookData getHookData() {
return this.hookData;
}

@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (!(o instanceof HookContext)) {
return false;
}
final HookContext<?> other = (HookContext<?>) o;
final Object this$flagKey = this.getFlagKey();
final Object other$flagKey = other.getFlagKey();
if (this$flagKey == null ? other$flagKey != null : !this$flagKey.equals(other$flagKey)) {
return false;
}
final Object this$type = this.getType();
final Object other$type = other.getType();
if (this$type == null ? other$type != null : !this$type.equals(other$type)) {
return false;
}
final Object this$defaultValue = this.getDefaultValue();
final Object other$defaultValue = other.getDefaultValue();
if (this$defaultValue == null ? other$defaultValue != null : !this$defaultValue.equals(other$defaultValue)) {
return false;
}
final Object this$ctx = this.getCtx();
final Object other$ctx = other.getCtx();
if (this$ctx == null ? other$ctx != null : !this$ctx.equals(other$ctx)) {
return false;
}
final Object this$clientMetadata = this.getClientMetadata();
final Object other$clientMetadata = other.getClientMetadata();
if (this$clientMetadata == null
? other$clientMetadata != null
: !this$clientMetadata.equals(other$clientMetadata)) {
return false;
}
final Object this$providerMetadata = this.getProviderMetadata();
final Object other$providerMetadata = other.getProviderMetadata();
if (this$providerMetadata == null
? other$providerMetadata != null
: !this$providerMetadata.equals(other$providerMetadata)) {
return false;
}
final Object this$hookData = this.getHookData();
final Object other$hookData = other.getHookData();
if (this$hookData == null ? other$hookData != null : !this$hookData.equals(other$hookData)) {
return false;
}
return true;
}

@Override
public int hashCode() {
final int PRIME = 59;
int result = 1;
final Object $flagKey = this.getFlagKey();
result = result * PRIME + ($flagKey == null ? 43 : $flagKey.hashCode());
final Object $type = this.getType();
result = result * PRIME + ($type == null ? 43 : $type.hashCode());
final Object $defaultValue = this.getDefaultValue();
result = result * PRIME + ($defaultValue == null ? 43 : $defaultValue.hashCode());
final Object $ctx = this.getCtx();
result = result * PRIME + ($ctx == null ? 43 : $ctx.hashCode());
final Object $clientMetadata = this.getClientMetadata();
result = result * PRIME + ($clientMetadata == null ? 43 : $clientMetadata.hashCode());
final Object $providerMetadata = this.getProviderMetadata();
result = result * PRIME + ($providerMetadata == null ? 43 : $providerMetadata.hashCode());
final Object $hookData = this.getHookData();
result = result * PRIME + ($hookData == null ? 43 : $hookData.hashCode());
return result;
}

@Override
public String toString() {
return "HookContext(flagKey=" + this.getFlagKey() + ", type=" + this.getType() + ", defaultValue="
+ this.getDefaultValue() + ", ctx=" + this.getCtx() + ", clientMetadata=" + this.getClientMetadata()
+ ", providerMetadata=" + this.getProviderMetadata() + ", hookData=" + this.getHookData() + ")";
}

void setCtx(@NonNull EvaluationContext ctx) {
this.ctx = ctx;
}

public HookContext<T> withFlagKey(@NonNull String flagKey) {
return this.flagKey == flagKey ? this
: new HookContext<T>(flagKey, this.type, this.defaultValue, this.ctx, this.clientMetadata,
this.providerMetadata, this.hookData);
}

public HookContext<T> withType(@NonNull FlagValueType type) {
return this.type == type ? this
: new HookContext<T>(this.flagKey, type, this.defaultValue, this.ctx, this.clientMetadata,
this.providerMetadata, this.hookData);
}

public HookContext<T> withDefaultValue(@NonNull T defaultValue) {
return this.defaultValue == defaultValue ? this
: new HookContext<T>(this.flagKey, this.type, defaultValue, this.ctx, this.clientMetadata,
this.providerMetadata, this.hookData);
}

public HookContext<T> withCtx(@NonNull EvaluationContext ctx) {
return this.ctx == ctx ? this
: new HookContext<T>(this.flagKey, this.type, this.defaultValue, ctx, this.clientMetadata,
this.providerMetadata, this.hookData);
}

public HookContext<T> withClientMetadata(ClientMetadata clientMetadata) {
return this.clientMetadata == clientMetadata ? this
: new HookContext<T>(this.flagKey, this.type, this.defaultValue, this.ctx, clientMetadata,
this.providerMetadata, this.hookData);
}

public HookContext<T> withProviderMetadata(Metadata providerMetadata) {
return this.providerMetadata == providerMetadata ? this
: new HookContext<T>(this.flagKey, this.type, this.defaultValue, this.ctx, this.clientMetadata,
providerMetadata, this.hookData);
}

public HookContext<T> withHookData(HookData hookData) {
return this.hookData == hookData ? this
: new HookContext<T>(this.flagKey, this.type, this.defaultValue, this.ctx, this.clientMetadata,
this.providerMetadata, hookData);
}

public static class HookContextBuilder<T> {
private @NonNull String flagKey;
private @NonNull FlagValueType type;
private @NonNull T defaultValue;
private @NonNull EvaluationContext ctx;
private ClientMetadata clientMetadata;
private Metadata providerMetadata;
private HookData hookData;

HookContextBuilder() {}

public HookContextBuilder<T> flagKey(@NonNull String flagKey) {
this.flagKey = flagKey;
return this;
}

public HookContextBuilder<T> type(@NonNull FlagValueType type) {
this.type = type;
return this;
}

public HookContextBuilder<T> defaultValue(@NonNull T defaultValue) {
this.defaultValue = defaultValue;
return this;
}

public HookContextBuilder<T> ctx(@NonNull EvaluationContext ctx) {
this.ctx = ctx;
return this;
}

public HookContextBuilder<T> clientMetadata(ClientMetadata clientMetadata) {
this.clientMetadata = clientMetadata;
return this;
}

public HookContextBuilder<T> providerMetadata(Metadata providerMetadata) {
this.providerMetadata = providerMetadata;
return this;
}

public HookContextBuilder<T> hookData(HookData hookData) {
this.hookData = hookData;
return this;
}

public HookContext<T> build() {
return new HookContext<T>(this.flagKey, this.type, this.defaultValue, this.ctx, this.clientMetadata,
this.providerMetadata, this.hookData);
}

public String toString() {
return "HookContext.HookContextBuilder(flagKey=" + this.flagKey + ", type=" + this.type + ", defaultValue="
+ this.defaultValue + ", ctx=" + this.ctx + ", clientMetadata=" + this.clientMetadata
+ ", providerMetadata=" + this.providerMetadata + ", hookData=" + this.hookData + ")";
}
}
}
81 changes: 81 additions & 0 deletions src/main/java/dev/openfeature/sdk/HookData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package dev.openfeature.sdk;

import java.util.HashMap;
import java.util.Map;

/**
* Hook data provides a way for hooks to maintain state across their execution stages.
* Each hook instance gets its own isolated data store that persists only for the duration
* of a single flag evaluation.
*/
public interface HookData {

/**
* Sets a value for the given key.
*
* @param key the key to store the value under
* @param value the value to store
*/
void set(String key, Object value);

/**
* Gets the value for the given key.
*
* @param key the key to retrieve the value for
* @return the value, or null if not found
*/
Object get(String key);

/**
* Gets the value for the given key, cast to the specified type.
*
* @param <T> the type to cast to
* @param key the key to retrieve the value for
* @param type the class to cast to
* @return the value cast to the specified type, or null if not found
* @throws ClassCastException if the value cannot be cast to the specified type
*/
<T> T get(String key, Class<T> type);

/**
* Default implementation uses a HashMap.
*/
static HookData create() {
return new DefaultHookData();
}

/**
* Default implementation of HookData.
*/
class DefaultHookData implements HookData {
private Map<String, Object> data;

@Override
public void set(String key, Object value) {
if (data == null) {
data = new HashMap<>();
}
data.put(key, value);
}

@Override
public Object get(String key) {
if (data == null) {
return null;
}
return data.get(key);
}

@Override
public <T> T get(String key, Class<T> type) {
Object value = get(key);
if (value == null) {
return null;
}
if (!type.isInstance(value)) {
throw new ClassCastException("Value for key '" + key + "' is not of type " + type.getName());
}
return type.cast(value);
}
}
}
Loading
Loading