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

chore: javadoc and tests for api, context #942

Merged
merged 2 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
toddbaert marked this conversation as resolved.
Show resolved Hide resolved
* 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.
toddbaert marked this conversation as resolved.
Show resolved Hide resolved
*/
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() {
Copy link
Member Author

Choose a reason for hiding this comment

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

This method is no longer needed, spies work better.

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);
Copy link
Member Author

Choose a reason for hiding this comment

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

instead of the specific "getMergedContext" method, I simply used a spy here.


// 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
Loading