Skip to content

chore: adds experimental plugin functionality #303

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package com.launchdarkly.sdk.android;

import static org.junit.Assert.assertEquals;

import android.app.Application;

import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;

import com.launchdarkly.sdk.EvaluationDetail;
import com.launchdarkly.sdk.LDContext;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.android.integrations.EnvironmentMetadata;
import com.launchdarkly.sdk.android.integrations.EvaluationSeriesContext;
import com.launchdarkly.sdk.android.integrations.Hook;
import com.launchdarkly.sdk.android.integrations.IdentifySeriesContext;
import com.launchdarkly.sdk.android.integrations.IdentifySeriesResult;
import com.launchdarkly.sdk.android.integrations.Plugin;
import com.launchdarkly.sdk.android.integrations.PluginMetadata;
import com.launchdarkly.sdk.android.integrations.TrackSeriesContext;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public class LDClientPluginsTest {

private static final String mobileKey = "test-mobile-key";
private static final String secondaryMobileKey = "test-secondary-mobile-key";
private static final LDContext ldContext = LDContext.create("userKey");
private Application application;

@Rule
public LogCaptureRule logging = new LogCaptureRule();

@Before
public void setUp() {
application = ApplicationProvider.getApplicationContext();
}

@Test
public void registerIsCalledForPlugins() throws Exception {

MockHook testHook = new MockHook();
MockPlugin testPlugin = new MockPlugin(Collections.singletonList(testHook));

try (LDClient ldClient = LDClient.init(application, makeOfflineConfig(List.of(testPlugin)), ldContext, 1)) {
ldClient.boolVariation("test-flag", false);
assertEquals(1, testPlugin.getHooksCalls.size());
assertEquals(1, testPlugin.registerCalls.size());
assertEquals(1, testHook.beforeEvaluationCalls.size());
assertEquals(1, testHook.afterEvaluationCalls.size());

EnvironmentMetadata environmentMetadata1 = (EnvironmentMetadata) testPlugin.getHooksCalls.get(0).get("environmentMetadata");
assertEquals(mobileKey, environmentMetadata1.getCredential());
assertEquals(environmentMetadata1, testPlugin.getHooksCalls.get(0).get("environmentMetadata"));
assertEquals("AndroidClient", environmentMetadata1.getSdkMetadata().getName());

assertEquals(ldClient, testPlugin.registerCalls.get(0).get("client"));
EnvironmentMetadata environmentMetadata2 = (EnvironmentMetadata) testPlugin.registerCalls.get(0).get("environmentMetadata");
assertEquals(mobileKey, environmentMetadata2.getCredential());
assertEquals("AndroidClient", environmentMetadata2.getSdkMetadata().getName());

logging.assertNoWarningsLogged();
logging.assertNoErrorsLogged();
}
}

@Test
public void pluginRegisterCalledForEachClientEnvironment() throws Exception {
MockHook testHook = new MockHook();
MockPlugin testPlugin = new MockPlugin(Collections.singletonList(testHook));

// create config with multiple mobile keys
LDConfig.Builder builder = new LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Disabled)
.mobileKey(mobileKey)
.secondaryMobileKeys(Map.of(
"secondaryEnvironment", secondaryMobileKey
))
.plugins(Components.plugins().setPlugins(Collections.singletonList(testPlugin)))
.offline(true)
.events(Components.noEvents())
.logAdapter(logging.logAdapter);
LDConfig config = builder.build();

try (LDClient ldClient = LDClient.init(application, config, ldContext, 1)) {
ldClient.boolVariation("test-flag", false);
assertEquals(2, testPlugin.getHooksCalls.size());
assertEquals(2, testPlugin.registerCalls.size());
assertEquals(1, testHook.beforeEvaluationCalls.size());
assertEquals(1, testHook.afterEvaluationCalls.size());

LDClient.getForMobileKey("secondaryEnvironment").boolVariation("test-flag", false);
assertEquals(2, testHook.beforeEvaluationCalls.size());
assertEquals(2, testHook.afterEvaluationCalls.size());

EnvironmentMetadata environmentMetadata1 = (EnvironmentMetadata) testPlugin.getHooksCalls.get(1).get("environmentMetadata");
assertEquals(mobileKey, environmentMetadata1.getCredential());
assertEquals(environmentMetadata1, testPlugin.getHooksCalls.get(1).get("environmentMetadata"));
assertEquals("AndroidClient", environmentMetadata1.getSdkMetadata().getName());

assertEquals(LDClient.getForMobileKey("secondaryEnvironment"), testPlugin.registerCalls.get(0).get("client"));
EnvironmentMetadata environmentMetadata2 = (EnvironmentMetadata) testPlugin.registerCalls.get(0).get("environmentMetadata");
assertEquals(secondaryMobileKey, environmentMetadata2.getCredential());
assertEquals("AndroidClient", environmentMetadata2.getSdkMetadata().getName());

logging.assertNoWarningsLogged();
logging.assertNoErrorsLogged();
}
}

private LDConfig makeOfflineConfig(List<Plugin> plugins) {
LDConfig.Builder builder = new LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Disabled)
.mobileKey(mobileKey)
.offline(true)
.events(Components.noEvents())
.logAdapter(logging.logAdapter);

if (plugins != null) {
builder.plugins(Components.plugins().setPlugins(plugins));
}

return builder.build();
}

private static class MockPlugin extends Plugin {

private final List<Hook> hooks;

public final List<Map<String, Object>> getHooksCalls = new ArrayList<>();
public final List<Map<String, Object>> registerCalls = new ArrayList<>();

public MockPlugin(List<Hook> hooks) {
this.hooks = hooks;
}

@NonNull
@Override
public PluginMetadata getMetadata() {
return new PluginMetadata() {
@NonNull
@Override
public String getName() {
return "mock-plugin";
}
};
}

@Override
public void register(LDClient client, EnvironmentMetadata metadata) {
registerCalls.add(Map.of(
"client", client,
"environmentMetadata", metadata
));
}

@NonNull
@Override
public List<Hook> getHooks(EnvironmentMetadata metadata) {
getHooksCalls.add(Map.of(
"environmentMetadata", metadata
));
return this.hooks;
}
}

private static class MockHook extends Hook {
public final List<Map<String, Object>> beforeEvaluationCalls = new ArrayList<>();
public final List<Map<String, Object>> afterEvaluationCalls = new ArrayList<>();
public final List<Map<String, Object>> beforeIdentifyCalls = new ArrayList<>();
public final List<Map<String, Object>> afterIdentifyCalls = new ArrayList<>();
public final List<Map<String, Object>> afterTrackCalls = new ArrayList<>();

public MockHook() {
super("MockHook");
}

@Override
public Map<String, Object> beforeEvaluation(EvaluationSeriesContext seriesContext, Map<String, Object> seriesData) {
beforeEvaluationCalls.add(Map.of(
"seriesContext", seriesContext,
"seriesData", seriesData
));
return Collections.unmodifiableMap(Collections.emptyMap());
}

@Override
public Map<String, Object> afterEvaluation(EvaluationSeriesContext seriesContext, Map<String, Object> seriesData, EvaluationDetail<LDValue> evaluationDetail) {
afterEvaluationCalls.add(Map.of(
"seriesContext", seriesContext,
"seriesData", seriesData,
"evaluationDetail", evaluationDetail
));
return Collections.unmodifiableMap(Collections.emptyMap());
}

@Override
public Map<String, Object> beforeIdentify(IdentifySeriesContext seriesContext, Map<String, Object> seriesData) {
beforeIdentifyCalls.add(Map.of(
"seriesContext", seriesContext,
"seriesData", seriesData
));
return Collections.unmodifiableMap(Collections.emptyMap());
}

@Override
public Map<String, Object> afterIdentify(IdentifySeriesContext seriesContext, Map<String, Object> seriesData, IdentifySeriesResult result) {
afterIdentifyCalls.add(Map.of(
"seriesContext", seriesContext,
"seriesData", seriesData,
"result", result
));
return Collections.unmodifiableMap(Collections.emptyMap());
}

@Override
public void afterTrack(TrackSeriesContext seriesContext) {
afterTrackCalls.add(Map.of(
"seriesContext", seriesContext
));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder;
import com.launchdarkly.sdk.android.integrations.HooksConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.PluginsConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder;
import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder;
import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder;
Expand Down Expand Up @@ -200,4 +201,24 @@ public static StreamingDataSourceBuilder streamingDataSource() {
public static HooksConfigurationBuilder hooks() {
return new ComponentsImpl.HooksConfigurationBuilderImpl();
}

/**
* Returns a builder for configuring plugins.
* Passing this to {@link LDConfig.Builder#plugins(com.launchdarkly.sdk.android.integrations.PluginsConfigurationBuilder)},
* after setting any desired plugins on the builder, applies this configuration to the SDK.
* <pre><code>
* List plugins = getPluginsFunc();
* LDConfig config = new LDConfig.Builder()
* .plugins(
* Components.plugins()
* .setPlugins(plugins)
* )
* .build();
* </code></pre>
*
* @return a {@link PluginsConfigurationBuilder} for plugins configuration
*/
public static PluginsConfigurationBuilder plugins() {
return new ComponentsImpl.PluginsConfigurationBuilderImpl();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder;
import com.launchdarkly.sdk.android.integrations.HooksConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.PluginsConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder;
import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder;
import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder;
Expand All @@ -18,6 +19,7 @@
import com.launchdarkly.sdk.android.subsystems.EventProcessor;
import com.launchdarkly.sdk.android.subsystems.HookConfiguration;
import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
import com.launchdarkly.sdk.android.subsystems.PluginsConfiguration;
import com.launchdarkly.sdk.internal.events.DefaultEventProcessor;
import com.launchdarkly.sdk.internal.events.DefaultEventSender;
import com.launchdarkly.sdk.internal.events.Event;
Expand Down Expand Up @@ -365,6 +367,14 @@ public HookConfiguration build() {
}
}

static final class PluginsConfigurationBuilderImpl extends PluginsConfigurationBuilder {

@Override
public PluginsConfiguration build() {
return new PluginsConfiguration(plugins);
}
}

// Marker interface for data source implementations that will require a FeatureFetcher
interface DataSourceRequiresFeatureFetcher {}
}
Loading