Skip to content

Commit

Permalink
chore: javadoc and tests for api, context (#942)
Browse files Browse the repository at this point in the history
* chore: javadoc and tests for api, context

Signed-off-by: Todd Baert <todd.baert@dynatrace.com>

* fixup: lint

Signed-off-by: Todd Baert <todd.baert@dynatrace.com>

---------

Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
  • Loading branch information
toddbaert authored Jun 14, 2024
1 parent a0b1d25 commit 4126b51
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 35 deletions.
57 changes: 49 additions & 8 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,62 @@ public static OpenFeatureAPI getInstance() {
return SingletonHolder.INSTANCE;
}

/**
* Get metadata about the default provider.
*
* @return the provider metadata
*/
public Metadata getProviderMetadata() {
return getProvider().getMetadata();
}

/**
* Get metadata about a registered provider using the client name.
* An unbound or empty client name will return metadata from the default provider.
*
* @param domain an identifier which logically binds clients with providers
* @return the provider metadata
*/
public Metadata getProviderMetadata(String domain) {
return getProvider(domain).getMetadata();
}

/**
* {@inheritDoc}
* A factory function for creating new, OpenFeature clients.
* Clients can contain their own state (e.g. logger, hook, context).
* Multiple clients can be used to segment feature flag configuration.
* All un-named or unbound clients use the default provider.
*
* @return a new client instance
*/
public Client getClient() {
return getClient(null, null);
}

/**
* {@inheritDoc}
* A factory function for creating new domainless OpenFeature clients.
* Clients can contain their own state (e.g. logger, hook, context).
* Multiple clients can be used to segment feature flag configuration.
* If there is already a provider bound to this domain, this provider will be used.
* Otherwise, the default provider is used until a provider is assigned to that domain.
*
* @param domain an identifier which logically binds clients with providers
* @return a new client instance
*/
public Client getClient(String domain) {
return getClient(domain, null);
}

/**
* {@inheritDoc}
* A factory function for creating new domainless OpenFeature clients.
* Clients can contain their own state (e.g. logger, hook, context).
* Multiple clients can be used to segment feature flag configuration.
* If there is already a provider bound to this domain, this provider will be used.
* Otherwise, the default provider is used until a provider is assigned to that domain.
*
* @param domain a identifier which logically binds clients with providers
* @param version a version identifier
* @return a new client instance
*/
public Client getClient(String domain, String version) {
return new OpenFeatureClient(this,
Expand All @@ -80,7 +112,10 @@ public Client getClient(String domain, String version) {
}

/**
* {@inheritDoc}
* Sets the global evaluation context, which will be used for all evaluations.
*
* @param evaluationContext the context
* @return api instance
*/
public OpenFeatureAPI setEvaluationContext(EvaluationContext evaluationContext) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
Expand All @@ -90,7 +125,9 @@ public OpenFeatureAPI setEvaluationContext(EvaluationContext evaluationContext)
}

/**
* {@inheritDoc}
* Gets the global evaluation context, which will be used for all evaluations.
*
* @return evaluation context
*/
public EvaluationContext getEvaluationContext() {
try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
Expand Down Expand Up @@ -250,7 +287,10 @@ public FeatureProvider getProvider(String domain) {
}

/**
* {@inheritDoc}
* Adds hooks for globally, used for all evaluations.
* Hooks are run in the order they're added in the before stage. They are run in reverse order for all other stages.
*
* @param hooks The hook to add.
*/
public void addHooks(Hook... hooks) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
Expand All @@ -259,7 +299,8 @@ public void addHooks(Hook... hooks) {
}

/**
* {@inheritDoc}
* Fetch the hooks associated to this client.
* @return A list of {@link Hook}s.
*/
public List<Hook> getHooks() {
try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {
Expand All @@ -268,7 +309,7 @@ public List<Hook> getHooks() {
}

/**
* {@inheritDoc}
* Removes all hooks.
*/
public void clearHooks() {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
Expand Down
10 changes: 0 additions & 10 deletions src/test/java/dev/openfeature/sdk/DoSomethingProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ class DoSomethingProvider implements FeatureProvider {
static final ImmutableMetadata DEFAULT_METADATA = ImmutableMetadata.builder().build();
private ImmutableMetadata flagMetadata;

private EvaluationContext savedContext;

public DoSomethingProvider() {
this.flagMetadata = DEFAULT_METADATA;
}
Expand All @@ -17,18 +15,13 @@ public DoSomethingProvider(ImmutableMetadata flagMetadata) {
this.flagMetadata = flagMetadata;
}

EvaluationContext getMergedContext() {
return savedContext;
}

@Override
public Metadata getMetadata() {
return () -> name;
}

@Override
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
savedContext = ctx;
return ProviderEvaluation.<Boolean>builder()
.value(!defaultValue)
.flagMetadata(flagMetadata)
Expand All @@ -45,7 +38,6 @@ public ProviderEvaluation<String> getStringEvaluation(String key, String default

@Override
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
savedContext = ctx;
return ProviderEvaluation.<Integer>builder()
.value(defaultValue * 100)
.flagMetadata(flagMetadata)
Expand All @@ -54,7 +46,6 @@ public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defa

@Override
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
savedContext = ctx;
return ProviderEvaluation.<Double>builder()
.value(defaultValue * 100)
.flagMetadata(flagMetadata)
Expand All @@ -63,7 +54,6 @@ public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double default

@Override
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext invocationContext) {
savedContext = invocationContext;
return ProviderEvaluation.<Value>builder()
.value(null)
.flagMetadata(flagMetadata)
Expand Down
55 changes: 38 additions & 17 deletions src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

Expand Down Expand Up @@ -307,11 +309,30 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
assertNotNull(result.getFlagMetadata());
}

@Specification(number="3.2.1.1", text="The API, Client and invocation MUST have a method for supplying evaluation context.")
@Specification(number="3.2.2.1", text="The API MUST have a method for setting the global evaluation context.")
@Test void api_context() {
String contextKey = "some-key";
String contextValue = "some-value";
DoSomethingProvider provider = spy( new DoSomethingProvider());
FeatureProviderTestUtils.setFeatureProvider(provider);

Map<String, Value> attributes = new HashMap<>();
attributes.put(contextKey, new Value(contextValue));
EvaluationContext apiCtx = new ImmutableContext(attributes);

// set the global context
api.setEvaluationContext(apiCtx);
Client client = api.getClient();
client.getBooleanValue("any-flag", false);

// assert that the value from the global context was passed to the provider
verify(provider).getBooleanEvaluation(any(), any(), argThat((arg) -> arg.getValue(contextKey).asString().equals(contextValue)));
}

@Specification(number="3.2.1.1", text="The API, Client and invocation MUST have a method for supplying evaluation context.")
@Specification(number="3.2.3", text="Evaluation context MUST be merged in the order: API (global; lowest precedence) -> transaction -> client -> invocation -> before hooks (highest precedence), with duplicate values being overwritten.")
@Test void multi_layer_context_merges_correctly() {
DoSomethingProvider provider = new DoSomethingProvider();
DoSomethingProvider provider = spy(new DoSomethingProvider());
FeatureProviderTestUtils.setFeatureProvider(provider);
TransactionContextPropagator transactionContextPropagator = new ThreadLocalTransactionContextPropagator();
api.setTransactionContextPropagator(transactionContextPropagator);
Expand Down Expand Up @@ -356,21 +377,21 @@ public void initialize(EvaluationContext evaluationContext) throws Exception {
invocationAttributes.put("invocation", new Value("4"));
EvaluationContext invocationCtx = new ImmutableContext(invocationAttributes);

// dosomethingprovider inverts this value.
assertTrue(c.getBooleanValue("key", false, invocationCtx));

EvaluationContext merged = provider.getMergedContext();
assertEquals("1", merged.getValue("api").asString());
assertEquals("2", merged.getValue("transaction").asString());
assertEquals("3", merged.getValue("client").asString());
assertEquals("4", merged.getValue("invocation").asString());
assertEquals("2", merged.getValue("common1").asString(), "transaction merge is incorrect");
assertEquals("3", merged.getValue("common2").asString(), "api client merge is incorrect");
assertEquals("4", merged.getValue("common3").asString(), "invocation merge is incorrect");
assertEquals("3", merged.getValue("common4").asString(), "api client merge is incorrect");
assertEquals("4", merged.getValue("common5").asString(), "invocation merge is incorrect");
assertEquals("4", merged.getValue("common6").asString(), "invocation merge is incorrect");

c.getBooleanValue("key", false, invocationCtx);

// assert the connect overrides
verify(provider).getBooleanEvaluation(any(), any(), argThat((arg) -> {
return arg.getValue("api").asString().equals("1") &&
arg.getValue("transaction").asString().equals("2") &&
arg.getValue("client").asString().equals("3") &&
arg.getValue("invocation").asString().equals("4") &&
arg.getValue("common1").asString().equals("2") &&
arg.getValue("common2").asString().equals("3") &&
arg.getValue("common3").asString().equals("4") &&
arg.getValue("common4").asString().equals("3") &&
arg.getValue("common5").asString().equals("4") &&
arg.getValue("common6").asString().equals("4");
}));
}

@Specification(number="3.3.1.1", text="The API SHOULD have a method for setting a transaction context propagator.")
Expand Down

0 comments on commit 4126b51

Please sign in to comment.