Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support mapping a client to a given provider. #388

Merged
merged 13 commits into from
May 19, 2023
Merged
4 changes: 3 additions & 1 deletion src/main/java/dev/openfeature/sdk/MutableContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.List;
import java.util.Map;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
Expand All @@ -16,6 +17,7 @@
* be modified after instantiation.
*/
@ToString
@EqualsAndHashCode
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
public class MutableContext implements EvaluationContext {

Expand Down Expand Up @@ -88,7 +90,7 @@ public MutableContext add(String key, List<Value> value) {
@Override
public EvaluationContext merge(EvaluationContext overridingContext) {
if (overridingContext == null) {
return new MutableContext(this.asMap());
return new MutableContext(this.targetingKey, this.asMap());
justinabrahms marked this conversation as resolved.
Show resolved Hide resolved
}

Map<String, Value> merged = this.merge(map -> new MutableStructure(map),
Expand Down
52 changes: 38 additions & 14 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -16,11 +15,11 @@
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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for the noop default provider. This ensures null safety where possible.

private final Map<String, FeatureProvider> providers = new ConcurrentHashMap<>();

private OpenFeatureAPI() {
this.apiHooks = new ArrayList<>();
Expand All @@ -39,7 +38,11 @@ public static OpenFeatureAPI getInstance() {
}

public Metadata getProviderMetadata() {
return provider.getMetadata();
return defaultProvider.getMetadata();
}

public Metadata getProviderMetadata(String clientName) {
justinabrahms marked this conversation as resolved.
Show resolved Hide resolved
return getProvider(clientName).getMetadata();
}

public Client getClient() {
Expand Down Expand Up @@ -73,23 +76,44 @@ public EvaluationContext getEvaluationContext() {
}

/**
* {@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.
justinabrahms marked this conversation as resolved.
Show resolved Hide resolved
* @param provider The provider to set.
*/
public FeatureProvider getProvider() {
try (AutoCloseableLock __ = providerLock.readLockAutoCloseable()) {
return this.provider;
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() {
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);
}


justinabrahms marked this conversation as resolved.
Show resolved Hide resolved
/**
* {@inheritDoc}
*/
Expand Down
7 changes: 2 additions & 5 deletions src/main/java/dev/openfeature/sdk/OpenFeatureClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,14 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,6 @@
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());
Expand Down
7 changes: 7 additions & 0 deletions src/test/java/dev/openfeature/sdk/EvalContextTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions src/test/java/dev/openfeature/sdk/ImmutableStructureTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,15 @@ void GettingAMissingValueShouldReturnNull() {
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());
}
}
9 changes: 0 additions & 9 deletions src/test/java/dev/openfeature/sdk/LockingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class LockingTest {
private OpenFeatureClient client;
private AutoCloseableReentrantReadWriteLock apiContextLock;
private AutoCloseableReentrantReadWriteLock apiHooksLock;
private AutoCloseableReentrantReadWriteLock apiProviderLock;
private AutoCloseableReentrantReadWriteLock clientContextLock;
private AutoCloseableReentrantReadWriteLock clientHooksLock;

Expand All @@ -33,10 +32,8 @@ void beforeEach() {
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());
Expand Down Expand Up @@ -91,12 +88,6 @@ void getContextShouldReadLockAndUnlock() {
verify(apiContextLock.readLock()).unlock();
}

@Test
void setProviderShouldWriteLockAndUnlock() {
api.setProvider(new DoSomethingProvider());
verify(apiProviderLock.writeLock()).lock();
verify(apiProviderLock.writeLock()).unlock();
}

@Test
void clearHooksShouldWriteLockAndUnlock() {
Expand Down
26 changes: 26 additions & 0 deletions src/test/java/dev/openfeature/sdk/OpenFeatureAPITest.java
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -57,6 +57,8 @@ void mergeContextTest() {
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);
Expand Down