Skip to content

Added support for a delegating data loader #182

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

Merged
merged 8 commits into from
Apr 2, 2025
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ nexusPublishing {
}

signing {
required { !project.hasProperty('publishToMavenLocal') }
def signingKey = System.env.MAVEN_CENTRAL_PGP_KEY
useInMemoryPgpKeys(signingKey, "")
sign publishing.publications
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/org/dataloader/DataLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.dataloader.impl.CompletableFutureKit;
import org.dataloader.stats.Statistics;
import org.dataloader.stats.StatisticsCollector;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

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

/**
Expand Down
188 changes: 188 additions & 0 deletions src/main/java/org/dataloader/DelegatingDataLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package org.dataloader;

import org.dataloader.annotations.PublicApi;
import org.dataloader.stats.Statistics;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
* This delegating {@link DataLoader} makes it easier to create wrappers of {@link DataLoader}s in case you want to change how
* values are returned for example.
* <p>
* The most common way would be to make a new {@link DelegatingDataLoader} subclass that overloads the {@link DelegatingDataLoader#load(Object, Object)}
* method.
* <p>
* For example the following allows you to change the returned value in some way :
* <pre>{@code
* DataLoader<String, String> rawLoader = createDataLoader();
* DelegatingDataLoader<String, String> delegatingDataLoader = new DelegatingDataLoader<>(rawLoader) {
* public CompletableFuture<String> load(@NonNull String key, @Nullable Object keyContext) {
* CompletableFuture<String> cf = super.load(key, keyContext);
* return cf.thenApply(v -> "|" + v + "|");
* }
*};
*}</pre>
*
* @param <K> type parameter indicating the type of the data load keys
* @param <V> type parameter indicating the type of the data that is returned
*/
@PublicApi
@NullMarked
public class DelegatingDataLoader<K, V> extends DataLoader<K, V> {

protected final DataLoader<K, V> delegate;

/**
* This can be called to unwrap a given {@link DataLoader} such that if it's a {@link DelegatingDataLoader} the underlying
* {@link DataLoader} is returned otherwise it's just passed in data loader
*
* @param dataLoader the dataLoader to unwrap
* @param <K> type parameter indicating the type of the data load keys
* @param <V> type parameter indicating the type of the data that is returned
* @return the delegate dataLoader OR just this current one if it's not wrapped
*/
public static <K, V> DataLoader<K, V> unwrap(DataLoader<K, V> dataLoader) {
if (dataLoader instanceof DelegatingDataLoader) {
return ((DelegatingDataLoader<K, V>) dataLoader).getDelegate();
}
return dataLoader;
}

public DelegatingDataLoader(DataLoader<K, V> delegate) {
super(delegate.getBatchLoadFunction(), delegate.getOptions());
this.delegate = delegate;
}

public DataLoader<K, V> getDelegate() {
return delegate;
}

/**
* The {@link DataLoader#load(Object)} and {@link DataLoader#loadMany(List)} type methods all call back
* to the {@link DataLoader#load(Object, Object)} and hence we don't override them.
*
* @param key the key to load
* @param keyContext a context object that is specific to this key
* @return the future of the value
*/
@Override
public CompletableFuture<V> load(@NonNull K key, @Nullable Object keyContext) {
return delegate.load(key, keyContext);
}

@Override
public DataLoader<K, V> transform(Consumer<DataLoaderFactory.Builder<K, V>> builderConsumer) {
return delegate.transform(builderConsumer);
}

@Override
public Instant getLastDispatchTime() {
return delegate.getLastDispatchTime();
}

@Override
public Duration getTimeSinceDispatch() {
return delegate.getTimeSinceDispatch();
}

@Override
public Optional<CompletableFuture<V>> getIfPresent(K key) {
return delegate.getIfPresent(key);
}

@Override
public Optional<CompletableFuture<V>> getIfCompleted(K key) {
return delegate.getIfCompleted(key);
}

@Override
public CompletableFuture<List<V>> dispatch() {
return delegate.dispatch();
}

@Override
public DispatchResult<V> dispatchWithCounts() {
return delegate.dispatchWithCounts();
}

@Override
public List<V> dispatchAndJoin() {
return delegate.dispatchAndJoin();
}

@Override
public int dispatchDepth() {
return delegate.dispatchDepth();
}

@Override
public Object getCacheKey(K key) {
return delegate.getCacheKey(key);
}

@Override
public Statistics getStatistics() {
return delegate.getStatistics();
}

@Override
public CacheMap<Object, V> getCacheMap() {
return delegate.getCacheMap();
}

@Override
public ValueCache<K, V> getValueCache() {
return delegate.getValueCache();
}

@Override
public DataLoader<K, V> clear(K key) {
delegate.clear(key);
return this;
}

@Override
public DataLoader<K, V> clear(K key, BiConsumer<Void, Throwable> handler) {
delegate.clear(key, handler);
return this;
}

@Override
public DataLoader<K, V> clearAll() {
delegate.clearAll();
return this;
}

@Override
public DataLoader<K, V> clearAll(BiConsumer<Void, Throwable> handler) {
delegate.clearAll(handler);
return this;
}

@Override
public DataLoader<K, V> prime(K key, V value) {
delegate.prime(key, value);
return this;
}

@Override
public DataLoader<K, V> prime(K key, Exception error) {
delegate.prime(key, error);
return this;
}

@Override
public DataLoader<K, V> prime(K key, CompletableFuture<V> value) {
delegate.prime(key, value);
return this;
}
}
8 changes: 4 additions & 4 deletions src/test/java/org/dataloader/DataLoaderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@ public void should_work_with_duplicate_keys_when_caching_disabled(TestDataLoader
assertThat(future1.get(), equalTo("A"));
assertThat(future2.get(), equalTo("B"));
assertThat(future3.get(), equalTo("A"));
if (factory instanceof MappedDataLoaderFactory || factory instanceof MappedPublisherDataLoaderFactory) {
if (factory.unwrap() instanceof MappedDataLoaderFactory || factory.unwrap() instanceof MappedPublisherDataLoaderFactory) {
assertThat(loadCalls, equalTo(singletonList(asList("A", "B"))));
} else {
assertThat(loadCalls, equalTo(singletonList(asList("A", "B", "A"))));
Expand Down Expand Up @@ -1152,12 +1152,12 @@ public void when_values_size_are_less_then_key_size(TestDataLoaderFactory factor

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

if (factory instanceof ListDataLoaderFactory) {
if (factory.unwrap() instanceof ListDataLoaderFactory) {
assertThat(cause(cf1), instanceOf(DataLoaderAssertionException.class));
assertThat(cause(cf2), instanceOf(DataLoaderAssertionException.class));
assertThat(cause(cf3), instanceOf(DataLoaderAssertionException.class));
assertThat(cause(cf4), instanceOf(DataLoaderAssertionException.class));
} else if (factory instanceof PublisherDataLoaderFactory) {
} else if (factory.unwrap() instanceof PublisherDataLoaderFactory) {
// some have completed progressively but the other never did
assertThat(cf1.join(), equalTo("A"));
assertThat(cf2.join(), equalTo("B"));
Expand Down Expand Up @@ -1187,7 +1187,7 @@ public void when_values_size_are_more_then_key_size(TestDataLoaderFactory factor
await().atMost(Duration.FIVE_SECONDS).until(() -> areAllDone(cf1, cf2, cf3, cf4));


if (factory instanceof ListDataLoaderFactory) {
if (factory.unwrap() instanceof ListDataLoaderFactory) {
assertThat(cause(cf1), instanceOf(DataLoaderAssertionException.class));
assertThat(cause(cf2), instanceOf(DataLoaderAssertionException.class));
assertThat(cause(cf3), instanceOf(DataLoaderAssertionException.class));
Expand Down
64 changes: 64 additions & 0 deletions src/test/java/org/dataloader/DelegatingDataLoaderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.dataloader;

import org.dataloader.fixtures.TestKit;
import org.dataloader.fixtures.parameterized.DelegatingDataLoaderFactory;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.concurrent.CompletableFuture;

import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

/**
* There are WAY more tests via the {@link DelegatingDataLoaderFactory}
* parameterized tests. All the basic {@link DataLoader} tests pass when wrapped in a {@link DelegatingDataLoader}
*/
public class DelegatingDataLoaderTest {

@Test
void canUnwrapDataLoaders() {
DataLoader<Object, Object> rawLoader = TestKit.idLoader();
DataLoader<Object, Object> delegateLoader = new DelegatingDataLoader<>(rawLoader);

assertThat(DelegatingDataLoader.unwrap(rawLoader), is(rawLoader));
assertThat(DelegatingDataLoader.unwrap(delegateLoader), is(rawLoader));
}

@Test
void canCreateAClassOk() {
DataLoader<String, String> rawLoader = TestKit.idLoader();
DelegatingDataLoader<String, String> delegatingDataLoader = new DelegatingDataLoader<>(rawLoader) {
@Override
public CompletableFuture<String> load(@NonNull String key, @Nullable Object keyContext) {
CompletableFuture<String> cf = super.load(key, keyContext);
return cf.thenApply(v -> "|" + v + "|");
}
};

assertThat(delegatingDataLoader.getDelegate(), is(rawLoader));


CompletableFuture<String> cfA = delegatingDataLoader.load("A");
CompletableFuture<String> cfB = delegatingDataLoader.load("B");
CompletableFuture<List<String>> cfCD = delegatingDataLoader.loadMany(List.of("C", "D"));

CompletableFuture<List<String>> dispatch = delegatingDataLoader.dispatch();

await().until(dispatch::isDone);

assertThat(cfA.join(), equalTo("|A|"));
assertThat(cfB.join(), equalTo("|B|"));
assertThat(cfCD.join(), equalTo(List.of("|C|", "|D|")));

assertThat(delegatingDataLoader.getIfPresent("A").isEmpty(), equalTo(false));
assertThat(delegatingDataLoader.getIfPresent("X").isEmpty(), equalTo(true));

assertThat(delegatingDataLoader.getIfCompleted("A").isEmpty(), equalTo(false));
assertThat(delegatingDataLoader.getIfCompleted("X").isEmpty(), equalTo(true));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.dataloader.fixtures.parameterized;

import org.dataloader.DataLoader;
import org.dataloader.DataLoaderOptions;
import org.dataloader.DelegatingDataLoader;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class DelegatingDataLoaderFactory implements TestDataLoaderFactory {
// its delegates all the way down to the turtles
private final TestDataLoaderFactory delegateFactory;

public DelegatingDataLoaderFactory(TestDataLoaderFactory delegateFactory) {
this.delegateFactory = delegateFactory;
}

@Override
public String toString() {
return "DelegatingDataLoaderFactory{" +
"delegateFactory=" + delegateFactory +
'}';
}

@Override
public TestDataLoaderFactory unwrap() {
return delegateFactory.unwrap();
}

private <K, V> DataLoader<K, V> mkDelegateDataLoader(DataLoader<K, V> dataLoader) {
return new DelegatingDataLoader<>(dataLoader);
}

@Override
public <K> DataLoader<K, K> idLoader(DataLoaderOptions options, List<Collection<K>> loadCalls) {
return mkDelegateDataLoader(delegateFactory.idLoader(options, loadCalls));
}

@Override
public <K> DataLoader<K, K> idLoaderDelayed(DataLoaderOptions options, List<Collection<K>> loadCalls, Duration delay) {
return mkDelegateDataLoader(delegateFactory.idLoaderDelayed(options, loadCalls, delay));
}

@Override
public <K> DataLoader<K, K> idLoaderBlowsUps(
DataLoaderOptions options, List<Collection<K>> loadCalls) {
return mkDelegateDataLoader(delegateFactory.idLoaderBlowsUps(options, loadCalls));
}

@Override
public <K> DataLoader<K, Object> idLoaderAllExceptions(DataLoaderOptions options, List<Collection<K>> loadCalls) {
return mkDelegateDataLoader(delegateFactory.idLoaderAllExceptions(options, loadCalls));
}

@Override
public DataLoader<Integer, Object> idLoaderOddEvenExceptions(DataLoaderOptions options, List<Collection<Integer>> loadCalls) {
return mkDelegateDataLoader(delegateFactory.idLoaderOddEvenExceptions(options, loadCalls));
}

@Override
public DataLoader<String, String> onlyReturnsNValues(int N, DataLoaderOptions options, ArrayList<Object> loadCalls) {
return mkDelegateDataLoader(delegateFactory.onlyReturnsNValues(N, options, loadCalls));
}

@Override
public DataLoader<String, String> idLoaderReturnsTooMany(int howManyMore, DataLoaderOptions options, ArrayList<Object> loadCalls) {
return mkDelegateDataLoader(delegateFactory.idLoaderReturnsTooMany(howManyMore, options, loadCalls));
}
}
Loading