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: implement spec 0.7.0 changes #655

Merged
merged 10 commits into from
Oct 23, 2023
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
<!-- x-hide-in-docs-end -->
<!-- The 'github-badges' class is used in the docs -->
<p align="center" class="github-badges">
<a href="https://github.com/open-feature/spec/tree/v0.6.0">
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.6.0&color=yellow&style=for-the-badge" />
<a href="https://github.com/open-feature/spec/tree/v0.7.0">
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge" />
</a>
<!-- x-release-please-start-version -->

Expand Down
10 changes: 7 additions & 3 deletions src/main/java/dev/openfeature/sdk/EventDetails.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@
/**
* The details of a particular event.
*/
@Data @SuperBuilder(toBuilder = true)
@Data
@SuperBuilder(toBuilder = true)
public class EventDetails extends ProviderEventDetails {
private String clientName;
private String providerName;

static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventDetails) {
return EventDetails.fromProviderEventDetails(providerEventDetails, null);
static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventDetails, String providerName) {
return EventDetails.fromProviderEventDetails(providerEventDetails, providerName, null);
}

static EventDetails fromProviderEventDetails(
ProviderEventDetails providerEventDetails,
@Nullable String providerName,
@Nullable String clientName) {
return EventDetails.builder()
.clientName(clientName)
.providerName(providerName)
.flagsChanged(providerEventDetails.getFlagsChanged())
.eventMetadata(providerEventDetails.getEventMetadata())
.message(providerEventDetails.getMessage())
Expand Down
20 changes: 14 additions & 6 deletions src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.openfeature.sdk;

import java.util.Optional;

import javax.annotation.Nullable;

import lombok.AllArgsConstructor;
Expand All @@ -8,7 +10,8 @@
import lombok.NoArgsConstructor;

/**
* Contains information about how the provider resolved a flag, including the resolved value.
* Contains information about how the provider resolved a flag, including the
* resolved value.
*
* @param <T> the type of the flag being evaluated.
*/
Expand All @@ -20,11 +23,15 @@ public class FlagEvaluationDetails<T> implements BaseEvaluation<T> {

private String flagKey;
private T value;
@Nullable private String variant;
@Nullable private String reason;
@Nullable
private String variant;
@Nullable
private String reason;
private ErrorCode errorCode;
@Nullable private String errorMessage;
@Builder.Default private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
@Nullable
private String errorMessage;
@Builder.Default
private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();

/**
* Generate detail payload from the provider response.
Expand All @@ -42,7 +49,8 @@ public static <T> FlagEvaluationDetails<T> from(ProviderEvaluation<T> providerEv
.reason(providerEval.getReason())
.errorMessage(providerEval.getErrorMessage())
.errorCode(providerEval.getErrorCode())
.flagMetadata(providerEval.getFlagMetadata())
.flagMetadata(
Optional.ofNullable(providerEval.getFlagMetadata()).orElse(ImmutableMetadata.builder().build()))
.build();
}
}
82 changes: 45 additions & 37 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

Expand Down Expand Up @@ -100,12 +101,12 @@ public EvaluationContext getEvaluationContext() {
public void setProvider(FeatureProvider provider) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(
provider,
provider,
this::attachEventProvider,
this::emitReady,
this::detachEventProvider,
this::emitError,
false);
false);
}
}

Expand All @@ -118,12 +119,12 @@ public void setProvider(FeatureProvider provider) {
public void setProvider(String clientName, FeatureProvider provider) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(clientName,
provider,
this::attachEventProvider,
this::emitReady,
this::detachEventProvider,
this::emitError,
false);
provider,
this::attachEventProvider,
this::emitReady,
this::detachEventProvider,
this::emitError,
false);
}
}

Expand All @@ -133,12 +134,12 @@ public void setProvider(String clientName, FeatureProvider provider) {
public void setProviderAndWait(FeatureProvider provider) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(
provider,
this::attachEventProvider,
this::emitReady,
this::detachEventProvider,
this::emitError,
true);
provider,
this::attachEventProvider,
this::emitReady,
this::detachEventProvider,
this::emitError,
true);
}
}

Expand All @@ -151,18 +152,18 @@ public void setProviderAndWait(FeatureProvider provider) {
public void setProviderAndWait(String clientName, FeatureProvider provider) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(clientName,
provider,
this::attachEventProvider,
this::emitReady,
this::detachEventProvider,
this::emitError,
true);
provider,
this::attachEventProvider,
this::emitReady,
this::detachEventProvider,
this::emitError,
true);
}
}

private void attachEventProvider(FeatureProvider provider) {
if (provider instanceof EventProvider) {
((EventProvider)provider).attach((p, event, details) -> {
((EventProvider) provider).attach((p, event, details) -> {
runHandlersForProvider(p, event, details);
});
}
Expand All @@ -174,7 +175,7 @@ private void emitReady(FeatureProvider provider) {

private void detachEventProvider(FeatureProvider provider) {
if (provider instanceof EventProvider) {
((EventProvider)provider).detach();
((EventProvider) provider).detach();
}
}

Expand Down Expand Up @@ -229,9 +230,10 @@ public void clearHooks() {

/**
* Shut down and reset the current status of OpenFeature API.
* This call cleans up all active providers and attempts to shut down internal event handling mechanisms.
* This call cleans up all active providers and attempts to shut down internal
* event handling mechanisms.
* Once shut down is complete, API is reset and ready to use again.
* */
*/
public void shutdown() {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.shutdown();
Expand Down Expand Up @@ -302,9 +304,9 @@ void removeHandler(String clientName, ProviderEvent event, Consumer<EventDetails

void addHandler(String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
// if the provider is READY, run immediately
if (ProviderEvent.PROVIDER_READY.equals(event)
&& ProviderState.READY.equals(this.providerRepository.getProvider(clientName).getState())) {
// if the provider is in the state associated with event, run immediately
if (Optional.ofNullable(this.providerRepository.getProvider(clientName).getState())
.orElse(ProviderState.READY).matchesEvent(event)) {
eventSupport.runHandler(handler, EventDetails.builder().clientName(clientName).build());
}
eventSupport.addClientHandler(clientName, event, handler);
Expand All @@ -315,30 +317,36 @@ void addHandler(String clientName, ProviderEvent event, Consumer<EventDetails> h
* Runs the handlers associated with a particular provider.
*
* @param provider the provider from where this event originated
* @param event the event type
* @param details the event details
* @param event the event type
* @param details the event details
*/
private void runHandlersForProvider(FeatureProvider provider, ProviderEvent event, ProviderEventDetails details) {
try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {

List<String> clientNamesForProvider = providerRepository
.getClientNamesForProvider(provider);

.getClientNamesForProvider(provider);

final String providerName = Optional.ofNullable(provider.getMetadata())
.map(metadata -> metadata.getName())
.orElse(null);

// run the global handlers
eventSupport.runGlobalHandlers(event, EventDetails.fromProviderEventDetails(details));
eventSupport.runGlobalHandlers(event, EventDetails.fromProviderEventDetails(details, providerName));

// run the handlers associated with named clients for this provider
clientNamesForProvider.forEach(name -> {
eventSupport.runClientHandlers(name, event, EventDetails.fromProviderEventDetails(details, name));
clientNamesForProvider.forEach(name -> {
eventSupport.runClientHandlers(name, event,
EventDetails.fromProviderEventDetails(details, providerName, name));
});

if (providerRepository.isDefaultProvider(provider)) {
// run handlers for clients that have no bound providers (since this is the default)
Set<String> allClientNames = eventSupport.getAllClientNames();
Set<String> boundClientNames = providerRepository.getAllBoundClientNames();
allClientNames.removeAll(boundClientNames);
allClientNames.forEach(name -> {
eventSupport.runClientHandlers(name, event, EventDetails.fromProviderEventDetails(details, name));
eventSupport.runClientHandlers(name, event,
EventDetails.fromProviderEventDetails(details, providerName, name));
});
}
}
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/dev/openfeature/sdk/ProviderState.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,17 @@
* Indicates the state of the provider.
*/
public enum ProviderState {
READY, NOT_READY, ERROR;
READY, NOT_READY, ERROR, STALE;

/**
* Returns true if the passed ProviderEvent maps to this ProviderState.
*
* @param event event to compare
* @return boolean if matches.
*/
boolean matchesEvent(ProviderEvent event) {
Kavindu-Dodan marked this conversation as resolved.
Show resolved Hide resolved
return this == READY && event == ProviderEvent.PROVIDER_READY
|| this == STALE && event == ProviderEvent.PROVIDER_STALE
|| this == ERROR && event == ProviderEvent.PROVIDER_ERROR;
}
}
11 changes: 10 additions & 1 deletion src/test/java/dev/openfeature/sdk/DoSomethingProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@ class DoSomethingProvider implements FeatureProvider {

static final String name = "Something";
// Flag evaluation metadata
static final ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
static final ImmutableMetadata DEFAULT_METADATA = ImmutableMetadata.builder().build();
private ImmutableMetadata flagMetadata;

private EvaluationContext savedContext;

public DoSomethingProvider() {
this.flagMetadata = DEFAULT_METADATA;
}

public DoSomethingProvider(ImmutableMetadata flagMetadata) {
this.flagMetadata = flagMetadata;
}

EvaluationContext getMergedContext() {
return savedContext;
}
Expand Down
45 changes: 40 additions & 5 deletions src/test/java/dev/openfeature/sdk/EventsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

class EventsTest {

private static final int TIMEOUT = 200;
private static final int TIMEOUT = 300;
private static final int INIT_DELAY = TIMEOUT / 2;

@AfterAll
Expand Down Expand Up @@ -470,7 +470,7 @@ void handlersRunIfOneThrows() throws Exception {
@Test
@DisplayName("should have all properties")
@Specification(number = "5.2.4", text = "The handler function MUST accept a event details parameter.")
@Specification(number = "5.2.3", text = "The event details MUST contain the client name associated with the event.")
@Specification(number = "5.2.3", text = "The `event details` MUST contain the `provider name` associated with the event.")
void shouldHaveAllProperties() throws Exception {
final Consumer<EventDetails> handler1 = mockHandler();
final Consumer<EventDetails> handler2 = mockHandler();
Expand Down Expand Up @@ -514,9 +514,9 @@ void shouldHaveAllProperties() throws Exception {

@Test
@DisplayName("if the provider is ready handlers must run immediately")
@Specification(number = "5.3.3", text = "PROVIDER_READY handlers attached after the provider is already in a ready state MUST run immediately.")
void readyMustRunImmediately() throws Exception {
final String name = "readyMustRunImmediately";
@Specification(number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.")
void matchingReadyEventsMustRunImmediately() throws Exception {
final String name = "matchingEventsMustRunImmediately";
final Consumer<EventDetails> handler = mockHandler();

// provider which is already ready
Expand All @@ -529,6 +529,40 @@ void readyMustRunImmediately() throws Exception {
verify(handler, timeout(TIMEOUT)).accept(any());
}

@Test
@DisplayName("if the provider is ready handlers must run immediately")
@Specification(number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.")
void matchingStaleEventsMustRunImmediately() throws Exception {
final String name = "matchingEventsMustRunImmediately";
final Consumer<EventDetails> handler = mockHandler();

// provider which is already stale
TestEventsProvider provider = new TestEventsProvider(ProviderState.STALE);
OpenFeatureAPI.getInstance().setProvider(name, provider);

// should run even thought handler was added after stale
Client client = OpenFeatureAPI.getInstance().getClient(name);
client.onProviderStale(handler);
verify(handler, timeout(TIMEOUT)).accept(any());
}

@Test
@DisplayName("if the provider is ready handlers must run immediately")
@Specification(number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.")
void matchingErrorEventsMustRunImmediately() throws Exception {
final String name = "matchingEventsMustRunImmediately";
final Consumer<EventDetails> handler = mockHandler();

// provider which is already in error
TestEventsProvider provider = new TestEventsProvider(ProviderState.ERROR);
OpenFeatureAPI.getInstance().setProvider(name, provider);

// should run even thought handler was added after error
Client client = OpenFeatureAPI.getInstance().getClient(name);
client.onProviderError(handler);
verify(handler, timeout(TIMEOUT)).accept(any());
}

@Test
@DisplayName("must persist across changes")
@Specification(number = "5.2.6", text = "Event handlers MUST persist across provider changes.")
Expand Down Expand Up @@ -560,6 +594,7 @@ void mustPersistAcrossChanges() throws Exception {

@Nested
class HandlerRemoval {
@Specification(number="5.2.7", text="The API and client MUST provide a function allowing the removal of event handlers.")
@Test
@DisplayName("should not run removed events")
void removedEventsShouldNotRun() {
Expand Down
Loading
Loading