Skip to content

Commit f5c7239

Browse files
authored
chore: adds experimental plugin functionality (#303)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Related issues** SDK-1328 **Describe the solution you've provided** Adds plugins component, configuration builders, associated tests to implement plugins specification.
1 parent e5c6daf commit f5c7239

File tree

12 files changed

+574
-3
lines changed

12 files changed

+574
-3
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package com.launchdarkly.sdk.android;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import android.app.Application;
6+
7+
import androidx.annotation.NonNull;
8+
import androidx.test.core.app.ApplicationProvider;
9+
10+
import com.launchdarkly.sdk.EvaluationDetail;
11+
import com.launchdarkly.sdk.LDContext;
12+
import com.launchdarkly.sdk.LDValue;
13+
import com.launchdarkly.sdk.android.integrations.EnvironmentMetadata;
14+
import com.launchdarkly.sdk.android.integrations.EvaluationSeriesContext;
15+
import com.launchdarkly.sdk.android.integrations.Hook;
16+
import com.launchdarkly.sdk.android.integrations.IdentifySeriesContext;
17+
import com.launchdarkly.sdk.android.integrations.IdentifySeriesResult;
18+
import com.launchdarkly.sdk.android.integrations.Plugin;
19+
import com.launchdarkly.sdk.android.integrations.PluginMetadata;
20+
import com.launchdarkly.sdk.android.integrations.TrackSeriesContext;
21+
22+
import org.junit.Before;
23+
import org.junit.Rule;
24+
import org.junit.Test;
25+
26+
import java.util.ArrayList;
27+
import java.util.Collections;
28+
import java.util.List;
29+
import java.util.Map;
30+
31+
public class LDClientPluginsTest {
32+
33+
private static final String mobileKey = "test-mobile-key";
34+
private static final String secondaryMobileKey = "test-secondary-mobile-key";
35+
private static final LDContext ldContext = LDContext.create("userKey");
36+
private Application application;
37+
38+
@Rule
39+
public LogCaptureRule logging = new LogCaptureRule();
40+
41+
@Before
42+
public void setUp() {
43+
application = ApplicationProvider.getApplicationContext();
44+
}
45+
46+
@Test
47+
public void registerIsCalledForPlugins() throws Exception {
48+
49+
MockHook testHook = new MockHook();
50+
MockPlugin testPlugin = new MockPlugin(Collections.singletonList(testHook));
51+
52+
try (LDClient ldClient = LDClient.init(application, makeOfflineConfig(List.of(testPlugin)), ldContext, 1)) {
53+
ldClient.boolVariation("test-flag", false);
54+
assertEquals(1, testPlugin.getHooksCalls.size());
55+
assertEquals(1, testPlugin.registerCalls.size());
56+
assertEquals(1, testHook.beforeEvaluationCalls.size());
57+
assertEquals(1, testHook.afterEvaluationCalls.size());
58+
59+
EnvironmentMetadata environmentMetadata1 = (EnvironmentMetadata) testPlugin.getHooksCalls.get(0).get("environmentMetadata");
60+
assertEquals(mobileKey, environmentMetadata1.getCredential());
61+
assertEquals(environmentMetadata1, testPlugin.getHooksCalls.get(0).get("environmentMetadata"));
62+
assertEquals("AndroidClient", environmentMetadata1.getSdkMetadata().getName());
63+
64+
assertEquals(ldClient, testPlugin.registerCalls.get(0).get("client"));
65+
EnvironmentMetadata environmentMetadata2 = (EnvironmentMetadata) testPlugin.registerCalls.get(0).get("environmentMetadata");
66+
assertEquals(mobileKey, environmentMetadata2.getCredential());
67+
assertEquals("AndroidClient", environmentMetadata2.getSdkMetadata().getName());
68+
69+
logging.assertNoWarningsLogged();
70+
logging.assertNoErrorsLogged();
71+
}
72+
}
73+
74+
@Test
75+
public void pluginRegisterCalledForEachClientEnvironment() throws Exception {
76+
MockHook testHook = new MockHook();
77+
MockPlugin testPlugin = new MockPlugin(Collections.singletonList(testHook));
78+
79+
// create config with multiple mobile keys
80+
LDConfig.Builder builder = new LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Disabled)
81+
.mobileKey(mobileKey)
82+
.secondaryMobileKeys(Map.of(
83+
"secondaryEnvironment", secondaryMobileKey
84+
))
85+
.plugins(Components.plugins().setPlugins(Collections.singletonList(testPlugin)))
86+
.offline(true)
87+
.events(Components.noEvents())
88+
.logAdapter(logging.logAdapter);
89+
LDConfig config = builder.build();
90+
91+
try (LDClient ldClient = LDClient.init(application, config, ldContext, 1)) {
92+
ldClient.boolVariation("test-flag", false);
93+
assertEquals(2, testPlugin.getHooksCalls.size());
94+
assertEquals(2, testPlugin.registerCalls.size());
95+
assertEquals(1, testHook.beforeEvaluationCalls.size());
96+
assertEquals(1, testHook.afterEvaluationCalls.size());
97+
98+
LDClient.getForMobileKey("secondaryEnvironment").boolVariation("test-flag", false);
99+
assertEquals(2, testHook.beforeEvaluationCalls.size());
100+
assertEquals(2, testHook.afterEvaluationCalls.size());
101+
102+
EnvironmentMetadata environmentMetadata1 = (EnvironmentMetadata) testPlugin.getHooksCalls.get(1).get("environmentMetadata");
103+
assertEquals(mobileKey, environmentMetadata1.getCredential());
104+
assertEquals(environmentMetadata1, testPlugin.getHooksCalls.get(1).get("environmentMetadata"));
105+
assertEquals("AndroidClient", environmentMetadata1.getSdkMetadata().getName());
106+
107+
assertEquals(LDClient.getForMobileKey("secondaryEnvironment"), testPlugin.registerCalls.get(0).get("client"));
108+
EnvironmentMetadata environmentMetadata2 = (EnvironmentMetadata) testPlugin.registerCalls.get(0).get("environmentMetadata");
109+
assertEquals(secondaryMobileKey, environmentMetadata2.getCredential());
110+
assertEquals("AndroidClient", environmentMetadata2.getSdkMetadata().getName());
111+
112+
logging.assertNoWarningsLogged();
113+
logging.assertNoErrorsLogged();
114+
}
115+
}
116+
117+
private LDConfig makeOfflineConfig(List<Plugin> plugins) {
118+
LDConfig.Builder builder = new LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Disabled)
119+
.mobileKey(mobileKey)
120+
.offline(true)
121+
.events(Components.noEvents())
122+
.logAdapter(logging.logAdapter);
123+
124+
if (plugins != null) {
125+
builder.plugins(Components.plugins().setPlugins(plugins));
126+
}
127+
128+
return builder.build();
129+
}
130+
131+
private static class MockPlugin extends Plugin {
132+
133+
private final List<Hook> hooks;
134+
135+
public final List<Map<String, Object>> getHooksCalls = new ArrayList<>();
136+
public final List<Map<String, Object>> registerCalls = new ArrayList<>();
137+
138+
public MockPlugin(List<Hook> hooks) {
139+
this.hooks = hooks;
140+
}
141+
142+
@NonNull
143+
@Override
144+
public PluginMetadata getMetadata() {
145+
return new PluginMetadata() {
146+
@NonNull
147+
@Override
148+
public String getName() {
149+
return "mock-plugin";
150+
}
151+
};
152+
}
153+
154+
@Override
155+
public void register(LDClient client, EnvironmentMetadata metadata) {
156+
registerCalls.add(Map.of(
157+
"client", client,
158+
"environmentMetadata", metadata
159+
));
160+
}
161+
162+
@NonNull
163+
@Override
164+
public List<Hook> getHooks(EnvironmentMetadata metadata) {
165+
getHooksCalls.add(Map.of(
166+
"environmentMetadata", metadata
167+
));
168+
return this.hooks;
169+
}
170+
}
171+
172+
private static class MockHook extends Hook {
173+
public final List<Map<String, Object>> beforeEvaluationCalls = new ArrayList<>();
174+
public final List<Map<String, Object>> afterEvaluationCalls = new ArrayList<>();
175+
public final List<Map<String, Object>> beforeIdentifyCalls = new ArrayList<>();
176+
public final List<Map<String, Object>> afterIdentifyCalls = new ArrayList<>();
177+
public final List<Map<String, Object>> afterTrackCalls = new ArrayList<>();
178+
179+
public MockHook() {
180+
super("MockHook");
181+
}
182+
183+
@Override
184+
public Map<String, Object> beforeEvaluation(EvaluationSeriesContext seriesContext, Map<String, Object> seriesData) {
185+
beforeEvaluationCalls.add(Map.of(
186+
"seriesContext", seriesContext,
187+
"seriesData", seriesData
188+
));
189+
return Collections.unmodifiableMap(Collections.emptyMap());
190+
}
191+
192+
@Override
193+
public Map<String, Object> afterEvaluation(EvaluationSeriesContext seriesContext, Map<String, Object> seriesData, EvaluationDetail<LDValue> evaluationDetail) {
194+
afterEvaluationCalls.add(Map.of(
195+
"seriesContext", seriesContext,
196+
"seriesData", seriesData,
197+
"evaluationDetail", evaluationDetail
198+
));
199+
return Collections.unmodifiableMap(Collections.emptyMap());
200+
}
201+
202+
@Override
203+
public Map<String, Object> beforeIdentify(IdentifySeriesContext seriesContext, Map<String, Object> seriesData) {
204+
beforeIdentifyCalls.add(Map.of(
205+
"seriesContext", seriesContext,
206+
"seriesData", seriesData
207+
));
208+
return Collections.unmodifiableMap(Collections.emptyMap());
209+
}
210+
211+
@Override
212+
public Map<String, Object> afterIdentify(IdentifySeriesContext seriesContext, Map<String, Object> seriesData, IdentifySeriesResult result) {
213+
afterIdentifyCalls.add(Map.of(
214+
"seriesContext", seriesContext,
215+
"seriesData", seriesData,
216+
"result", result
217+
));
218+
return Collections.unmodifiableMap(Collections.emptyMap());
219+
}
220+
221+
@Override
222+
public void afterTrack(TrackSeriesContext seriesContext) {
223+
afterTrackCalls.add(Map.of(
224+
"seriesContext", seriesContext
225+
));
226+
}
227+
}
228+
}

launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder;
55
import com.launchdarkly.sdk.android.integrations.HooksConfigurationBuilder;
66
import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder;
7+
import com.launchdarkly.sdk.android.integrations.PluginsConfigurationBuilder;
78
import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder;
89
import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder;
910
import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder;
@@ -200,4 +201,24 @@ public static StreamingDataSourceBuilder streamingDataSource() {
200201
public static HooksConfigurationBuilder hooks() {
201202
return new ComponentsImpl.HooksConfigurationBuilderImpl();
202203
}
204+
205+
/**
206+
* Returns a builder for configuring plugins.
207+
* Passing this to {@link LDConfig.Builder#plugins(com.launchdarkly.sdk.android.integrations.PluginsConfigurationBuilder)},
208+
* after setting any desired plugins on the builder, applies this configuration to the SDK.
209+
* <pre><code>
210+
* List plugins = getPluginsFunc();
211+
* LDConfig config = new LDConfig.Builder()
212+
* .plugins(
213+
* Components.plugins()
214+
* .setPlugins(plugins)
215+
* )
216+
* .build();
217+
* </code></pre>
218+
*
219+
* @return a {@link PluginsConfigurationBuilder} for plugins configuration
220+
*/
221+
public static PluginsConfigurationBuilder plugins() {
222+
return new ComponentsImpl.PluginsConfigurationBuilderImpl();
223+
}
203224
}

launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder;
88
import com.launchdarkly.sdk.android.integrations.HooksConfigurationBuilder;
99
import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder;
10+
import com.launchdarkly.sdk.android.integrations.PluginsConfigurationBuilder;
1011
import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder;
1112
import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder;
1213
import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder;
@@ -18,6 +19,7 @@
1819
import com.launchdarkly.sdk.android.subsystems.EventProcessor;
1920
import com.launchdarkly.sdk.android.subsystems.HookConfiguration;
2021
import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
22+
import com.launchdarkly.sdk.android.subsystems.PluginsConfiguration;
2123
import com.launchdarkly.sdk.internal.events.DefaultEventProcessor;
2224
import com.launchdarkly.sdk.internal.events.DefaultEventSender;
2325
import com.launchdarkly.sdk.internal.events.Event;
@@ -365,6 +367,14 @@ public HookConfiguration build() {
365367
}
366368
}
367369

370+
static final class PluginsConfigurationBuilderImpl extends PluginsConfigurationBuilder {
371+
372+
@Override
373+
public PluginsConfiguration build() {
374+
return new PluginsConfiguration(plugins);
375+
}
376+
}
377+
368378
// Marker interface for data source implementations that will require a FeatureFetcher
369379
interface DataSourceRequiresFeatureFetcher {}
370380
}

0 commit comments

Comments
 (0)