Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: errorCode as enum, reason as string #80

Merged
merged 4 commits into from
Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion .github/workflows/pullrequest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
${{ runner.os }}-maven-

- name: Build with Maven
run: mvn --batch-mode --update-snapshots verify -P integration-test
run: mvn --batch-mode --update-snapshots verify # -P integration-test - add this back once we have a compatible flagd
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the integration-test profile disables the integration test suite as discussed in the description.


- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
<dependency>
<groupId>dev.openfeature.contrib.providers</groupId>
<artifactId>flagd</artifactId>
<!-- TODO: update this version -->
<version>0.3.2</version>
<scope>test</scope>
</dependency>
Expand Down
11 changes: 9 additions & 2 deletions src/main/java/dev/openfeature/javasdk/BaseEvaluation.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@ public interface BaseEvaluation<T> {
* Describes how we came to the value that we're returning.
* @return {Reason}
*/
Reason getReason();
String getReason();

/**
* The error code, if applicable. Should only be set when the Reason is ERROR.
* @return {ErrorCode}
*/
String getErrorCode();
ErrorCode getErrorCode();

/**
* The error message (usually from exception.getMessage()), if applicable.
* Should only be set when the Reason is ERROR.
* @return {String}
*/
String getErrorMessage();
}
2 changes: 1 addition & 1 deletion src/main/java/dev/openfeature/javasdk/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package dev.openfeature.javasdk;

public enum ErrorCode {
PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, GENERAL
PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, TARGETING_KEY_MISSING, INVALID_CONTEXT, GENERAL
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec change.

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ public class FlagEvaluationDetails<T> implements BaseEvaluation<T> {
private String flagKey;
private T value;
@Nullable private String variant;
private Reason reason;
@Nullable private String errorCode;
@Nullable private String reason;
private ErrorCode errorCode;
@Nullable private String errorMessage;

/**
* Generate detail payload from the provider response.
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/dev/openfeature/javasdk/NoOpProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defa
return ProviderEvaluation.<Boolean>builder()
.value(defaultValue)
.variant(PASSED_IN_DEFAULT)
.reason(Reason.DEFAULT)
.reason(Reason.DEFAULT.toString())
Copy link
Member Author

@toddbaert toddbaert Sep 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's still good to have an enum of default reasons. We just convert them to strings where applicable. We can instead use string constants... anyone have opinions on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this a good idea to keep the enum 👍

.build();
}

Expand All @@ -34,7 +34,7 @@ public ProviderEvaluation<String> getStringEvaluation(String key, String default
return ProviderEvaluation.<String>builder()
.value(defaultValue)
.variant(PASSED_IN_DEFAULT)
.reason(Reason.DEFAULT)
.reason(Reason.DEFAULT.toString())
.build();
}

Expand All @@ -43,7 +43,7 @@ public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defa
return ProviderEvaluation.<Integer>builder()
.value(defaultValue)
.variant(PASSED_IN_DEFAULT)
.reason(Reason.DEFAULT)
.reason(Reason.DEFAULT.toString())
.build();
}

Expand All @@ -52,7 +52,7 @@ public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double default
return ProviderEvaluation.<Double>builder()
.value(defaultValue)
.variant(PASSED_IN_DEFAULT)
.reason(Reason.DEFAULT)
.reason(Reason.DEFAULT.toString())
.build();
}

Expand All @@ -62,7 +62,7 @@ public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultVa
return ProviderEvaluation.<Value>builder()
.value(defaultValue)
.variant(PASSED_IN_DEFAULT)
.reason(Reason.DEFAULT)
.reason(Reason.DEFAULT.toString())
.build();
}
}
10 changes: 8 additions & 2 deletions src/main/java/dev/openfeature/javasdk/OpenFeatureClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.Map;

import dev.openfeature.javasdk.exceptions.GeneralError;
import dev.openfeature.javasdk.exceptions.OpenFeatureError;
import dev.openfeature.javasdk.internal.ObjectUtils;
import lombok.Getter;
import lombok.Setter;
Expand Down Expand Up @@ -94,9 +95,14 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
if (details == null) {
details = FlagEvaluationDetails.<T>builder().build();
}
if (e instanceof OpenFeatureError) {
details.setErrorCode(((OpenFeatureError)e).getErrorCode());
} else {
details.setErrorCode(ErrorCode.GENERAL);
}
details.setErrorMessage(e.getMessage());
details.setValue(defaultValue);
details.setReason(Reason.ERROR);
details.setErrorCode(e.getMessage());
details.setReason(Reason.ERROR.toString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider whether a provider might want to different reason to ERROR?

Copy link
Member Author

@toddbaert toddbaert Sep 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec says that reason must indicate an error in error cases (1.4.8). This is in the evaluationAPI section, not the provider section: https://github.com/open-feature/spec/blob/f74a8d98763bcac5d9a5c67a40ca9d7c2d445180/specification/sections/01-flag-evaluation.md#requirement-148. This is more of a SDK functionality concern in my opinion than an provider one.

I suppose the provider could provide a more specific error reason, but I think additional information can be better conveyed by the errorMessage and errorCode fields, and that should be the provider's role here. We can also have exceptions thrown outside of providers (hooks) - it's probably unrealistic to expect them to set a reason since flag resolution is not their concern.

hookSupport.errorHooks(type, hookCtx, e, mergedHooks, hints);
} finally {
hookSupport.afterAllHooks(type, hookCtx, mergedHooks, hints);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
public class ProviderEvaluation<T> implements BaseEvaluation<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be useful to have an overload for setting the reason based on a Reason? I am not really sure how these lombok classes work, aside from magic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hesitant to add it b/c it's not something lombok can do for us. I'm lazy.

T value;
@Nullable String variant;
Reason reason;
@Nullable String errorCode;
@Nullable private String reason;
ErrorCode errorCode;
@Nullable private String errorMessage;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
@StandardException
public class FlagNotFoundError extends OpenFeatureError {
private static final long serialVersionUID = 1L;
@Getter private final ErrorCode errorCode = ErrorCode.GENERAL;
@Getter private final ErrorCode errorCode = ErrorCode.FLAG_NOT_FOUND;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was an existing bug, unless I'm missing something.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.openfeature.javasdk.exceptions;

import dev.openfeature.javasdk.ErrorCode;
import lombok.Getter;
import lombok.experimental.StandardException;

@StandardException
public class InvalidContextError extends OpenFeatureError {
private static final long serialVersionUID = 1L;

@Getter private final ErrorCode errorCode = ErrorCode.INVALID_CONTEXT;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.openfeature.javasdk.exceptions;

import dev.openfeature.javasdk.ErrorCode;
import lombok.Getter;
import lombok.experimental.StandardException;

@StandardException
public class TargetingKeyMissingError extends OpenFeatureError {
private static final long serialVersionUID = 1L;

@Getter private final ErrorCode errorCode = ErrorCode.TARGETING_KEY_MISSING;

}
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
package dev.openfeature.javasdk;

import dev.openfeature.javasdk.exceptions.FlagNotFoundError;

public class AlwaysBrokenProvider implements FeatureProvider {

@Override
public Metadata getMetadata() {
return new Metadata() {
@Override
public String getName() {
throw new NotImplementedException("BORK");
throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
}
};
}

@Override
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
throw new NotImplementedException("BORK");
throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
}

@Override
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
throw new NotImplementedException("BORK");
throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
}

@Override
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
throw new NotImplementedException("BORK");
throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
}

@Override
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
throw new NotImplementedException("BORK");
throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
}

@Override
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext invocationContext) {
throw new NotImplementedException("BORK");
throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ class DeveloperExperienceTest implements HookFixtures {
api.setProvider(new AlwaysBrokenProvider());
Client client = api.getClient();
FlagEvaluationDetails<Boolean> retval = client.getBooleanDetails(flagKey, false);
assertEquals("BORK", retval.getErrorCode());
assertEquals(Reason.ERROR, retval.getReason());
assertEquals(ErrorCode.FLAG_NOT_FOUND, retval.getErrorCode());
assertEquals(TestConstants.BROKEN_MESSAGE, retval.getErrorMessage());
assertEquals(Reason.ERROR.toString(), retval.getReason());
assertFalse(retval.getValue());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,16 @@ private Client _client() {
}

@Specification(number="1.4.9", text="Methods, functions, or operations on the client MUST NOT throw exceptions, or otherwise abnormally terminate. Flag evaluation calls must always return the default value in the event of abnormal execution. Exceptions include functions or methods for the purposes for configuration or setup.")
@Specification(number="1.4.7", text="In cases of abnormal execution, the evaluation details structure's error code field MUST contain a string identifying an error occurred during flag evaluation and the nature of the error.")
@Specification(number="1.4.7", text="In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.")
@Specification(number="1.4.12", text="In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.")
@Test void broken_provider() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(new AlwaysBrokenProvider());
Client c = api.getClient();
assertFalse(c.getBooleanValue("key", false));
FlagEvaluationDetails<Boolean> details = c.getBooleanDetails("key", false);
assertEquals("BORK", details.getErrorCode());
assertEquals(ErrorCode.FLAG_NOT_FOUND, details.getErrorCode());
assertEquals(TestConstants.BROKEN_MESSAGE, details.getErrorMessage());
}

@Specification(number="1.4.10", text="In the case of abnormal execution, the client SHOULD log an informative error message.")
Expand All @@ -200,8 +202,8 @@ private Client _client() {
api.setProvider(new AlwaysBrokenProvider());
Client c = api.getClient();
FlagEvaluationDetails<Boolean> result = c.getBooleanDetails("test", false);
assertEquals(Reason.ERROR, result.getReason());
assertThat(TEST_LOGGER.getLoggingEvents()).contains(LoggingEvent.error("Unable to correctly evaluate flag with key {} due to exception {}", "test", "BORK"));
assertEquals(Reason.ERROR.toString(), result.getReason());
assertThat(TEST_LOGGER.getLoggingEvents()).contains(LoggingEvent.error("Unable to correctly evaluate flag with key {} due to exception {}", "test", TestConstants.BROKEN_MESSAGE));
}

@Specification(number="1.2.2", text="The client interface MUST define a metadata member or accessor, containing an immutable name field or accessor of type string, which corresponds to the name value supplied during client creation.")
Expand All @@ -221,7 +223,7 @@ private Client _client() {
api.setProvider(new AlwaysBrokenProvider());
Client c = api.getClient();
FlagEvaluationDetails<Boolean> result = c.getBooleanDetails("test", false);
assertEquals(Reason.ERROR, result.getReason());
assertEquals(Reason.ERROR.toString(), result.getReason());
}

@Specification(number="3.2.1", text="The API, Client and invocation MUST have a method for supplying evaluation context.")
Expand Down
11 changes: 5 additions & 6 deletions src/test/java/dev/openfeature/javasdk/ProviderSpecTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,10 @@ public class ProviderSpecTest {

}

@Specification(number="2.6", text="The provider SHOULD populate the flag resolution structure's " +
"reason field with a string indicating the semantic reason for the returned flag value.")
@Specification(number="2.6", text="The `provider` SHOULD populate the `flag resolution` structure's `reason` field with `\"DEFAULT\",` `\"TARGETING_MATCH\"`, `\"SPLIT\"`, `\"DISABLED\"`, `\"UNKNOWN\"`, `\"ERROR\"` or some other string indicating the semantic reason for the returned flag value.")
@Test void has_reason() {
ProviderEvaluation<Boolean> result = p.getBooleanEvaluation("key", false, new EvaluationContext());
assertEquals(Reason.DEFAULT, result.getReason());
assertEquals(Reason.DEFAULT.toString(), result.getReason());
}

@Specification(number="2.7", text="In cases of normal execution, the provider MUST NOT populate " +
Expand All @@ -55,9 +54,9 @@ public class ProviderSpecTest {
assertNull(result.getErrorCode());
}

@Specification(number="2.8", text="In cases of abnormal execution, the provider MUST indicate an " +
"error using the idioms of the implementation language, with an associated error code having possible " +
"values PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, or GENERAL.")
@Specification(number="2.8", text="In cases of abnormal execution, the `provider` **MUST** indicate an error using the idioms of the implementation language, with an associated `error code` and optional associated `error message`.")
@Specification(number="2.11", text="In cases of normal execution, the `provider` **MUST NOT** populate the `flag resolution` structure's `error message` field, or otherwise must populate it with a null or falsy value.")
@Specification(number="2.12", text="In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional detail about the nature of the error.")
@Test void up_to_provider_implementation() {}

@Specification(number="2.5", text="In cases of normal execution, the provider SHOULD populate the " +
Expand Down
5 changes: 5 additions & 0 deletions src/test/java/dev/openfeature/javasdk/TestConstants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.openfeature.javasdk;

public class TestConstants {
public static final String BROKEN_MESSAGE = "This is borked.";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeFalse;

import dev.openfeature.contrib.providers.flagd.FlagdProvider;
import dev.openfeature.javasdk.Client;
import dev.openfeature.javasdk.ErrorCode;
import dev.openfeature.javasdk.EvaluationContext;
import dev.openfeature.javasdk.FlagEvaluationDetails;
import dev.openfeature.javasdk.OpenFeatureAPI;
Expand Down Expand Up @@ -132,7 +130,7 @@ public void the_resolved_boolean_value_should_be_the_variant_should_be_and_the_r
String expectedVariant, String expectedReason) {
assertEquals(Boolean.valueOf(expectedValue), booleanFlagDetails.getValue());
assertEquals(expectedVariant, booleanFlagDetails.getVariant());
assertEquals(Reason.valueOf(expectedReason), booleanFlagDetails.getReason());
assertEquals(expectedReason, booleanFlagDetails.getReason());
}

// string details
Expand All @@ -147,7 +145,7 @@ public void the_resolved_string_value_should_be_the_variant_should_be_and_the_re
String expectedVariant, String expectedReason) {
assertEquals(expectedValue, this.stringFlagDetails.getValue());
assertEquals(expectedVariant, this.stringFlagDetails.getVariant());
assertEquals(Reason.valueOf(expectedReason), this.stringFlagDetails.getReason());
assertEquals(expectedReason, this.stringFlagDetails.getReason());
}

// integer details
Expand All @@ -161,7 +159,7 @@ public void the_resolved_integer_value_should_be_the_variant_should_be_and_the_r
String expectedVariant, String expectedReason) {
assertEquals(expectedValue, this.intFlagDetails.getValue());
assertEquals(expectedVariant, this.intFlagDetails.getVariant());
assertEquals(Reason.valueOf(expectedReason), this.intFlagDetails.getReason());
assertEquals(expectedReason, this.intFlagDetails.getReason());
}

// float/double details
Expand All @@ -175,7 +173,7 @@ public void the_resolved_float_value_should_be_the_variant_should_be_and_the_rea
String expectedVariant, String expectedReason) {
assertEquals(expectedValue, this.doubleFlagDetails.getValue());
assertEquals(expectedVariant, this.doubleFlagDetails.getVariant());
assertEquals(Reason.valueOf(expectedReason), this.doubleFlagDetails.getReason());
assertEquals(expectedReason, this.doubleFlagDetails.getReason());
}

// object details
Expand All @@ -198,7 +196,7 @@ public void the_resolved_object_value_should_be_contain_fields_and_with_values_a
@Then("the variant should be {string}, and the reason should be {string}")
public void the_variant_should_be_and_the_reason_should_be(String expectedVariant, String expectedReason) {
assertEquals(expectedVariant, this.objectFlagDetails.getVariant());
assertEquals(Reason.valueOf(expectedReason), this.objectFlagDetails.getReason());
assertEquals(expectedReason, this.objectFlagDetails.getReason());
}

/*
Expand Down Expand Up @@ -255,8 +253,9 @@ public void then_the_default_string_value_should_be_returned() {

@Then("the reason should indicate an error and the error code should indicate a missing flag with {string}")
public void the_reason_should_indicate_an_error_and_the_error_code_should_be_flag_not_found(String errorCode) {
assertEquals(Reason.ERROR, notFoundDetails.getReason());
assertTrue(notFoundDetails.getErrorCode().contains(errorCode));
assertEquals(Reason.ERROR.toString(), notFoundDetails.getReason());
assertTrue(notFoundDetails.getErrorMessage().contains(errorCode));
// TODO: add errorCode assertion once flagd provider is updated.
}

// type mismatch
Expand All @@ -275,8 +274,9 @@ public void then_the_default_integer_value_should_be_returned() {

@Then("the reason should indicate an error and the error code should indicate a type mismatch with {string}")
public void the_reason_should_indicate_an_error_and_the_error_code_should_be_type_mismatch(String errorCode) {
assertEquals(Reason.ERROR, typeErrorDetails.getReason());
assertTrue(typeErrorDetails.getErrorCode().contains(errorCode));
assertEquals(Reason.ERROR.toString(), typeErrorDetails.getReason());
assertTrue(typeErrorDetails.getErrorMessage().contains(errorCode));
// TODO: add errorCode assertion once flagd provider is updated.
}

}