Skip to content

Commit 8e55666

Browse files
authored
support for streams/iterators (#2067)
Allowing DataFetchers to return Stream<T> and Iterator<T>.
1 parent 7a8e903 commit 8e55666

3 files changed

Lines changed: 149 additions & 31 deletions

File tree

src/main/java/graphql/execution/ExecutionStrategy.java

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@
3939
import org.slf4j.LoggerFactory;
4040

4141
import java.util.ArrayList;
42-
import java.util.Collection;
4342
import java.util.Collections;
4443
import java.util.List;
4544
import java.util.Map;
45+
import java.util.OptionalInt;
4646
import java.util.concurrent.CompletableFuture;
4747
import java.util.concurrent.CompletionException;
4848
import java.util.function.Supplier;
@@ -56,7 +56,9 @@
5656
import static graphql.execution.FieldValueInfo.CompleteValueType.OBJECT;
5757
import static graphql.execution.FieldValueInfo.CompleteValueType.SCALAR;
5858
import static graphql.schema.DataFetchingEnvironmentImpl.newDataFetchingEnvironment;
59+
import static graphql.schema.GraphQLTypeUtil.isEnum;
5960
import static graphql.schema.GraphQLTypeUtil.isList;
61+
import static graphql.schema.GraphQLTypeUtil.isScalar;
6062
import static java.util.concurrent.CompletableFuture.completedFuture;
6163

6264
/**
@@ -421,10 +423,10 @@ protected FieldValueInfo completeValue(ExecutionContext executionContext, Execut
421423
return FieldValueInfo.newFieldValueInfo(NULL).fieldValue(fieldValue).build();
422424
} else if (isList(fieldType)) {
423425
return completeValueForList(executionContext, parameters, result);
424-
} else if (fieldType instanceof GraphQLScalarType) {
426+
} else if (isScalar(fieldType)) {
425427
fieldValue = completeValueForScalar(executionContext, parameters, (GraphQLScalarType) fieldType, result);
426428
return FieldValueInfo.newFieldValueInfo(SCALAR).fieldValue(fieldValue).build();
427-
} else if (fieldType instanceof GraphQLEnumType) {
429+
} else if (isEnum(fieldType)) {
428430
fieldValue = completeValueForEnum(executionContext, parameters, (GraphQLEnumType) fieldType, result);
429431
return FieldValueInfo.newFieldValueInfo(ENUM).fieldValue(fieldValue).build();
430432
}
@@ -494,19 +496,19 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext,
494496
*/
495497
protected FieldValueInfo completeValueForList(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Iterable<Object> iterableValues) {
496498

497-
Collection<Object> values = FpKit.toCollection(iterableValues);
499+
OptionalInt size = FpKit.toSize(iterableValues);
498500
ExecutionStepInfo executionStepInfo = parameters.getExecutionStepInfo();
499501

500-
InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, values);
502+
InstrumentationFieldCompleteParameters instrumentationParams = new InstrumentationFieldCompleteParameters(executionContext, parameters, () -> executionStepInfo, iterableValues);
501503
Instrumentation instrumentation = executionContext.getInstrumentation();
502504

503505
InstrumentationContext<ExecutionResult> completeListCtx = instrumentation.beginFieldListComplete(
504506
instrumentationParams
505507
);
506508

507-
List<FieldValueInfo> fieldValueInfos = new ArrayList<>(values.size());
509+
List<FieldValueInfo> fieldValueInfos = new ArrayList<>(size.orElse(1));
508510
int index = 0;
509-
for (Object item : values) {
511+
for (Object item : iterableValues) {
510512
ResultPath indexedPath = parameters.getPath().segment(index);
511513

512514
ExecutionStepInfo stepInfoForListElement = executionStepInfoFactory.newExecutionStepInfoForListElement(executionStepInfo, index);
@@ -519,7 +521,7 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext,
519521
ExecutionStrategyParameters newParameters = parameters.transform(builder ->
520522
builder.executionStepInfo(stepInfoForListElement)
521523
.nonNullFieldValidator(nonNullableFieldValidator)
522-
.listSize(values.size())
524+
.listSize(size.orElse(-1)) // -1 signals that we don't know the size
523525
.localContext(value.getLocalContext())
524526
.currentListIndex(finalIndex)
525527
.path(indexedPath)
@@ -674,14 +676,15 @@ protected GraphQLObjectType resolveType(ExecutionContext executionContext, Execu
674676

675677

676678
protected Iterable<Object> toIterable(ExecutionContext context, ExecutionStrategyParameters parameters, Object result) {
677-
if (result.getClass().isArray() || result instanceof Iterable) {
678-
return toIterable(result);
679+
if (FpKit.isIterable(result)) {
680+
return FpKit.toIterable(result);
679681
}
680682

681683
handleTypeMismatchProblem(context, parameters, result);
682684
return null;
683685
}
684686

687+
685688
private void handleTypeMismatchProblem(ExecutionContext context, ExecutionStrategyParameters parameters, Object result) {
686689
TypeMismatchError error = new TypeMismatchError(parameters.getPath(), parameters.getExecutionStepInfo().getUnwrappedNonNullType());
687690
logNotSafe.warn("{} got {}", error.getMessage(), result.getClass());

src/main/java/graphql/util/FpKit.java

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
import java.util.LinkedHashMap;
1111
import java.util.List;
1212
import java.util.Map;
13+
import java.util.NoSuchElementException;
1314
import java.util.Optional;
15+
import java.util.OptionalInt;
1416
import java.util.concurrent.CompletableFuture;
1517
import java.util.function.BiFunction;
1618
import java.util.function.BinaryOperator;
@@ -19,6 +21,7 @@
1921
import java.util.function.Supplier;
2022
import java.util.stream.Collectors;
2123
import java.util.stream.IntStream;
24+
import java.util.stream.Stream;
2225

2326
import static java.util.Collections.singletonList;
2427
import static java.util.function.Function.identity;
@@ -31,10 +34,10 @@ public class FpKit {
3134
// From a list of named things, get a map of them by name, merging them according to the merge function
3235
public static <T> Map<String, T> getByName(List<T> namedObjects, Function<T, String> nameFn, BinaryOperator<T> mergeFunc) {
3336
return namedObjects.stream().collect(Collectors.toMap(
34-
nameFn,
35-
identity(),
36-
mergeFunc,
37-
LinkedHashMap::new)
37+
nameFn,
38+
identity(),
39+
mergeFunc,
40+
LinkedHashMap::new)
3841
);
3942
}
4043

@@ -45,10 +48,10 @@ public static <T, NewKey> Map<NewKey, List<T>> groupingBy(Collection<T> list, Fu
4548

4649
public static <T, NewKey> Map<NewKey, T> groupingByUniqueKey(Collection<T> list, Function<T, NewKey> keyFunction) {
4750
return list.stream().collect(Collectors.toMap(
48-
keyFunction,
49-
identity(),
50-
throwingMerger(),
51-
LinkedHashMap::new)
51+
keyFunction,
52+
identity(),
53+
throwingMerger(),
54+
LinkedHashMap::new)
5255
);
5356
}
5457

@@ -75,17 +78,15 @@ public static <T> BinaryOperator<T> mergeFirst() {
7578
*
7679
* @param iterableResult the result object
7780
* @param <T> the type of thing
78-
*
7981
* @return an Iterable from that object
80-
*
8182
* @throws java.lang.ClassCastException if its not an Iterable
8283
*/
8384
@SuppressWarnings("unchecked")
8485
public static <T> Collection<T> toCollection(Object iterableResult) {
8586
if (iterableResult.getClass().isArray()) {
8687
List<Object> collect = IntStream.range(0, Array.getLength(iterableResult))
87-
.mapToObj(i -> Array.get(iterableResult, i))
88-
.collect(Collectors.toList());
88+
.mapToObj(i -> Array.get(iterableResult, i))
89+
.collect(Collectors.toList());
8990
return (List<T>) collect;
9091
}
9192
if (iterableResult instanceof Collection) {
@@ -100,14 +101,79 @@ public static <T> Collection<T> toCollection(Object iterableResult) {
100101
return list;
101102
}
102103

104+
public static boolean isIterable(Object result) {
105+
return result.getClass().isArray() || result instanceof Iterable || result instanceof Stream || result instanceof Iterator;
106+
}
107+
108+
109+
@SuppressWarnings("unchecked")
110+
public static <T> Iterable<T> toIterable(Object iterableResult) {
111+
if (iterableResult instanceof Iterable) {
112+
return ((Iterable<T>) iterableResult);
113+
}
114+
115+
if (iterableResult instanceof Stream) {
116+
return ((Stream<T>) iterableResult)::iterator;
117+
}
118+
119+
if (iterableResult instanceof Iterator) {
120+
return () -> (Iterator<T>) iterableResult;
121+
}
122+
123+
if (iterableResult.getClass().isArray()) {
124+
return () -> new ArrayIterator<>(iterableResult);
125+
}
126+
127+
throw new ClassCastException("not Iterable: " + iterableResult.getClass());
128+
}
129+
130+
private static class ArrayIterator<T> implements Iterator<T> {
131+
132+
private final Object array;
133+
private final int size;
134+
private int i;
135+
136+
private ArrayIterator(Object array) {
137+
this.array = array;
138+
this.size = Array.getLength(array);
139+
this.i = 0;
140+
}
141+
142+
@Override
143+
public boolean hasNext() {
144+
return i < size;
145+
}
146+
147+
@SuppressWarnings("unchecked")
148+
@Override
149+
public T next() {
150+
if (!hasNext()) {
151+
throw new NoSuchElementException();
152+
}
153+
return (T) Array.get(array, i++);
154+
}
155+
156+
}
157+
158+
public static OptionalInt toSize(Object iterableResult) {
159+
if (iterableResult instanceof Collection) {
160+
return OptionalInt.of(((Collection<?>) iterableResult).size());
161+
}
162+
163+
if (iterableResult.getClass().isArray()) {
164+
return OptionalInt.of(Array.getLength(iterableResult));
165+
}
166+
167+
return OptionalInt.empty();
168+
}
169+
103170
/**
104171
* Concatenates (appends) a single elements to an existing list
105172
*
106173
* @param l the list onto which to append the element
107174
* @param t the element to append
108175
* @param <T> the type of elements of the list
109-
*
110-
* @return a <strong>new</strong> list componsed of the first list elements and the new element
176+
* @return a <strong>new</strong> list composed of the first list elements and the new element
111177
*/
112178
public static <T> List<T> concat(List<T> l, T t) {
113179
return concat(l, singletonList(t));
@@ -119,7 +185,6 @@ public static <T> List<T> concat(List<T> l, T t) {
119185
* @param l1 the first list to concatenate
120186
* @param l2 the second list to concatenate
121187
* @param <T> the type of element of the lists
122-
*
123188
* @return a <strong>new</strong> list composed of the two concatenated lists elements
124189
*/
125190
public static <T> List<T> concat(List<T> l1, List<T> l2) {
@@ -152,7 +217,7 @@ public static <T> List<List<T>> transposeMatrix(List<? extends List<T>> matrix)
152217
for (int j = 0; j < colCount; j++) {
153218
T val = matrix.get(i).get(j);
154219
if (result.size() <= j) {
155-
result.add(j, new ArrayList());
220+
result.add(j, new ArrayList<>());
156221
}
157222
result.get(j).add(i, val);
158223
}
@@ -166,15 +231,15 @@ public static <T> CompletableFuture<List<T>> flatList(CompletableFuture<List<Lis
166231

167232
public static <T> List<T> flatList(List<List<T>> listLists) {
168233
return listLists.stream()
169-
.flatMap(List::stream)
170-
.collect(Collectors.toList());
234+
.flatMap(List::stream)
235+
.collect(Collectors.toList());
171236
}
172237

173238
public static <T> Optional<T> findOne(Collection<T> list, Predicate<T> filter) {
174239
return list
175-
.stream()
176-
.filter(filter)
177-
.findFirst();
240+
.stream()
241+
.filter(filter)
242+
.findFirst();
178243
}
179244

180245
public static <T> T findOneOrNull(List<T> list, Predicate<T> filter) {

src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import spock.lang.Specification
2727

2828
import java.util.concurrent.CompletableFuture
2929
import java.util.concurrent.CompletionException
30+
import java.util.stream.Stream
3031

3132
import static ExecutionStrategyParameters.newParameters
3233
import static graphql.Scalars.GraphQLString
@@ -690,6 +691,55 @@ class ExecutionStrategyTest extends Specification {
690691
executionResult.get().data == [1L, 2L, 3L]
691692
}
692693

694+
def "#842 completes value for java.util.Stream"() {
695+
given:
696+
ExecutionContext executionContext = buildContext()
697+
Stream<Long> result = Stream.of(1L, 2L, 3L)
698+
def fieldType = list(Scalars.GraphQLLong)
699+
def fldDef = newFieldDefinition().name("test").type(fieldType).build()
700+
def executionStepInfo = ExecutionStepInfo.newExecutionStepInfo().type(fieldType).path(ResultPath.rootPath()).fieldDefinition(fldDef).build()
701+
NonNullableFieldValidator nullableFieldValidator = new NonNullableFieldValidator(executionContext, executionStepInfo)
702+
703+
def parameters = newParameters()
704+
.executionStepInfo(executionStepInfo)
705+
.source(result)
706+
.nonNullFieldValidator(nullableFieldValidator)
707+
.fields(mergedSelectionSet(["fld": [mergedField(Field.newField().build())]]))
708+
.field(mergedField(Field.newField().build()))
709+
.build()
710+
711+
when:
712+
def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue
713+
714+
then:
715+
executionResult.get().data == [1L, 2L, 3L]
716+
}
717+
718+
def "#842 completes value for java.util.Iterator"() {
719+
given:
720+
ExecutionContext executionContext = buildContext()
721+
Iterator<Long> result = Arrays.asList(1L, 2L, 3L).iterator()
722+
def fieldType = list(Scalars.GraphQLLong)
723+
def fldDef = newFieldDefinition().name("test").type(fieldType).build()
724+
def executionStepInfo = ExecutionStepInfo.newExecutionStepInfo().type(fieldType).path(ResultPath.rootPath()).fieldDefinition(fldDef).build()
725+
NonNullableFieldValidator nullableFieldValidator = new NonNullableFieldValidator(executionContext, executionStepInfo)
726+
727+
def parameters = newParameters()
728+
.executionStepInfo(executionStepInfo)
729+
.source(result)
730+
.nonNullFieldValidator(nullableFieldValidator)
731+
.fields(mergedSelectionSet(["fld": [mergedField(Field.newField().build())]]))
732+
.field(mergedField(Field.newField().build()))
733+
.build()
734+
735+
when:
736+
def executionResult = executionStrategy.completeValue(executionContext, parameters).fieldValue
737+
738+
then:
739+
executionResult.get().data == [1L, 2L, 3L]
740+
}
741+
742+
693743
def "#820 processes DataFetcherResult"() {
694744
given:
695745

0 commit comments

Comments
 (0)