Skip to content

Commit fe5a20f

Browse files
toddbaertthiyagu06
andauthored
feat: implement spec 0.7.0 changes (#655)
* feat: implement spec 0.7.0 changes * run any event handler immediately if the provider is in the associated state, not just ready * add providerName to event details * add STALE provider state * update/add associated tests * also fixed spec/test associations mismatches from previous changes Signed-off-by: Todd Baert <[email protected]> Co-authored-by: Thiyagu GK <[email protected]>
1 parent fb4d369 commit fe5a20f

16 files changed

+254
-91
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
<!-- x-hide-in-docs-end -->
1414
<!-- The 'github-badges' class is used in the docs -->
1515
<p align="center" class="github-badges">
16-
<a href="https://github.com/open-feature/spec/tree/v0.6.0">
17-
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.6.0&color=yellow&style=for-the-badge" />
16+
<a href="https://github.com/open-feature/spec/tree/v0.7.0">
17+
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge" />
1818
</a>
1919
<!-- x-release-please-start-version -->
2020

src/main/java/dev/openfeature/sdk/EventDetails.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,23 @@
77
/**
88
* The details of a particular event.
99
*/
10-
@Data @SuperBuilder(toBuilder = true)
10+
@Data
11+
@SuperBuilder(toBuilder = true)
1112
public class EventDetails extends ProviderEventDetails {
1213
private String clientName;
14+
private String providerName;
1315

14-
static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventDetails) {
15-
return EventDetails.fromProviderEventDetails(providerEventDetails, null);
16+
static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventDetails, String providerName) {
17+
return EventDetails.fromProviderEventDetails(providerEventDetails, providerName, null);
1618
}
1719

1820
static EventDetails fromProviderEventDetails(
1921
ProviderEventDetails providerEventDetails,
22+
@Nullable String providerName,
2023
@Nullable String clientName) {
2124
return EventDetails.builder()
2225
.clientName(clientName)
26+
.providerName(providerName)
2327
.flagsChanged(providerEventDetails.getFlagsChanged())
2428
.eventMetadata(providerEventDetails.getEventMetadata())
2529
.message(providerEventDetails.getMessage())

src/main/java/dev/openfeature/sdk/FlagEvaluationDetails.java

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package dev.openfeature.sdk;
22

3+
import java.util.Optional;
4+
35
import javax.annotation.Nullable;
46

57
import lombok.AllArgsConstructor;
@@ -8,7 +10,8 @@
810
import lombok.NoArgsConstructor;
911

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

2124
private String flagKey;
2225
private T value;
23-
@Nullable private String variant;
24-
@Nullable private String reason;
26+
@Nullable
27+
private String variant;
28+
@Nullable
29+
private String reason;
2530
private ErrorCode errorCode;
26-
@Nullable private String errorMessage;
27-
@Builder.Default private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
31+
@Nullable
32+
private String errorMessage;
33+
@Builder.Default
34+
private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
2835

2936
/**
3037
* Generate detail payload from the provider response.
@@ -42,7 +49,8 @@ public static <T> FlagEvaluationDetails<T> from(ProviderEvaluation<T> providerEv
4249
.reason(providerEval.getReason())
4350
.errorMessage(providerEval.getErrorMessage())
4451
.errorCode(providerEval.getErrorCode())
45-
.flagMetadata(providerEval.getFlagMetadata())
52+
.flagMetadata(
53+
Optional.ofNullable(providerEval.getFlagMetadata()).orElse(ImmutableMetadata.builder().build()))
4654
.build();
4755
}
4856
}

src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java

+45-37
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.ArrayList;
44
import java.util.Arrays;
55
import java.util.List;
6+
import java.util.Optional;
67
import java.util.Set;
78
import java.util.function.Consumer;
89

@@ -100,12 +101,12 @@ public EvaluationContext getEvaluationContext() {
100101
public void setProvider(FeatureProvider provider) {
101102
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
102103
providerRepository.setProvider(
103-
provider,
104+
provider,
104105
this::attachEventProvider,
105106
this::emitReady,
106107
this::detachEventProvider,
107108
this::emitError,
108-
false);
109+
false);
109110
}
110111
}
111112

@@ -118,12 +119,12 @@ public void setProvider(FeatureProvider provider) {
118119
public void setProvider(String clientName, FeatureProvider provider) {
119120
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
120121
providerRepository.setProvider(clientName,
121-
provider,
122-
this::attachEventProvider,
123-
this::emitReady,
124-
this::detachEventProvider,
125-
this::emitError,
126-
false);
122+
provider,
123+
this::attachEventProvider,
124+
this::emitReady,
125+
this::detachEventProvider,
126+
this::emitError,
127+
false);
127128
}
128129
}
129130

@@ -133,12 +134,12 @@ public void setProvider(String clientName, FeatureProvider provider) {
133134
public void setProviderAndWait(FeatureProvider provider) {
134135
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
135136
providerRepository.setProvider(
136-
provider,
137-
this::attachEventProvider,
138-
this::emitReady,
139-
this::detachEventProvider,
140-
this::emitError,
141-
true);
137+
provider,
138+
this::attachEventProvider,
139+
this::emitReady,
140+
this::detachEventProvider,
141+
this::emitError,
142+
true);
142143
}
143144
}
144145

@@ -151,18 +152,18 @@ public void setProviderAndWait(FeatureProvider provider) {
151152
public void setProviderAndWait(String clientName, FeatureProvider provider) {
152153
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
153154
providerRepository.setProvider(clientName,
154-
provider,
155-
this::attachEventProvider,
156-
this::emitReady,
157-
this::detachEventProvider,
158-
this::emitError,
159-
true);
155+
provider,
156+
this::attachEventProvider,
157+
this::emitReady,
158+
this::detachEventProvider,
159+
this::emitError,
160+
true);
160161
}
161162
}
162163

163164
private void attachEventProvider(FeatureProvider provider) {
164165
if (provider instanceof EventProvider) {
165-
((EventProvider)provider).attach((p, event, details) -> {
166+
((EventProvider) provider).attach((p, event, details) -> {
166167
runHandlersForProvider(p, event, details);
167168
});
168169
}
@@ -174,7 +175,7 @@ private void emitReady(FeatureProvider provider) {
174175

175176
private void detachEventProvider(FeatureProvider provider) {
176177
if (provider instanceof EventProvider) {
177-
((EventProvider)provider).detach();
178+
((EventProvider) provider).detach();
178179
}
179180
}
180181

@@ -229,9 +230,10 @@ public void clearHooks() {
229230

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

303305
void addHandler(String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
304306
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
305-
// if the provider is READY, run immediately
306-
if (ProviderEvent.PROVIDER_READY.equals(event)
307-
&& ProviderState.READY.equals(this.providerRepository.getProvider(clientName).getState())) {
307+
// if the provider is in the state associated with event, run immediately
308+
if (Optional.ofNullable(this.providerRepository.getProvider(clientName).getState())
309+
.orElse(ProviderState.READY).matchesEvent(event)) {
308310
eventSupport.runHandler(handler, EventDetails.builder().clientName(clientName).build());
309311
}
310312
eventSupport.addClientHandler(clientName, event, handler);
@@ -315,30 +317,36 @@ void addHandler(String clientName, ProviderEvent event, Consumer<EventDetails> h
315317
* Runs the handlers associated with a particular provider.
316318
*
317319
* @param provider the provider from where this event originated
318-
* @param event the event type
319-
* @param details the event details
320+
* @param event the event type
321+
* @param details the event details
320322
*/
321323
private void runHandlersForProvider(FeatureProvider provider, ProviderEvent event, ProviderEventDetails details) {
322324
try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
323-
325+
324326
List<String> clientNamesForProvider = providerRepository
325-
.getClientNamesForProvider(provider);
326-
327+
.getClientNamesForProvider(provider);
328+
329+
final String providerName = Optional.ofNullable(provider.getMetadata())
330+
.map(metadata -> metadata.getName())
331+
.orElse(null);
332+
327333
// run the global handlers
328-
eventSupport.runGlobalHandlers(event, EventDetails.fromProviderEventDetails(details));
334+
eventSupport.runGlobalHandlers(event, EventDetails.fromProviderEventDetails(details, providerName));
329335

330336
// run the handlers associated with named clients for this provider
331-
clientNamesForProvider.forEach(name -> {
332-
eventSupport.runClientHandlers(name, event, EventDetails.fromProviderEventDetails(details, name));
337+
clientNamesForProvider.forEach(name -> {
338+
eventSupport.runClientHandlers(name, event,
339+
EventDetails.fromProviderEventDetails(details, providerName, name));
333340
});
334-
341+
335342
if (providerRepository.isDefaultProvider(provider)) {
336343
// run handlers for clients that have no bound providers (since this is the default)
337344
Set<String> allClientNames = eventSupport.getAllClientNames();
338345
Set<String> boundClientNames = providerRepository.getAllBoundClientNames();
339346
allClientNames.removeAll(boundClientNames);
340347
allClientNames.forEach(name -> {
341-
eventSupport.runClientHandlers(name, event, EventDetails.fromProviderEventDetails(details, name));
348+
eventSupport.runClientHandlers(name, event,
349+
EventDetails.fromProviderEventDetails(details, providerName, name));
342350
});
343351
}
344352
}

src/main/java/dev/openfeature/sdk/ProviderState.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,17 @@
44
* Indicates the state of the provider.
55
*/
66
public enum ProviderState {
7-
READY, NOT_READY, ERROR;
7+
READY, NOT_READY, ERROR, STALE;
8+
9+
/**
10+
* Returns true if the passed ProviderEvent maps to this ProviderState.
11+
*
12+
* @param event event to compare
13+
* @return boolean if matches.
14+
*/
15+
boolean matchesEvent(ProviderEvent event) {
16+
return this == READY && event == ProviderEvent.PROVIDER_READY
17+
|| this == STALE && event == ProviderEvent.PROVIDER_STALE
18+
|| this == ERROR && event == ProviderEvent.PROVIDER_ERROR;
19+
}
820
}

src/test/java/dev/openfeature/sdk/DoSomethingProvider.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@ class DoSomethingProvider implements FeatureProvider {
44

55
static final String name = "Something";
66
// Flag evaluation metadata
7-
static final ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
7+
static final ImmutableMetadata DEFAULT_METADATA = ImmutableMetadata.builder().build();
8+
private ImmutableMetadata flagMetadata;
89

910
private EvaluationContext savedContext;
1011

12+
public DoSomethingProvider() {
13+
this.flagMetadata = DEFAULT_METADATA;
14+
}
15+
16+
public DoSomethingProvider(ImmutableMetadata flagMetadata) {
17+
this.flagMetadata = flagMetadata;
18+
}
19+
1120
EvaluationContext getMergedContext() {
1221
return savedContext;
1322
}

src/test/java/dev/openfeature/sdk/EventsTest.java

+40-5
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
class EventsTest {
2626

27-
private static final int TIMEOUT = 200;
27+
private static final int TIMEOUT = 300;
2828
private static final int INIT_DELAY = TIMEOUT / 2;
2929

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

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

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

532+
@Test
533+
@DisplayName("if the provider is ready handlers must run immediately")
534+
@Specification(number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.")
535+
void matchingStaleEventsMustRunImmediately() throws Exception {
536+
final String name = "matchingEventsMustRunImmediately";
537+
final Consumer<EventDetails> handler = mockHandler();
538+
539+
// provider which is already stale
540+
TestEventsProvider provider = new TestEventsProvider(ProviderState.STALE);
541+
OpenFeatureAPI.getInstance().setProvider(name, provider);
542+
543+
// should run even thought handler was added after stale
544+
Client client = OpenFeatureAPI.getInstance().getClient(name);
545+
client.onProviderStale(handler);
546+
verify(handler, timeout(TIMEOUT)).accept(any());
547+
}
548+
549+
@Test
550+
@DisplayName("if the provider is ready handlers must run immediately")
551+
@Specification(number = "5.3.3", text = "Handlers attached after the provider is already in the associated state, MUST run immediately.")
552+
void matchingErrorEventsMustRunImmediately() throws Exception {
553+
final String name = "matchingEventsMustRunImmediately";
554+
final Consumer<EventDetails> handler = mockHandler();
555+
556+
// provider which is already in error
557+
TestEventsProvider provider = new TestEventsProvider(ProviderState.ERROR);
558+
OpenFeatureAPI.getInstance().setProvider(name, provider);
559+
560+
// should run even thought handler was added after error
561+
Client client = OpenFeatureAPI.getInstance().getClient(name);
562+
client.onProviderError(handler);
563+
verify(handler, timeout(TIMEOUT)).accept(any());
564+
}
565+
532566
@Test
533567
@DisplayName("must persist across changes")
534568
@Specification(number = "5.2.6", text = "Event handlers MUST persist across provider changes.")
@@ -560,6 +594,7 @@ void mustPersistAcrossChanges() throws Exception {
560594

561595
@Nested
562596
class HandlerRemoval {
597+
@Specification(number="5.2.7", text="The API and client MUST provide a function allowing the removal of event handlers.")
563598
@Test
564599
@DisplayName("should not run removed events")
565600
void removedEventsShouldNotRun() {

0 commit comments

Comments
 (0)