feat: Support mapping a client to a given provider. (#388)
* Support mapping a client to a given provider. Signed-off-by: Justin Abrahms <justin@abrah.ms> * Add a few javadocs. Signed-off-by: Justin Abrahms <justin@abrah.ms> * Special case the null client name Signed-off-by: Justin Abrahms <justin@abrah.ms> * Add some missing test cases. Signed-off-by: Justin Abrahms <justin@abrah.ms> * Moving to an object map unwraps the values. Signed-off-by: Justin Abrahms <justin@abrah.ms> * Fix equality test. Signed-off-by: Justin Abrahms <justin@abrah.ms> * Carry targeting key when copying over null object. Signed-off-by: Justin Abrahms <justin@abrah.ms> * Test provider name, not object equality. Signed-off-by: Justin Abrahms <justin@abrah.ms> * Client-based getProvider is now an overload; Use read lock, not write lock. Signed-off-by: Justin Abrahms <justin@abrah.ms> * Update src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java Co-authored-by: Lars Opitz <lars@lars-opitz.de> Signed-off-by: Justin Abrahms <justin@abrah.ms> * Simplify locking logic around providers. There's no such thing as "API without a provider set" anymore. We now default to NoOpProvider in the API (not client). Signed-off-by: Justin Abrahms <justin@abrah.ms> * Add a few missing tests Signed-off-by: Justin Abrahms <justin@abrah.ms> --------- Signed-off-by: Justin Abrahms <justin@abrah.ms> Co-authored-by: Lars Opitz <lars@lars-opitz.de> Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
This commit is contained in:
parent
1af8e966a4
commit
d4c43d74bc
|
|
@ -4,6 +4,7 @@ import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
@ -16,6 +17,7 @@ import lombok.experimental.Delegate;
|
||||||
* be modified after instantiation.
|
* be modified after instantiation.
|
||||||
*/
|
*/
|
||||||
@ToString
|
@ToString
|
||||||
|
@EqualsAndHashCode
|
||||||
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
|
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
|
||||||
public class MutableContext implements EvaluationContext {
|
public class MutableContext implements EvaluationContext {
|
||||||
|
|
||||||
|
|
@ -88,7 +90,7 @@ public class MutableContext implements EvaluationContext {
|
||||||
@Override
|
@Override
|
||||||
public EvaluationContext merge(EvaluationContext overridingContext) {
|
public EvaluationContext merge(EvaluationContext overridingContext) {
|
||||||
if (overridingContext == null) {
|
if (overridingContext == null) {
|
||||||
return new MutableContext(this.asMap());
|
return new MutableContext(this.targetingKey, this.asMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Value> merged = this.merge(map -> new MutableStructure(map),
|
Map<String, Value> merged = this.merge(map -> new MutableStructure(map),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
package dev.openfeature.sdk;
|
package dev.openfeature.sdk;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Arrays;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|
@ -16,11 +15,11 @@ import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
|
||||||
public class OpenFeatureAPI {
|
public class OpenFeatureAPI {
|
||||||
// package-private multi-read/single-write lock
|
// package-private multi-read/single-write lock
|
||||||
static AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
|
static AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
|
||||||
static AutoCloseableReentrantReadWriteLock providerLock = new AutoCloseableReentrantReadWriteLock();
|
|
||||||
static AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();
|
static AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();
|
||||||
private FeatureProvider provider;
|
|
||||||
private EvaluationContext evaluationContext;
|
private EvaluationContext evaluationContext;
|
||||||
private List<Hook> apiHooks;
|
private final List<Hook> apiHooks;
|
||||||
|
private FeatureProvider defaultProvider = new NoOpProvider();
|
||||||
|
private final Map<String, FeatureProvider> providers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private OpenFeatureAPI() {
|
private OpenFeatureAPI() {
|
||||||
this.apiHooks = new ArrayList<>();
|
this.apiHooks = new ArrayList<>();
|
||||||
|
|
@ -39,7 +38,11 @@ public class OpenFeatureAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Metadata getProviderMetadata() {
|
public Metadata getProviderMetadata() {
|
||||||
return provider.getMetadata();
|
return defaultProvider.getMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Metadata getProviderMetadata(String clientName) {
|
||||||
|
return getProvider(clientName).getMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Client getClient() {
|
public Client getClient() {
|
||||||
|
|
@ -73,23 +76,44 @@ public class OpenFeatureAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* Set the default provider.
|
||||||
*/
|
*/
|
||||||
public void setProvider(FeatureProvider provider) {
|
public void setProvider(FeatureProvider provider) {
|
||||||
try (AutoCloseableLock __ = providerLock.writeLockAutoCloseable()) {
|
if (provider == null) {
|
||||||
this.provider = provider;
|
throw new IllegalArgumentException("Provider cannot be null");
|
||||||
}
|
}
|
||||||
|
defaultProvider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* Add a provider for a named client.
|
||||||
|
* @param clientName The name of the client.
|
||||||
|
* @param provider The provider to set.
|
||||||
|
*/
|
||||||
|
public void setProvider(String clientName, FeatureProvider provider) {
|
||||||
|
if (provider == null) {
|
||||||
|
throw new IllegalArgumentException("Provider cannot be null");
|
||||||
|
}
|
||||||
|
this.providers.put(clientName, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the default provider.
|
||||||
*/
|
*/
|
||||||
public FeatureProvider getProvider() {
|
public FeatureProvider getProvider() {
|
||||||
try (AutoCloseableLock __ = providerLock.readLockAutoCloseable()) {
|
return defaultProvider;
|
||||||
return this.provider;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a provider for a named client. If not found, return the default.
|
||||||
|
* @param name The client name to look for.
|
||||||
|
* @return A named {@link FeatureProvider}
|
||||||
|
*/
|
||||||
|
public FeatureProvider getProvider(String name) {
|
||||||
|
return Optional.ofNullable(name).map(this.providers::get).orElse(defaultProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -99,17 +99,14 @@ public class OpenFeatureClient implements Client {
|
||||||
FlagEvaluationDetails<T> details = null;
|
FlagEvaluationDetails<T> details = null;
|
||||||
List<Hook> mergedHooks = null;
|
List<Hook> mergedHooks = null;
|
||||||
HookContext<T> hookCtx = null;
|
HookContext<T> hookCtx = null;
|
||||||
FeatureProvider provider = null;
|
FeatureProvider provider;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final EvaluationContext apiContext;
|
final EvaluationContext apiContext;
|
||||||
final EvaluationContext clientContext;
|
final EvaluationContext clientContext;
|
||||||
|
|
||||||
// openfeatureApi.getProvider() must be called once to maintain a consistent reference
|
// openfeatureApi.getProvider() must be called once to maintain a consistent reference
|
||||||
provider = ObjectUtils.defaultIfNull(openfeatureApi.getProvider(), () -> {
|
provider = openfeatureApi.getProvider(this.name);
|
||||||
log.debug("No provider configured, using no-op provider.");
|
|
||||||
return new NoOpProvider();
|
|
||||||
});
|
|
||||||
|
|
||||||
mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks,
|
mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks,
|
||||||
openfeatureApi.getHooks());
|
openfeatureApi.getHooks());
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package dev.openfeature.sdk;
|
||||||
|
|
||||||
|
import io.cucumber.java.eo.Do;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public class ClientProviderMappingTest {
|
||||||
|
@Test
|
||||||
|
void clientProviderTest() {
|
||||||
|
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||||
|
|
||||||
|
api.setProvider("client1", new DoSomethingProvider());
|
||||||
|
api.setProvider("client2", new NoOpProvider());
|
||||||
|
|
||||||
|
Client c1 = api.getClient("client1");
|
||||||
|
Client c2 = api.getClient("client2");
|
||||||
|
|
||||||
|
assertTrue(c1.getBooleanValue("test", false));
|
||||||
|
assertFalse(c2.getBooleanValue("test", false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,15 +19,6 @@ import dev.openfeature.sdk.fixtures.HookFixtures;
|
||||||
class DeveloperExperienceTest implements HookFixtures {
|
class DeveloperExperienceTest implements HookFixtures {
|
||||||
transient String flagKey = "mykey";
|
transient String flagKey = "mykey";
|
||||||
|
|
||||||
@Test void noProviderSet() {
|
|
||||||
final String noOp = "no-op";
|
|
||||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
|
||||||
api.setProvider(null);
|
|
||||||
Client client = api.getClient();
|
|
||||||
String retval = client.getStringValue(flagKey, noOp);
|
|
||||||
assertEquals(noOp, retval);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test void simpleBooleanFlag() {
|
@Test void simpleBooleanFlag() {
|
||||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||||
api.setProvider(new NoOpProvider());
|
api.setProvider(new NoOpProvider());
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,13 @@ public class EvalContextTest {
|
||||||
assertEquals(key1, ctxMerged.getTargetingKey());
|
assertEquals(key1, ctxMerged.getTargetingKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test void merge_null_returns_value() {
|
||||||
|
MutableContext ctx1 = new MutableContext("key");
|
||||||
|
ctx1.add("mything", "value");
|
||||||
|
EvaluationContext result = ctx1.merge(null);
|
||||||
|
assertEquals(ctx1, result);
|
||||||
|
}
|
||||||
|
|
||||||
@Test void merge_targeting_key() {
|
@Test void merge_targeting_key() {
|
||||||
String key1 = "key1";
|
String key1 = "key1";
|
||||||
MutableContext ctx1 = new MutableContext(key1);
|
MutableContext ctx1 = new MutableContext(key1);
|
||||||
|
|
|
||||||
|
|
@ -111,4 +111,15 @@ class ImmutableStructureTest {
|
||||||
Object value = structure.getValue("missing");
|
Object value = structure.getValue("missing");
|
||||||
assertNull(value);
|
assertNull(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test void objectMapTest() {
|
||||||
|
Map<String, Value> attrs = new HashMap<>();
|
||||||
|
attrs.put("test", new Value(45));
|
||||||
|
ImmutableStructure structure = new ImmutableStructure(attrs);
|
||||||
|
|
||||||
|
Map<String, Integer> expected = new HashMap<>();
|
||||||
|
expected.put("test", 45);
|
||||||
|
|
||||||
|
assertEquals(expected, structure.asObjectMap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ class LockingTest {
|
||||||
private OpenFeatureClient client;
|
private OpenFeatureClient client;
|
||||||
private AutoCloseableReentrantReadWriteLock apiContextLock;
|
private AutoCloseableReentrantReadWriteLock apiContextLock;
|
||||||
private AutoCloseableReentrantReadWriteLock apiHooksLock;
|
private AutoCloseableReentrantReadWriteLock apiHooksLock;
|
||||||
private AutoCloseableReentrantReadWriteLock apiProviderLock;
|
|
||||||
private AutoCloseableReentrantReadWriteLock clientContextLock;
|
private AutoCloseableReentrantReadWriteLock clientContextLock;
|
||||||
private AutoCloseableReentrantReadWriteLock clientHooksLock;
|
private AutoCloseableReentrantReadWriteLock clientHooksLock;
|
||||||
|
|
||||||
|
|
@ -33,10 +32,8 @@ class LockingTest {
|
||||||
client = (OpenFeatureClient) api.getClient();
|
client = (OpenFeatureClient) api.getClient();
|
||||||
|
|
||||||
apiContextLock = setupLock(apiContextLock, mockInnerReadLock(), mockInnerWriteLock());
|
apiContextLock = setupLock(apiContextLock, mockInnerReadLock(), mockInnerWriteLock());
|
||||||
apiProviderLock = setupLock(apiProviderLock, mockInnerReadLock(), mockInnerWriteLock());
|
|
||||||
apiHooksLock = setupLock(apiHooksLock, mockInnerReadLock(), mockInnerWriteLock());
|
apiHooksLock = setupLock(apiHooksLock, mockInnerReadLock(), mockInnerWriteLock());
|
||||||
OpenFeatureAPI.contextLock = apiContextLock;
|
OpenFeatureAPI.contextLock = apiContextLock;
|
||||||
OpenFeatureAPI.providerLock = apiProviderLock;
|
|
||||||
OpenFeatureAPI.hooksLock = apiHooksLock;
|
OpenFeatureAPI.hooksLock = apiHooksLock;
|
||||||
|
|
||||||
clientContextLock = setupLock(clientContextLock, mockInnerReadLock(), mockInnerWriteLock());
|
clientContextLock = setupLock(clientContextLock, mockInnerReadLock(), mockInnerWriteLock());
|
||||||
|
|
@ -91,12 +88,6 @@ class LockingTest {
|
||||||
verify(apiContextLock.readLock()).unlock();
|
verify(apiContextLock.readLock()).unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void setProviderShouldWriteLockAndUnlock() {
|
|
||||||
api.setProvider(new DoSomethingProvider());
|
|
||||||
verify(apiProviderLock.writeLock()).lock();
|
|
||||||
verify(apiProviderLock.writeLock()).unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void clearHooksShouldWriteLockAndUnlock() {
|
void clearHooksShouldWriteLockAndUnlock() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package dev.openfeature.sdk;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
public class OpenFeatureAPITest {
|
||||||
|
@Test
|
||||||
|
void namedProviderTest() {
|
||||||
|
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||||
|
FeatureProvider provider = new NoOpProvider();
|
||||||
|
api.setProvider("namedProviderTest", provider);
|
||||||
|
assertEquals(provider.getMetadata().getName(), api.getProviderMetadata("namedProviderTest").getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void settingDefaultProviderToNullErrors() {
|
||||||
|
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> api.setProvider(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void settingNamedClientProviderToNullErrors() {
|
||||||
|
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> api.setProvider("client-name", null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ class OpenFeatureClientTest implements HookFixtures {
|
||||||
@DisplayName("should not throw exception if hook has different type argument than hookContext")
|
@DisplayName("should not throw exception if hook has different type argument than hookContext")
|
||||||
void shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext() {
|
void shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext() {
|
||||||
OpenFeatureAPI api = mock(OpenFeatureAPI.class);
|
OpenFeatureAPI api = mock(OpenFeatureAPI.class);
|
||||||
when(api.getProvider()).thenReturn(new DoSomethingProvider());
|
when(api.getProvider(any())).thenReturn(new DoSomethingProvider());
|
||||||
when(api.getHooks()).thenReturn(Arrays.asList(mockBooleanHook(), mockStringHook()));
|
when(api.getHooks()).thenReturn(Arrays.asList(mockBooleanHook(), mockStringHook()));
|
||||||
|
|
||||||
OpenFeatureClient client = new OpenFeatureClient(api, "name", "version");
|
OpenFeatureClient client = new OpenFeatureClient(api, "name", "version");
|
||||||
|
|
@ -57,6 +57,8 @@ class OpenFeatureClientTest implements HookFixtures {
|
||||||
context -> context.getTargetingKey().equals(targetingKey)))).thenReturn(ProviderEvaluation.<Boolean>builder()
|
context -> context.getTargetingKey().equals(targetingKey)))).thenReturn(ProviderEvaluation.<Boolean>builder()
|
||||||
.value(true).build());
|
.value(true).build());
|
||||||
when(api.getProvider()).thenReturn(mockProvider);
|
when(api.getProvider()).thenReturn(mockProvider);
|
||||||
|
when(api.getProvider(any())).thenReturn(mockProvider);
|
||||||
|
|
||||||
|
|
||||||
OpenFeatureClient client = new OpenFeatureClient(api, "name", "version");
|
OpenFeatureClient client = new OpenFeatureClient(api, "name", "version");
|
||||||
client.setEvaluationContext(ctx);
|
client.setEvaluationContext(ctx);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue