Skip to content

Commit b56f38e

Browse files
authored
Merge pull request #182 from graphql-java/delegating-dataloader-support
Added support for a delegating data loader
2 parents 212049c + 6d217cc commit b56f38e

File tree

8 files changed

+346
-10
lines changed

8 files changed

+346
-10
lines changed

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ nexusPublishing {
176176
}
177177

178178
signing {
179+
required { !project.hasProperty('publishToMavenLocal') }
179180
def signingKey = System.env.MAVEN_CENTRAL_PGP_KEY
180181
useInMemoryPgpKeys(signingKey, "")
181182
sign publishing.publications

src/main/java/org/dataloader/DataLoader.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.dataloader.impl.CompletableFutureKit;
2222
import org.dataloader.stats.Statistics;
2323
import org.dataloader.stats.StatisticsCollector;
24+
import org.jspecify.annotations.NonNull;
2425
import org.jspecify.annotations.NullMarked;
2526
import org.jspecify.annotations.Nullable;
2627

@@ -517,8 +518,8 @@ public Optional<CompletableFuture<V>> getIfCompleted(K key) {
517518
* @param keyContext a context object that is specific to this key
518519
* @return the future of the value
519520
*/
520-
public CompletableFuture<V> load(K key, Object keyContext) {
521-
return helper.load(key, keyContext);
521+
public CompletableFuture<V> load(@NonNull K key, @Nullable Object keyContext) {
522+
return helper.load(nonNull(key), keyContext);
522523
}
523524

524525
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package org.dataloader;
2+
3+
import org.dataloader.annotations.PublicApi;
4+
import org.dataloader.stats.Statistics;
5+
import org.jspecify.annotations.NonNull;
6+
import org.jspecify.annotations.NullMarked;
7+
import org.jspecify.annotations.Nullable;
8+
9+
import java.time.Duration;
10+
import java.time.Instant;
11+
import java.util.List;
12+
import java.util.Optional;
13+
import java.util.concurrent.CompletableFuture;
14+
import java.util.function.BiConsumer;
15+
import java.util.function.Consumer;
16+
17+
/**
18+
* This delegating {@link DataLoader} makes it easier to create wrappers of {@link DataLoader}s in case you want to change how
19+
* values are returned for example.
20+
* <p>
21+
* The most common way would be to make a new {@link DelegatingDataLoader} subclass that overloads the {@link DelegatingDataLoader#load(Object, Object)}
22+
* method.
23+
* <p>
24+
* For example the following allows you to change the returned value in some way :
25+
* <pre>{@code
26+
* DataLoader<String, String> rawLoader = createDataLoader();
27+
* DelegatingDataLoader<String, String> delegatingDataLoader = new DelegatingDataLoader<>(rawLoader) {
28+
* public CompletableFuture<String> load(@NonNull String key, @Nullable Object keyContext) {
29+
* CompletableFuture<String> cf = super.load(key, keyContext);
30+
* return cf.thenApply(v -> "|" + v + "|");
31+
* }
32+
*};
33+
*}</pre>
34+
*
35+
* @param <K> type parameter indicating the type of the data load keys
36+
* @param <V> type parameter indicating the type of the data that is returned
37+
*/
38+
@PublicApi
39+
@NullMarked
40+
public class DelegatingDataLoader<K, V> extends DataLoader<K, V> {
41+
42+
protected final DataLoader<K, V> delegate;
43+
44+
/**
45+
* This can be called to unwrap a given {@link DataLoader} such that if it's a {@link DelegatingDataLoader} the underlying
46+
* {@link DataLoader} is returned otherwise it's just passed in data loader
47+
*
48+
* @param dataLoader the dataLoader to unwrap
49+
* @param <K> type parameter indicating the type of the data load keys
50+
* @param <V> type parameter indicating the type of the data that is returned
51+
* @return the delegate dataLoader OR just this current one if it's not wrapped
52+
*/
53+
public static <K, V> DataLoader<K, V> unwrap(DataLoader<K, V> dataLoader) {
54+
if (dataLoader instanceof DelegatingDataLoader) {
55+
return ((DelegatingDataLoader<K, V>) dataLoader).getDelegate();
56+
}
57+
return dataLoader;
58+
}
59+
60+
public DelegatingDataLoader(DataLoader<K, V> delegate) {
61+
super(delegate.getBatchLoadFunction(), delegate.getOptions());
62+
this.delegate = delegate;
63+
}
64+
65+
public DataLoader<K, V> getDelegate() {
66+
return delegate;
67+
}
68+
69+
/**
70+
* The {@link DataLoader#load(Object)} and {@link DataLoader#loadMany(List)} type methods all call back
71+
* to the {@link DataLoader#load(Object, Object)} and hence we don't override them.
72+
*
73+
* @param key the key to load
74+
* @param keyContext a context object that is specific to this key
75+
* @return the future of the value
76+
*/
77+
@Override
78+
public CompletableFuture<V> load(@NonNull K key, @Nullable Object keyContext) {
79+
return delegate.load(key, keyContext);
80+
}
81+
82+
@Override
83+
public DataLoader<K, V> transform(Consumer<DataLoaderFactory.Builder<K, V>> builderConsumer) {
84+
return delegate.transform(builderConsumer);
85+
}
86+
87+
@Override
88+
public Instant getLastDispatchTime() {
89+
return delegate.getLastDispatchTime();
90+
}
91+
92+
@Override
93+
public Duration getTimeSinceDispatch() {
94+
return delegate.getTimeSinceDispatch();
95+
}
96+
97+
@Override
98+
public Optional<CompletableFuture<V>> getIfPresent(K key) {
99+
return delegate.getIfPresent(key);
100+
}
101+
102+
@Override
103+
public Optional<CompletableFuture<V>> getIfCompleted(K key) {
104+
return delegate.getIfCompleted(key);
105+
}
106+
107+
@Override
108+
public CompletableFuture<List<V>> dispatch() {
109+
return delegate.dispatch();
110+
}
111+
112+
@Override
113+
public DispatchResult<V> dispatchWithCounts() {
114+
return delegate.dispatchWithCounts();
115+
}
116+
117+
@Override
118+
public List<V> dispatchAndJoin() {
119+
return delegate.dispatchAndJoin();
120+
}
121+
122+
@Override
123+
public int dispatchDepth() {
124+
return delegate.dispatchDepth();
125+
}
126+
127+
@Override
128+
public Object getCacheKey(K key) {
129+
return delegate.getCacheKey(key);
130+
}
131+
132+
@Override
133+
public Statistics getStatistics() {
134+
return delegate.getStatistics();
135+
}
136+
137+
@Override
138+
public CacheMap<Object, V> getCacheMap() {
139+
return delegate.getCacheMap();
140+
}
141+
142+
@Override
143+
public ValueCache<K, V> getValueCache() {
144+
return delegate.getValueCache();
145+
}
146+
147+
@Override
148+
public DataLoader<K, V> clear(K key) {
149+
delegate.clear(key);
150+
return this;
151+
}
152+
153+
@Override
154+
public DataLoader<K, V> clear(K key, BiConsumer<Void, Throwable> handler) {
155+
delegate.clear(key, handler);
156+
return this;
157+
}
158+
159+
@Override
160+
public DataLoader<K, V> clearAll() {
161+
delegate.clearAll();
162+
return this;
163+
}
164+
165+
@Override
166+
public DataLoader<K, V> clearAll(BiConsumer<Void, Throwable> handler) {
167+
delegate.clearAll(handler);
168+
return this;
169+
}
170+
171+
@Override
172+
public DataLoader<K, V> prime(K key, V value) {
173+
delegate.prime(key, value);
174+
return this;
175+
}
176+
177+
@Override
178+
public DataLoader<K, V> prime(K key, Exception error) {
179+
delegate.prime(key, error);
180+
return this;
181+
}
182+
183+
@Override
184+
public DataLoader<K, V> prime(K key, CompletableFuture<V> value) {
185+
delegate.prime(key, value);
186+
return this;
187+
}
188+
}

src/test/java/org/dataloader/DataLoaderTest.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ public void should_work_with_duplicate_keys_when_caching_disabled(TestDataLoader
785785
assertThat(future1.get(), equalTo("A"));
786786
assertThat(future2.get(), equalTo("B"));
787787
assertThat(future3.get(), equalTo("A"));
788-
if (factory instanceof MappedDataLoaderFactory || factory instanceof MappedPublisherDataLoaderFactory) {
788+
if (factory.unwrap() instanceof MappedDataLoaderFactory || factory.unwrap() instanceof MappedPublisherDataLoaderFactory) {
789789
assertThat(loadCalls, equalTo(singletonList(asList("A", "B"))));
790790
} else {
791791
assertThat(loadCalls, equalTo(singletonList(asList("A", "B", "A"))));
@@ -1152,12 +1152,12 @@ public void when_values_size_are_less_then_key_size(TestDataLoaderFactory factor
11521152

11531153
await().atMost(Duration.FIVE_SECONDS).until(() -> areAllDone(cf1, cf2, cf3, cf4));
11541154

1155-
if (factory instanceof ListDataLoaderFactory) {
1155+
if (factory.unwrap() instanceof ListDataLoaderFactory) {
11561156
assertThat(cause(cf1), instanceOf(DataLoaderAssertionException.class));
11571157
assertThat(cause(cf2), instanceOf(DataLoaderAssertionException.class));
11581158
assertThat(cause(cf3), instanceOf(DataLoaderAssertionException.class));
11591159
assertThat(cause(cf4), instanceOf(DataLoaderAssertionException.class));
1160-
} else if (factory instanceof PublisherDataLoaderFactory) {
1160+
} else if (factory.unwrap() instanceof PublisherDataLoaderFactory) {
11611161
// some have completed progressively but the other never did
11621162
assertThat(cf1.join(), equalTo("A"));
11631163
assertThat(cf2.join(), equalTo("B"));
@@ -1187,7 +1187,7 @@ public void when_values_size_are_more_then_key_size(TestDataLoaderFactory factor
11871187
await().atMost(Duration.FIVE_SECONDS).until(() -> areAllDone(cf1, cf2, cf3, cf4));
11881188

11891189

1190-
if (factory instanceof ListDataLoaderFactory) {
1190+
if (factory.unwrap() instanceof ListDataLoaderFactory) {
11911191
assertThat(cause(cf1), instanceOf(DataLoaderAssertionException.class));
11921192
assertThat(cause(cf2), instanceOf(DataLoaderAssertionException.class));
11931193
assertThat(cause(cf3), instanceOf(DataLoaderAssertionException.class));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.dataloader;
2+
3+
import org.dataloader.fixtures.TestKit;
4+
import org.dataloader.fixtures.parameterized.DelegatingDataLoaderFactory;
5+
import org.jspecify.annotations.NonNull;
6+
import org.jspecify.annotations.Nullable;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.util.List;
10+
import java.util.concurrent.CompletableFuture;
11+
12+
import static org.awaitility.Awaitility.await;
13+
import static org.hamcrest.CoreMatchers.equalTo;
14+
import static org.hamcrest.CoreMatchers.is;
15+
import static org.hamcrest.MatcherAssert.assertThat;
16+
17+
/**
18+
* There are WAY more tests via the {@link DelegatingDataLoaderFactory}
19+
* parameterized tests. All the basic {@link DataLoader} tests pass when wrapped in a {@link DelegatingDataLoader}
20+
*/
21+
public class DelegatingDataLoaderTest {
22+
23+
@Test
24+
void canUnwrapDataLoaders() {
25+
DataLoader<Object, Object> rawLoader = TestKit.idLoader();
26+
DataLoader<Object, Object> delegateLoader = new DelegatingDataLoader<>(rawLoader);
27+
28+
assertThat(DelegatingDataLoader.unwrap(rawLoader), is(rawLoader));
29+
assertThat(DelegatingDataLoader.unwrap(delegateLoader), is(rawLoader));
30+
}
31+
32+
@Test
33+
void canCreateAClassOk() {
34+
DataLoader<String, String> rawLoader = TestKit.idLoader();
35+
DelegatingDataLoader<String, String> delegatingDataLoader = new DelegatingDataLoader<>(rawLoader) {
36+
@Override
37+
public CompletableFuture<String> load(@NonNull String key, @Nullable Object keyContext) {
38+
CompletableFuture<String> cf = super.load(key, keyContext);
39+
return cf.thenApply(v -> "|" + v + "|");
40+
}
41+
};
42+
43+
assertThat(delegatingDataLoader.getDelegate(), is(rawLoader));
44+
45+
46+
CompletableFuture<String> cfA = delegatingDataLoader.load("A");
47+
CompletableFuture<String> cfB = delegatingDataLoader.load("B");
48+
CompletableFuture<List<String>> cfCD = delegatingDataLoader.loadMany(List.of("C", "D"));
49+
50+
CompletableFuture<List<String>> dispatch = delegatingDataLoader.dispatch();
51+
52+
await().until(dispatch::isDone);
53+
54+
assertThat(cfA.join(), equalTo("|A|"));
55+
assertThat(cfB.join(), equalTo("|B|"));
56+
assertThat(cfCD.join(), equalTo(List.of("|C|", "|D|")));
57+
58+
assertThat(delegatingDataLoader.getIfPresent("A").isEmpty(), equalTo(false));
59+
assertThat(delegatingDataLoader.getIfPresent("X").isEmpty(), equalTo(true));
60+
61+
assertThat(delegatingDataLoader.getIfCompleted("A").isEmpty(), equalTo(false));
62+
assertThat(delegatingDataLoader.getIfCompleted("X").isEmpty(), equalTo(true));
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.dataloader.fixtures.parameterized;
2+
3+
import org.dataloader.DataLoader;
4+
import org.dataloader.DataLoaderOptions;
5+
import org.dataloader.DelegatingDataLoader;
6+
7+
import java.time.Duration;
8+
import java.util.ArrayList;
9+
import java.util.Collection;
10+
import java.util.List;
11+
12+
public class DelegatingDataLoaderFactory implements TestDataLoaderFactory {
13+
// its delegates all the way down to the turtles
14+
private final TestDataLoaderFactory delegateFactory;
15+
16+
public DelegatingDataLoaderFactory(TestDataLoaderFactory delegateFactory) {
17+
this.delegateFactory = delegateFactory;
18+
}
19+
20+
@Override
21+
public String toString() {
22+
return "DelegatingDataLoaderFactory{" +
23+
"delegateFactory=" + delegateFactory +
24+
'}';
25+
}
26+
27+
@Override
28+
public TestDataLoaderFactory unwrap() {
29+
return delegateFactory.unwrap();
30+
}
31+
32+
private <K, V> DataLoader<K, V> mkDelegateDataLoader(DataLoader<K, V> dataLoader) {
33+
return new DelegatingDataLoader<>(dataLoader);
34+
}
35+
36+
@Override
37+
public <K> DataLoader<K, K> idLoader(DataLoaderOptions options, List<Collection<K>> loadCalls) {
38+
return mkDelegateDataLoader(delegateFactory.idLoader(options, loadCalls));
39+
}
40+
41+
@Override
42+
public <K> DataLoader<K, K> idLoaderDelayed(DataLoaderOptions options, List<Collection<K>> loadCalls, Duration delay) {
43+
return mkDelegateDataLoader(delegateFactory.idLoaderDelayed(options, loadCalls, delay));
44+
}
45+
46+
@Override
47+
public <K> DataLoader<K, K> idLoaderBlowsUps(
48+
DataLoaderOptions options, List<Collection<K>> loadCalls) {
49+
return mkDelegateDataLoader(delegateFactory.idLoaderBlowsUps(options, loadCalls));
50+
}
51+
52+
@Override
53+
public <K> DataLoader<K, Object> idLoaderAllExceptions(DataLoaderOptions options, List<Collection<K>> loadCalls) {
54+
return mkDelegateDataLoader(delegateFactory.idLoaderAllExceptions(options, loadCalls));
55+
}
56+
57+
@Override
58+
public DataLoader<Integer, Object> idLoaderOddEvenExceptions(DataLoaderOptions options, List<Collection<Integer>> loadCalls) {
59+
return mkDelegateDataLoader(delegateFactory.idLoaderOddEvenExceptions(options, loadCalls));
60+
}
61+
62+
@Override
63+
public DataLoader<String, String> onlyReturnsNValues(int N, DataLoaderOptions options, ArrayList<Object> loadCalls) {
64+
return mkDelegateDataLoader(delegateFactory.onlyReturnsNValues(N, options, loadCalls));
65+
}
66+
67+
@Override
68+
public DataLoader<String, String> idLoaderReturnsTooMany(int howManyMore, DataLoaderOptions options, ArrayList<Object> loadCalls) {
69+
return mkDelegateDataLoader(delegateFactory.idLoaderReturnsTooMany(howManyMore, options, loadCalls));
70+
}
71+
}

0 commit comments

Comments
 (0)