Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions;
import com.devcycle.sdk.server.common.api.IDevCycleApi;
import com.devcycle.sdk.server.common.api.IDevCycleClient;
import com.devcycle.sdk.server.common.exception.AfterHookError;
import com.devcycle.sdk.server.common.exception.BeforeHookError;
import com.devcycle.sdk.server.common.exception.DevCycleException;
import com.devcycle.sdk.server.common.logging.DevCycleLogger;
import com.devcycle.sdk.server.common.model.*;
Expand All @@ -17,16 +19,15 @@
import retrofit2.Response;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.*;

public final class DevCycleCloudClient implements IDevCycleClient {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final IDevCycleApi api;
private final DevCycleCloudOptions dvcOptions;
private final DevCycleProvider openFeatureProvider;
private final EvalHooksRunner evalHooksRunner;

public DevCycleCloudClient(String sdkKey) {
this(sdkKey, DevCycleCloudOptions.builder().build());
Expand All @@ -50,6 +51,7 @@ public DevCycleCloudClient(String sdkKey, DevCycleCloudOptions options) {
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);

this.openFeatureProvider = new DevCycleProvider(this);
this.evalHooksRunner = new EvalHooksRunner();
}

/**
Expand Down Expand Up @@ -109,23 +111,46 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {
}

TypeEnum variableType = TypeEnum.fromClass(defaultValue.getClass());
Variable<T> variable;
Variable<T> variable = null;
HookContext<T> context = new HookContext<T>(user, key, defaultValue);
ArrayList<EvalHook<T>> hooks = new ArrayList<EvalHook<T>>(evalHooksRunner.getHooks());
ArrayList<EvalHook<T>> reversedHooks = new ArrayList<>(hooks);
Collections.reverse(reversedHooks);

try {
Throwable beforeError = null;

try {
context = context.merge(evalHooksRunner.executeBefore(hooks, context));
} catch (Throwable e) {
beforeError = e;
}

Call<Variable> response = api.getVariableByKey(user, key, dvcOptions.getEnableEdgeDB());
variable = getResponseWithRetries(response, 5);
if (variable.getType() != variableType) {
throw new IllegalArgumentException("Variable type mismatch, returning default value");
}
if (beforeError != null) {
throw beforeError;
}

evalHooksRunner.executeAfter(reversedHooks, context, variable);
variable.setIsDefaulted(false);
} catch (Exception exception) {
variable = (Variable<T>) Variable.builder()
.key(key)
.type(variableType)
.value(defaultValue)
.defaultValue(defaultValue)
.isDefaulted(true)
.build();
} catch (Throwable exception) {
if (!(exception instanceof BeforeHookError || exception instanceof AfterHookError)) {
variable = (Variable<T>) Variable.builder()
.key(key)
.type(variableType)
.value(defaultValue)
.defaultValue(defaultValue)
.isDefaulted(true)
.build();
}

evalHooksRunner.executeError(reversedHooks, context, exception);
} finally {
evalHooksRunner.executeFinally(reversedHooks, context, Optional.ofNullable(variable));
}
return variable;
}
Expand Down Expand Up @@ -226,6 +251,13 @@ private <T> T getResponseWithRetries(Call<T> call, int maxRetries) throws DevCyc
throw new DevCycleException(HttpResponseCode.SERVER_ERROR, errorResponse);
}

public void addHook(EvalHook hook) {
this.evalHooksRunner.addHook(hook);
}

public void clearHooks() {
this.evalHooksRunner.clearHooks();
}

private <T> T getResponse(Call<T> call) throws DevCycleException {
ErrorResponse errorResponse = ErrorResponse.builder().build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.devcycle.sdk.server.common.exception;

/**
* Exception thrown when an after hook fails during variable evaluation.
*/
public class AfterHookError extends RuntimeException {
public AfterHookError(String message) {
super(message);
}

public AfterHookError(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.devcycle.sdk.server.common.exception;

/**
* Exception thrown when a before hook fails during variable evaluation.
*/
public class BeforeHookError extends RuntimeException {
public BeforeHookError(String message) {
super(message);
}

public BeforeHookError(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.devcycle.sdk.server.common.model;

import java.util.Optional;

public interface EvalHook<T> {

default Optional<HookContext<T>> before(HookContext<T> ctx) {
return Optional.empty();
}
default void after(HookContext<T> ctx, Variable<T> variable) {}
default void error(HookContext<T> ctx, Throwable e) {}
default void onFinally(HookContext<T> ctx, Optional<Variable<T>> variable) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.devcycle.sdk.server.common.model;

import com.devcycle.sdk.server.common.exception.AfterHookError;
import com.devcycle.sdk.server.common.exception.BeforeHookError;
import com.devcycle.sdk.server.common.logging.DevCycleLogger;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
* A class that manages evaluation hooks for the DevCycle SDK.
* Provides functionality to add and clear hooks, storing them in an array.
*/
public class EvalHooksRunner<T> {
private List<EvalHook<T>> hooks;

/**
* Default constructor initializes an empty list of hooks.
*/
public EvalHooksRunner() {
this.hooks = new ArrayList<>();
}

/**
* Adds a single hook to the collection.
*
* @param hook The hook to add
*/
public void addHook(EvalHook<T> hook) {
if (hook != null) {
hooks.add(hook);
}
}

/**
* Clears all hooks from the collection.
*/
public void clearHooks() {
hooks.clear();
}

public List<EvalHook<T>> getHooks() {
return this.hooks;
}

/**
* Runs all before hooks in order.
*
* @param context The context to pass to the hooks
* @param <T> The type of the variable value
* @return The potentially modified context
*/
public <T> HookContext<T> executeBefore(ArrayList<EvalHook<T>> hooks, HookContext<T> context) {
HookContext<T> beforeContext = context;
for (EvalHook<T> hook : hooks) {
try {
Optional<HookContext<T>> newContext = hook.before(beforeContext);
if (newContext.isPresent()) {
beforeContext = beforeContext.merge(newContext.get());
}
} catch (Exception e) {
throw new BeforeHookError("Before hook failed", e);
}
}
return beforeContext;
}

/**
* Runs all after hooks in reverse order.
*
* @param context The context to pass to the hooks
* @param variable The variable result to pass to the hooks
* @param <T> The type of the variable value
*/
public void executeAfter(ArrayList<EvalHook<T>> hooks, HookContext<T> context, Variable<T> variable) {
for (EvalHook<T> hook : hooks) {
try {
hook.after(context, variable);
} catch (Exception e) {
throw new AfterHookError("After hook failed", e);
}
}
}

/**
* Runs all error hooks in reverse order.
*
* @param context The context to pass to the hooks
* @param error The error that occurred
* @param <T> The type of the variable value
*/
public void executeError(ArrayList<EvalHook<T>> hooks, HookContext<T> context, Throwable error) {
for (EvalHook<T> hook : hooks) {
try {
hook.error(context, error);
} catch (Exception hookError) {
// Log hook error but don't throw to avoid masking the original error
DevCycleLogger.error("Error hook failed: " + hookError.getMessage(), hookError);
}
}
}

/**
* Runs all finally hooks in reverse order.
*
* @param context The context to pass to the hooks
* @param variable The variable result to pass to the hooks (may be null)
*/
public void executeFinally(ArrayList<EvalHook<T>> hooks, HookContext<T> context, Optional<Variable<T>> variable) {
for (EvalHook<T> hook : hooks) {
try {
hook.onFinally(context, variable);
} catch (Exception e) {
// Log finally hook error but don't throw
DevCycleLogger.error("Finally hook failed: " + e.getMessage(), e);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.devcycle.sdk.server.common.model;

import java.util.Map;

/**
* Context object passed to hooks during variable evaluation.
* Contains the user, variable key, default value, and additional context data.
*/
public class HookContext<T> {
private DevCycleUser user;
private final String key;
private final T defaultValue;
private Variable<T> variableDetails;

public HookContext(DevCycleUser user, String key, T defaultValue) {
this.user = user;
this.key = key;
this.defaultValue = defaultValue;
}

public HookContext(DevCycleUser user, String key, T defaultValue, Variable<T> variable) {
this.user = user;
this.key = key;
this.defaultValue = defaultValue;
this.variableDetails = variable;
}

public DevCycleUser getUser() {
return user;
}

public String getKey() {
return key;
}

public T getDefaultValue() {
return defaultValue;
}

public Variable<T> getVariableDetails() { return variableDetails; }

public HookContext<T> merge(HookContext<T> other) {
if (other == null) {
return this;
}
return new HookContext<>(other.getUser(), key, defaultValue, variableDetails);
}
}
Loading
Loading