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.Map;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
|
@ -16,6 +17,7 @@ import lombok.experimental.Delegate;
|
|||
* be modified after instantiation.
|
||||
*/
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
|
||||
public class MutableContext implements EvaluationContext {
|
||||
|
||||
|
|
@ -88,7 +90,7 @@ public class MutableContext implements EvaluationContext {
|
|||
@Override
|
||||
public EvaluationContext merge(EvaluationContext overridingContext) {
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
|
@ -16,11 +15,11 @@ import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
|
|||
public class OpenFeatureAPI {
|
||||
// package-private multi-read/single-write lock
|
||||
static AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
|
||||
static AutoCloseableReentrantReadWriteLock providerLock = new AutoCloseableReentrantReadWriteLock();
|
||||
static AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();
|
||||
private FeatureProvider provider;
|
||||
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() {
|
||||
this.apiHooks = new ArrayList<>();
|
||||
|
|
@ -39,7 +38,11 @@ public class OpenFeatureAPI {
|
|||
}
|
||||
|
||||
public Metadata getProviderMetadata() {
|
||||
return provider.getMetadata();
|
||||
return defaultProvider.getMetadata();
|
||||
}
|
||||
|
||||
public Metadata getProviderMetadata(String clientName) {
|
||||
return getProvider(clientName).getMetadata();
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
|
|
@ -73,23 +76,44 @@ public class OpenFeatureAPI {
|
|||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* Set the default provider.
|
||||
*/
|
||||
public void setProvider(FeatureProvider provider) {
|
||||
try (AutoCloseableLock __ = providerLock.writeLockAutoCloseable()) {
|
||||
this.provider = provider;
|
||||
if (provider == null) {
|
||||
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() {
|
||||
try (AutoCloseableLock __ = providerLock.readLockAutoCloseable()) {
|
||||
return this.provider;
|
||||
}
|
||||
return defaultProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -99,17 +99,14 @@ public class OpenFeatureClient implements Client {
|
|||
FlagEvaluationDetails<T> details = null;
|
||||
List<Hook> mergedHooks = null;
|
||||
HookContext<T> hookCtx = null;
|
||||
FeatureProvider provider = null;
|
||||
FeatureProvider provider;
|
||||
|
||||
try {
|
||||
final EvaluationContext apiContext;
|
||||
final EvaluationContext clientContext;
|
||||
|
||||
// openfeatureApi.getProvider() must be called once to maintain a consistent reference
|
||||
provider = ObjectUtils.defaultIfNull(openfeatureApi.getProvider(), () -> {
|
||||
log.debug("No provider configured, using no-op provider.");
|
||||
return new NoOpProvider();
|
||||
});
|
||||
provider = openfeatureApi.getProvider(this.name);
|
||||
|
||||
mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks,
|
||||
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 {
|
||||
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() {
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new NoOpProvider());
|
||||
|
|
|
|||
|
|
@ -162,6 +162,13 @@ public class EvalContextTest {
|
|||
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() {
|
||||
String key1 = "key1";
|
||||
MutableContext ctx1 = new MutableContext(key1);
|
||||
|
|
|
|||
|
|
@ -111,4 +111,15 @@ class ImmutableStructureTest {
|
|||
Object value = structure.getValue("missing");
|
||||
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 AutoCloseableReentrantReadWriteLock apiContextLock;
|
||||
private AutoCloseableReentrantReadWriteLock apiHooksLock;
|
||||
private AutoCloseableReentrantReadWriteLock apiProviderLock;
|
||||
private AutoCloseableReentrantReadWriteLock clientContextLock;
|
||||
private AutoCloseableReentrantReadWriteLock clientHooksLock;
|
||||
|
||||
|
|
@ -33,10 +32,8 @@ class LockingTest {
|
|||
client = (OpenFeatureClient) api.getClient();
|
||||
|
||||
apiContextLock = setupLock(apiContextLock, mockInnerReadLock(), mockInnerWriteLock());
|
||||
apiProviderLock = setupLock(apiProviderLock, mockInnerReadLock(), mockInnerWriteLock());
|
||||
apiHooksLock = setupLock(apiHooksLock, mockInnerReadLock(), mockInnerWriteLock());
|
||||
OpenFeatureAPI.contextLock = apiContextLock;
|
||||
OpenFeatureAPI.providerLock = apiProviderLock;
|
||||
OpenFeatureAPI.hooksLock = apiHooksLock;
|
||||
|
||||
clientContextLock = setupLock(clientContextLock, mockInnerReadLock(), mockInnerWriteLock());
|
||||
|
|
@ -91,12 +88,6 @@ class LockingTest {
|
|||
verify(apiContextLock.readLock()).unlock();
|
||||
}
|
||||
|
||||
@Test
|
||||
void setProviderShouldWriteLockAndUnlock() {
|
||||
api.setProvider(new DoSomethingProvider());
|
||||
verify(apiProviderLock.writeLock()).lock();
|
||||
verify(apiProviderLock.writeLock()).unlock();
|
||||
}
|
||||
|
||||
@Test
|
||||
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")
|
||||
void shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext() {
|
||||
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()));
|
||||
|
||||
OpenFeatureClient client = new OpenFeatureClient(api, "name", "version");
|
||||
|
|
@ -57,6 +57,8 @@ class OpenFeatureClientTest implements HookFixtures {
|
|||
context -> context.getTargetingKey().equals(targetingKey)))).thenReturn(ProviderEvaluation.<Boolean>builder()
|
||||
.value(true).build());
|
||||
when(api.getProvider()).thenReturn(mockProvider);
|
||||
when(api.getProvider(any())).thenReturn(mockProvider);
|
||||
|
||||
|
||||
OpenFeatureClient client = new OpenFeatureClient(api, "name", "version");
|
||||
client.setEvaluationContext(ctx);
|
||||
|
|
|
|||
Loading…
Reference in New Issue