Skip to content
Open
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
3 changes: 3 additions & 0 deletions src/main/java/dev/openfeature/sdk/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

/**
* Interface used to resolve flags of varying types.
*
* <p><b>API note:</b> not intended for external implementation. Additive method changes
* (such as new flag-value-type accessors) are considered non-breaking.
*/
public interface Client extends Features, Tracking, EventBus<Client> {
ClientMetadata getMetadata();
Expand Down
172 changes: 154 additions & 18 deletions src/main/java/dev/openfeature/sdk/FeatureProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,185 @@
* should extend {@link EventProvider}
*/
public interface FeatureProvider {

/** Maximum integer losslessly representable as an IEEE-754 double: 2^53 - 1. */
long MAX_SAFE_INTEGER = 9_007_199_254_740_991L;

/**
* Returns provider-identifying metadata (typically the provider name).
*
* @return provider metadata
*/
Metadata getMetadata();

/**
* Returns provider-defined hooks that run alongside API/client/invocation hooks during
* flag evaluation. Provider hooks are managed by the provider, not the application author.
*
* @return list of provider hooks; empty by default
*/
default List<Hook> getProviderHooks() {
return new ArrayList<>();
}

/**
* Resolves a boolean flag value.
*
* @param key flag key
* @param defaultValue value to return in the {@link ProviderEvaluation} if resolution fails
* @param ctx merged evaluation context (may be empty, never {@code null})
* @return provider evaluation containing the resolved value or an error
*/
ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx);

/**
* Resolves a string flag value.
*
* @param key flag key
* @param defaultValue value to return in the {@link ProviderEvaluation} if resolution fails
* @param ctx merged evaluation context (may be empty, never {@code null})
* @return provider evaluation containing the resolved value or an error
*/
ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx);

/**
* Resolves a 32-bit integer flag value. For flags whose values may exceed
* {@link Integer#MAX_VALUE}, use {@link #getLongEvaluation} instead.
*
* @param key flag key
* @param defaultValue value to return in the {@link ProviderEvaluation} if resolution fails
* @param ctx merged evaluation context (may be empty, never {@code null})
* @return provider evaluation containing the resolved value or an error
*/
ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx);

/**
* Resolves a double-precision floating-point flag value.
*
* @param key flag key
* @param defaultValue value to return in the {@link ProviderEvaluation} if resolution fails
* @param ctx merged evaluation context (may be empty, never {@code null})
* @return provider evaluation containing the resolved value or an error
*/
ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx);

/**
* Resolves a 64-bit integer (Long) flag value.
*
* <p>The default implementation delegates to {@link #getDoubleEvaluation} and returns a
* {@link ProviderEvaluation} with {@link ErrorCode#TYPE_MISMATCH} for values outside the
* safe-integer range ({@code [-(2^53 - 1), 2^53 - 1]}) or non-integral doubles (NaN,
* +/-Infinity, fractional). Providers that natively support 64-bit integer flags should
* override this method.
*
* @param key flag key
* @param defaultValue value to return in the {@link ProviderEvaluation} if resolution fails
* @param ctx merged evaluation context (may be empty, never {@code null})
* @return provider evaluation containing the resolved value or an error
*/
default ProviderEvaluation<Long> getLongEvaluation(String key, Long defaultValue, EvaluationContext ctx) {
if (defaultValue != null && !isWithinSafeRange(defaultValue)) {
return longError(
defaultValue,
"Default value " + defaultValue
+ " exceeds safe integer range [-(2^53 - 1), 2^53 - 1] for double-backed long evaluation");
}

Double doubleDefault = defaultValue == null ? null : (double) defaultValue;
ProviderEvaluation<Double> result = getDoubleEvaluation(key, doubleDefault, ctx);

Double boxed = result.getValue();
Long longValue;
if (boxed == null) {
longValue = defaultValue;
} else {
double value = boxed;
if (Double.isNaN(value) || Double.isInfinite(value)) {
return longError(defaultValue, "Cannot convert " + value + " to long", result);
}
if (value != Math.floor(value)) {
return longError(defaultValue, "Cannot convert fractional value " + value + " to long", result);
}
if (Math.abs(value) > MAX_SAFE_INTEGER) {
return longError(
defaultValue,
"Value " + value + " exceeds safe integer range [-(2^53 - 1), 2^53 - 1] for long",
result);
}
longValue = (long) value;
}

return ProviderEvaluation.<Long>builder()
.value(longValue)
.reason(result.getReason())
.variant(result.getVariant())
.errorCode(result.getErrorCode())
.errorMessage(result.getErrorMessage())
.flagMetadata(result.getFlagMetadata())
.build();
}

// avoid Math.abs; Math.abs(Long.MIN_VALUE) == Long.MIN_VALUE (two's-complement overflow)
private static boolean isWithinSafeRange(long value) {
return value >= -MAX_SAFE_INTEGER && value <= MAX_SAFE_INTEGER;
}

private static ProviderEvaluation<Long> longError(Long defaultValue, String message) {
return ProviderEvaluation.<Long>builder()
.value(defaultValue)
.reason(Reason.ERROR.toString())
.errorCode(ErrorCode.TYPE_MISMATCH)
.errorMessage(message)
.build();
}

// preserve upstream metadata/variant; override with type error
private static ProviderEvaluation<Long> longError(
Long defaultValue, String message, ProviderEvaluation<Double> upstream) {
return ProviderEvaluation.<Long>builder()
.value(defaultValue)
.reason(Reason.ERROR.toString())
.errorCode(ErrorCode.TYPE_MISMATCH)
.errorMessage(message)
.variant(upstream.getVariant())
.flagMetadata(upstream.getFlagMetadata())
.build();
}

/**
* Resolves a structured (object) flag value. Values are wrapped in {@link Value} which can
* carry booleans, strings, numbers, structures, and lists.
*
* @param key flag key
* @param defaultValue value to return in the {@link ProviderEvaluation} if resolution fails
* @param ctx merged evaluation context (may be empty, never {@code null})
* @return provider evaluation containing the resolved value or an error
*/
ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx);

/**
* This method is called before a provider is used to evaluate flags. Providers
* can overwrite this method,
* if they have special initialization needed prior being called for flag
* evaluation.
* Called once before a provider is used to evaluate flags. Providers can override this method
* if they have special initialization needed prior to being called for flag evaluation.
*
* <p>It is ok if the method is expensive; it is executed in the background. All runtime
* exceptions will be caught and logged.
*
* <p>
* It is ok if the method is expensive as it is executed in the background. All
* runtime exceptions will be
* caught and logged.
* </p>
* @param evaluationContext the API-level evaluation context at the time of initialization
* @throws Exception any exception thrown here transitions the provider to
* {@link ProviderState#ERROR} (or {@link ProviderState#FATAL} for
* {@link dev.openfeature.sdk.exceptions.FatalError})
*/
default void initialize(EvaluationContext evaluationContext) throws Exception {
// Intentionally left blank
}

/**
* This method is called when a new provider is about to be used to evaluate
* flags, or the SDK is shut down.
* Providers can overwrite this method, if they have special shutdown actions
* needed.
* Called when a provider is about to be replaced or the SDK is shutting down. Providers can
* override this method if they have resources to release (background threads, connections,
* caches, etc.).
*
* <p>
* It is ok if the method is expensive as it is executed in the background. All
* runtime exceptions will be
* caught and logged.
* </p>
* <p>It is ok if the method is expensive; it is executed in the background. All runtime
* exceptions will be caught and logged.
*/
default void shutdown() {
// Intentionally left blank
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/dev/openfeature/sdk/Features.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

/**
* An API for the type-specific fetch methods offered to users.
*
* <p><b>API note:</b> not intended for external implementation. Additive method changes
* (such as new flag-value-type accessors) are considered non-breaking.
*/
public interface Features {

Expand Down Expand Up @@ -44,6 +47,19 @@ FlagEvaluationDetails<String> getStringDetails(
FlagEvaluationDetails<Integer> getIntegerDetails(
String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);

Long getLongValue(String key, Long defaultValue);

Long getLongValue(String key, Long defaultValue, EvaluationContext ctx);

Long getLongValue(String key, Long defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);

FlagEvaluationDetails<Long> getLongDetails(String key, Long defaultValue);

FlagEvaluationDetails<Long> getLongDetails(String key, Long defaultValue, EvaluationContext ctx);

FlagEvaluationDetails<Long> getLongDetails(
String key, Long defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);

Double getDoubleValue(String key, Double defaultValue);

Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx);
Expand Down
1 change: 1 addition & 0 deletions src/main/java/dev/openfeature/sdk/FlagValueType.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
public enum FlagValueType {
STRING,
INTEGER,
LONG,
DOUBLE,
OBJECT,
BOOLEAN;
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/dev/openfeature/sdk/LongHook.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.openfeature.sdk;

/**
* An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic
* to the lifecycle of flag evaluation.
*
* @see Hook
*/
public interface LongHook extends Hook<Long> {

@Override
default boolean supportsFlagValueType(FlagValueType flagValueType) {
return FlagValueType.LONG == flagValueType;
}
}
33 changes: 33 additions & 0 deletions src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ private <T> ProviderEvaluation<?> createProviderEvaluation(
return provider.getStringEvaluation(key, (String) defaultValue, invocationContext);
case INTEGER:
return provider.getIntegerEvaluation(key, (Integer) defaultValue, invocationContext);
case LONG:
return provider.getLongEvaluation(key, (Long) defaultValue, invocationContext);
case DOUBLE:
return provider.getDoubleEvaluation(key, (Double) defaultValue, invocationContext);
case OBJECT:
Expand Down Expand Up @@ -407,6 +409,37 @@ public FlagEvaluationDetails<Integer> getIntegerDetails(
return this.evaluateFlag(FlagValueType.INTEGER, key, defaultValue, ctx, options);
}

@Override
public Long getLongValue(String key, Long defaultValue) {
return getLongDetails(key, defaultValue).getValue();
}

@Override
public Long getLongValue(String key, Long defaultValue, EvaluationContext ctx) {
return getLongDetails(key, defaultValue, ctx).getValue();
}

@Override
public Long getLongValue(String key, Long defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
return getLongDetails(key, defaultValue, ctx, options).getValue();
}

@Override
public FlagEvaluationDetails<Long> getLongDetails(String key, Long defaultValue) {
return getLongDetails(key, defaultValue, null);
}

@Override
public FlagEvaluationDetails<Long> getLongDetails(String key, Long defaultValue, EvaluationContext ctx) {
return getLongDetails(key, defaultValue, ctx, FlagEvaluationOptions.EMPTY);
}

@Override
public FlagEvaluationDetails<Long> getLongDetails(
String key, Long defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
return this.evaluateFlag(FlagValueType.LONG, key, defaultValue, ctx, options);
}

@Override
public Double getDoubleValue(String key, Double defaultValue) {
return getDoubleValue(key, defaultValue, null);
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/dev/openfeature/sdk/Value.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ public Value(Integer value) {
this.innerObject = value;
}

public Value(Long value) {
this.innerObject = value;
}

public Value(Double value) {
this.innerObject = value;
}
Expand Down Expand Up @@ -213,6 +217,19 @@ public Integer asInteger() {
return null;
}

/**
* Retrieve the underlying numeric value as a Long, or null.
* If the value is a non-integral number, it will be truncated using Number#longValue().
*
* @return Long
*/
public Long asLong() {
if (this.isNumber() && !this.isNull()) {
return ((Number) this.innerObject).longValue();
}
return null;
}

/**
* Retrieve the underlying numeric value as a Double, or null.
*
Expand Down Expand Up @@ -301,6 +318,8 @@ public static Value objectToValue(Object object) {
return new Value((Boolean) object);
} else if (object instanceof Integer) {
return new Value((Integer) object);
} else if (object instanceof Long) {
return new Value((Long) object);
} else if (object instanceof Double) {
return new Value((Double) object);
} else if (object instanceof Structure) {
Expand Down
Loading
Loading