Skip to content

Commit d4c43d7

Browse files
justinabrahmslopitzbeeme1mr
authored
feat: Support mapping a client to a given provider. (#388)
* Support mapping a client to a given provider. Signed-off-by: Justin Abrahms <[email protected]> * Add a few javadocs. Signed-off-by: Justin Abrahms <[email protected]> * Special case the null client name Signed-off-by: Justin Abrahms <[email protected]> * Add some missing test cases. Signed-off-by: Justin Abrahms <[email protected]> * Moving to an object map unwraps the values. Signed-off-by: Justin Abrahms <[email protected]> * Fix equality test. Signed-off-by: Justin Abrahms <[email protected]> * Carry targeting key when copying over null object. Signed-off-by: Justin Abrahms <[email protected]> * Test provider name, not object equality. Signed-off-by: Justin Abrahms <[email protected]> * Client-based getProvider is now an overload; Use read lock, not write lock. Signed-off-by: Justin Abrahms <[email protected]> * Update src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java Co-authored-by: Lars Opitz <[email protected]> Signed-off-by: Justin Abrahms <[email protected]> * 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 <[email protected]> * Add a few missing tests Signed-off-by: Justin Abrahms <[email protected]> --------- Signed-off-by: Justin Abrahms <[email protected]> Co-authored-by: Lars Opitz <[email protected]> Co-authored-by: Michael Beemer <[email protected]>
1 parent 1af8e96 commit d4c43d7

10 files changed

+113
-39
lines changed

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.util.List;
55
import java.util.Map;
66

7+
import lombok.EqualsAndHashCode;
78
import lombok.Getter;
89
import lombok.Setter;
910
import lombok.ToString;
@@ -16,6 +17,7 @@
1617
* be modified after instantiation.
1718
*/
1819
@ToString
20+
@EqualsAndHashCode
1921
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
2022
public class MutableContext implements EvaluationContext {
2123

@@ -88,7 +90,7 @@ public MutableContext add(String key, List<Value> value) {
8890
@Override
8991
public EvaluationContext merge(EvaluationContext overridingContext) {
9092
if (overridingContext == null) {
91-
return new MutableContext(this.asMap());
93+
return new MutableContext(this.targetingKey, this.asMap());
9294
}
9395

9496
Map<String, Value> merged = this.merge(map -> new MutableStructure(map),

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

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

3-
import java.util.ArrayList;
4-
import java.util.Arrays;
5-
import java.util.List;
3+
import java.util.*;
4+
import java.util.concurrent.ConcurrentHashMap;
65

76
import javax.annotation.Nullable;
87

@@ -16,11 +15,11 @@
1615
public class OpenFeatureAPI {
1716
// package-private multi-read/single-write lock
1817
static AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
19-
static AutoCloseableReentrantReadWriteLock providerLock = new AutoCloseableReentrantReadWriteLock();
2018
static AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();
21-
private FeatureProvider provider;
2219
private EvaluationContext evaluationContext;
23-
private List<Hook> apiHooks;
20+
private final List<Hook> apiHooks;
21+
private FeatureProvider defaultProvider = new NoOpProvider();
22+
private final Map<String, FeatureProvider> providers = new ConcurrentHashMap<>();
2423

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

4140
public Metadata getProviderMetadata() {
42-
return provider.getMetadata();
41+
return defaultProvider.getMetadata();
42+
}
43+
44+
public Metadata getProviderMetadata(String clientName) {
45+
return getProvider(clientName).getMetadata();
4346
}
4447

4548
public Client getClient() {
@@ -73,23 +76,44 @@ public EvaluationContext getEvaluationContext() {
7376
}
7477

7578
/**
76-
* {@inheritDoc}
79+
* Set the default provider.
7780
*/
7881
public void setProvider(FeatureProvider provider) {
79-
try (AutoCloseableLock __ = providerLock.writeLockAutoCloseable()) {
80-
this.provider = provider;
82+
if (provider == null) {
83+
throw new IllegalArgumentException("Provider cannot be null");
8184
}
85+
defaultProvider = provider;
8286
}
8387

8488
/**
85-
* {@inheritDoc}
89+
* Add a provider for a named client.
90+
* @param clientName The name of the client.
91+
* @param provider The provider to set.
8692
*/
87-
public FeatureProvider getProvider() {
88-
try (AutoCloseableLock __ = providerLock.readLockAutoCloseable()) {
89-
return this.provider;
93+
public void setProvider(String clientName, FeatureProvider provider) {
94+
if (provider == null) {
95+
throw new IllegalArgumentException("Provider cannot be null");
9096
}
97+
this.providers.put(clientName, provider);
9198
}
9299

100+
/**
101+
* Return the default provider.
102+
*/
103+
public FeatureProvider getProvider() {
104+
return defaultProvider;
105+
}
106+
107+
/**
108+
* Fetch a provider for a named client. If not found, return the default.
109+
* @param name The client name to look for.
110+
* @return A named {@link FeatureProvider}
111+
*/
112+
public FeatureProvider getProvider(String name) {
113+
return Optional.ofNullable(name).map(this.providers::get).orElse(defaultProvider);
114+
}
115+
116+
93117
/**
94118
* {@inheritDoc}
95119
*/

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

+2-5
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,14 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
9999
FlagEvaluationDetails<T> details = null;
100100
List<Hook> mergedHooks = null;
101101
HookContext<T> hookCtx = null;
102-
FeatureProvider provider = null;
102+
FeatureProvider provider;
103103

104104
try {
105105
final EvaluationContext apiContext;
106106
final EvaluationContext clientContext;
107107

108108
// openfeatureApi.getProvider() must be called once to maintain a consistent reference
109-
provider = ObjectUtils.defaultIfNull(openfeatureApi.getProvider(), () -> {
110-
log.debug("No provider configured, using no-op provider.");
111-
return new NoOpProvider();
112-
});
109+
provider = openfeatureApi.getProvider(this.name);
113110

114111
mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks,
115112
openfeatureApi.getHooks());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package dev.openfeature.sdk;
2+
3+
import io.cucumber.java.eo.Do;
4+
import org.junit.jupiter.api.Test;
5+
6+
import static org.junit.jupiter.api.Assertions.assertFalse;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
public class ClientProviderMappingTest {
10+
@Test
11+
void clientProviderTest() {
12+
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
13+
14+
api.setProvider("client1", new DoSomethingProvider());
15+
api.setProvider("client2", new NoOpProvider());
16+
17+
Client c1 = api.getClient("client1");
18+
Client c2 = api.getClient("client2");
19+
20+
assertTrue(c1.getBooleanValue("test", false));
21+
assertFalse(c2.getBooleanValue("test", false));
22+
}
23+
}

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

-9
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,6 @@
1919
class DeveloperExperienceTest implements HookFixtures {
2020
transient String flagKey = "mykey";
2121

22-
@Test void noProviderSet() {
23-
final String noOp = "no-op";
24-
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
25-
api.setProvider(null);
26-
Client client = api.getClient();
27-
String retval = client.getStringValue(flagKey, noOp);
28-
assertEquals(noOp, retval);
29-
}
30-
3122
@Test void simpleBooleanFlag() {
3223
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
3324
api.setProvider(new NoOpProvider());

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

+7
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,13 @@ public class EvalContextTest {
162162
assertEquals(key1, ctxMerged.getTargetingKey());
163163
}
164164

165+
@Test void merge_null_returns_value() {
166+
MutableContext ctx1 = new MutableContext("key");
167+
ctx1.add("mything", "value");
168+
EvaluationContext result = ctx1.merge(null);
169+
assertEquals(ctx1, result);
170+
}
171+
165172
@Test void merge_targeting_key() {
166173
String key1 = "key1";
167174
MutableContext ctx1 = new MutableContext(key1);

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

+11
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,15 @@ void GettingAMissingValueShouldReturnNull() {
111111
Object value = structure.getValue("missing");
112112
assertNull(value);
113113
}
114+
115+
@Test void objectMapTest() {
116+
Map<String, Value> attrs = new HashMap<>();
117+
attrs.put("test", new Value(45));
118+
ImmutableStructure structure = new ImmutableStructure(attrs);
119+
120+
Map<String, Integer> expected = new HashMap<>();
121+
expected.put("test", 45);
122+
123+
assertEquals(expected, structure.asObjectMap());
124+
}
114125
}

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

-9
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ class LockingTest {
1919
private OpenFeatureClient client;
2020
private AutoCloseableReentrantReadWriteLock apiContextLock;
2121
private AutoCloseableReentrantReadWriteLock apiHooksLock;
22-
private AutoCloseableReentrantReadWriteLock apiProviderLock;
2322
private AutoCloseableReentrantReadWriteLock clientContextLock;
2423
private AutoCloseableReentrantReadWriteLock clientHooksLock;
2524

@@ -33,10 +32,8 @@ void beforeEach() {
3332
client = (OpenFeatureClient) api.getClient();
3433

3534
apiContextLock = setupLock(apiContextLock, mockInnerReadLock(), mockInnerWriteLock());
36-
apiProviderLock = setupLock(apiProviderLock, mockInnerReadLock(), mockInnerWriteLock());
3735
apiHooksLock = setupLock(apiHooksLock, mockInnerReadLock(), mockInnerWriteLock());
3836
OpenFeatureAPI.contextLock = apiContextLock;
39-
OpenFeatureAPI.providerLock = apiProviderLock;
4037
OpenFeatureAPI.hooksLock = apiHooksLock;
4138

4239
clientContextLock = setupLock(clientContextLock, mockInnerReadLock(), mockInnerWriteLock());
@@ -91,12 +88,6 @@ void getContextShouldReadLockAndUnlock() {
9188
verify(apiContextLock.readLock()).unlock();
9289
}
9390

94-
@Test
95-
void setProviderShouldWriteLockAndUnlock() {
96-
api.setProvider(new DoSomethingProvider());
97-
verify(apiProviderLock.writeLock()).lock();
98-
verify(apiProviderLock.writeLock()).unlock();
99-
}
10091

10192
@Test
10293
void clearHooksShouldWriteLockAndUnlock() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dev.openfeature.sdk;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertThrows;
7+
8+
public class OpenFeatureAPITest {
9+
@Test
10+
void namedProviderTest() {
11+
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
12+
FeatureProvider provider = new NoOpProvider();
13+
api.setProvider("namedProviderTest", provider);
14+
assertEquals(provider.getMetadata().getName(), api.getProviderMetadata("namedProviderTest").getName());
15+
}
16+
17+
@Test void settingDefaultProviderToNullErrors() {
18+
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
19+
assertThrows(IllegalArgumentException.class, () -> api.setProvider(null));
20+
}
21+
22+
@Test void settingNamedClientProviderToNullErrors() {
23+
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
24+
assertThrows(IllegalArgumentException.class, () -> api.setProvider("client-name", null));
25+
}
26+
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class OpenFeatureClientTest implements HookFixtures {
2727
@DisplayName("should not throw exception if hook has different type argument than hookContext")
2828
void shouldNotThrowExceptionIfHookHasDifferentTypeArgumentThanHookContext() {
2929
OpenFeatureAPI api = mock(OpenFeatureAPI.class);
30-
when(api.getProvider()).thenReturn(new DoSomethingProvider());
30+
when(api.getProvider(any())).thenReturn(new DoSomethingProvider());
3131
when(api.getHooks()).thenReturn(Arrays.asList(mockBooleanHook(), mockStringHook()));
3232

3333
OpenFeatureClient client = new OpenFeatureClient(api, "name", "version");
@@ -57,6 +57,8 @@ void mergeContextTest() {
5757
context -> context.getTargetingKey().equals(targetingKey)))).thenReturn(ProviderEvaluation.<Boolean>builder()
5858
.value(true).build());
5959
when(api.getProvider()).thenReturn(mockProvider);
60+
when(api.getProvider(any())).thenReturn(mockProvider);
61+
6062

6163
OpenFeatureClient client = new OpenFeatureClient(api, "name", "version");
6264
client.setEvaluationContext(ctx);

0 commit comments

Comments
 (0)