Skip to content

Commit fd27bec

Browse files
committed
feat: added evaluation hooks
1 parent 6dbb6cf commit fd27bec

File tree

8 files changed

+378
-0
lines changed

8 files changed

+378
-0
lines changed

src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudClient.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
import java.util.Collections;
2121
import java.util.HashMap;
2222
import java.util.Map;
23+
import java.util.Optional;
2324

2425
public final class DevCycleCloudClient implements IDevCycleClient {
2526

2627
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
2728
private final IDevCycleApi api;
2829
private final DevCycleCloudOptions dvcOptions;
2930
private final DevCycleProvider openFeatureProvider;
31+
private final EvalHooksRunner evalHooksRunner;
3032

3133
public DevCycleCloudClient(String sdkKey) {
3234
this(sdkKey, DevCycleCloudOptions.builder().build());
@@ -50,6 +52,7 @@ public DevCycleCloudClient(String sdkKey, DevCycleCloudOptions options) {
5052
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
5153

5254
this.openFeatureProvider = new DevCycleProvider(this);
55+
this.evalHooksRunner = new EvalHooksRunner();
5356
}
5457

5558
/**
@@ -110,13 +113,17 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {
110113

111114
TypeEnum variableType = TypeEnum.fromClass(defaultValue.getClass());
112115
Variable<T> variable;
116+
HookContext context = new HookContext(user, key, defaultValue);
113117

114118
try {
119+
HookContext newContext = context.merge(evalHooksRunner.executeBefore(context));
115120
Call<Variable> response = api.getVariableByKey(user, key, dvcOptions.getEnableEdgeDB());
116121
variable = getResponseWithRetries(response, 5);
117122
if (variable.getType() != variableType) {
118123
throw new IllegalArgumentException("Variable type mismatch, returning default value");
119124
}
125+
evalHooksRunner.executeAfter(newContext, variable);
126+
evalHooksRunner.executeFinally(newContext, variable);
120127
variable.setIsDefaulted(false);
121128
} catch (Exception exception) {
122129
variable = (Variable<T>) Variable.builder()
@@ -126,6 +133,8 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {
126133
.defaultValue(defaultValue)
127134
.isDefaulted(true)
128135
.build();
136+
evalHooksRunner.executeError(context, exception);
137+
evalHooksRunner.executeFinally(context, variable);
129138
}
130139
return variable;
131140
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.devcycle.sdk.server.common.exception;
2+
3+
/**
4+
* Exception thrown when an after hook fails during variable evaluation.
5+
*/
6+
public class AfterHookError extends RuntimeException {
7+
public AfterHookError(String message) {
8+
super(message);
9+
}
10+
11+
public AfterHookError(String message, Throwable cause) {
12+
super(message, cause);
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.devcycle.sdk.server.common.exception;
2+
3+
/**
4+
* Exception thrown when a before hook fails during variable evaluation.
5+
*/
6+
public class BeforeHookError extends RuntimeException {
7+
public BeforeHookError(String message) {
8+
super(message);
9+
}
10+
11+
public BeforeHookError(String message, Throwable cause) {
12+
super(message, cause);
13+
}
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.devcycle.sdk.server.common.model;
2+
3+
import java.util.Optional;
4+
5+
public interface EvalHook<T> {
6+
7+
default Optional<HookContext<T>> before(HookContext<T> ctx) {
8+
return Optional.empty();
9+
}
10+
default void after(HookContext<T> ctx, Variable<T> variable) {}
11+
default void error(HookContext<T> ctx, Throwable e) {}
12+
default void onFinally(HookContext<T> ctx, Variable<T> variable) {}
13+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.devcycle.sdk.server.common.model;
2+
3+
import com.devcycle.sdk.server.common.exception.AfterHookError;
4+
import com.devcycle.sdk.server.common.exception.BeforeHookError;
5+
6+
import javax.swing.text.html.Option;
7+
import java.util.ArrayList;
8+
import java.util.Collections;
9+
import java.util.List;
10+
import java.util.Optional;
11+
import java.util.concurrent.CompletableFuture;
12+
13+
/**
14+
* A class that manages evaluation hooks for the DevCycle SDK.
15+
* Provides functionality to add and clear hooks, storing them in an array.
16+
*/
17+
public class EvalHooksRunner {
18+
private List<EvalHook> hooks;
19+
20+
/**
21+
* Default constructor initializes an empty list of hooks.
22+
*/
23+
public EvalHooksRunner() {
24+
this.hooks = new ArrayList<>();
25+
}
26+
27+
/**
28+
* Adds a single hook to the collection.
29+
*
30+
* @param hook The hook to add
31+
*/
32+
public void addHook(EvalHook hook) {
33+
if (hook != null) {
34+
hooks.add(hook);
35+
}
36+
}
37+
38+
/**
39+
* Clears all hooks from the collection.
40+
*/
41+
public void clearHooks() {
42+
hooks.clear();
43+
}
44+
45+
/**
46+
* Runs all before hooks in order.
47+
*
48+
* @param context The context to pass to the hooks
49+
* @param <T> The type of the variable value
50+
* @return The potentially modified context
51+
*/
52+
public <T> HookContext<T> executeBefore(HookContext<T> context) {
53+
HookContext<T> beforeContext = context;
54+
for (EvalHook<T> hook : hooks) {
55+
try {
56+
Optional<HookContext<T>> newContext = hook.before(beforeContext);
57+
if (newContext.isPresent()) {
58+
beforeContext = beforeContext.merge(newContext.get());
59+
}
60+
} catch (Exception e) {
61+
throw new BeforeHookError("Before hook failed", e);
62+
}
63+
}
64+
return beforeContext;
65+
}
66+
67+
/**
68+
* Runs all after hooks in reverse order.
69+
*
70+
* @param context The context to pass to the hooks
71+
* @param variable The variable result to pass to the hooks
72+
* @param <T> The type of the variable value
73+
*/
74+
public <T> void executeAfter(HookContext<T> context, Variable<T> variable) {
75+
for (EvalHook<T> hook : hooks) {
76+
try {
77+
hook.after(context, variable);
78+
} catch (Exception e) {
79+
throw new AfterHookError("After hook failed", e);
80+
}
81+
}
82+
}
83+
84+
/**
85+
* Runs all error hooks in reverse order.
86+
*
87+
* @param context The context to pass to the hooks
88+
* @param error The error that occurred
89+
* @param <T> The type of the variable value
90+
*/
91+
public <T> void executeError(HookContext<T> context, Throwable error) {
92+
for (EvalHook<T> hook : hooks) {
93+
try {
94+
hook.error(context, error);
95+
} catch (Exception hookError) {
96+
// Log hook error but don't throw to avoid masking the original error
97+
System.err.println("Error hook failed: " + hookError.getMessage());
98+
}
99+
}
100+
}
101+
102+
/**
103+
* Runs all finally hooks in reverse order.
104+
*
105+
* @param context The context to pass to the hooks
106+
* @param variable The variable result to pass to the hooks (may be null)
107+
*/
108+
public void executeFinally(HookContext context, Variable variable) {
109+
for (EvalHook hook : hooks) {
110+
try {
111+
hook.onFinally(context, variable);
112+
} catch (Exception e) {
113+
// Log finally hook error but don't throw
114+
System.err.println("Finally hook failed: " + e.getMessage());
115+
}
116+
}
117+
}
118+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.devcycle.sdk.server.common.model;
2+
3+
import java.util.Map;
4+
5+
/**
6+
* Context object passed to hooks during variable evaluation.
7+
* Contains the user, variable key, default value, and additional context data.
8+
*/
9+
public class HookContext<T> {
10+
private DevCycleUser user;
11+
private final String key;
12+
private final T defaultValue;
13+
private Variable<T> variableDetails;
14+
15+
public HookContext(DevCycleUser user, String key, T defaultValue) {
16+
this.user = user;
17+
this.key = key;
18+
this.defaultValue = defaultValue;
19+
}
20+
21+
public HookContext(DevCycleUser user, String key, T defaultValue, Variable<T> variable) {
22+
this.user = user;
23+
this.key = key;
24+
this.defaultValue = defaultValue;
25+
this.variableDetails = variable;
26+
}
27+
28+
public DevCycleUser getUser() {
29+
return user;
30+
}
31+
32+
public String getKey() {
33+
return key;
34+
}
35+
36+
public T getDefaultValue() {
37+
return defaultValue;
38+
}
39+
40+
public Variable<T> getVariableDetails() { return variableDetails; }
41+
42+
public HookContext<T> merge(HookContext<T> other) {
43+
if (other == null) {
44+
return this;
45+
}
46+
return new HookContext<>(other.getUser(), key, defaultValue, variableDetails);
47+
}
48+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.devcycle.sdk.server.common.model;
2+
3+
import java.util.concurrent.CompletableFuture;
4+
5+
/**
6+
* Functional interface for resolving variables during hook evaluation.
7+
*
8+
* @param <T> The type of the variable value
9+
*/
10+
@FunctionalInterface
11+
public interface VariableResolver<T> {
12+
/**
13+
* Resolves a variable based on the provided context.
14+
*
15+
* @param context The hook context containing user, key, and default value
16+
* @return A CompletableFuture that completes with the resolved variable
17+
*/
18+
CompletableFuture<Variable<T>> resolve(HookContext<T> context);
19+
}

0 commit comments

Comments
 (0)