feat: implement domain scoping (#934)

* initial changes for implementing domain scoping

Signed-off-by: jarebudev <23311805+jarebudev@users.noreply.github.com>

* completed the rest of the renaming and tidying up

Signed-off-by: jarebudev <23311805+jarebudev@users.noreply.github.com>

* applied code review suggested changes

Signed-off-by: jarebudev <23311805+jarebudev@users.noreply.github.com>

* introdcue a dedicated interface for client metadata

Signed-off-by: Kavindu Dodanduwa <kavindudodanduwa@gmail.com>

* add deprecated default getName to be backward compatible as much as possible

Signed-off-by: Kavindu Dodanduwa <kavindudodanduwa@gmail.com>

---------

Signed-off-by: jarebudev <23311805+jarebudev@users.noreply.github.com>
Signed-off-by: Kavindu Dodanduwa <kavindudodanduwa@gmail.com>
Co-authored-by: Kavindu Dodanduwa <Kavindu-Dodan@users.noreply.github.com>
Co-authored-by: Kavindu Dodanduwa <kavindudodanduwa@gmail.com>
This commit is contained in:
jarebudev 2024-06-11 17:49:46 +01:00 committed by GitHub
parent 4ea74d8a0a
commit 5c0aaaa8bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 199 additions and 188 deletions

View File

@ -126,7 +126,7 @@ See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs.
| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
| ✅ | [Logging](#logging) | Integrate with popular logging packages. |
| ✅ | [Named clients](#named-clients) | Utilize multiple providers in a single application. |
| ✅ | [Domains](#domains) | Logically bind clients with providers. |
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
| ✅ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). |
@ -160,7 +160,7 @@ To register a provider in a non-blocking manner, you can use the `setProvider` m
```
In some situations, it may be beneficial to register multiple providers in the same application.
This is possible using [named clients](#named-clients), which is covered in more details below.
This is possible using [domains](#domains), which is covered in more detail below.
### Targeting
@ -219,27 +219,27 @@ Once you've added a hook as a dependency, it can be registered at the global, cl
The Java SDK uses SLF4J. See the [SLF4J manual](https://slf4j.org/manual.html) for complete documentation.
### Named clients
### Domains
Clients can be given a name.
A name is a logical identifier which can be used to associate clients with a particular provider.
If a name has no associated provider, the global provider is used.
Clients can be assigned to a domain.
A domain is a logical identifier which can be used to associate clients with a particular provider.
If a domain has no associated provider, the global provider is used.
```java
FeatureProvider scopedProvider = new MyProvider();
// registering the default provider
OpenFeatureAPI.getInstance().setProvider(LocalProvider());
// registering a named provider
OpenFeatureAPI.getInstance().setProvider("clientForCache", new CachedProvider());
// registering a provider to a domain
OpenFeatureAPI.getInstance().setProvider("my-domain", new CachedProvider());
// a client backed by default provider
// A client bound to the default provider
Client clientDefault = OpenFeatureAPI.getInstance().getClient();
// a client backed by CachedProvider
Client clientNamed = OpenFeatureAPI.getInstance().getClient("clientForCache");
// A client bound to the CachedProvider provider
Client domainScopedClient = OpenFeatureAPI.getInstance().getClient("my-domain");
```
Named providers can be set in a blocking or non-blocking way.
Providers for domains can be set in a blocking or non-blocking way.
For more details, please refer to the [providers](#providers) section.
### Eventing

View File

@ -6,7 +6,7 @@ import java.util.List;
* Interface used to resolve flags of varying types.
*/
public interface Client extends Features, EventBus<Client> {
Metadata getMetadata();
ClientMetadata getMetadata();
/**
* Return an optional client-level evaluation context.

View File

@ -0,0 +1,14 @@
package dev.openfeature.sdk;
/**
* Metadata specific to an OpenFeature {@code Client}.
*/
public interface ClientMetadata {
String getDomain();
@Deprecated
// this is here for compatibility with getName() exposed from {@link Metadata}
default String getName() {
return getDomain();
}
}

View File

@ -9,7 +9,7 @@ import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder(toBuilder = true)
public class EventDetails extends ProviderEventDetails {
private String clientName;
private String domain;
private String providerName;
static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventDetails, String providerName) {
@ -19,9 +19,9 @@ public class EventDetails extends ProviderEventDetails {
static EventDetails fromProviderEventDetails(
ProviderEventDetails providerEventDetails,
String providerName,
String clientName) {
String domain) {
return builder()
.clientName(clientName)
.domain(domain)
.providerName(providerName)
.flagsChanged(providerEventDetails.getFlagsChanged())
.eventMetadata(providerEventDetails.getEventMetadata())

View File

@ -33,19 +33,19 @@ class EventSupport {
});
/**
* Run all the event handlers associated with this client name.
* If the client name is null, handlers attached to unnamed clients will run.
* Run all the event handlers associated with this domain.
* If the domain is null, handlers attached to unnamed clients will run.
*
* @param clientName the client name to run event handlers for, or null
* @param domain the domain to run event handlers for, or null
* @param event the event type
* @param eventDetails the event details
*/
public void runClientHandlers(String clientName, ProviderEvent event, EventDetails eventDetails) {
clientName = Optional.ofNullable(clientName)
public void runClientHandlers(String domain, ProviderEvent event, EventDetails eventDetails) {
domain = Optional.ofNullable(domain)
.orElse(defaultClientUuid);
// run handlers if they exist
Optional.ofNullable(handlerStores.get(clientName))
Optional.ofNullable(handlerStores.get(domain))
.filter(store -> Optional.of(store).isPresent())
.map(store -> store.handlerMap.get(event))
.ifPresent(handlers -> handlers
@ -66,15 +66,14 @@ class EventSupport {
}
/**
* Add a handler for the specified client name, or all unnamed clients.
* Add a handler for the specified domain, or all unnamed clients.
*
* @param clientName the client name to add handlers for, or else the unnamed
* client
* @param domain the domain to add handlers for, or else unnamed
* @param event the event type
* @param handler the handler function to run
*/
public void addClientHandler(String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
final String name = Optional.ofNullable(clientName)
public void addClientHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
final String name = Optional.ofNullable(domain)
.orElse(defaultClientUuid);
// lazily create and cache a HandlerStore if it doesn't exist
@ -90,15 +89,15 @@ class EventSupport {
/**
* Remove a client event handler for the specified event type.
*
* @param clientName the name of the client handler to remove, or null to remove
* @param domain the domain of the client handler to remove, or null to remove
* from unnamed clients
* @param event the event type
* @param handler the handler ref to be removed
*/
public void removeClientHandler(String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
clientName = Optional.ofNullable(clientName)
public void removeClientHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
domain = Optional.ofNullable(domain)
.orElse(defaultClientUuid);
this.handlerStores.get(clientName).removeHandler(event, handler);
this.handlerStores.get(domain).removeHandler(event, handler);
}
/**
@ -122,11 +121,11 @@ class EventSupport {
}
/**
* Get all client names for which we have event handlers registered.
* Get all domain names for which we have event handlers registered.
*
* @return set of client names
* @return set of domain names
*/
public Set<String> getAllClientNames() {
public Set<String> getAllDomainNames() {
return this.handlerStores.keySet();
}

View File

@ -16,7 +16,7 @@ public class HookContext<T> {
@NonNull FlagValueType type;
@NonNull T defaultValue;
@NonNull EvaluationContext ctx;
Metadata clientMetadata;
ClientMetadata clientMetadata;
Metadata providerMetadata;
/**
@ -30,7 +30,7 @@ public class HookContext<T> {
* @param <T> type that the flag is evaluating against
* @return resulting context for hook
*/
public static <T> HookContext<T> from(String key, FlagValueType type, Metadata clientMetadata,
public static <T> HookContext<T> from(String key, FlagValueType type, ClientMetadata clientMetadata,
Metadata providerMetadata, EvaluationContext ctx, T defaultValue) {
return HookContext.<T>builder()
.flagKey(key)

View File

@ -1,5 +1,10 @@
package dev.openfeature.sdk;
import dev.openfeature.sdk.exceptions.OpenFeatureError;
import dev.openfeature.sdk.internal.AutoCloseableLock;
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -7,11 +12,6 @@ import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import dev.openfeature.sdk.exceptions.OpenFeatureError;
import dev.openfeature.sdk.internal.AutoCloseableLock;
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;
/**
* A global singleton which holds base configuration for the OpenFeature
* library.
@ -52,8 +52,8 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
return getProvider().getMetadata();
}
public Metadata getProviderMetadata(String clientName) {
return getProvider(clientName).getMetadata();
public Metadata getProviderMetadata(String domain) {
return getProvider(domain).getMetadata();
}
/**
@ -66,16 +66,16 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
/**
* {@inheritDoc}
*/
public Client getClient(String name) {
return getClient(name, null);
public Client getClient(String domain) {
return getClient(domain, null);
}
/**
* {@inheritDoc}
*/
public Client getClient(String name, String version) {
public Client getClient(String domain, String version) {
return new OpenFeatureClient(this,
name,
domain,
version);
}
@ -154,14 +154,14 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
}
/**
* Add a provider for a named client.
* Add a provider for a domain.
*
* @param clientName The name of the client.
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
*/
public void setProvider(String clientName, FeatureProvider provider) {
public void setProvider(String domain, FeatureProvider provider) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(clientName,
providerRepository.setProvider(domain,
provider,
this::attachEventProvider,
this::emitReady,
@ -187,14 +187,14 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
}
/**
* Add a provider for a named client and wait for initialization to finish.
* Add a provider for a domain and wait for initialization to finish.
*
* @param clientName The name of the client.
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
*/
public void setProviderAndWait(String clientName, FeatureProvider provider) throws OpenFeatureError {
public void setProviderAndWait(String domain, FeatureProvider provider) throws OpenFeatureError {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(clientName,
providerRepository.setProvider(domain,
provider,
this::attachEventProvider,
this::emitReady,
@ -240,13 +240,13 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
}
/**
* Fetch a provider for a named client. If not found, return the default.
* Fetch a provider for a domain. If not found, return the default.
*
* @param name The client name to look for.
* @param domain The domain to look for.
* @return A named {@link FeatureProvider}
*/
public FeatureProvider getProvider(String name) {
return providerRepository.getProvider(name);
public FeatureProvider getProvider(String domain) {
return providerRepository.getProvider(domain);
}
/**
@ -344,20 +344,20 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
return this;
}
void removeHandler(String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
void removeHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
eventSupport.removeClientHandler(clientName, event, handler);
eventSupport.removeClientHandler(domain, event, handler);
}
}
void addHandler(String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
void addHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
// if the provider is in the state associated with event, run immediately
if (Optional.ofNullable(this.providerRepository.getProvider(clientName).getState())
if (Optional.ofNullable(this.providerRepository.getProvider(domain).getState())
.orElse(ProviderState.READY).matchesEvent(event)) {
eventSupport.runHandler(handler, EventDetails.builder().clientName(clientName).build());
eventSupport.runHandler(handler, EventDetails.builder().domain(domain).build());
}
eventSupport.addClientHandler(clientName, event, handler);
eventSupport.addClientHandler(domain, event, handler);
}
}
@ -371,8 +371,8 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
private void runHandlersForProvider(FeatureProvider provider, ProviderEvent event, ProviderEventDetails details) {
try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
List<String> clientNamesForProvider = providerRepository
.getClientNamesForProvider(provider);
List<String> domainsForProvider = providerRepository
.getDomainsForProvider(provider);
final String providerName = Optional.ofNullable(provider.getMetadata())
.map(metadata -> metadata.getName())
@ -381,20 +381,20 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
// run the global handlers
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, providerName, name));
// run the handlers associated with domains for this provider
domainsForProvider.forEach(domain -> {
eventSupport.runClientHandlers(domain, event,
EventDetails.fromProviderEventDetails(details, providerName, domain));
});
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, providerName, name));
Set<String> allDomainNames = eventSupport.getAllDomainNames();
Set<String> boundDomains = providerRepository.getAllBoundDomains();
allDomainNames.removeAll(boundDomains);
allDomainNames.forEach(domain -> {
eventSupport.runClientHandlers(domain, event,
EventDetails.fromProviderEventDetails(details, providerName, domain));
});
}
}

View File

@ -26,7 +26,7 @@ public class OpenFeatureClient implements Client {
private final OpenFeatureAPI openfeatureApi;
@Getter
private final String name;
private final String domain;
@Getter
private final String version;
private final List<Hook> clientHooks;
@ -39,16 +39,16 @@ public class OpenFeatureClient implements Client {
* Deprecated public constructor. Use OpenFeature.API.getClient() instead.
*
* @param openFeatureAPI Backing global singleton
* @param name Name of the client (used by observability tools).
* @param domain An identifier which logically binds clients with providers (used by observability tools).
* @param version Version of the client (used by observability tools).
* @deprecated Do not use this constructor. It's for internal use only.
* Clients created using it will not run event handlers.
* Use the OpenFeatureAPI's getClient factory method instead.
*/
@Deprecated() // TODO: eventually we will make this non-public. See issue #872
public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String version) {
public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String domain, String version) {
this.openfeatureApi = openFeatureAPI;
this.name = name;
this.domain = domain;
this.version = version;
this.clientHooks = new ArrayList<>();
this.hookSupport = new HookSupport();
@ -110,7 +110,7 @@ public class OpenFeatureClient implements Client {
try {
// openfeatureApi.getProvider() must be called once to maintain a consistent reference
provider = openfeatureApi.getProvider(this.name);
provider = openfeatureApi.getProvider(this.domain);
mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks,
openfeatureApi.getHooks());
@ -359,8 +359,8 @@ public class OpenFeatureClient implements Client {
}
@Override
public Metadata getMetadata() {
return () -> name;
public ClientMetadata getMetadata() {
return () -> domain;
}
/**
@ -400,7 +400,7 @@ public class OpenFeatureClient implements Client {
*/
@Override
public Client on(ProviderEvent event, Consumer<EventDetails> handler) {
OpenFeatureAPI.getInstance().addHandler(name, event, handler);
OpenFeatureAPI.getInstance().addHandler(domain, event, handler);
return this;
}
@ -409,7 +409,7 @@ public class OpenFeatureClient implements Client {
*/
@Override
public Client removeHandler(ProviderEvent event, Consumer<EventDetails> handler) {
OpenFeatureAPI.getInstance().removeHandler(name, event, handler);
OpenFeatureAPI.getInstance().removeHandler(domain, event, handler);
return this;
}
}

View File

@ -36,22 +36,22 @@ class ProviderRepository {
}
/**
* Fetch a provider for a named client. If not found, return the default.
* Fetch a provider for a domain. If not found, return the default.
*
* @param name The client name to look for.
* @param domain The domain to look for.
* @return A named {@link FeatureProvider}
*/
public FeatureProvider getProvider(String name) {
return Optional.ofNullable(name).map(this.providers::get).orElse(this.defaultProvider.get());
public FeatureProvider getProvider(String domain) {
return Optional.ofNullable(domain).map(this.providers::get).orElse(this.defaultProvider.get());
}
public List<String> getClientNamesForProvider(FeatureProvider provider) {
public List<String> getDomainsForProvider(FeatureProvider provider) {
return providers.entrySet().stream()
.filter(entry -> entry.getValue().equals(provider))
.map(entry -> entry.getKey()).collect(Collectors.toList());
}
public Set<String> getAllBoundClientNames() {
public Set<String> getAllBoundDomains() {
return providers.keySet();
}
@ -75,14 +75,14 @@ class ProviderRepository {
}
/**
* Add a provider for a named client.
* Add a provider for a domain.
*
* @param clientName The name of the client.
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
* @param waitForInit When true, wait for initialization to finish, then returns.
* Otherwise, initialization happens in the background.
*/
public void setProvider(String clientName,
public void setProvider(String domain,
FeatureProvider provider,
Consumer<FeatureProvider> afterSet,
Consumer<FeatureProvider> afterInit,
@ -92,13 +92,13 @@ class ProviderRepository {
if (provider == null) {
throw new IllegalArgumentException("Provider cannot be null");
}
if (clientName == null) {
throw new IllegalArgumentException("clientName cannot be null");
if (domain == null) {
throw new IllegalArgumentException("domain cannot be null");
}
prepareAndInitializeProvider(clientName, provider, afterSet, afterInit, afterShutdown, afterError, waitForInit);
prepareAndInitializeProvider(domain, provider, afterSet, afterInit, afterShutdown, afterError, waitForInit);
}
private void prepareAndInitializeProvider(String clientName,
private void prepareAndInitializeProvider(String domain,
FeatureProvider newProvider,
Consumer<FeatureProvider> afterSet,
Consumer<FeatureProvider> afterInit,
@ -112,8 +112,8 @@ class ProviderRepository {
}
// provider is set immediately, on this thread
FeatureProvider oldProvider = clientName != null
? this.providers.put(clientName, newProvider)
FeatureProvider oldProvider = domain != null
? this.providers.put(domain, newProvider)
: this.defaultProvider.getAndSet(newProvider);
if (waitForInit) {

View File

@ -6,11 +6,8 @@ public class AlwaysBrokenProvider implements FeatureProvider {
@Override
public Metadata getMetadata() {
return new Metadata() {
@Override
public String getName() {
throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
}
return () -> {
throw new FlagNotFoundError(TestConstants.BROKEN_MESSAGE);
};
}

View File

@ -77,14 +77,11 @@ class EventProviderTest {
class TestEventProvider extends EventProvider {
private static final String NAME = "TestEventProvider";
@Override
public Metadata getMetadata() {
return new Metadata() {
@Override
public String getName() {
return "TestEventProvider";
}
};
return () -> NAME;
}
@Override

View File

@ -48,7 +48,7 @@ class EventsTest {
@Specification(number = "5.3.1", text = "If the provider's initialize function terminates normally," +
" PROVIDER_READY handlers MUST run.")
void apiInitReady() {
final Consumer<EventDetails> handler = (Consumer<EventDetails>)mockHandler();
final Consumer<EventDetails> handler = mockHandler();
final String name = "apiInitReady";
TestEventsProvider provider = new TestEventsProvider(INIT_DELAY);
@ -105,7 +105,7 @@ class EventsTest {
@Specification(number = "5.2.2", text = "The API MUST provide a function for associating handler functions"
+
" with a particular provider event type.")
void apiShouldSupportAllEventTypes() throws Exception {
void apiShouldSupportAllEventTypes() {
final String name = "apiShouldSupportAllEventTypes";
final Consumer<EventDetails> handler1 = mockHandler();
final Consumer<EventDetails> handler2 = mockHandler();
@ -191,7 +191,7 @@ class EventsTest {
@Test
@DisplayName("should fire initial READY event when provider init succeeds after client retrieved")
@Specification(number = "5.3.1", text = "If the provider's initialize function terminates normally, PROVIDER_READY handlers MUST run.")
void initReadyProviderBefore() throws InterruptedException {
void initReadyProviderBefore() {
final Consumer<EventDetails> handler = mockHandler();
final String name = "initReadyProviderBefore";
@ -201,7 +201,7 @@ class EventsTest {
// set provider after getting a client
OpenFeatureAPI.getInstance().setProvider(name, provider);
verify(handler, timeout(TIMEOUT).atLeastOnce())
.accept(argThat(details -> details.getClientName().equals(name)));
.accept(argThat(details -> details.getDomain().equals(name)));
}
@Test
@ -217,7 +217,7 @@ class EventsTest {
Client client = OpenFeatureAPI.getInstance().getClient(name);
client.onProviderReady(handler);
verify(handler, timeout(TIMEOUT).atLeastOnce())
.accept(argThat(details -> details.getClientName().equals(name)));
.accept(argThat(details -> details.getDomain().equals(name)));
}
@Test
@ -234,7 +234,7 @@ class EventsTest {
// set provider after getting a client
OpenFeatureAPI.getInstance().setProvider(name, provider);
verify(handler, timeout(TIMEOUT)).accept(argThat(details -> {
return name.equals(details.getClientName())
return name.equals(details.getDomain())
&& errMessage.equals(details.getMessage());
}));
}
@ -253,7 +253,7 @@ class EventsTest {
Client client = OpenFeatureAPI.getInstance().getClient(name);
client.onProviderError(handler);
verify(handler, timeout(TIMEOUT)).accept(argThat(details -> {
return name.equals(details.getClientName())
return name.equals(details.getDomain())
&& errMessage.equals(details.getMessage());
}));
}
@ -277,7 +277,7 @@ class EventsTest {
client.onProviderConfigurationChanged(handler);
provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build());
verify(handler, timeout(TIMEOUT)).accept(argThat(details -> details.getClientName().equals(name)));
verify(handler, timeout(TIMEOUT)).accept(argThat(details -> details.getDomain().equals(name)));
}
@Test
@ -295,7 +295,7 @@ class EventsTest {
OpenFeatureAPI.getInstance().setProvider(name, provider);
provider.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, EventDetails.builder().build());
verify(handler, timeout(TIMEOUT)).accept(argThat(details -> details.getClientName().equals(name)));
verify(handler, timeout(TIMEOUT)).accept(argThat(details -> details.getDomain().equals(name)));
}
@Test
@ -306,7 +306,7 @@ class EventsTest {
@Specification(number = "5.2.1", text = "The client MUST provide a function for associating handler functions"
+
" with a particular provider event type.")
void shouldSupportAllEventTypes() throws Exception {
void shouldSupportAllEventTypes() {
final String name = "shouldSupportAllEventTypes";
final Consumer<EventDetails> handler1 = mockHandler();
final Consumer<EventDetails> handler2 = mockHandler();
@ -325,7 +325,7 @@ class EventsTest {
Arrays.asList(ProviderEvent.values()).stream().forEach(eventType -> {
provider.mockEvent(eventType, ProviderEventDetails.builder().build());
});
ArgumentMatcher<EventDetails> nameMatches = (EventDetails details) -> details.getClientName()
ArgumentMatcher<EventDetails> nameMatches = (EventDetails details) -> details.getDomain()
.equals(name);
verify(handler1, timeout(TIMEOUT).atLeastOnce()).accept(argThat(nameMatches));
verify(handler2, timeout(TIMEOUT).atLeastOnce()).accept(argThat(nameMatches));
@ -337,7 +337,7 @@ class EventsTest {
@Test
@DisplayName("shutdown provider should not run handlers")
void shouldNotRunHandlers() throws Exception {
void shouldNotRunHandlers() {
final Consumer<EventDetails> handler1 = mockHandler();
final Consumer<EventDetails> handler2 = mockHandler();
final String name = "shouldNotRunHandlers";
@ -369,7 +369,7 @@ class EventsTest {
@DisplayName("other client handlers should not run")
@Specification(number = "5.1.3", text = "When a provider signals the occurrence of a particular event, " +
"event handlers on clients which are not associated with that provider MUST NOT run.")
void otherClientHandlersShouldNotRun() throws Exception {
void otherClientHandlersShouldNotRun() {
final String name1 = "otherClientHandlersShouldNotRun1";
final String name2 = "otherClientHandlersShouldNotRun2";
final Consumer<EventDetails> handlerToRun = mockHandler();
@ -396,7 +396,7 @@ class EventsTest {
@DisplayName("bound named client handlers should not run with default")
@Specification(number = "5.1.3", text = "When a provider signals the occurrence of a particular event, " +
"event handlers on clients which are not associated with that provider MUST NOT run.")
void boundShouldNotRunWithDefault() throws Exception {
void boundShouldNotRunWithDefault() {
final String name = "boundShouldNotRunWithDefault";
final Consumer<EventDetails> handlerNotToRun = mockHandler();
@ -422,7 +422,7 @@ class EventsTest {
@DisplayName("unbound named client handlers should run with default")
@Specification(number = "5.1.3", text = "When a provider signals the occurrence of a particular event, " +
"event handlers on clients which are not associated with that provider MUST NOT run.")
void unboundShouldRunWithDefault() throws Exception {
void unboundShouldRunWithDefault() {
final String name = "unboundShouldRunWithDefault";
final Consumer<EventDetails> handlerToRun = mockHandler();
@ -445,7 +445,7 @@ class EventsTest {
@Test
@DisplayName("subsequent handlers run if earlier throws")
@Specification(number = "5.2.5", text = "If a handler function terminates abnormally, other handler functions MUST run.")
void handlersRunIfOneThrows() throws Exception {
void handlersRunIfOneThrows() {
final String name = "handlersRunIfOneThrows";
final Consumer<EventDetails> errorHandler = mockHandler();
doThrow(new NullPointerException()).when(errorHandler).accept(any());
@ -471,7 +471,7 @@ class EventsTest {
@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 `provider name` associated with the event.")
void shouldHaveAllProperties() throws Exception {
void shouldHaveAllProperties() {
final Consumer<EventDetails> handler1 = mockHandler();
final Consumer<EventDetails> handler2 = mockHandler();
final String name = "shouldHaveAllProperties";
@ -508,14 +508,14 @@ class EventsTest {
return metadata.equals(eventDetails.getEventMetadata())
&& flagsChanged.equals(eventDetails.getFlagsChanged())
&& message.equals(eventDetails.getMessage())
&& name.equals(eventDetails.getClientName());
&& name.equals(eventDetails.getDomain());
}));
}
@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 matchingReadyEventsMustRunImmediately() throws Exception {
void matchingReadyEventsMustRunImmediately() {
final String name = "matchingEventsMustRunImmediately";
final Consumer<EventDetails> handler = mockHandler();
@ -532,7 +532,7 @@ class EventsTest {
@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 {
void matchingStaleEventsMustRunImmediately() {
final String name = "matchingEventsMustRunImmediately";
final Consumer<EventDetails> handler = mockHandler();
@ -549,7 +549,7 @@ class EventsTest {
@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 {
void matchingErrorEventsMustRunImmediately() {
final String name = "matchingEventsMustRunImmediately";
final Consumer<EventDetails> handler = mockHandler();
@ -566,7 +566,7 @@ class EventsTest {
@Test
@DisplayName("must persist across changes")
@Specification(number = "5.2.6", text = "Event handlers MUST persist across provider changes.")
void mustPersistAcrossChanges() throws Exception {
void mustPersistAcrossChanges() {
final String name = "mustPersistAcrossChanges";
final Consumer<EventDetails> handler = mockHandler();
@ -578,7 +578,7 @@ class EventsTest {
client.onProviderConfigurationChanged(handler);
provider1.mockEvent(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, ProviderEventDetails.builder().build());
ArgumentMatcher<EventDetails> nameMatches = (EventDetails details) -> details.getClientName().equals(name);
ArgumentMatcher<EventDetails> nameMatches = (EventDetails details) -> details.getDomain().equals(name);
verify(handler, timeout(TIMEOUT).times(1)).accept(argThat(nameMatches));

View File

@ -135,10 +135,13 @@ class FlagEvaluationSpecTest implements HookFixtures {
assertEquals(h2, api.getHooks().get(1));
}
@Specification(number="1.1.6", text="The API MUST provide a function for creating a client which accepts the following options: - name (optional): A logical string identifier for the client.")
@Test void namedClient() {
assertThatCode(() -> api.getClient("Sir Calls-a-lot")).doesNotThrowAnyException();
// TODO: Doesn't say that you can *get* the client name.. which seems useful?
@Specification(number="1.1.6", text="The API MUST provide a function for creating a client which accepts the following options: - domain (optional): A logical string identifier for binding clients to provider.")
@Test void domainName() {
assertNull(api.getClient().getMetadata().getDomain());
String domain = "Sir Calls-a-lot";
Client clientForDomain = api.getClient(domain);
assertEquals(domain, clientForDomain.getMetadata().getDomain());
}
@Specification(number="1.2.1", text="The client MUST provide a method to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")
@ -274,14 +277,18 @@ class FlagEvaluationSpecTest implements HookFixtures {
ArgumentMatchers.isA(FlagNotFoundError.class));
}
@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.")
@Specification(number="1.2.2", text="The client interface MUST define a metadata member or accessor, containing an immutable domain field or accessor of type string, which corresponds to the domain value supplied during client creation. In previous drafts, this property was called name. For backwards compatibility, implementations should consider name an alias to domain.")
@Test void clientMetadata() {
Client c = _client();
assertNull(c.getMetadata().getName());
assertNull(c.getMetadata().getDomain());
String domainName = "test domain";
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
Client c2 = api.getClient("test");
assertEquals("test", c2.getMetadata().getName());
Client c2 = api.getClient(domainName);
assertEquals(domainName, c2.getMetadata().getName());
assertEquals(domainName, c2.getMetadata().getDomain());
}
@Specification(number="1.4.9", text="In cases of abnormal execution (network failure, unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.")

View File

@ -9,17 +9,18 @@ class HookContextTest {
@Specification(number="4.2.2.2", text="Condition: The client metadata field in the hook context MUST be immutable.")
@Specification(number="4.2.2.3", text="Condition: The provider metadata field in the hook context MUST be immutable.")
@Test void metadata_field_is_type_metadata() {
ClientMetadata clientMetadata = mock(ClientMetadata.class);
Metadata meta = mock(Metadata.class);
HookContext<Object> hc = HookContext.from(
"key",
FlagValueType.BOOLEAN,
meta,
clientMetadata,
meta,
new ImmutableContext(),
false
);
assertTrue(Metadata.class.isAssignableFrom(hc.getClientMetadata().getClass()));
assertTrue(ClientMetadata.class.isAssignableFrom(hc.getClientMetadata().getClass()));
assertTrue(Metadata.class.isAssignableFrom(hc.getProviderMetadata().getClass()));
}

View File

@ -19,7 +19,6 @@ import org.junit.jupiter.params.provider.EnumSource;
import dev.openfeature.sdk.fixtures.HookFixtures;
class HookSupportTest implements HookFixtures {
@Test
@DisplayName("should merge EvaluationContexts on before hooks correctly")
void shouldMergeEvaluationContextsOnBeforeHooksCorrectly() {

View File

@ -8,6 +8,8 @@ import static org.mockito.Mockito.*;
class InitializeBehaviorSpecTest {
private static final String DOMAIN_NAME = "mydomain";
@BeforeEach
void setupTest() {
OpenFeatureAPI.getInstance().setProvider(new NoOpProvider());
@ -60,7 +62,7 @@ class InitializeBehaviorSpecTest {
FeatureProvider featureProvider = mock(FeatureProvider.class);
doReturn(ProviderState.NOT_READY).when(featureProvider).getState();
OpenFeatureAPI.getInstance().setProvider("clientName", featureProvider);
OpenFeatureAPI.getInstance().setProvider(DOMAIN_NAME, featureProvider);
verify(featureProvider, timeout(1000)).initialize(any());
}
@ -76,7 +78,7 @@ class InitializeBehaviorSpecTest {
doReturn(ProviderState.NOT_READY).when(featureProvider).getState();
doThrow(TestException.class).when(featureProvider).initialize(any());
assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider("clientName", featureProvider))
assertThatCode(() -> OpenFeatureAPI.getInstance().setProvider(DOMAIN_NAME, featureProvider))
.doesNotThrowAnyException();
verify(featureProvider, timeout(1000)).initialize(any());

View File

@ -15,7 +15,7 @@ import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
class OpenFeatureAPITest {
private static final String CLIENT_NAME = "client name";
private static final String DOMAIN_NAME = "my domain";
private OpenFeatureAPI api;
@ -33,16 +33,16 @@ class OpenFeatureAPITest {
.isEqualTo(api.getProviderMetadata("namedProviderTest").getName());
}
@Specification(number="1.1.3", text="The API MUST provide a function to bind a given provider to one or more client names. If the client-name already has a bound provider, it is overwritten with the new mapping.")
@Specification(number="1.1.3", text="The API MUST provide a function to bind a given provider to one or more clients using a domain. If the domain already has a bound provider, it is overwritten with the new mapping.")
@Test
void namedProviderOverwrittenTest() {
String name = "namedProviderOverwrittenTest";
String domain = "namedProviderOverwrittenTest";
FeatureProvider provider1 = new NoOpProvider();
FeatureProvider provider2 = new DoSomethingProvider();
FeatureProviderTestUtils.setFeatureProvider(name, provider1);
FeatureProviderTestUtils.setFeatureProvider(name, provider2);
FeatureProviderTestUtils.setFeatureProvider(domain, provider1);
FeatureProviderTestUtils.setFeatureProvider(domain, provider2);
assertThat(OpenFeatureAPI.getInstance().getProvider(name).getMetadata().getName())
assertThat(OpenFeatureAPI.getInstance().getProvider(domain).getMetadata().getName())
.isEqualTo(DoSomethingProvider.name);
}
@ -71,8 +71,8 @@ class OpenFeatureAPITest {
}
@Test
void settingNamedClientProviderToNullErrors() {
assertThatCode(() -> api.setProvider(CLIENT_NAME, null)).isInstanceOf(IllegalArgumentException.class);
void settingDomainProviderToNullErrors() {
assertThatCode(() -> api.setProvider(DOMAIN_NAME, null)).isInstanceOf(IllegalArgumentException.class);
}
@Test

View File

@ -33,8 +33,8 @@ import dev.openfeature.sdk.testutils.exception.TestException;
class ProviderRepositoryTest {
private static final String CLIENT_NAME = "client name";
private static final String ANOTHER_CLIENT_NAME = "another client name";
private static final String DOMAIN_NAME = "domain name";
private static final String ANOTHER_DOMAIN_NAME = "another domain name";
private static final int TIMEOUT = 5000;
private final ExecutorService executorService = Executors.newCachedThreadPool();
@ -101,13 +101,13 @@ class ProviderRepositoryTest {
@Test
@DisplayName("should reject null as named provider")
void shouldRejectNullAsNamedProvider() {
assertThatCode(() -> providerRepository.setProvider(CLIENT_NAME, null, mockAfterSet(), mockAfterInit(),
assertThatCode(() -> providerRepository.setProvider(DOMAIN_NAME, null, mockAfterSet(), mockAfterInit(),
mockAfterShutdown(), mockAfterError(), false))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
@DisplayName("should reject null as client name")
@DisplayName("should reject null as domain name")
void shouldRejectNullAsDefaultProvider() {
NoOpProvider provider = new NoOpProvider();
assertThatCode(() -> providerRepository.setProvider(null, provider, mockAfterSet(), mockAfterInit(),
@ -116,8 +116,8 @@ class ProviderRepositoryTest {
}
@Test
@DisplayName("should immediately return when calling the named client provider mutator")
void shouldImmediatelyReturnWhenCallingTheNamedClientProviderMutator() throws Exception {
@DisplayName("should immediately return when calling the domain provider mutator")
void shouldImmediatelyReturnWhenCallingTheDomainProviderMutator() throws Exception {
FeatureProvider featureProvider = createMockedProvider();
doDelayResponse(Duration.ofSeconds(10)).when(featureProvider).initialize(any());
@ -126,7 +126,7 @@ class ProviderRepositoryTest {
.pollDelay(Duration.ofMillis(1))
.atMost(Duration.ofSeconds(1))
.until(() -> {
providerRepository.setProvider("named client", featureProvider, mockAfterSet(),
providerRepository.setProvider("a domain", featureProvider, mockAfterSet(),
mockAfterInit(), mockAfterShutdown(), mockAfterError(), false);
verify(featureProvider, timeout(TIMEOUT)).initialize(any());
return true;
@ -137,7 +137,7 @@ class ProviderRepositoryTest {
@DisplayName("should avoid additional initialization call if provider has been initialized already")
void shouldAvoidAdditionalInitializationCallIfProviderHasBeenInitializedAlready() throws Exception {
FeatureProvider provider = createMockedReadyProvider();
setFeatureProvider(CLIENT_NAME, provider);
setFeatureProvider(DOMAIN_NAME, provider);
verify(provider, never()).initialize(any());
}
@ -176,7 +176,7 @@ class ProviderRepositoryTest {
FeatureProvider oldProvider = createMockedProvider();
FeatureProvider newProvider = createMockedProvider();
setFeatureProvider(oldProvider);
setFeatureProvider(CLIENT_NAME, oldProvider);
setFeatureProvider(DOMAIN_NAME, oldProvider);
setFeatureProvider(newProvider);
@ -194,7 +194,7 @@ class ProviderRepositoryTest {
doDelayResponse(Duration.ofSeconds(10)).when(newProvider).initialize(any());
Future<?> providerMutation = executorService
.submit(() -> providerRepository.setProvider(CLIENT_NAME, newProvider, mockAfterSet(),
.submit(() -> providerRepository.setProvider(DOMAIN_NAME, newProvider, mockAfterSet(),
mockAfterInit(), mockAfterShutdown(), mockAfterError(), false));
await()
@ -209,11 +209,11 @@ class ProviderRepositoryTest {
void shouldNotCallShutdownIfReplacedProviderIsBoundToMultipleNames() throws InterruptedException {
FeatureProvider oldProvider = createMockedProvider();
FeatureProvider newProvider = createMockedProvider();
setFeatureProvider(CLIENT_NAME, oldProvider);
setFeatureProvider(DOMAIN_NAME, oldProvider);
setFeatureProvider(ANOTHER_CLIENT_NAME, oldProvider);
setFeatureProvider(ANOTHER_DOMAIN_NAME, oldProvider);
setFeatureProvider(CLIENT_NAME, newProvider);
setFeatureProvider(DOMAIN_NAME, newProvider);
verify(oldProvider, never()).shutdown();
}
@ -224,9 +224,9 @@ class ProviderRepositoryTest {
FeatureProvider oldProvider = createMockedProvider();
FeatureProvider newProvider = createMockedProvider();
setFeatureProvider(oldProvider);
setFeatureProvider(CLIENT_NAME, oldProvider);
setFeatureProvider(DOMAIN_NAME, oldProvider);
setFeatureProvider(CLIENT_NAME, newProvider);
setFeatureProvider(DOMAIN_NAME, newProvider);
verify(oldProvider, never()).shutdown();
}
@ -293,8 +293,8 @@ class ProviderRepositoryTest {
FeatureProvider featureProvider2 = createMockedProvider();
setFeatureProvider(featureProvider1);
setFeatureProvider(CLIENT_NAME, featureProvider1);
setFeatureProvider(ANOTHER_CLIENT_NAME, featureProvider2);
setFeatureProvider(DOMAIN_NAME, featureProvider1);
setFeatureProvider(ANOTHER_DOMAIN_NAME, featureProvider2);
providerRepository.shutdown();
verify(featureProvider1, timeout(TIMEOUT)).shutdown();

View File

@ -15,6 +15,8 @@ import static org.mockito.Mockito.*;
class ShutdownBehaviorSpecTest {
private String DOMAIN = "myDomain";
@BeforeEach
void resetFeatureProvider() {
setFeatureProvider(new NoOpProvider());
@ -59,11 +61,10 @@ class ShutdownBehaviorSpecTest {
@Test
@DisplayName("must invoke shutdown method on previously registered provider once it should not be used for flag evaluation anymore")
void mustInvokeShutdownMethodOnPreviouslyRegisteredProviderOnceItShouldNotBeUsedForFlagEvaluationAnymore() {
String clientName = "clientName";
FeatureProvider featureProvider = ProviderFixture.createMockedProvider();
setFeatureProvider(clientName, featureProvider);
setFeatureProvider(clientName, new NoOpProvider());
setFeatureProvider(DOMAIN, featureProvider);
setFeatureProvider(DOMAIN, new NoOpProvider());
verify(featureProvider, timeout(1000)).shutdown();
}
@ -75,12 +76,11 @@ class ShutdownBehaviorSpecTest {
@Test
@DisplayName("should catch exception thrown by the named client provider on shutdown")
void shouldCatchExceptionThrownByTheNamedClientProviderOnShutdown() {
String clientName = "clientName";
FeatureProvider featureProvider = ProviderFixture.createMockedProvider();
doThrow(TestException.class).when(featureProvider).shutdown();
setFeatureProvider(clientName, featureProvider);
setFeatureProvider(clientName, new NoOpProvider());
setFeatureProvider(DOMAIN, featureProvider);
setFeatureProvider(DOMAIN, new NoOpProvider());
verify(featureProvider, timeout(1000)).shutdown();
}
@ -96,7 +96,7 @@ class ShutdownBehaviorSpecTest {
FeatureProvider defaultProvider = ProviderFixture.createMockedProvider();
FeatureProvider namedProvider = ProviderFixture.createMockedProvider();
setFeatureProvider(defaultProvider);
setFeatureProvider("clientName", namedProvider);
setFeatureProvider(DOMAIN, namedProvider);
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
synchronized (OpenFeatureAPI.class) {

View File

@ -24,8 +24,8 @@ public class FeatureProviderTestUtils {
.until(() -> extractor.apply(OpenFeatureAPI.getInstance()) == provider);
}
public static void setFeatureProvider(String namedProvider, FeatureProvider provider) {
OpenFeatureAPI.getInstance().setProvider(namedProvider, provider);
waitForProviderInitializationComplete(api -> api.getProvider(namedProvider), provider);
public static void setFeatureProvider(String domain, FeatureProvider provider) {
OpenFeatureAPI.getInstance().setProvider(domain, provider);
waitForProviderInitializationComplete(api -> api.getProvider(domain), provider);
}
}

View File

@ -18,12 +18,7 @@ public class TestEventsProvider extends EventProvider {
private boolean shutDown = false;
private int initTimeoutMs = 0;
private String name = "test";
private Metadata metadata = new Metadata() {
@Override
public String getName() {
return name;
}
};
private Metadata metadata = () -> name;
@Override
public ProviderState getState() {