diff --git a/src/main/java/graphql/execution/Async.java b/src/main/java/graphql/execution/Async.java index ee4a65b80e..e49bbf4960 100644 --- a/src/main/java/graphql/execution/Async.java +++ b/src/main/java/graphql/execution/Async.java @@ -4,8 +4,10 @@ import graphql.Internal; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; @@ -13,6 +15,7 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; @Internal @SuppressWarnings("FutureReturnValueIgnored") @@ -24,41 +27,27 @@ public interface CFFactory { } public static CompletableFuture> each(List> futures) { - CompletableFuture> overallResult = new CompletableFuture<>(); - - CompletableFuture - .allOf(futures.toArray(new CompletableFuture[0])) - .whenComplete((noUsed, exception) -> { - if (exception != null) { - overallResult.completeExceptionally(exception); - return; - } - List results = new ArrayList<>(); - for (CompletableFuture future : futures) { - results.add(future.join()); - } - overallResult.complete(results); - }); - return overallResult; + Assert.assertNotNull(futures); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])) + .thenApply(noUsed -> futures + .stream() + .map(CompletableFuture::join) + .collect(Collectors.toList())); } public static CompletableFuture> each(Iterable list, BiFunction> cfFactory) { - List> futures = new ArrayList<>(); - int index = 0; - for (T t : list) { - CompletableFuture cf; - try { - cf = cfFactory.apply(t, index++); - Assert.assertNotNull(cf, "cfFactory must return a non null value"); - } catch (Exception e) { - cf = new CompletableFuture<>(); - // Async.each makes sure that it is not a CompletionException inside a CompletionException - cf.completeExceptionally(new CompletionException(e)); - } - futures.add(cf); - } - return each(futures); - + Assert.assertNotNull(list); + Assert.assertNotNull(cfFactory); + + int index[] = {0}; + return each( + StreamSupport + .stream(list.spliterator(), false) + .map(o -> tryCatch(() -> + Assert.assertNotNull(cfFactory.apply(o, index[0]++), "cfFactory must return a non null value"))) + .collect(Collectors.toList()) + ); } public static CompletableFuture> eachSequentially(Iterable list, CFFactory cfFactory) { diff --git a/src/main/java/graphql/execution/FieldCollector.java b/src/main/java/graphql/execution/FieldCollector.java index 22f893f64f..b475e58c56 100644 --- a/src/main/java/graphql/execution/FieldCollector.java +++ b/src/main/java/graphql/execution/FieldCollector.java @@ -120,7 +120,7 @@ private String getFieldEntryKey(Field field) { } - private boolean doesFragmentConditionMatch(FieldCollectorParameters parameters, InlineFragment inlineFragment) { + protected boolean doesFragmentConditionMatch(FieldCollectorParameters parameters, InlineFragment inlineFragment) { if (inlineFragment.getTypeCondition() == null) { return true; } @@ -129,7 +129,7 @@ private boolean doesFragmentConditionMatch(FieldCollectorParameters parameters, return checkTypeCondition(parameters, conditionType); } - private boolean doesFragmentConditionMatch(FieldCollectorParameters parameters, FragmentDefinition fragmentDefinition) { + protected boolean doesFragmentConditionMatch(FieldCollectorParameters parameters, FragmentDefinition fragmentDefinition) { GraphQLType conditionType; conditionType = getTypeFromAST(parameters.getGraphQLSchema(), fragmentDefinition.getTypeCondition()); return checkTypeCondition(parameters, conditionType); diff --git a/src/main/java/graphql/execution3/DAGExecutionStrategy.java b/src/main/java/graphql/execution3/DAGExecutionStrategy.java new file mode 100644 index 0000000000..ad7d7723f5 --- /dev/null +++ b/src/main/java/graphql/execution3/DAGExecutionStrategy.java @@ -0,0 +1,234 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import static graphql.Assert.assertNotNull; +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.execution.Async; +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionPath; +import graphql.execution.ExecutionStepInfo; +import static graphql.execution.ExecutionStepInfo.newExecutionStepInfo; +import graphql.execution.ExecutionStepInfoFactory; +import graphql.execution.FetchedValue; +import graphql.execution.MergedField; +import graphql.execution.nextgen.ValueFetcher; +import graphql.language.Field; +import graphql.language.Node; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeUtil; +import graphql.util.DependenciesIterator; +import graphql.util.Edge; +import graphql.util.TriFunction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author gkesler + */ +public class DAGExecutionStrategy implements ExecutionStrategy { + public DAGExecutionStrategy (ExecutionContext executionContext) { + this.executionContext = Objects.requireNonNull(executionContext); + this.executionInfoFactory = new ExecutionStepInfoFactory(); + this.valueFetcher = new ValueFetcher(); + } + + /** + * Executes a graphql request according to the schedule + * provided by executionPlan + * + * @param executionPlan a {@code graphql.util.DependencyGraph} specialization that provides + * order of field resolution requests + * + * @return a CompletableFuture holding the result of execution. + */ + @Override + public CompletableFuture execute(ExecutionPlan executionPlan) { + assertNotNull(executionPlan); + + ExecutionPlanContextImpl executionPlanContext = new ExecutionPlanContextImpl(); + return CompletableFuture + .completedFuture(executionPlan.orderDependencies(executionPlanContext)) + .thenCompose(this::resolveClosure) + .thenApply(noResult -> executionPlanContext.getResult()); + } + + private CompletableFuture>> resolveClosure (DependenciesIterator> closure) { + if (closure.hasNext()) { + Collection> nodes = closure.next(); + List> tasks = new ArrayList<>(nodes.size()); + + Iterator> toBeResolved = nodes.iterator(); + while (toBeResolved.hasNext()) { + NodeVertex field = toBeResolved.next(); + toBeResolved.remove(); + + tasks.add( + CompletableFuture + .completedFuture(field) + .thenCompose(this::fetchNode) + .thenAccept(closure::close) + ); + } + + // execute fetch&resove asynchronously + return Async + .each(tasks) + .thenApply(noResult -> closure) + .thenCompose(this::resolveClosure); + } else { + return CompletableFuture + .completedFuture(closure); + } + } + + private void provideSource (NodeVertex source, NodeVertex sink) { + LOGGER.debug("provideSource: source={}, sink={}", source, sink); + source.accept(sink, new NodeVertexVisitor>() { + @Override + public NodeVertex visit(OperationVertex source, NodeVertex sink) { + source + .executionStepInfo( + newExecutionStepInfo() + .type((GraphQLOutputType)source.getType()) + .path(ExecutionPath.rootPath()) + .build() + ) + // prepare placeholder for this operation result + // since operation does not have its external reslution, + // it's result is stored in the source object + .result(source.getSource()); + + return visitNode(source, sink); + } + + @Override + public NodeVertex visitNode(NodeVertex source, NodeVertex sink) { + return sink + .parentExecutionStepInfo(source.getExecutionStepInfo()) + .source(Results.flatten((List)source.getResult())); + } + }); + } + + private void joinResults (FieldVertex source, NodeVertex sink) { + LOGGER.debug("afterResolve: source={}, sink={}", source, sink); + Results.joinResultsOf(source); + } + + private boolean tryResolve (NodeVertex node) { + LOGGER.debug("tryResolve: node={}", node); + return node.accept(true, new NodeVertexVisitor() { + @Override + public Boolean visit(FieldVertex node, Boolean data) { + List sources = (List)node.getSource(); + // if sources is empty, no need to fetch data. + // even if it is fetched, it won't be joined anyways + return sources == null || sources.isEmpty(); + } + + @Override + public Boolean visit(DocumentVertex node, Boolean data) { + node.result(result); + return true; + } + }); + } + + private CompletableFuture> fetchNode (NodeVertex node) { + LOGGER.debug("fetchNode: node={}", node); + + FieldVertex fieldNode = node.as(FieldVertex.class); + MergedField sameFields = MergedField + .newMergedField(fieldNode.getNode()) + .build(); + ExecutionStepInfo sourceExecutionStepInfo = node.getParentExecutionStepInfo(); + GraphQLOutputType parentType = (GraphQLOutputType)GraphQLTypeUtil.unwrapAll(sourceExecutionStepInfo.getType()); + ExecutionStepInfo parentExecutionStepInfo = sourceExecutionStepInfo + .transform(builder -> builder + .parentInfo(sourceExecutionStepInfo.getParent()) + .type(parentType)); + ExecutionStepInfo executionStepInfo = executionInfoFactory + .newExecutionStepInfoForSubField(executionContext, sameFields, parentExecutionStepInfo); + + TriFunction>> valuesFetcher = + fieldNode.isRoot() ? this::fetchRootValues : this::fetchBatchedValues; + + + return valuesFetcher.apply(fieldNode, sameFields, executionStepInfo) + .thenApply(fetchedValues -> + fetchedValues + .stream() + .map(FetchedValue::getFetchedValue) + .map(fv -> Results.checkAndFixNILs(fv, fieldNode)) + .collect(Collectors.toList()) + ) + .thenApply(fetchedValues -> + (NodeVertex)node + .executionStepInfo(executionStepInfo) + .result(fetchedValues) + ); + } + + private CompletableFuture> fetchRootValues (FieldVertex fieldNode, MergedField sameFields, ExecutionStepInfo executionStepInfo) { + return valueFetcher + .fetchValue(executionContext, executionContext.getRoot(), null/*toDoLocalContext*/, sameFields, executionStepInfo) + .thenApply(Collections::singletonList); + } + + private CompletableFuture> fetchBatchedValues (FieldVertex fieldNode, MergedField sameFields, ExecutionStepInfo executionStepInfo) { + List sources = (List)fieldNode.getSource(); + return valueFetcher + .fetchBatchedValues(executionContext, sources, sameFields, + Stream + .generate(() -> executionStepInfo) + .limit(sources.size()) + .collect(Collectors.toList()) + ); + } + + private class ExecutionPlanContextImpl implements ExecutionPlanContext { + @Override + public void prepareResolve(Edge, ?> edge) { + provideSource((NodeVertex)edge.getSource(), (NodeVertex)edge.getSink()); + } + + @Override + public void whenResolved(Edge, ?> edge) { + joinResults((FieldVertex)edge.getSource(), (NodeVertex)edge.getSink()); + } + + @Override + public boolean resolve(NodeVertex node) { + return tryResolve((NodeVertex)node); + } + + public ExecutionResult getResult() { + return new ExecutionResultImpl(result.get(0), executionContext.getErrors()); + } + }; + + private final ExecutionContext executionContext; + private final ExecutionStepInfoFactory executionInfoFactory; + private final ValueFetcher valueFetcher; + private final List result = Arrays.asList(new LinkedHashMap()); + + private static final Logger LOGGER = LoggerFactory.getLogger(DAGExecutionStrategy.class); +} diff --git a/src/main/java/graphql/execution3/DocumentVertex.java b/src/main/java/graphql/execution3/DocumentVertex.java new file mode 100644 index 0000000000..6cad54619b --- /dev/null +++ b/src/main/java/graphql/execution3/DocumentVertex.java @@ -0,0 +1,45 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import graphql.execution.ExecutionStepInfo; +import graphql.language.Document; +import graphql.schema.GraphQLType; +import java.util.Objects; + +/** + * ExecutionPlan Vertex created around Document node + */ +public class DocumentVertex extends NodeVertex { + public DocumentVertex(Document node) { + super(Objects.requireNonNull(node), null); + } + + @Override + public DocumentVertex parentExecutionStepInfo(ExecutionStepInfo parentExecutionStepInfo) { + return (DocumentVertex)super.parentExecutionStepInfo(parentExecutionStepInfo); + } + + @Override + public DocumentVertex executionStepInfo(ExecutionStepInfo value) { + return (DocumentVertex)super.executionStepInfo(value); + } + + @Override + public DocumentVertex source(Object source) { + return (DocumentVertex)super.source(source); + } + + @Override + public DocumentVertex result(Object result) { + return (DocumentVertex)super.result(result); + } + + @Override + U accept(U data, NodeVertexVisitor visitor) { + return (U)visitor.visit(this, data); + } +} diff --git a/src/main/java/graphql/execution3/Execution.java b/src/main/java/graphql/execution3/Execution.java new file mode 100644 index 0000000000..180c11f8c9 --- /dev/null +++ b/src/main/java/graphql/execution3/Execution.java @@ -0,0 +1,93 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.GraphQLError; +import graphql.execution.Async; +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionId; +import graphql.language.Document; +import graphql.schema.GraphQLSchema; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import static graphql.execution3.ExecutionPlan.newExecutionPlanBuilder; + +public class Execution { + /** + * Executes GraphQL request using {@link graphql.execution3.ExecutionStrategy} class + * to instantiate ExecutionStrategy + * + * @param strategyClass ExecutionStrategy class to instantiate + * @param document root of GraphQL AST + * @param schema GraphQL schema + * @param executionId assigned execution id + * @param executionInput input parameters, variables, etc. + * @return CompletableFuture that when completes holds result of this execution + */ + public CompletableFuture execute (Class strategyClass, Document document, + GraphQLSchema schema, ExecutionId executionId, ExecutionInput executionInput) { + return execute(executionContext -> newExecutionStrategy(strategyClass, executionContext), document, schema, executionId, executionInput); + } + + /** + * Executes GraphQL request using {@link graphql.execution3.ExecutionStrategy} class + * to instantiate ExecutionStrategy + * + * @param strategyCreator ExecutionStrategy factory + * @param document root of GraphQL AST + * @param schema GraphQL schema + * @param executionId assigned execution id + * @param executionInput input parameters, variables, etc. + * @return CompletableFuture that when completes holds result of this execution + */ + public CompletableFuture execute (Function strategyCreator, Document document, + GraphQLSchema schema, ExecutionId executionId, ExecutionInput executionInput) { + try { + return doExecute(strategyCreator, document, schema, executionId, executionInput); + } catch (RuntimeException rte) { + if (rte instanceof GraphQLError) + return CompletableFuture.completedFuture(new ExecutionResultImpl((GraphQLError) rte)); + + return Async.exceptionallyCompletedFuture(rte); + } + } + + private CompletableFuture doExecute (Function strategyCreator, Document document, + GraphQLSchema schema, ExecutionId executionId, ExecutionInput executionInput) { + ExecutionPlan executionPlan = newExecutionPlanBuilder() + .schema(schema) + .document(document) + .operation(executionInput.getOperationName()) + .variables(executionInput.getVariables()) + .build(); + + ExecutionContext executionContext = executionPlan + .newExecutionContextBuilder(executionInput.getOperationName()) + .root(executionInput.getRoot()) + .context(executionInput.getContext()) + .dataLoaderRegistry(executionInput.getDataLoaderRegistry()) + .executionId(executionId) + .build(); + + return strategyCreator + .apply(executionContext) + .execute(executionPlan); + } + + private static ExecutionStrategy newExecutionStrategy (Class strategyClass, ExecutionContext executionContext) { + try { + return strategyClass + .getConstructor(ExecutionContext.class) + .newInstance(executionContext); + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/graphql/execution3/ExecutionPlan.java b/src/main/java/graphql/execution3/ExecutionPlan.java new file mode 100644 index 0000000000..36c4f0d1dc --- /dev/null +++ b/src/main/java/graphql/execution3/ExecutionPlan.java @@ -0,0 +1,543 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import graphql.execution.ConditionalNodes; +import graphql.execution.ExecutionContextBuilder; +import graphql.execution.FieldCollector; +import graphql.execution.FieldCollectorParameters; +import graphql.execution.UnknownOperationException; +import graphql.execution.ValuesResolver; +import static graphql.execution.nextgen.Common.getOperationRootType; +import graphql.introspection.Introspection; +import graphql.language.Document; +import graphql.language.Field; +import graphql.language.FragmentDefinition; +import graphql.language.FragmentSpread; +import graphql.language.InlineFragment; +import graphql.language.Node; +import graphql.language.NodeTraverser; +import graphql.language.NodeVisitorStub; +import graphql.language.OperationDefinition; +import graphql.language.SelectionSet; +import graphql.language.VariableDefinition; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLFieldsContainer; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeUtil; +import graphql.util.DefaultTraverserContext; +import graphql.util.DependenciesIterator; +import graphql.util.DependencyGraph; +import graphql.util.DependencyGraphContext; +import graphql.util.Edge; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; +import graphql.util.TraverserState; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ExecutionPlan extends DependencyGraph> { + /** + * Retrieves GraphQL schema associated with this execution plan + * + * @return GraphQL schema + */ + public GraphQLSchema getSchema() { + return schema; + } + + /** + * Retrieves GraphQL AST root Node associated with this execution plan + * + * @return AST root + */ + public Document getDocument() { + return document; + } + + /** + * Locates an OperationDefinition corresponding to the specified openationName + * https://facebook.github.io/graphql/June2018/#sec-Executing-Requests + * + * @param operationName name of operation to execute, could be null + * @return OperationDefinition for the specified operation name + * @throws UnknownOperationException if OperationDefinition could not be found + */ + public OperationDefinition getOperation (String operationName) { + if (operationName == null && operationsByName.size() > 1) + throw new UnknownOperationException("Must provide operation name if query contains multiple operations."); + + return Optional + .ofNullable(operationsByName.get(operationName)) + .orElseThrow(() -> new UnknownOperationException(String.format("Unknown operation named '%s'.", operationName))); + } + + /** + * Retrieves all OperationDefinitions that exist in the document keyed off + * their operation names + * + * @return a map containing OperationDefinitions + */ + public Map getOperationsByName() { + return Collections.unmodifiableMap(operationsByName); + } + + /** + * Retrieves all FragmentDefinitions that exist in the document keyed off + * their fragment names + * + * @return a map containing FragmentDefinitions + */ + public Map getFragmentsByName() { + return Collections.unmodifiableMap(fragmentsByName); + } + + /** + * After the ExecutionPLan is built, retrieves coerced variables for the selected operation + * + * @return variables map + */ + public Map getVariables() { + return Collections.unmodifiableMap(variables); + } + + protected void prepareResolve (ExecutionPlanContext context, Edge, ?> edge) { + context.prepareResolve(edge); + } + + protected void whenResolved (ExecutionPlanContext context, Edge, ?> edge) { + context.whenResolved(edge); + } + + @Override + public DependenciesIterator> orderDependencies(DependencyGraphContext context) { + return new IteratorImpl(context); + } + + private class IteratorImpl extends DependenciesIteratorImpl { + public IteratorImpl(DependencyGraphContext context) { + super(context); + } + + @Override + public synchronized void close(NodeVertex node) { + super.close(node); + } + + @Override + public synchronized void close(Collection> resolvedSet) { + super.close(resolvedSet); + } + + } + + /** + * Creates and initializes ExecutionContextBuilder + * + * @param operationName selected operation name for this execution + * @return new ExecutionContextBuilder + */ + public ExecutionContextBuilder newExecutionContextBuilder (String operationName) { + return ExecutionContextBuilder.newExecutionContextBuilder() + .graphQLSchema(schema) + .document(document) + .fragmentsByName(fragmentsByName) + .variables(variables) + .operationDefinition(getOperation(operationName)); + } + + static Builder newExecutionPlanBuilder () { + return new Builder(new ExecutionPlan()); + } + + static class Builder extends NodeVisitorStub { + private Builder (ExecutionPlan executionPlan) { + this.executionPlan = Objects.requireNonNull(executionPlan); + } + + public Builder schema (GraphQLSchema schema) { + executionPlan.schema = Objects.requireNonNull(schema); + return this; + } + + public Builder document (Document document) { + executionPlan.document = Objects.requireNonNull(document); + + // to optimize a bit on searching for operations and fragments, + // let's re-organize this a little + // FIXME: re-organize Document node to keep operations and fragments indexed + executionPlan.operationsByName = new HashMap<>(); + executionPlan.fragmentsByName = new HashMap<>(); + + document + .getDefinitions() + .forEach(definition -> NodeTraverser.oneVisitWithResult(definition, new NodeVisitorStub() { + @Override + public TraversalControl visitOperationDefinition(OperationDefinition node, TraverserContext context) { + executionPlan.operationsByName.put(node.getName(), node); + return TraversalControl.QUIT; + } + + @Override + public TraversalControl visitFragmentDefinition(FragmentDefinition node, TraverserContext context) { + executionPlan.fragmentsByName.put(node.getName(), node); + return TraversalControl.QUIT; + } + })); + + return this; + } + + /** + * Adds an operation to the list of selected operations + * + * @param operationName operation name to select + * @return this instance + * @throws {@link graphql.execution.UnknownOperationException} in case the operation is not found + */ + public Builder operation (String operationName) { + operations.add(executionPlan.getOperation(operationName)); + return this; + } + + /** + * Stores variables for the coercion. + * Note! Coercion happens only at the time of building the execution plan + * + * @param variables variables map + * @return this instance + */ + public Builder variables (Map variables) { + executionPlan.variables = Objects.requireNonNull(variables); + return this; + } + + private List getChildrenOf (Node node) { + return NodeTraverser.oneVisitWithResult(node, new NodeVisitorStub() { + @Override + public TraversalControl visitFragmentSpread(FragmentSpread node, TraverserContext context) { + Collection children = Optional + .ofNullable(executionPlan.fragmentsByName.get(node.getName())) + .map(Node::getChildren) + // per https://facebook.github.io/graphql/June2018/#sec-Field-Collection d.v. + .orElseGet(Collections::emptyList); + + context.setAccumulate(children); + return TraversalControl.QUIT; + } + + @Override + protected TraversalControl visitNode(Node node, TraverserContext context) { + context.setAccumulate(node.getChildren()); + return TraversalControl.QUIT; + } + }); + } + + /** + * Builds execution plan according to the input query document, + * GraphQL schema, selected operations and provided variables + * + * @return execution plan to execute the query document + */ + public ExecutionPlan build () { + Objects.requireNonNull(executionPlan.schema); + + // fix default operation if wasn't provided + if (operations.isEmpty()) + operation(null); + + // validate variables against all selected operations + // Note! coerceArgumentValues throws a RuntimeException to be handled later + ValuesResolver valuesResolver = new ValuesResolver(); + List variableDefinitions = operations + .stream() + .flatMap(od -> od.getVariableDefinitions().stream()) + .collect(Collectors.toList()); + executionPlan.variables = valuesResolver.coerceArgumentValues(executionPlan.schema, variableDefinitions, executionPlan.variables); + + // make DocumentVertex the universal source in this graph in order to bootstrap + // executions in a standartized way through regular DependencyGraph sorting callback mechanizm + DocumentVertex documentVertex = executionPlan().addNode(new DocumentVertex(executionPlan.document)); + // walk Operations ASTs to record dependencies between fields + NodeTraverser traverser = new NodeTraverser(this::getChildrenOf, documentVertex); + traverser.depthFirst(this, operations, Builder::newTraverserState); + + return executionPlan; + } + + private static TraverserState.StackTraverserState newTraverserState (Object initialData) { + return TraverserState.newStackState(initialData, Builder::newTraverserContext); + } + + private static DefaultTraverserContext newTraverserContext (TraverserState.TraverserContextBuilder builder) { + Objects.requireNonNull(builder); + + TraverserContext parent = builder.getParentContext(); + Map, Object> vars = builder.getVars(); + + return new DefaultTraverserContext( + builder.getNode(), + parent, + builder.getVisited(), + vars, + builder.getSharedContextData(), + builder.getNodeLocation(), + builder.isRootContext()) { + Object accumulate; + + @Override + public S getVar(Class key) { + return (S)vars.computeIfAbsent(key, k -> Optional + .ofNullable(parent) + .map(p -> p.getVar((Class)k)) + .orElse(null)); + } + + @Override + public TraverserContext setVar(Class key, S value) { + vars.put(key, value); + return this; + } + + @Override + public void setAccumulate(Object accumulate) { + this.accumulate = accumulate; + } + + @Override + public U getNewAccumulate() { + return (U)Optional + .ofNullable(accumulate) + .orElseGet(() -> accumulate = getParentAccumulate()); + } + }; + } + + private OperationVertex newOperationVertex (OperationDefinition operationDefinition) { + GraphQLObjectType operationType = getOperationRootType(executionPlan.schema, operationDefinition); + return new OperationVertex(operationDefinition, operationType); + } + + private FieldVertex newFieldVertex (Field field, GraphQLFieldsContainer parentType, FieldVertex inScopeOf) { + GraphQLFieldDefinition fieldDefinition = Introspection.getFieldDef(executionPlan.schema, parentType, field.getName()); + return new FieldVertex(field, fieldDefinition.getType(), parentType, inScopeOf); + } + + private > DependencyGraph executionPlan () { + return (DependencyGraph)executionPlan; + } + + private boolean isFieldVertex (NodeVertex vertex) { + return NodeVertexVisitor.whenFieldVertex(vertex, false, field -> true); + } + + // NodeVisitor methods + + @Override + public TraversalControl visitOperationDefinition(OperationDefinition node, TraverserContext context) { + switch (context.getVar(NodeTraverser.LeaveOrEnter.class)) { + case ENTER: { + OperationVertex vertex = executionPlan().addNode(newOperationVertex(node)); + // make OperationVertex available for any time access to any of its subordinates + context.setVar(OperationVertex.class, vertex); + + // propagate my parent vertex to my children + context.setAccumulate(vertex); + break; + } + case LEAVE: { + // In order to simplify dependency management between operations, + // clear indegrees in this OperationVertex + // This will make this vertex the ultimate sink in this sub-graph + // In order to simplify propagation of the initial root value to the fields, + // add disconnected field vertices as dependencies to the root DocumentVertex + DocumentVertex documentVertex = (DocumentVertex)context.getSharedContextData(); + OperationVertex operationVertex = (OperationVertex)context.getNewAccumulate(); + // make this operation a dependency of a document to allow + // standard bootstrapping. + operationVertex.asNodeVertex().dependsOn(documentVertex.asNodeVertex(), executionPlan::prepareResolve); + // change dependencies order by moving all field dependencies from + // this operation vertex up to document vertex that will provide + // a standard bootstrapping. + // the dependencies order will looke like: + // field_1 + // field_2 + // document <-- { field_3 } <-- operation + // ... + // field_N + // this will allow to easy add dependencies between operations, since + // an operation now will become "resolved" only after all its fields + // are resolved + operationVertex + .adjacencySet() + .stream() + .filter(this::isFieldVertex) + .map(v -> v.as(FieldVertex.class).root(true)) + .forEach(v -> + v.asNodeVertex().undependsOn( + operationVertex.asNodeVertex(), + edge -> + // record a dependency on document + // but execute an action bound to the operation->field edge + // field bootstrap will be available via overarching operation + v.asNodeVertex().dependsOn( + documentVertex.asNodeVertex(), + (ExecutionPlanContext ctx, Edge e) -> executionPlan.prepareResolve(ctx, edge) + )) + ); + + break; + } + } + + return TraversalControl.CONTINUE; + } + + @Override + public TraversalControl visitSelectionSet(SelectionSet node, TraverserContext context) { + switch (context.getVar(NodeTraverser.LeaveOrEnter.class)) { + case ENTER: { + NodeVertex parentVertex = (NodeVertex)context.getParentAccumulate(); + + // set up parameters to collect child fields + FieldCollectorParameters collectorParams = FieldCollectorParameters.newParameters() + .schema(executionPlan.schema) + .objectType((GraphQLObjectType)GraphQLTypeUtil.unwrapAll(parentVertex.getType())) + .fragments(executionPlan.fragmentsByName) + .variables(executionPlan.variables) + .build(); + context.setVar(FieldCollectorParameters.class, collectorParams); + } + } + + return TraversalControl.CONTINUE; + } + + @Override + public TraversalControl visitInlineFragment(InlineFragment node, TraverserContext context) { + switch (context.getVar(NodeTraverser.LeaveOrEnter.class)) { + case ENTER: { + if (!FIELD_COLLECTOR.shouldCollectInlineFragment(this, node, context.getVar(FieldCollectorParameters.class))) + return TraversalControl.ABORT; + } + } + + return TraversalControl.CONTINUE; + } + + @Override + public TraversalControl visitFragmentSpread(FragmentSpread node, TraverserContext context) { + switch (context.getVar(NodeTraverser.LeaveOrEnter.class)) { + case ENTER: { + if (!FIELD_COLLECTOR.shouldCollectFragmentSpread(this, node, context.getVar(FieldCollectorParameters.class))) + return TraversalControl.ABORT; + } + } + + return TraversalControl.CONTINUE; + } + + @Override + public TraversalControl visitField(Field node, TraverserContext context) { + switch (context.getVar(NodeTraverser.LeaveOrEnter.class)) { + case ENTER: { + if (!FIELD_COLLECTOR.shouldCollectField(this, node)) + return TraversalControl.ABORT; + + // create a vertex for this node and add dependency on the parent one + TraverserContext parentContext = context.getParentContext(); + NodeVertex parentVertex = (NodeVertex)parentContext.getNewAccumulate(); + + FieldVertex vertex = executionPlan().addNode( + newFieldVertex(node, (GraphQLFieldsContainer)GraphQLTypeUtil.unwrapAll(parentVertex.getType()), parentContext.getVar(FieldVertex.class)) + ); + + // Note! the ordering of the below dependencies is important: + // 1. complete previous resolve + // 2. prepare to the next resolve + OperationVertex operationVertex = context.getVar(OperationVertex.class); + // action in this dependency will be executed when this vertex is resolved + operationVertex.asNodeVertex().dependsOn(vertex.asNodeVertex(), + (ExecutionPlanContext ctx, Edge e) -> executionPlan.whenResolved(ctx, new NodeEdge(vertex, parentVertex))); + + // action in this dependency will be executed right after the source had been resolve + // in order to prepare to the next resolve + vertex.asNodeVertex().dependsOn(parentVertex.asNodeVertex(), executionPlan::prepareResolve); + + // propagate current scope further to children + if (node.getAlias() != null) + context.setVar(FieldVertex.class, vertex); + + // propagate my vertex to my children + context.setAccumulate(vertex); + } + } + + return TraversalControl.CONTINUE; + } + + private static class FieldCollectorHelper extends FieldCollector { + boolean shouldCollectField (Builder outer, Field node) { + if (!conditionalNodes.shouldInclude(outer.executionPlan.variables, node.getDirectives())) + return false; + + return true; + } + + boolean shouldCollectInlineFragment (Builder outer, InlineFragment node, FieldCollectorParameters collectorParams) { + if (!conditionalNodes.shouldInclude(outer.executionPlan.variables, node.getDirectives())) + return false; + + if (!doesFragmentConditionMatch(collectorParams, node)) + return false; + + return true; + } + + boolean shouldCollectFragmentSpread (Builder outer, FragmentSpread node, FieldCollectorParameters collectorParams) { + if (!conditionalNodes.shouldInclude(outer.executionPlan.variables, node.getDirectives())) + return false; + + FragmentDefinition fragmentDefinition = outer.executionPlan.fragmentsByName.get(node.getName()); + if (!conditionalNodes.shouldInclude(outer.executionPlan.variables, fragmentDefinition.getDirectives())) + return false; + + if (!doesFragmentConditionMatch(collectorParams, fragmentDefinition)) + return false; + + return true; + } + + private final ConditionalNodes conditionalNodes = new ConditionalNodes(); + } + + private final ExecutionPlan executionPlan; + private final Collection operations = new ArrayList<>(); + + private static final FieldCollectorHelper FIELD_COLLECTOR = new FieldCollectorHelper(); + } + + private /*final*/ GraphQLSchema schema; + private /*final*/ Document document; + private /*final*/ Map operationsByName = Collections.emptyMap(); + private /*final*/ Map fragmentsByName = Collections.emptyMap(); + private /*final*/ Map variables = Collections.emptyMap(); + + private static final Logger LOGGER = LoggerFactory.getLogger(Builder.class); +} diff --git a/src/main/java/graphql/execution3/ExecutionPlanContext.java b/src/main/java/graphql/execution3/ExecutionPlanContext.java new file mode 100644 index 0000000000..93443db7e5 --- /dev/null +++ b/src/main/java/graphql/execution3/ExecutionPlanContext.java @@ -0,0 +1,37 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import graphql.language.Node; +import graphql.schema.GraphQLType; +import graphql.util.DependencyGraphContext; +import graphql.util.Edge; + +public interface ExecutionPlanContext extends DependencyGraphContext { + /** + * A callback method called when ExecutionPlan needs to prepare edge vertices for execution. + * For instance, propagate & transform results from source vertex to become sources in sink vertex + * + * @param edge the edge being followed (source -- to --> sink) + */ + void prepareResolve (Edge, ?> edge); + /** + * A callback method called when ExecutionPlan has finished resolving the edge. + * Resolution results are stored in the edge source vertex results + * Edge sink is a vertex that may accumulate the overall result. + * + * @param edge the edge being followed + */ + void whenResolved (Edge, ?> edge); + /** + * A callback method called when ExecutionPlan attempts to auto resolve a vertex, + * i.e. without invoking external data fetcher + * + * @param node vertex to attempt to auto resolve + * @return {@code true} if auto resolved, {@code false} otherwise + */ + boolean resolve (NodeVertex node); +} diff --git a/src/main/java/graphql/execution3/ExecutionStrategy.java b/src/main/java/graphql/execution3/ExecutionStrategy.java new file mode 100644 index 0000000000..dad1a791c3 --- /dev/null +++ b/src/main/java/graphql/execution3/ExecutionStrategy.java @@ -0,0 +1,22 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import graphql.ExecutionResult; +import java.util.concurrent.CompletableFuture; + +public interface ExecutionStrategy { + /** + * Executes a graphql request according to the schedule + * provided by executionPlan + * + * @param executionPlan a {@code graphql.util.DependencyGraph} specialization that provides + * order of field resolution requests + * + * @return a CompletableFuture holding the result of execution. + */ + CompletableFuture execute (ExecutionPlan executionPlan); +} diff --git a/src/main/java/graphql/execution3/FieldVertex.java b/src/main/java/graphql/execution3/FieldVertex.java new file mode 100644 index 0000000000..65b365fac6 --- /dev/null +++ b/src/main/java/graphql/execution3/FieldVertex.java @@ -0,0 +1,227 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import graphql.execution.ExecutionStepInfo; +import graphql.language.Field; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLFieldsContainer; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLModifiedType; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeVisitorStub; +import graphql.schema.TypeTraverser; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; +import java.util.Objects; +import java.util.Optional; + +/** + * A vertex wrapping Field AST node + */ +public class FieldVertex extends NodeVertex { + public FieldVertex(Field node, GraphQLOutputType type, GraphQLFieldsContainer definedIn) { + this(node, type, definedIn, null); + } + + public FieldVertex(Field node, GraphQLOutputType type, GraphQLFieldsContainer definedIn, NodeVertex inScopeOf) { + super(Objects.requireNonNull(node), Objects.requireNonNull(type)); + + Object[] results = {Kind.Object, Cardinality.OneToOne, false/*not null*/, false/*not null elements*/}; + TypeTraverser.oneVisitWithResult(type, new GraphQLTypeVisitorStub() { + @Override + public TraversalControl visitGraphQLModifiedType(GraphQLModifiedType node, TraverserContext context) { + return node.getWrappedType().accept(context, this); + } + + @Override + public TraversalControl visitGraphQLNonNull(GraphQLNonNull node, TraverserContext context) { + results[results[1] == Cardinality.OneToOne ? 2 : 3] = true; + return super.visitGraphQLNonNull(node, context); + } + + @Override + public TraversalControl visitGraphQLList(GraphQLList node, TraverserContext context) { + results[1] = Cardinality.OneToMany; + return super.visitGraphQLList(node, context); + } + + @Override + public TraversalControl visitGraphQLScalarType(GraphQLScalarType node, TraverserContext context) { + results[0] = Kind.Scalar; + return super.visitGraphQLType(node, context); + } + + @Override + public TraversalControl visitGraphQLEnumType(GraphQLEnumType node, TraverserContext context) { + results[0] = Kind.Enum; + return super.visitGraphQLType(node, context); + } + }); + + this.kind = (Kind)results[0]; + this.cardinality = (Cardinality)results[1]; + this.notNull = (boolean)results[2]; + this.notNullItems = (boolean)results[3]; + this.definedIn = Objects.requireNonNull(definedIn); + this.inScopeOf = inScopeOf; + } + + /** + * Parent container where this Field is defined + * + * @return parent GraphQL type + */ + public GraphQLFieldsContainer getDefinedIn() { + return definedIn; + } + + /** + * If a direct or indirect parent (source) of this vertex has alias, + * retrieves that aliased source. + * + * @return aliased direct or indirect aliased source of this vertex + */ + public Object getInScopeOf() { + return inScopeOf; + } + + /** + * Retrieves result of analysis on the kind of object this Field represents + * + * @return analysis result of this Field + */ + public Kind getKind() { + return kind; + } + + /** + * Retrieves cardinality of this Field + * + * @return cardinality analysis result + */ + public Cardinality getCardinality() { + return cardinality; + } + + /** + * Verifies if this Field value must not be null. + * If this constraint is not satisfied, the parent result is null. + * + * @return "not null" constraint for this field + */ + public boolean isNotNull() { + return notNull; + } + + /** + * Verifies if this Field element value (if this Field is a GraphQLList) must not be null. + * If this constraint is not satisfied, the result for the entire list is null. + * + * @return "not null" constraint for a list item + */ + public boolean isNotNullItems() { + return notNullItems; + } + + /** + * Key to store this field in the parent result + * https://facebook.github.io/graphql/June2018/#CollectFields() + * + * @return response key + */ + public String getResponseKey () { + return Optional + .ofNullable(node.getAlias()) + .orElseGet(node::getName); + } + + @Override + public FieldVertex executionStepInfo(ExecutionStepInfo value) { + return (FieldVertex)super.executionStepInfo(value); + } + + @Override + public FieldVertex source(Object source) { + return (FieldVertex)super.source(source); + } + + @Override + public FieldVertex result(Object result) { + return (FieldVertex)super.result(result); + } + + @Override + public FieldVertex parentExecutionStepInfo(ExecutionStepInfo value) { + return (FieldVertex)super.parentExecutionStepInfo(value); + } + + public boolean isRoot () { + return root; + } + + public FieldVertex root (boolean value) { + this.root = value; + return this; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + Objects.hashCode(this.node.getName()); + hash = 97 * hash + Objects.hashCode(this.node.getAlias()); + hash = 97 * hash + Objects.hashCode(this.type); + hash = 97 * hash + Objects.hashCode(this.definedIn); + hash = 97 * hash + Objects.hashCode(this.inScopeOf); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (super.equals(obj)) { + FieldVertex other = (FieldVertex)obj; + return Objects.equals(this.definedIn, other.definedIn) && + Objects.equals(this.inScopeOf, other.inScopeOf); + } + + return false; + } + + @Override + protected StringBuilder toString(StringBuilder builder) { + return super + .toString(builder) + .append(", definedIn=").append(definedIn) + .append(", inScopeOf=").append(inScopeOf); + } + + @Override + U accept(U data, NodeVertexVisitor visitor) { + return (U)visitor.visit(this, data); + } + + public enum Kind { + Scalar, + Enum, + Object + } + + public enum Cardinality { + OneToOne, + OneToMany + } + + private final Kind kind; + private final Cardinality cardinality; + private final boolean notNull; + private final boolean notNullItems; + private final GraphQLFieldsContainer definedIn; + private final Object inScopeOf; + private /*final*/ boolean root = false; +} diff --git a/src/main/java/graphql/execution3/NodeEdge.java b/src/main/java/graphql/execution3/NodeEdge.java new file mode 100644 index 0000000000..c7c9bf61c7 --- /dev/null +++ b/src/main/java/graphql/execution3/NodeEdge.java @@ -0,0 +1,25 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import graphql.language.Node; +import graphql.schema.GraphQLType; +import graphql.util.DependencyGraphContext; +import graphql.util.Edge; +import java.util.function.BiConsumer; + +/** + * A specialization of Edge class used occasionally wen building ExecutionPlan + */ +public class NodeEdge extends Edge, NodeEdge> { + public > NodeEdge(N source, N sink) { + super((NodeVertex)source, (NodeVertex)sink); + } + + public > NodeEdge(N source, N sink, BiConsumer action) { + super((NodeVertex)source, (NodeVertex)sink, action); + } +} diff --git a/src/main/java/graphql/execution3/NodeVertex.java b/src/main/java/graphql/execution3/NodeVertex.java new file mode 100644 index 0000000000..abdcb59622 --- /dev/null +++ b/src/main/java/graphql/execution3/NodeVertex.java @@ -0,0 +1,195 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import graphql.execution.ExecutionStepInfo; +import graphql.language.Node; +import graphql.schema.GraphQLType; +import graphql.util.DependencyGraphContext; +import graphql.util.Vertex; +import java.util.Objects; + +/** + * A base class for ExecutionPlan vertices + * + * @param actual type of Node associated with this Vertex + * @param GraphQLType from the GraphQLSchema dictionary that describes the Node + */ +public abstract class NodeVertex extends Vertex> { + protected NodeVertex (N node, T type) { + this.node = node; + this.type = type; + } + + /** + * Retrieves AST node associated with the vertex. + * + * @return AST node for this vertex + */ + public N getNode() { + return node; + } + + /** + * Retrieves GraphQL type if any associated with the AST node + * + * @return GraphQLType for this vertex + */ + public T getType() { + return type; + } + + /** + * A shortcut to the parent (source) vertex ExecutionStepInfo + * + * @return parent's ExecutionStepInfo + */ + public ExecutionStepInfo getParentExecutionStepInfo() { + return parentExecutionStepInfo; + } + + /** + * A shortcut to the parent (source) vertex ExecutionStepInfo + * + * @param parentExecutionStepInfo new value + * @return parent's ExecutionStepInfo + */ + public NodeVertex parentExecutionStepInfo(ExecutionStepInfo parentExecutionStepInfo) { + this.parentExecutionStepInfo = parentExecutionStepInfo; + return this; + } + + /** + * ExecutionStepInfo associated with this vertex + * + * @return this vertex ExecutionStepInfo + */ + public ExecutionStepInfo getExecutionStepInfo () { + return executionStepInfo; + } + + /** + * ExecutionStepInfo associated with this vertex + * + * @param value new value + * @return this vertex ExecutionStepInfo + */ + public NodeVertex executionStepInfo (ExecutionStepInfo value) { + this.executionStepInfo = Objects.requireNonNull(value); + return this; + } + + /** + * A transformed result of parent (source) execution. + * Transformation is necessary to filter out results that certainly + * cannot be resolved and then joined. + * So this property contains source results filtered out nulls + * + * @return source objects to use when resolving this vertex + */ + public Object getSource() { + return source; + } + + /** + * A transformed result of parent (source) execution.Transformation is necessary + * to filter out results that certainly cannot be resolved and then joined. + * So this property contains source results filtered out nulls + * + * @param source new value + * @return source objects to use when resolving this vertex + */ + public NodeVertex source(Object source) { + this.source = source; + return this; + } + + /** + * Contains results of this vertex resolution + * + * @return this vertex results + */ + public Object getResult() { + return result; + } + + /** + * Contains results of this vertex resolution + * + * @param result new value + * @return this vertex results + */ + public NodeVertex result(Object result) { + this.result = result; + return this; + } + + @Override + public final boolean resolve(DependencyGraphContext context) { + return ((ExecutionPlanContext)context).resolve(this); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + Objects.hashCode(this.node); + hash = 97 * hash + Objects.hashCode(this.type); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final NodeVertex other = (NodeVertex) obj; + if (!equals(this.node, other.node)) { + return false; + } + if (!Objects.equals(this.type, other.type)) { + return false; + } + return true; + } + + private static boolean equals (Node thisNode, Node otherNode) { + return thisNode.isEqualTo(otherNode); + } + + @Override + protected StringBuilder toString(StringBuilder builder) { + return super + .toString(builder) + .append(", node=").append(node) + .append(", type=").append(type); + } + + > U as (Class castTo) { + if (castTo.isAssignableFrom(getClass())) + return (U)castTo.cast(this); + + throw new IllegalArgumentException(String.format("could not cast to '%s'", castTo.getName())); + } + + NodeVertex asNodeVertex () { + return (NodeVertex)this; + } + + abstract U accept (U data, NodeVertexVisitor visitor); + + protected final N node; + protected final T type; + protected /*final*/ ExecutionStepInfo parentExecutionStepInfo; + protected /*final*/ ExecutionStepInfo executionStepInfo; + protected /*final*/ Object source; + protected /*final*/ Object result; +} diff --git a/src/main/java/graphql/execution3/NodeVertexVisitor.java b/src/main/java/graphql/execution3/NodeVertexVisitor.java new file mode 100644 index 0000000000..d570bcf6ca --- /dev/null +++ b/src/main/java/graphql/execution3/NodeVertexVisitor.java @@ -0,0 +1,65 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import graphql.language.Node; +import graphql.schema.GraphQLType; +import java.util.Objects; +import java.util.function.Function; + +interface NodeVertexVisitor { + default U visit (OperationVertex node, U data) { + return visitNode(node, data); + } + + default U visit (FieldVertex node, U data) { + return visitNode(node, data); + } + + default U visit (DocumentVertex node, U data) { + return visitNode(node, data); + } + + default U visitNode (NodeVertex vertex, U data) { + return data; + } + + static U whenOperationVertex (NodeVertex node, U data, Function when) { + Objects.requireNonNull(node); + Objects.requireNonNull(when); + + return node.accept(data, new NodeVertexVisitor() { + @Override + public U visit(OperationVertex node, U data) { + return when.apply(node); + } + }); + } + + static U whenFieldVertex (NodeVertex node, U data, Function when) { + Objects.requireNonNull(node); + Objects.requireNonNull(when); + + return node.accept(data, new NodeVertexVisitor() { + @Override + public U visit(FieldVertex node, U data) { + return when.apply(node); + } + }); + } + + static U whenDocumentVertex (NodeVertex node, U data, Function when) { + Objects.requireNonNull(node); + Objects.requireNonNull(when); + + return node.accept(data, new NodeVertexVisitor() { + @Override + public U visit(DocumentVertex node, U data) { + return when.apply(node); + } + }); + } +} diff --git a/src/main/java/graphql/execution3/OperationVertex.java b/src/main/java/graphql/execution3/OperationVertex.java new file mode 100644 index 0000000000..8c60502409 --- /dev/null +++ b/src/main/java/graphql/execution3/OperationVertex.java @@ -0,0 +1,54 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import graphql.execution.ExecutionStepInfo; +import graphql.language.OperationDefinition; +import graphql.schema.GraphQLObjectType; +import java.util.Objects; + +/** + * ExecutionPlan vertex created around OperationDefinition + */ +public class OperationVertex extends NodeVertex { + public OperationVertex(OperationDefinition node, GraphQLObjectType type) { + super(Objects.requireNonNull(node), Objects.requireNonNull(type)); + } + + @Override + public OperationVertex parentExecutionStepInfo(ExecutionStepInfo parentExecutionStepInfo) { + return (OperationVertex)super.parentExecutionStepInfo(parentExecutionStepInfo); + } + + @Override + public OperationVertex executionStepInfo(ExecutionStepInfo value) { + return (OperationVertex)super.executionStepInfo(value); + } + + @Override + public OperationVertex source(Object source) { + return (OperationVertex)super.source(source); + } + + @Override + public OperationVertex result(Object result) { + return (OperationVertex)super.result(result); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + Objects.hashCode(this.node.getName()); + hash = 97 * hash + Objects.hashCode(this.node.getOperation()); + hash = 97 * hash + Objects.hashCode(this.type); + return hash; + } + + @Override + U accept(U data, NodeVertexVisitor visitor) { + return (U)visitor.visit(this, data); + } +} diff --git a/src/main/java/graphql/execution3/Results.java b/src/main/java/graphql/execution3/Results.java new file mode 100644 index 0000000000..e83036c877 --- /dev/null +++ b/src/main/java/graphql/execution3/Results.java @@ -0,0 +1,205 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.execution3; + +import static graphql.Assert.assertNotNull; +import graphql.execution.nextgen.ValueFetcher; +import static graphql.execution3.NodeVertexVisitor.whenFieldVertex; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Helper to manipulate with fetched data and collect/join results + */ +public class Results { + private Results () { + // disable instantiation + } + + /** + * Joins results of the provided vertex to the sources (parent results) + * + * @param node a resolved Field vertex + */ + public static void joinResultsOf (FieldVertex node) { + assertNotNull(node); + + String on = node.getResponseKey(); + List sources = (List)node.getSource(); + List results = Optional + .ofNullable((List)node.getResult()) + .orElseGet(Collections::emptyList); + + // will join relation dataset to target dataset + // since we don't have any keys to build a cartesian product, + // will be assuming that relation and target dataset s are alerady sorted by the + // same key, so a pair {target[i], relation[i]} represents that cartesian product + // we can use to join them together + int index[] = {0}; + results + .stream() + .limit(Math.min(sources.size(), results.size())) + .forEach(value -> { + Map targetMap = (Map)sources.get(index[0]++); + targetMap.put(on, value); + + // Here, verify if the value to join is valid + if (value == null && node.isNotNull()) { + // need to set the paren't corresponding value to null + bubbleUpNIL(node, on); + } + }); + } + + private static void bubbleUpNIL (FieldVertex node, String responseKey) { + node + .dependencySet() + .forEach(v -> { + boolean hasNull = false; + ListIterator results = asList(v.getResult()).listIterator(); + while (results.hasNext()) { + Object o = results.next(); + if (o == null) { + hasNull = true; + continue; + } + + boolean hasNullItems = false; + ListIterator items = asList(o).listIterator(); + while (items.hasNext()) { + Map value = (Map)items.next(); + + if (value.getOrDefault(responseKey, ValueFetcher.NULL_VALUE) == null) { + items.set(null); + hasNullItems = true; + } + } + + if (hasNullItems && whenFieldVertex(v, node.isNotNull(), FieldVertex::isNotNullItems)) { + results.set(null); + hasNull = true; + } + } + + if (hasNull) { + whenFieldVertex(v, null, n -> { + joinResultsOf(n); + return null; + }); + } + }); + }; + + /** + * Takes care of ValueFetcher.NULL_VALUE. + * Replaces them to nulls + * + * @param fetchedValue fetched result + * @param fieldNode destination Field vertex + * @return corrected value + */ + public static Object checkAndFixNILs (Object fetchedValue, FieldVertex fieldNode) { + return (isNIL(fetchedValue) || isOneToOne(fieldNode)) + ? fixNIL(fetchedValue, () -> null) + : fixNILs((List)fetchedValue, fieldNode); + } + + private static boolean isNIL (Object value) { + return value == null || value == ValueFetcher.NULL_VALUE; + } + + private static boolean isOneToOne (FieldVertex fieldNode) { + return fieldNode.getCardinality() == FieldVertex.Cardinality.OneToOne; + } + + private static Object fixNIL (Object fetchedValue, Supplier mapper) { + return (isNIL(fetchedValue)) + ? mapper.get() + : fetchedValue; + } + + private static Object fixNILs (List fetchedValues, FieldVertex fieldNode) { + boolean hasNulls[] = {false}; + fetchedValues = flatten(fetchedValues, o -> true) + .stream() + .map(o -> fixNIL(o, + () -> { + hasNulls[0] = true; + return null; + } + )) + .collect(Collectors.toList()); + + return (hasNulls[0] && fieldNode.isNotNullItems()) + ? null + : fetchedValues; + } + + /** + * "Flattens" possibly multi-dimensional list into a single-dimensional one + * filtering out {@code null} values. + * + * @param result multi-dimensional list + * @return single-dimensional list + */ + public static List flatten (List result) { + return flatten(result, o -> o != null); + } + + /** + * "Flattens" possibly multi-dimensional list into a single-dimensional one + * filtering out values that don't match provided predicate. + * + * @param filter predicate to filter out result elements + * @param result multi-dimensional list + * @return single-dimensional list + */ + public static List flatten (List result, Predicate filter) { + assertNotNull(filter); + + return Optional + .ofNullable(result) + .map(res -> res + .stream() + .flatMap(Results::asStream) + .filter(filter) + .collect(Collectors.toList()) + ) + .orElseGet(Collections::emptyList); + } + + private static Stream asStream (Object o) { + return (o instanceof Collection) + ? ((Collection)o) + .stream() + .flatMap(Results::asStream) + : Stream.of(o); + } + + private static List asList (Object o) { + return (o instanceof List) + ? (List)o + : Arrays.asList(o); + } + + private static Object asObject (Object o) { + List singletonList; + return (o instanceof List && (singletonList = (List)o).size() <= 1) + ? singletonList.size() == 1 + ? singletonList.get(0) + : null + : o; + } +} diff --git a/src/main/java/graphql/language/Document.java b/src/main/java/graphql/language/Document.java index 05c7ac5626..f2b78f6d9a 100644 --- a/src/main/java/graphql/language/Document.java +++ b/src/main/java/graphql/language/Document.java @@ -4,11 +4,11 @@ import graphql.Internal; import graphql.PublicApi; import graphql.util.TraversalControl; -import graphql.util.TraverserContext; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import graphql.util.TraverserContext; import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer; diff --git a/src/main/java/graphql/language/ListType.java b/src/main/java/graphql/language/ListType.java index 586c4d898e..4d0a69196c 100644 --- a/src/main/java/graphql/language/ListType.java +++ b/src/main/java/graphql/language/ListType.java @@ -4,11 +4,11 @@ import graphql.Internal; import graphql.PublicApi; import graphql.util.TraversalControl; -import graphql.util.TraverserContext; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import graphql.util.TraverserContext; import static graphql.language.NodeChildrenContainer.newNodeChildrenContainer; diff --git a/src/main/java/graphql/language/NodeTraverser.java b/src/main/java/graphql/language/NodeTraverser.java index 38fa2f3b13..db61dda0d5 100644 --- a/src/main/java/graphql/language/NodeTraverser.java +++ b/src/main/java/graphql/language/NodeTraverser.java @@ -4,7 +4,8 @@ import graphql.util.DefaultTraverserContext; import graphql.util.TraversalControl; import graphql.util.Traverser; -import graphql.util.TraverserContext; +import static graphql.util.Traverser.newTraverser; +import graphql.util.TraverserState; import graphql.util.TraverserVisitor; import java.util.Collection; @@ -12,6 +13,9 @@ import java.util.List; import java.util.Map; import java.util.function.Function; +import graphql.util.TraverserContext; +import graphql.util.TraverserState.QueueTraverserState; +import graphql.util.TraverserState.StackTraverserState; /** * Lets you traverse a {@link Node} tree. @@ -30,16 +34,29 @@ public enum LeaveOrEnter { private final Map, Object> rootVars; private final Function> getChildren; + private final Object initialData; - public NodeTraverser(Map, Object> rootVars, Function> getChildren) { + public NodeTraverser(Map, Object> rootVars, Function> getChildren, Object initialData) { this.rootVars = rootVars; this.getChildren = getChildren; + this.initialData = initialData; } - public NodeTraverser() { - this(Collections.emptyMap(), Node::getChildren); + public NodeTraverser(Map, Object> rootVars, Function> getChildren) { + this(rootVars, getChildren, null); } + public NodeTraverser(Function> getChildren, Object initialData) { + this(Collections.emptyMap(), getChildren, initialData); + } + + public NodeTraverser(Object initialData) { + this(Collections.emptyMap(), Node::getChildren, initialData); + } + + public NodeTraverser() { + this(null); + } /** * depthFirst traversal with a enter/leave phase. @@ -53,6 +70,19 @@ public Object depthFirst(NodeVisitor nodeVisitor, Node root) { return depthFirst(nodeVisitor, Collections.singleton(root)); } + /** + * depthFirst traversal with a enter/leave phase. + * + * @param nodeVisitor the visitor of the nodes + * @param root the root node + * @param stateCreator factory to create TraverserState instance + * + * @return the accumulation result of this traversal + */ + public Object depthFirst(NodeVisitor nodeVisitor, Node root, Function> stateCreator) { + return depthFirst(nodeVisitor, Collections.singleton(root), stateCreator); + } + /** * depthFirst traversal with a enter/leave phase. * @@ -62,8 +92,66 @@ public Object depthFirst(NodeVisitor nodeVisitor, Node root) { * @return the accumulation result of this traversal */ public Object depthFirst(NodeVisitor nodeVisitor, Collection roots) { - TraverserVisitor nodeTraverserVisitor = new TraverserVisitor() { + return depthFirst(nodeVisitor, roots, TraverserState::newStackState); + } + /** + * depthFirst traversal with a enter/leave phase. + * + * @param nodeVisitor the visitor of the nodes + * @param roots the root nodes + * @param stateCreator factory to create TraverserState instance + * + * @return the accumulation result of this traversal + */ + public Object depthFirst(NodeVisitor nodeVisitor, Collection roots, Function> stateCreator) { + return doTraverse(roots, decorate(nodeVisitor), stateCreator); + } + + /** + * breadthFirst traversal with a enter/leave phase. + * + * @param nodeVisitor the visitor of the nodes + * @param root the root node + */ + public void breadthFirst(NodeVisitor nodeVisitor, Node root) { + breadthFirst(nodeVisitor, Collections.singleton(root)); + } + + /** + * breadthFirst traversal with a enter/leave phase. + * + * @param nodeVisitor the visitor of the nodes + * @param root the root node + * @param stateCreator factory to create TraverserState instance + */ + public void breadthFirst(NodeVisitor nodeVisitor, Node root, Function> stateCreator) { + breadthFirst(nodeVisitor, Collections.singleton(root)); + } + + /** + * breadthFirst traversal with a enter/leave phase. + * + * @param nodeVisitor the visitor of the nodes + * @param roots the root nodes + */ + public void breadthFirst(NodeVisitor nodeVisitor, Collection roots) { + breadthFirst(nodeVisitor, roots, TraverserState::newQueueState); + } + + /** + * breadthFirst traversal with a enter/leave phase. + * + * @param nodeVisitor the visitor of the nodes + * @param roots the root nodes + * @param stateCreator factory to create TraverserState instance + */ + public void breadthFirst(NodeVisitor nodeVisitor, Collection roots, Function> stateCreator) { + doTraverse(roots, decorate(nodeVisitor), stateCreator); + } + + private static TraverserVisitor decorate (NodeVisitor nodeVisitor) { + return new TraverserVisitor() { @Override public TraversalControl enter(TraverserContext context) { context.setVar(LeaveOrEnter.class, LeaveOrEnter.ENTER); @@ -76,9 +164,8 @@ public TraversalControl leave(TraverserContext context) { return context.thisNode().accept(context, nodeVisitor); } }; - return doTraverse(roots, nodeTraverserVisitor); } - + /** * Version of {@link #preOrder(NodeVisitor, Collection)} with one root. * @@ -100,6 +187,19 @@ public Object preOrder(NodeVisitor nodeVisitor, Node root) { * @return the accumulation result of this traversal */ public Object preOrder(NodeVisitor nodeVisitor, Collection roots) { + return preOrder(nodeVisitor, roots, TraverserState::newStackState); + } + + /** + * Pre-Order traversal: This is a specialized version of depthFirst with only the enter phase. + * + * @param nodeVisitor the visitor of the nodes + * @param roots the root nodes + * @param stateCreator TraverserState factory + * + * @return the accumulation result of this traversal + */ + public Object preOrder(NodeVisitor nodeVisitor, Collection roots, Function> stateCreator) { TraverserVisitor nodeTraverserVisitor = new TraverserVisitor() { @Override @@ -115,6 +215,7 @@ public TraversalControl leave(TraverserContext context) { }; return doTraverse(roots, nodeTraverserVisitor); + } /** @@ -128,7 +229,7 @@ public TraversalControl leave(TraverserContext context) { public Object postOrder(NodeVisitor nodeVisitor, Node root) { return postOrder(nodeVisitor, Collections.singleton(root)); } - + /** * Post-Order traversal: This is a specialized version of depthFirst with only the leave phase. * @@ -138,6 +239,19 @@ public Object postOrder(NodeVisitor nodeVisitor, Node root) { * @return the accumulation result of this traversal */ public Object postOrder(NodeVisitor nodeVisitor, Collection roots) { + return postOrder(nodeVisitor, roots, TraverserState::newStackState); + } + + /** + * Post-Order traversal: This is a specialized version of depthFirst with only the leave phase. + * + * @param nodeVisitor the visitor of the nodes + * @param roots the root nodes + * @param stateCreator TraverserState factory + * + * @return the accumulation result of this traversal + */ + public Object postOrder(NodeVisitor nodeVisitor, Collection roots, Function> stateCreator) { TraverserVisitor nodeTraverserVisitor = new TraverserVisitor() { @Override @@ -152,11 +266,15 @@ public TraversalControl leave(TraverserContext context) { } }; - return doTraverse(roots, nodeTraverserVisitor); + return doTraverse(roots, nodeTraverserVisitor, stateCreator); } - private Object doTraverse(Collection roots, TraverserVisitor traverserVisitor) { - Traverser nodeTraverser = Traverser.depthFirst(this.getChildren); + private Object doTraverse(Collection roots, TraverserVisitor traverserVisitor) { + return doTraverse(roots, traverserVisitor, TraverserState::newStackState); + } + + private Object doTraverse(Collection roots, TraverserVisitor traverserVisitor, Function> stateCreator) { + Traverser nodeTraverser = newTraverser(stateCreator.apply(initialData), getChildren, null); nodeTraverser.rootVars(rootVars); return nodeTraverser.traverse(roots, traverserVisitor).getAccumulatedResult(); } diff --git a/src/main/java/graphql/schema/GraphQLTypeResolvingVisitor.java b/src/main/java/graphql/schema/GraphQLTypeResolvingVisitor.java index 313c88bde8..d3d1e62a1a 100644 --- a/src/main/java/graphql/schema/GraphQLTypeResolvingVisitor.java +++ b/src/main/java/graphql/schema/GraphQLTypeResolvingVisitor.java @@ -8,6 +8,7 @@ import java.util.stream.Collectors; import static graphql.Assert.assertNotNull; + import static graphql.util.TraversalControl.CONTINUE; @Internal diff --git a/src/main/java/graphql/schema/GraphQLTypeVisitorStub.java b/src/main/java/graphql/schema/GraphQLTypeVisitorStub.java index f3877c82ec..4b86ecdf56 100644 --- a/src/main/java/graphql/schema/GraphQLTypeVisitorStub.java +++ b/src/main/java/graphql/schema/GraphQLTypeVisitorStub.java @@ -2,9 +2,9 @@ import graphql.PublicApi; import graphql.util.TraversalControl; -import graphql.util.TraverserContext; import static graphql.util.TraversalControl.CONTINUE; +import graphql.util.TraverserContext; /** * Base implementation of {@link GraphQLTypeVisitor} for convenience. @@ -54,12 +54,12 @@ public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType node, @Override public TraversalControl visitGraphQLList(GraphQLList node, TraverserContext context) { - return visitGraphQLType(node, context); + return visitGraphQLModifiedType(node, context); } @Override public TraversalControl visitGraphQLNonNull(GraphQLNonNull node, TraverserContext context) { - return visitGraphQLType(node, context); + return visitGraphQLModifiedType(node, context); } @Override @@ -82,6 +82,11 @@ public TraversalControl visitGraphQLUnionType(GraphQLUnionType node, TraverserCo return visitGraphQLType(node, context); } + @Override + public TraversalControl visitGraphQLModifiedType (GraphQLModifiedType node, TraverserContext context) { + return visitGraphQLType(node, context); + } + protected TraversalControl visitGraphQLType(GraphQLType node, TraverserContext context) { return CONTINUE; } diff --git a/src/main/java/graphql/schema/TypeTraverser.java b/src/main/java/graphql/schema/TypeTraverser.java index c3aaea6eb3..5288233c55 100644 --- a/src/main/java/graphql/schema/TypeTraverser.java +++ b/src/main/java/graphql/schema/TypeTraverser.java @@ -2,9 +2,11 @@ import graphql.PublicApi; +import graphql.language.Node; +import graphql.language.NodeVisitor; +import graphql.util.DefaultTraverserContext; import graphql.util.TraversalControl; import graphql.util.Traverser; -import graphql.util.TraverserContext; import graphql.util.TraverserResult; import graphql.util.TraverserVisitor; @@ -15,6 +17,7 @@ import java.util.function.Function; import static graphql.util.TraversalControl.CONTINUE; +import graphql.util.TraverserContext; @PublicApi public class TypeTraverser { @@ -58,6 +61,13 @@ private TraverserResult doTraverse(Traverser traverser, Collection< return traverser.traverse(roots, traverserDelegateVisitor); } + @SuppressWarnings("TypeParameterUnusedInFormals") + public static T oneVisitWithResult(GraphQLType type, GraphQLTypeVisitor typeVisitor) { + DefaultTraverserContext context = DefaultTraverserContext.simple(type); + type.accept(context, typeVisitor); + return (T)context.getNewAccumulate(); + } + private static class TraverserDelegateVisitor implements TraverserVisitor { private final GraphQLTypeVisitor before; diff --git a/src/main/java/graphql/util/DependenciesIterator.java b/src/main/java/graphql/util/DependenciesIterator.java new file mode 100644 index 0000000000..f967d80f5b --- /dev/null +++ b/src/main/java/graphql/util/DependenciesIterator.java @@ -0,0 +1,47 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.util; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Objects; + +/** + * Iterator over dependent vertices in their topological order + * + * @param the actual Vertex subtype used + */ +public interface DependenciesIterator> extends Iterator> { + /** + * Marks provided vertices as resolved, so their dependent vertices will be selected + * in the next iteration. + * + * @see java.util.Iterator#next() + * + * @param node vertex to be marked as resolved + */ + void close (N node); + + /** + * Marks provided vertices as resolved, so their dependent vertices will be selected + * in the next iteration. + * + * @see java.util.Iterator#next() + * + * @param resolvedSet vertices to be marked as resolved + */ + default void close (Collection resolvedSet) { + Objects.requireNonNull(resolvedSet); + + Iterator closure = resolvedSet.iterator(); + while (closure.hasNext()) { + N vertex = closure.next(); + closure.remove(); + + close(vertex); + } + } +} diff --git a/src/main/java/graphql/util/DependencyGraph.java b/src/main/java/graphql/util/DependencyGraph.java new file mode 100644 index 0000000000..2e42c8020e --- /dev/null +++ b/src/main/java/graphql/util/DependencyGraph.java @@ -0,0 +1,292 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.util; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Collections; +import static java.util.Collections.newSetFromMap; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Basic DAG (dependency graph) implementation + * The main purpose of DependencyGraph is to perform topological sort of its vertices + * + * @param type of vertices in the DependencyGraph + */ +public class DependencyGraph> { + public DependencyGraph () { + this(16); + } + + public DependencyGraph (int order) { + this(new LinkedHashMap<>(order), new HashMap<>(order), 0); + } + + public DependencyGraph (DependencyGraph other) { + this(new LinkedHashMap<>(assertNotNull(other).vertices), new HashMap<>(other.verticesById), other.nextId); + } + + private DependencyGraph (Map vertices, Map verticesById, int startId) { + this.vertices = assertNotNull((Map)vertices); + this.verticesById = assertNotNull((Map)verticesById); + this.nextId = startId; + } + + public U addNode (U maybeNode) { + assertNotNull(maybeNode); + + return (U)vertices.computeIfAbsent(maybeNode, + node -> { + int id = nextId++; + verticesById.put(id, node.id(id)); + return node; + }); + } + + public U getNode (U maybeNode) { + return (U)Optional + .ofNullable(maybeNode.getId()) + .map(id -> Optional.ofNullable(verticesById.get(id))) + .orElseGet(() -> Optional.ofNullable(vertices.get(maybeNode))) + .orElse(null); + } + + protected DependencyGraph addEdge (Edge edge) { + assertNotNull(edge); + + edges.add((Edge)edge); + return this; + } + + public DependencyGraph addDependency (N maybeSink, N maybeSource) { + return addDependency(maybeSink, maybeSource, Edge::emptyAction); + } + + public DependencyGraph addDependency (N maybeSink, N maybeSource, BiConsumer> edgeAction) { + // note reverse ordering of Vertex arguments. + // an Edge points from source - to -> sink, we say "sink depends on source" + return addEdge(new Edge<>(addNode(maybeSource), addNode(maybeSink), edgeAction)); + } + + public Collection getDependencies (N maybeSource) { + return Optional + .ofNullable(getNode(maybeSource)) + .map(Vertex::dependencySet) + .orElseThrow(() -> new IllegalArgumentException("Node " + maybeSource + " not found")); + } + + public int order () { + return vertices.size(); + } + + public int size () { + return edges.size(); + } + + public DependenciesIterator orderDependencies (DependencyGraphContext context) { + return new DependenciesIteratorImpl(context); + } + + protected class DependenciesIteratorImpl implements DependenciesIterator, TraverserVisitor { + @Override + public boolean hasNext() { + if (!lastClosure.isEmpty()) { + // to let automatic advancing when external resolution is not needed + close(lastClosure); + } + + if (currentClosure.isEmpty()) { + currentClosure = calculateNext(); + } + + boolean isDone = currentClosure.isEmpty(); + return (isDone && closed.size() != vertices.size()) + ? assertShouldNeverHappen("couldn't calculate next closure") + : !isDone; + } + + Set calculateNext () { + Set nextClosure = closureCreator.get(); + return (Set)Optional + .ofNullable(traverser + .rootVar(Collection.class, nextClosure) + .traverse( + unclosed + .stream() + .filter(this::canBeResolved) + .collect(Collectors.toList()), + this + ) + .getAccumulatedResult()) + .orElse(nextClosure); + } + + @Override + public Collection next() { + if (currentClosure.isEmpty()) + throw new NoSuchElementException("next closure hasn't been calculated yet"); + + lastClosure = currentClosure; + currentClosure = Collections.emptySet(); + return lastClosure; + } + + @Override + public void close(N node) { + closeResolved(node); + } + + private boolean closeNode (N maybeNode, boolean autoResolve) { + N node = Optional + .ofNullable(getNode(maybeNode)) + .orElseThrow(() -> new IllegalArgumentException("node not found: " + maybeNode)); + + if (node.resolve(context) || !autoResolve) { + closed.add(node); + unclosed.remove(node); + lastClosure.remove(node); + + node.fireResolved(context); + return true; + } + + return false; + } + + private boolean closeResolved (N maybeNode) { + return closeNode(maybeNode, false/*autoClose*/); + } + + private boolean autoClose (N maybeNode) { + return closeNode(maybeNode, true/*autoResolve*/); + } + + @Override + public TraversalControl enter(TraverserContext context) { + TraverserContext parentContext = context.getParentContext(); + Collection closure = parentContext.getVar(Collection.class); + context + .setVar(Collection.class, closure) // to be propagated to children + .setAccumulate(closure); // to be returned + + N node = context.thisNode(); + if (autoClose(node)) { + return TraversalControl.CONTINUE; + } else { + closure.add(node); + return TraversalControl.ABORT; + } + } + + @Override + public TraversalControl leave(TraverserContext context) { + return TraversalControl.CONTINUE; + } + + @Override + public TraversalControl backRef(TraverserContext context) { + assertShouldNeverHappen("cycle around node with id={}", context.thisNode().getId()); + return TraversalControl.QUIT; + } + + protected DependenciesIteratorImpl (DependencyGraphContext context, Supplier> closureCreator) { + this.context = assertNotNull(context); + this.closureCreator = assertNotNull(closureCreator); + this.unclosed = closureCreator.get(); + this.closed = closureCreator.get(); + + unclosed.addAll(vertices.values()); + } + + protected DependenciesIteratorImpl (DependencyGraphContext context) { + this(context, () -> newSetFromMap(new IdentityHashMap<>())); + } + + private boolean canBeResolved (N vertex) { + return closed.containsAll(vertex.dependencySet()); + } + + final DependencyGraphContext context; + final Supplier> closureCreator; + final Collection unclosed; + final Collection closed; + final Traverser traverser = Traverser.breadthFirst(v -> v.adjacencySet(this::canBeResolved), null); + Set currentClosure = Collections.emptySet(); + Set lastClosure = Collections.emptySet(); + } + + protected int nextId = 0; + protected final Map vertices; + protected final Map verticesById; + protected final Set> edges = new AbstractSet>() { + @Override + public boolean add(Edge e) { + assertNotNull(e); + + return e.connectEndpoints(); + } + + @Override + public Iterator> iterator() { + return new Iterator>() { + @Override + public boolean hasNext() { + boolean hasNext; + while (!(hasNext = current.hasNext()) && partitions.hasNext()) + current = partitions.next(); + + return hasNext; + } + + @Override + public Edge next() { + return (last = current.next()); + } + + @Override + public void remove() { + current.remove(); + last.disconnectEndpoints(); + } + + final Iterator>> partitions = Stream.concat( + verticesById + .values() + .stream() + .map(v -> v.indegrees.iterator()), + Stream.of(Collections.>emptyIterator()) + ) + .collect(Collectors.toList()) + .iterator(); + Iterator> current = partitions.next(); + Edge last; + }; + } + + @Override + public int size() { + return verticesById + .values() + .stream() + .collect(Collectors.summingInt(v -> v.indegrees.size())); + } + }; +} diff --git a/src/main/java/graphql/util/DependencyGraphContext.java b/src/main/java/graphql/util/DependencyGraphContext.java new file mode 100644 index 0000000000..a979947218 --- /dev/null +++ b/src/main/java/graphql/util/DependencyGraphContext.java @@ -0,0 +1,12 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.util; + +/** + * Signature interface to use when sorting the dependency graph + */ +public interface DependencyGraphContext { +} diff --git a/src/main/java/graphql/util/Edge.java b/src/main/java/graphql/util/Edge.java new file mode 100644 index 0000000000..830abeca0f --- /dev/null +++ b/src/main/java/graphql/util/Edge.java @@ -0,0 +1,114 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.util; + +import java.util.Objects; +import java.util.function.BiConsumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents an edge between two vertices in the DependencyGraph + * The direction of edge is from source -- to --> sink + * This is opposite from the represented dependency direction, e.g.from sink -- to --> source + * + * @param the actual Vertex subtype used + * @param the Edge subtype + */ +public class Edge, E extends Edge> { + protected Edge (N source, N sink) { + this(source, sink, Edge::emptyAction); + } + + protected Edge (N source, N sink, BiConsumer action) { + this.source = Objects.requireNonNull(source, "From Vertex MUST be specified"); + this.sink = Objects.requireNonNull(sink, "To Vertex MUST be specified"); + this.action = Objects.requireNonNull((BiConsumer)action, "Edge action MUST be specified"); + } + + public N getSource () { + return source; + } + + public N getSink () { + return sink; + } + + public BiConsumer getAction() { + return action; + } + + protected boolean connectEndpoints () { + if (source != sink) {// do not record dependency on the same vertex + return source.indegrees.add(this) && + sink.outdegrees.add(this); + } else { + LOGGER.warn("ignoring short circuit dependency: {}", this); + return false; + } + } + + protected boolean disconnectEndpoints () { + return source.indegrees.remove(this) && + sink.outdegrees.remove(this); + } + + protected void fire (DependencyGraphContext context) { + action.accept(context, (E)this); + } + + static void emptyAction (DependencyGraphContext context, Edge edge) { + } + + @Override + public int hashCode() { + int hash = 5; + hash = 89 * hash + Objects.hashCode(this.source); + hash = 89 * hash + Objects.hashCode(this.sink); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Edge other = (Edge) obj; + if (!Objects.equals(this.source, other.source)) { + return false; + } + if (!Objects.equals(this.sink, other.sink)) { + return false; + } + return true; + } + + @Override + public String toString() { + return toString(new StringBuilder(getClass().getSimpleName()).append('{')) + .append('}') + .toString(); + } + + protected StringBuilder toString (StringBuilder builder) { + return builder + .append("source=").append(source) + .append(", sink=").append(sink) + .append(", action=").append(action); + } + + protected final N source; + protected final N sink; + protected final BiConsumer action; + + private static final Logger LOGGER = LoggerFactory.getLogger(Edge.class); +} diff --git a/src/main/java/graphql/util/TernaryOperator.java b/src/main/java/graphql/util/TernaryOperator.java new file mode 100644 index 0000000000..32d1157bba --- /dev/null +++ b/src/main/java/graphql/util/TernaryOperator.java @@ -0,0 +1,24 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.util; + +/** + * Represents an operation upon an operand of Boolean type and two other operands of the same type, + * producing a result of the same type as the operands. + * This is a specialization of {@link graphql.util.TriFunction} for the case where + * the operands and the result are all of the same type. + * Follows semantics of Java ternary operation + * + * {@code condition ? true-branch : false-branch} + * + * + * This is a functional interface whose functional method is BiFunction.apply(Object, Object). + * + * @param the type of the operands and result of the operator + */ +@FunctionalInterface +public interface TernaryOperator extends TriFunction { +} diff --git a/src/main/java/graphql/util/Traverser.java b/src/main/java/graphql/util/Traverser.java index fa89d10c25..d09b0afde9 100644 --- a/src/main/java/graphql/util/Traverser.java +++ b/src/main/java/graphql/util/Traverser.java @@ -34,6 +34,8 @@ private Traverser(TraverserState traverserState, Function Function>> wrapListFunction(Function> listFn) { + assertNotNull(listFn); + return node -> { List childs = listFn.apply(node); Map> result = new LinkedHashMap<>(); @@ -42,6 +44,10 @@ private Traverser(TraverserState traverserState, Function Traverser newTraverser (TraverserState traverserState, Function> getChildren, Object initialAccumulate) { + return new Traverser<>(traverserState, wrapListFunction(getChildren), initialAccumulate); + } + public Traverser rootVars(Map, Object> rootVars) { this.rootVars.putAll(assertNotNull(rootVars)); return this; @@ -90,17 +96,19 @@ public TraverserResult traverse(T root, TraverserVisitor visitor) { return traverse(Collections.singleton(root), visitor); } - public TraverserResult traverse(Collection roots, TraverserVisitor visitor) { + public TraverserResult traverse(Collection roots, TraverserVisitor v) { + TraverserVisitor visitor = (TraverserVisitor)v; + assertNotNull(roots); assertNotNull(visitor); - // "artificial" parent context for all roots with rootVars - DefaultTraverserContext rootContext = traverserState.newRootContext(rootVars); + DefaultTraverserContext rootContext = (DefaultTraverserContext)traverserState.newRootContext(rootVars); traverserState.addNewContexts(roots, rootContext); - DefaultTraverserContext currentContext; + DefaultTraverserContext currentContext = rootContext; Object currentAccValue = initialAccumulate; + traverseLoop: while (!traverserState.isEmpty()) { Object top = traverserState.pop(); @@ -109,7 +117,7 @@ public TraverserResult traverse(Collection roots, TraverserVisitor< Map>> childrenContextMap = ((TraverserState.EndList) top).childrenContextMap; // end-of-list marker, we are done recursing children, // mark the current node as fully visited - currentContext = (DefaultTraverserContext) traverserState.pop(); + currentContext = (DefaultTraverserContext)traverserState.pop(); currentContext.setCurAccValue(currentAccValue); currentContext.setChildrenContexts(childrenContextMap); TraversalControl traversalControl = visitor.leave(currentContext); @@ -158,6 +166,7 @@ public TraverserResult traverse(Collection roots, TraverserVisitor< } } } + TraverserResult traverserResult = new TraverserResult(currentAccValue); return traverserResult; } diff --git a/src/main/java/graphql/util/TraverserContext.java b/src/main/java/graphql/util/TraverserContext.java index 1e5896356b..98418c93c1 100644 --- a/src/main/java/graphql/util/TraverserContext.java +++ b/src/main/java/graphql/util/TraverserContext.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; /** @@ -68,6 +69,23 @@ public interface TraverserContext { * @return context associated with the node parent */ TraverserContext getParentContext(); + + /** + * The parent new accumulate value. + * + * @see #getParentContext() + * @see #getNewAccumulate() + * + * @param and me + * + * @return the accumulate value + */ + default U getParentAccumulate () { + return Optional + .ofNullable(getParentContext()) + .map(TraverserContext::getNewAccumulate) + .orElse(null); + } /** * The list of parent nodes starting from the current parent. @@ -89,6 +107,7 @@ public interface TraverserContext { * * @return list of breadcrumbs. the first element is the location inside the parent. */ + List> getBreadcrumbs(); /** @@ -103,7 +122,10 @@ public interface TraverserContext { * * @return {@code true} if a node had been already visited */ - boolean isVisited(); + default boolean isVisited() { + return visitedNodes() + .contains(thisNode()); + } /** * Obtains all visited nodes and values received by the {@link TraverserVisitor#enter(graphql.util.TraverserContext) } @@ -122,7 +144,7 @@ public interface TraverserContext { * @return a variable value or {@code null} */ S getVar(Class key); - + /** * Searches for a context variable starting from the parent * up the hierarchy of contexts until the first variable is found. @@ -196,5 +218,4 @@ public interface TraverserContext { * @return the children contexts. If the childs are a simple list the key is null. */ Map>> getChildrenContexts(); - } diff --git a/src/main/java/graphql/util/TraverserState.java b/src/main/java/graphql/util/TraverserState.java index d40ac8af2f..86a9f01870 100644 --- a/src/main/java/graphql/util/TraverserState.java +++ b/src/main/java/graphql/util/TraverserState.java @@ -14,6 +14,7 @@ import java.util.function.Function; import static graphql.Assert.assertNotNull; +import java.util.Objects; @Internal public abstract class TraverserState { @@ -22,14 +23,18 @@ public abstract class TraverserState { private final Deque state; private final Set visited = new LinkedHashSet<>(); + private final Function, DefaultTraverserContext> contextFactory; - // used for depth first traversal - private static class StackTraverserState extends TraverserState { + public static class StackTraverserState extends TraverserState { private StackTraverserState(Object sharedContextData) { super(sharedContextData); } + + private StackTraverserState(Object sharedContextData, Function, DefaultTraverserContext> contextFactory) { + super(sharedContextData, contextFactory); + } @Override public void pushAll(TraverserContext traverserContext, Function>> getChildren) { @@ -46,7 +51,7 @@ public void pushAll(TraverserContext traverserContext, Function= 0; i--) { U child = assertNotNull(children.get(i), "null child for key " + key); NodeLocation nodeLocation = new NodeLocation(key, i); - DefaultTraverserContext context = super.newContext(child, traverserContext, nodeLocation); + TraverserContext context = super.newContext(child, traverserContext, nodeLocation); super.state.push(context); childrenContextMap.computeIfAbsent(key, notUsed -> new ArrayList<>()); childrenContextMap.get(key).add(0, context); @@ -57,13 +62,16 @@ public void pushAll(TraverserContext traverserContext, Function extends TraverserState { + public static class QueueTraverserState extends TraverserState { private QueueTraverserState(Object sharedContextData) { super(sharedContextData); } + private QueueTraverserState(Object sharedContextData, Function, DefaultTraverserContext> contextFactory) { + super(sharedContextData, contextFactory); + } + @Override public void pushAll(TraverserContext traverserContext, Function>> getChildren) { Map>> childrenContextMap = new LinkedHashMap<>(); @@ -74,7 +82,7 @@ public void pushAll(TraverserContext traverserContext, Function context = super.newContext(child, traverserContext, nodeLocation); + TraverserContext context = super.newContext(child, traverserContext, nodeLocation); childrenContextMap.computeIfAbsent(key, notUsed -> new ArrayList<>()); childrenContextMap.get(key).add(context); super.state.add(context); @@ -92,17 +100,30 @@ public static class EndList { public Map>> childrenContextMap; } - private TraverserState(Object sharedContextData) { + private TraverserState(Object initialData) { + this(initialData, TraverserState::newContext); + } + + private TraverserState(Object sharedContextData, Function, DefaultTraverserContext> contextFactory) { this.sharedContextData = sharedContextData; this.state = new ArrayDeque<>(32); + this.contextFactory = assertNotNull(contextFactory); + } + + public static QueueTraverserState newQueueState(Object initialData) { + return new QueueTraverserState<>(initialData); + } + + public static QueueTraverserState newQueueState(Object initialData, Function, DefaultTraverserContext> contextFactory) { + return new QueueTraverserState<>(initialData, contextFactory); } - public static TraverserState newQueueState(Object sharedContextData) { - return new QueueTraverserState<>(sharedContextData); + public static StackTraverserState newStackState(Object initialData) { + return new StackTraverserState<>(initialData); } - public static TraverserState newStackState(Object sharedContextData) { - return new StackTraverserState<>(sharedContextData); + public static StackTraverserState newStackState(Object initialData, Function, DefaultTraverserContext> contextFactory) { + return new StackTraverserState<>(initialData, contextFactory); } public abstract void pushAll(TraverserContext o, Function>> getChildren); @@ -125,21 +146,114 @@ public void addVisited(T visited) { this.visited.add(visited); } + private static DefaultTraverserContext newContext (TraverserContextBuilder builder) { + assertNotNull(builder); - public DefaultTraverserContext newRootContext(Map, Object> vars) { + return new DefaultTraverserContext<>( + builder.getNode(), + builder.getParentContext(), + builder.getVisited(), + builder.getVars(), + builder.getSharedContextData(), + builder.getNodeLocation(), + builder.isRootContext() + ); + } + + public TraverserContext newRootContext(Map, Object> vars) { return newContextImpl(null, null, vars, null, true); } - private DefaultTraverserContext newContext(T o, TraverserContext parent, NodeLocation position) { + private TraverserContext newContext(T o, TraverserContext parent, NodeLocation position) { return newContextImpl(o, parent, new LinkedHashMap<>(), position, false); } - private DefaultTraverserContext newContextImpl(T curNode, + private TraverserContext newContextImpl(T curNode, TraverserContext parent, Map, Object> vars, NodeLocation nodeLocation, boolean isRootContext) { assertNotNull(vars); - return new DefaultTraverserContext<>(curNode, parent, visited, vars, sharedContextData, nodeLocation, isRootContext); + + return new TraverserContextBuilder<>(this) + .thisNode(curNode) + .parentContext(parent) + .vars(vars) + .nodeLocation(nodeLocation) + .rootContext(isRootContext) + .build(contextFactory); + } + + public static final class TraverserContextBuilder { + private final TraverserState outer; + + private /*final*/ T node; + private /*final*/ TraverserContext parentContext; + private /*final*/ Map, Object> vars; + private /*final*/ NodeLocation nodeLocation; + boolean /*final*/ isRootContext; + + public TraverserContextBuilder (TraverserState outer) { + this.outer = Objects.requireNonNull(outer); + } + + public DefaultTraverserContext build (Function, ? extends DefaultTraverserContext> creator) { + assertNotNull(creator); + return creator.apply(this); + } + + public TraverserContextBuilder thisNode (T node) { + this.node = node; + return this; + } + + public TraverserContextBuilder parentContext (TraverserContext parentContext) { + this.parentContext = (TraverserContext)parentContext; + return this; + } + + public TraverserContextBuilder vars (Map, Object> vars) { + this.vars = Objects.requireNonNull(vars); + return this; + } + + public TraverserContextBuilder rootContext (boolean value) { + this.isRootContext = value; + return this; + } + + public TraverserContextBuilder nodeLocation (NodeLocation nodeLocation) { + this.nodeLocation = nodeLocation; + return this; + } + + public T getNode() { + return node; + } + + public TraverserContext getParentContext() { + return parentContext; + } + + public Map, Object> getVars() { + return vars; + } + + public Object getSharedContextData () { + return outer.sharedContextData; + } + + public Set getVisited () { + return outer.visited; + } + + public boolean isRootContext() { + return isRootContext; + } + + public NodeLocation getNodeLocation() { + return nodeLocation; + } + } } diff --git a/src/main/java/graphql/util/TraverserVisitor.java b/src/main/java/graphql/util/TraverserVisitor.java index f7f2bc7c90..95860acb8a 100644 --- a/src/main/java/graphql/util/TraverserVisitor.java +++ b/src/main/java/graphql/util/TraverserVisitor.java @@ -26,6 +26,5 @@ public interface TraverserVisitor { */ default TraversalControl backRef(TraverserContext context) { return TraversalControl.CONTINUE; - } - + } } diff --git a/src/main/java/graphql/util/TriConsumer.java b/src/main/java/graphql/util/TriConsumer.java new file mode 100644 index 0000000000..f4e9c416f9 --- /dev/null +++ b/src/main/java/graphql/util/TriConsumer.java @@ -0,0 +1,51 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.util; + +import java.util.Objects; + +/** + * Represents an operation that accepts three input arguments and returns no result. + * This is the three-arity specialization of {@link java.util.function.Consumer}. + * Unlike most other functional interfaces, TriConsumer is expected to operate via side-effects. + * + * This is a functional interface whose functional method is accept(Object, Object). + * + * @param the type of the first argument to the operation + * @param the type of the second argument to the operation + * @param the type of the third argument to the operation + */ +@FunctionalInterface +public interface TriConsumer { + /** + * Performs this operation on the given arguments. + * + * @param u the first operation argument + * @param v the second operation argument + * @param w the third operation argument + */ + void accept (U u, V v, W w); + + /** + * Returns a composed TriConsumer that performs, in sequence, + * this operation followed by the after operation.If performing either + * operation throws an exception, it is relayed to the caller of the composed operation. + * + * If performing this operation throws an exception, the after operation will not be performed. + * + * @param after the operation to perform after this operation + * @return a composed TriConsumer that performs in sequence this operation followed by after operation + * @throws NullPointerException if after is null + */ + default TriConsumer andThen (TriConsumer after) { + Objects.requireNonNull(after); + + return (U u, V v, W w) -> { + accept(u, v, w); + after.accept(u, v, w); + }; + } +} diff --git a/src/main/java/graphql/util/TriFunction.java b/src/main/java/graphql/util/TriFunction.java new file mode 100644 index 0000000000..188ba9828e --- /dev/null +++ b/src/main/java/graphql/util/TriFunction.java @@ -0,0 +1,53 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.util; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Represents a function that accepts three arguments and produces a result. + * This is the tree-arity specialization of {@link java.util.function.Function}. + * + * This is a functional interface whose functional method is apply(Object, Object, Object). + * + * @param the type of the first argument to the function + * @param the type of the second argument to the function + * @param the type of the third argument to the function + * @param the type of the result of the function + */ +@FunctionalInterface +public interface TriFunction { + /** + * Applies this function to the given arguments. + * + * @param u the first function argument + * @param v the second function argument + * @param w the third function argument + * @return the function result + */ + R apply (U u, V v, W w); + + /** + * Returns a composed function that first applies this function to its input, + * and then applies the after function to the result. + * + * If evaluation of either function throws an exception, + * it is relayed to the caller of the composed function. + * + * @param the type of output of the after funciton and the composed function + * + * @param after the function to apply after this function is applied + * @return the composed function that first applies this function and + * then applies the after function + * @throws NullPointerException if after is null + */ + default TriFunction andThen (Function after) { + Objects.requireNonNull(after); + + return (U u, V v, W w) -> after.apply(apply(u, v, w)); + } +} diff --git a/src/main/java/graphql/util/TriPredicate.java b/src/main/java/graphql/util/TriPredicate.java new file mode 100644 index 0000000000..c08cff4fb1 --- /dev/null +++ b/src/main/java/graphql/util/TriPredicate.java @@ -0,0 +1,80 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.util; + +import java.util.Objects; + +/** + * Represents a predicate (boolean-valued function) of three arguments. + * This is the three-arity specialization of {@link java.util.function.Predicate}. + * + * This is a functional interface whose functional method is test(Object, Object). + * + * @param the type of the first argument to the operation + * @param the type of the second argument to the operation + * @param the type of the third argument to the operation + */ +@FunctionalInterface +public interface TriPredicate { + /** + * Evaluates this predicate on the given arguments + * + * @param u the first operation argument + * @param v the second operation argument + * @param w the third operation argument + * @return {@code true} if the input arguments match the predicate, {@code false} otherwise. + */ + boolean test (U u, V v, W w); + + /** + * Returns a predicate that represents the logical negation of this predicate. + * + * @return a predicate that represents the logical negation of this predicate. + */ + default TriPredicate negate () { + return (U u, V v, W w) -> !test(u, v, w); + } + + /** + * Returns a composed predicate that represents a short-circuiting logical AND + * of this predicate and another. When evaluating the composed predicate, + * if this predicate is false, then the other predicate is not evaluated. + * + * Any exceptions thrown during evaluation of either predicate are relayed to + * the caller; if evaluation of this predicate throws an exception, + * the other predicate will not be evaluated. + * + * @param other a predicate that will be logically-ANDed with this predicate + * @return a composed predicate that represents the short-circuiting logical AND + * of this predicate and the other predicate + * @throws NullPointerException if other is null + */ + default TriPredicate and (TriPredicate other) { + Objects.requireNonNull(other); + + return (U u, V v, W w) -> test(u, v, w) && other.test(u, v, w); + } + + /** + * Returns a composed predicate that represents a short-circuiting logical OR + * of this predicate and another. When evaluating the composed predicate, + * if this predicate is true, then the other predicate is not evaluated. + * + * Any exceptions thrown during evaluation of either predicate are relayed to + * the caller; if evaluation of this predicate throws an exception, + * the other predicate will not be evaluated. + * + * @param other a predicate that will be logically-ORed with this predicate + * @return a composed predicate that represents the short-circuiting logical OR + * of this predicate and the other predicate + * @throws NullPointerException if other is null + */ + default TriPredicate or (TriPredicate other) { + Objects.requireNonNull(other); + + return (U u, V v, W w) -> test(u, v, w) || other.test(u, v, w); + } +} diff --git a/src/main/java/graphql/util/Vertex.java b/src/main/java/graphql/util/Vertex.java new file mode 100644 index 0000000000..bf0ca49509 --- /dev/null +++ b/src/main/java/graphql/util/Vertex.java @@ -0,0 +1,146 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package graphql.util; + +import static graphql.Assert.assertTrue; +import static graphql.Assert.assertNotNull; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents a Vertex in a DependencyGraph + * + * @param the actual subtype of the Vertex + */ +public abstract class Vertex> { + public Object getId () { + return id; + } + + protected N id (Object id) { + this.id = id; + return (N)this; + } + + public N addEdge (Edge edge) { + assertNotNull(edge); + assertTrue(edge.getSink() == this, "Edge MUST sink to this vertex"); + + edge.connectEndpoints(); + return (N)this; + } + + public N dependsOn (N source, BiConsumer> edgeAction) { + assertNotNull(source); + assertNotNull(edgeAction); + + return addEdge(new Edge<>(source, (N)this, edgeAction)); + } + + private static void emptyEdgeConsumer (Edge edge) { + } + + public N undependsOn (N source) { + return undependsOn(source, Vertex::emptyEdgeConsumer); + } + + public N undependsOn (N source, Consumer> whenDisconnecting) { + Objects.requireNonNull(source); + + new ArrayList<>(outdegrees) + .stream() + .filter(edge -> edge.getSource() == source) + .peek(whenDisconnecting) + .forEach(Edge::disconnectEndpoints); + + return (N)this; + } + + public N disconnect () { + Stream.concat( + new ArrayList<>(indegrees).stream(), + new ArrayList<>(outdegrees).stream() + ) + .forEach(Edge::disconnectEndpoints); + + return (N)this; + } + + private static boolean alwaysTrue (Object v) { + return true; + } + + public List adjacencySet () { + return adjacencySet(Vertex::alwaysTrue); + } + + public List adjacencySet (Predicate filter) { + assertNotNull(filter); + + return indegrees + .stream() + .map(Edge::getSink) + .filter(filter) + .collect(Collectors.toList()); + } + + public List dependencySet () { + return dependencySet(Vertex::alwaysTrue); + } + + public List dependencySet (Predicate filter) { + assertNotNull(filter); + + return outdegrees + .stream() + .map(Edge::getSource) + .filter(filter) + .collect(Collectors.toList()); + } + + public boolean resolve (DependencyGraphContext context) { + return false; + } + + protected void fireResolved (DependencyGraphContext context) { + indegrees.forEach(edge -> edge.fire(context)); + } + + @Override + public String toString() { + return toString(new StringBuilder(getClass().getSimpleName()).append('{')) + .append('}') + .toString(); + } + + protected StringBuilder toString (StringBuilder builder) { + return builder + .append("id=").append(id) + .append(", dependencies=").append( + outdegrees + .stream() + .map(Edge::getSource) + .map(Vertex::toString) + .collect(Collectors.joining(", ", "on ->", " ")) + ); + } + + protected Object id; + protected final Set> outdegrees = new LinkedHashSet<>(); + protected final Set> indegrees = new LinkedHashSet<>(); + + private static final Logger LOGGER = LoggerFactory.getLogger(Vertex.class); +} diff --git a/src/test/groovy/graphql/execution3/DAGExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution3/DAGExecutionStrategyTest.groovy new file mode 100644 index 0000000000..b31b19f29b --- /dev/null +++ b/src/test/groovy/graphql/execution3/DAGExecutionStrategyTest.groovy @@ -0,0 +1,477 @@ +package graphql.execution3 + +import graphql.ExecutionInput +import graphql.TestUtil +import graphql.execution.ExecutionId +import graphql.schema.DataFetcher +import spock.lang.Specification +import spock.lang.Ignore + +class DAGExecutionStrategyTest extends Specification { + + //@Ignore + def "test simple execution"() { + def fooData = [id: "fooId", bar: [id: "barId", name: "someBar"]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + id + bar { + id + name + } + }} + """) + + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .build() + + Execution execution = new Execution() + + when: + def monoResult = execution.execute(DAGExecutionStrategy, document, schema, ExecutionId.generate(), executionInput) + def result = monoResult.get() + + + then: + result.getData() == [foo: fooData] + + + } + + //@Ignore + def "test execution with lists"() { + def fooData = [[id: "fooId1", bar: [[id: "barId1", name: "someBar1"], [id: "barId2", name: "someBar2"]]], + [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: [Foo] + } + type Foo { + id: ID + bar: [Bar] + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + id + bar { + id + name + } + }} + """) + + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .build() + + + Execution execution = new Execution(); + + when: + def monoResult = execution.execute(DAGExecutionStrategy, document, schema, ExecutionId.generate(), executionInput) + def result = monoResult.get() + + + then: + result.getData() == [foo: fooData] + + } + + //@Ignore + def "test execution with null element "() { + def fooData = [[id: "fooId1", bar: null], + [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: [Foo] + } + type Foo { + id: ID + bar: [Bar] + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + id + bar { + id + name + } + }} + """) + + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .build() + + + Execution execution = new Execution() + + when: + def monoResult = execution.execute(DAGExecutionStrategy, document, schema, ExecutionId.generate(), executionInput) + def result = monoResult.get() + + + then: + result.getData() == [foo: fooData] + + } + + //@Ignore + def "test execution with null element in list"() { + def fooData = [[id: "fooId1", bar: [[id: "barId1", name: "someBar1"], null]], + [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: [Foo] + } + type Foo { + id: ID + bar: [Bar] + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + id + bar { + id + name + } + }} + """) + + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .build() + + + Execution execution = new Execution() + + when: + def monoResult = execution.execute(DAGExecutionStrategy, document, schema, ExecutionId.generate(), executionInput) + def result = monoResult.get() + + + then: + result.getData() == [foo: fooData] + + } + + //@Ignore + def "test execution with null element in non null list"() { + def fooData = [[id: "fooId1", bar: [[id: "barId1", name: "someBar1"], null]], + [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: [Foo] + } + type Foo { + id: ID + bar: [Bar!] + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + id + bar { + id + name + } + }} + """) + + def expectedFooData = [[id: "fooId1", bar: null], + [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] + + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .build() + + + Execution execution = new Execution() + + when: + def monoResult = execution.execute(DAGExecutionStrategy, document, schema, ExecutionId.generate(), executionInput) + def result = monoResult.get() + + + then: + result.getData() == [foo: expectedFooData] + + } + + //@Ignore + def "test execution with null element bubbling up because of non null "() { + def fooData = [[id: "fooId1", bar: [[id: "barId1", name: "someBar1"], null]], + [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: [Foo] + } + type Foo { + id: ID + bar: [Bar!]! + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + id + bar { + id + name + } + }} + """) + + def expectedFooData = [null, + [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] + + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .build() + + + Execution execution = new Execution() + + when: + def monoResult = execution.execute(DAGExecutionStrategy, document, schema, ExecutionId.generate(), executionInput) + def result = monoResult.get() + + + then: + result.getData() == [foo: expectedFooData] + + } + + //@Ignore + def "test execution with null element bubbling up to top "() { + def fooData = [[id: "fooId1", bar: [[id: "barId1", name: "someBar1"], null]], + [id: "fooId2", bar: [[id: "barId3", name: "someBar3"], [id: "barId4", name: "someBar4"]]]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: [Foo!]! + } + type Foo { + id: ID + bar: [Bar!]! + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + id + bar { + id + name + } + }} + """) + + + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .build() + + + Execution execution = new Execution() + + when: + def monoResult = execution.execute(DAGExecutionStrategy, document, schema, ExecutionId.generate(), executionInput) + def result = monoResult.get() + + + then: + result.getData() == null + + } + + //@Ignore + def "test list"() { + def fooData = [[id: "fooId1"], [id: "fooId2"], [id: "fooId3"]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: [Foo] + } + type Foo { + id: ID + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + id + }} + """) + + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .build() + + + Execution execution = new Execution(); + + when: + def monoResult = execution.execute(DAGExecutionStrategy, document, schema, ExecutionId.generate(), executionInput) + def result = monoResult.get() + + + then: + result.getData() == [foo: fooData] + + + } + + //@Ignore + def "test list in lists "() { + def fooData = [[bar: [[id: "barId1"], [id: "barId2"]]], [bar: null], [bar: [[id: "barId3"], [id: "barId4"], [id: "barId5"]]]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: [Foo] + } + type Foo { + bar: [Bar] + } + type Bar { + id: ID + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + bar { + id + } + }} + """) + + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .build() + + + Execution execution = new Execution(); + + when: + def monoResult = execution.execute(DAGExecutionStrategy, document, schema, ExecutionId.generate(), executionInput) + def result = monoResult.get() + + + then: + result.getData() == [foo: fooData] + + + } + + //@Ignore + def "test simple batching with null value in list"() { + def fooData = [[id: "fooId1"], null, [id: "fooId3"]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: [Foo] + } + type Foo { + id: ID + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + id + }} + """) + + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .build() + + + Execution execution = new Execution(); + + when: + def monoResult = execution.execute(DAGExecutionStrategy, document, schema, ExecutionId.generate(), executionInput) + def result = monoResult.get() + + + then: + result.getData() == [foo: fooData] + + + } + +} + diff --git a/src/test/groovy/graphql/execution3/ExecutionPlanBuilderTest.groovy b/src/test/groovy/graphql/execution3/ExecutionPlanBuilderTest.groovy new file mode 100644 index 0000000000..ef0d314d24 --- /dev/null +++ b/src/test/groovy/graphql/execution3/ExecutionPlanBuilderTest.groovy @@ -0,0 +1,486 @@ +package graphql.execution3 + +import graphql.ExecutionInput +import graphql.TestUtil +import graphql.execution.ExecutionId +import graphql.schema.DataFetcher +import graphql.schema.GraphQLType +import graphql.language.OperationDefinition +import graphql.language.OperationDefinition.Operation +import graphql.language.Field +import graphql.language.Node +import graphql.util.DependencyGraphContext +import graphql.util.Edge +import spock.lang.Ignore +import spock.lang.Specification + +class TestGraphContext implements ExecutionPlanContext { + void prepareResolve (Edge, ?> edge) { + } + + void whenResolved (Edge, ?> edge) { + } + + boolean resolve (NodeVertex node) { + if (node instanceof DocumentVertex) + return true + + return false + } +} + +class ExecutionPlanBuilderTest extends Specification { +// @Ignore + def "test simple query"() { + def fooData = [id: "fooId", bar: [id: "barId", name: "someBar"]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + id + bar { + id + name + } + }} + """) + + def builder = ExecutionPlan.newExecutionPlanBuilder() + .schema(schema) + .document(document) + .operation(null) + + when: + def plan = builder.build() + + def Query = plan.getNode new OperationVertex(new OperationDefinition(null, Operation.QUERY), schema.getType("Query")) + def Query_foo = plan.getNode new FieldVertex(new Field("foo"), schema.getType("Foo"), schema.getType("Query")) + def Foo_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Foo")) + def Foo_bar = plan.getNode new FieldVertex(new Field("bar"), schema.getType("Bar"), schema.getType("Foo")) + def Bar_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Bar")) + def Bar_name = plan.getNode new FieldVertex(new Field("name"), schema.getType("String"), schema.getType("Bar")) + + def order = plan.orderDependencies(new TestGraphContext()) + + then: + plan.order() == 7 + + order.hasNext() == true + order.next() == [Query_foo] as Set + order.hasNext() == true + order.next() == [Foo_id, Foo_bar] as Set + order.hasNext() == true + order.next() == [Bar_id, Bar_name] as Set + order.hasNext() == true + order.next() == [Query] as Set + order.hasNext() == false + } + + @Ignore + def "test simple execution with inline fragments"() { + def fooData = [id: "fooId", bar: [id: "barId", name: "someBar"]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + ... on Foo { + id + bar { + ... on Bar { + id + name + } + id + name + } + } + bar { + id + name + } + }} + """) + + def builder = ExecutionPlan.newExecutionPlanBuilder() + .schema(schema) + .document(document) + .operation(null) + + when: + def plan = builder.build() + + def Query = plan.getNode new OperationVertex(new OperationDefinition(null, Operation.QUERY), schema.getType("Query")) + def Query_foo = plan.getNode new FieldVertex(new Field("foo"), schema.getType("Foo"), schema.getType("Query")) + def Foo_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Foo")) + def Foo_bar = plan.getNode new FieldVertex(new Field("bar"), schema.getType("Bar"), schema.getType("Foo")) + def Bar_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Bar")) + def Bar_name = plan.getNode new FieldVertex(new Field("name"), schema.getType("String"), schema.getType("Bar")) + + def order = plan.orderDependencies(new TestGraphContext()) + + then: + plan.order() == 7 + + order.hasNext() == true + order.next() == [Query_foo] as Set + order.hasNext() == true + order.next() == [Foo_id, Foo_bar] as Set + order.hasNext() == true + order.next() == [Bar_id, Bar_name] as Set + order.hasNext() == true + order.next() == [Query] as Set + order.hasNext() == false + } + + @Ignore + def "test simple execution with redundant inline fragments"() { + def fooData = [id: "fooId", bar: [id: "barId", name: "someBar"]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + ... on Foo { + id + bar { + ... on Bar { + id + name + } + id + name + } + } + ... on Foo { + id + bar { + ... on Bar { + id + name + } + id + name + } + } + bar { + ... on Bar { + id + name + } + id + name + } + }} + """) + + def builder = ExecutionPlan.newExecutionPlanBuilder() + .schema(schema) + .document(document) + .operation(null) + + when: + def plan = builder.build() + + def Query = plan.getNode new OperationVertex(new OperationDefinition(null, Operation.QUERY), schema.getType("Query")) + def Query_foo = plan.getNode new FieldVertex(new Field("foo"), schema.getType("Foo"), schema.getType("Query")) + def Foo_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Foo")) + def Foo_bar = plan.getNode new FieldVertex(new Field("bar"), schema.getType("Bar"), schema.getType("Foo")) + def Bar_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Bar")) + def Bar_name = plan.getNode new FieldVertex(new Field("name"), schema.getType("String"), schema.getType("Bar")) + + def order = plan.orderDependencies(new TestGraphContext()) + + then: + plan.order() == 7 + + order.hasNext() == true + order.next() == [Query_foo] as Set + order.hasNext() == true + order.next() == [Foo_id, Foo_bar] as Set + order.hasNext() == true + order.next() == [Bar_id, Bar_name] as Set + order.hasNext() == true + order.next() == [Query] as Set + order.hasNext() == false + } + + @Ignore + def "test simple execution with fragment spreads"() { + def fooData = [id: "fooId", bar: [id: "barId", name: "someBar"]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + ...F1 + bar { + id + name + } + }} + fragment F1 on Foo { + id + bar { + ...B1 + id + name + } + } + fragment B1 on Bar { + id + name + } + """) + + def builder = ExecutionPlan.newExecutionPlanBuilder() + .schema(schema) + .document(document) + .operation(null) + + when: + def plan = builder.build() + + def Query = plan.getNode new OperationVertex(new OperationDefinition(null, Operation.QUERY), schema.getType("Query")) + def Query_foo = plan.getNode new FieldVertex(new Field("foo"), schema.getType("Foo"), schema.getType("Query")) + def Foo_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Foo")) + def Foo_bar = plan.getNode new FieldVertex(new Field("bar"), schema.getType("Bar"), schema.getType("Foo")) + def Bar_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Bar")) + def Bar_name = plan.getNode new FieldVertex(new Field("name"), schema.getType("String"), schema.getType("Bar")) + + def order = plan.orderDependencies(new TestGraphContext()) + + then: + plan.order() == 7 + + order.hasNext() == true + order.next() == [Query_foo] as Set + order.hasNext() == true + order.next() == [Foo_id, Foo_bar] as Set + order.hasNext() == true + order.next() == [Bar_id, Bar_name] as Set + order.hasNext() == true + order.next() == [Query] as Set + order.hasNext() == false + } + + @Ignore + def "test simple execution with redundant fragment spreads"() { + def fooData = [id: "fooId", bar: [id: "barId", name: "someBar"]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + ...F1 + ...F1 + bar { + id + name + } + }} + fragment F1 on Foo { + id + bar { + ...B1 + ...B1 + id + name + } + ...F2 + } + fragment F2 on Foo { + id + bar { + id + name + } + } + fragment B1 on Bar { + id + name + ...B2 + } + fragment B2 on Bar { + id + name + } + """) + + def builder = ExecutionPlan.newExecutionPlanBuilder() + .schema(schema) + .document(document) + .operation(null) + + when: + def plan = builder.build() + + def Query = plan.getNode new OperationVertex(new OperationDefinition(null, Operation.QUERY), schema.getType("Query")) + def Query_foo = plan.getNode new FieldVertex(new Field("foo"), schema.getType("Foo"), schema.getType("Query")) + def Foo_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Foo")) + def Foo_bar = plan.getNode new FieldVertex(new Field("bar"), schema.getType("Bar"), schema.getType("Foo")) + def Bar_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Bar")) + def Bar_name = plan.getNode new FieldVertex(new Field("name"), schema.getType("String"), schema.getType("Bar")) + + def order = plan.orderDependencies(new TestGraphContext()) + + then: + plan.order() == 7 + + order.hasNext() == true + order.next() == [Query_foo] as Set + order.hasNext() == true + order.next() == [Foo_id, Foo_bar] as Set + order.hasNext() == true + order.next() == [Bar_id, Bar_name] as Set + order.hasNext() == true + order.next() == [Query] as Set + order.hasNext() == false + } + + @Ignore + def "test simple query with aliases"() { + def fooData = [id: "fooId", bar: [id: "barId", name: "someBar"]] + def dataFetchers = [ + Query: [foo: { env -> fooData } as DataFetcher] + ] + def schema = TestUtil.schema(""" + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar + } + type Bar { + id: ID + name: String + } + """, dataFetchers) + + + def document = graphql.TestUtil.parseQuery(""" + {foo { + id + bar { + id + name + } + bar1: bar { + id + name + } + }} + """) + + def builder = ExecutionPlan.newExecutionPlanBuilder() + .schema(schema) + .document(document) + .operation(null) + + when: + def plan = builder.build() + + def Query = plan.getNode new OperationVertex(new OperationDefinition(null, Operation.QUERY), schema.getType("Query")) + def Query_foo = plan.getNode new FieldVertex(new Field("foo"), schema.getType("Foo"), schema.getType("Query")) + def Foo_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Foo")) + def Foo_bar = plan.getNode new FieldVertex(new Field("bar"), schema.getType("Bar"), schema.getType("Foo")) + def Foo_bar1 = plan.getNode new FieldVertex(Field.newField("bar").alias("bar1").build(), schema.getType("Bar"), schema.getType("Foo")) + def Bar_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Bar")) + def Bar_name = plan.getNode new FieldVertex(new Field("name"), schema.getType("String"), schema.getType("Bar")) + def Bar1_id = plan.getNode new FieldVertex(new Field("id"), schema.getType("ID"), schema.getType("Bar"), Foo_bar1) + def Bar1_name = plan.getNode new FieldVertex(new Field("name"), schema.getType("String"), schema.getType("Bar"), Foo_bar1) + + def order = plan.orderDependencies(new TestGraphContext()) + + then: + plan.order() == 10 + + order.hasNext() == true + order.next() == [Query_foo] as Set + order.hasNext() == true + order.next() == [Foo_id, Foo_bar, Foo_bar1] as Set + order.hasNext() == true + order.next() == [Bar_id, Bar_name, Bar1_id, Bar1_name] as Set + order.hasNext() == true + order.next() == [Query] as Set + order.hasNext() == false + } +} + diff --git a/src/test/groovy/graphql/util/DependencyGraphTest.groovy b/src/test/groovy/graphql/util/DependencyGraphTest.groovy new file mode 100644 index 0000000000..1f4aa13b64 --- /dev/null +++ b/src/test/groovy/graphql/util/DependencyGraphTest.groovy @@ -0,0 +1,382 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package graphql.util + +import spock.lang.Specification + +class TestVertex extends Vertex { + public int hashCode () { + return Objects.hashCode(value) + } + + public boolean equals (Object o) { + if (o.is(this)) + return true + else if (o == null) + return false + else if (o instanceof TestVertex) { + TestVertex other = (TestVertex)o + return this.value == other.value + } + + return false + } + + String value +} + +class DependencyGraphTest extends Specification { + def "test empty graph ordering"() { + given: + def graph = [] as DependencyGraph + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + + then: + graph.size() == 0 + graph.order() == 0 + ordering.hasNext() == false + } + + def "test 1 vertex ordering"() { + given: + def v1 = new TestVertex(value: "v1") + def graph = [] as DependencyGraph + graph + .addDependency(v1, v1) + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + + then: + graph.size() == 0 + graph.order() == 1 + ordering.hasNext() == true + ordering.next() == [v1] as Set + ordering.hasNext() == false + } + + def "test 2 independent vertices ordering"() { + given: + def v1 = new TestVertex(value: "v1") + def v2 = new TestVertex(value: "v2") + def graph = [] as DependencyGraph + graph + .addDependency(v1, v1) + .addDependency(v2, v2) + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + + then: + graph.size() == 0 + graph.order() == 2 + ordering.hasNext() == true + ordering.next() == [v1, v2] as Set + ordering.hasNext() == false + } + + def "test 2 dependent vertices ordering"() { + given: + def v1 = new TestVertex(value: "v1") + def v2 = new TestVertex(value: "v2") + def graph = [] as DependencyGraph + graph + .addDependency(v1, v2) + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + + then: + graph.size() == 1 + graph.order() == 2 + ordering.hasNext() == true + ordering.next() == [v2] as Set + ordering.hasNext() == true + ordering.next() == [v1] as Set + ordering.hasNext() == false + } + + def "test 2 nodes undepend"() { + given: + def v1 = new TestVertex(value: "v1") + def v2 = new TestVertex(value: "v2") + def graph = [] as DependencyGraph + graph + .addDependency(v1, v2) + + when: + v1.undependsOn(v2) + def ordering = graph.orderDependencies([] as DependencyGraphContext) + + then: + graph.size() == 0 + graph.order() == 2 + v1.dependencySet().isEmpty() == true + v2.adjacencySet().isEmpty() == true + ordering.hasNext() == true + ordering.next() == [v1, v2] as Set + ordering.hasNext() == false + } + + def "test possible https://en.wikipedia.org/wiki/Dependency_graph example"() { + given: + def a = new TestVertex(value: "a") + def b = new TestVertex(value: "b") + def c = new TestVertex(value: "c") + def d = new TestVertex(value: "d") + def graph = [] as DependencyGraph + graph + .addDependency(a, b) + .addDependency(a, c) + .addDependency(b, d) + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + + then: + graph.order() == 4 + graph.size() == 3 + ordering.hasNext() == true + ordering.next() == [c, d] as Set + ordering.hasNext() == true + ordering.next() == [b] as Set + ordering.hasNext() == true + ordering.next() == [a] as Set + ordering.hasNext() == false + } + + def "test disconnect https://en.wikipedia.org/wiki/Dependency_graph example"() { + given: + def a = new TestVertex(value: "a") + def b = new TestVertex(value: "b") + def c = new TestVertex(value: "c") + def d = new TestVertex(value: "d") + def graph = [] as DependencyGraph + graph + .addDependency(a, b) + .addDependency(a, c) + .addDependency(b, d) + + when: + a.disconnect() + def ordering = graph.orderDependencies([] as DependencyGraphContext) + + then: + graph.order() == 4 + graph.size() == 1 + ordering.hasNext() == true + ordering.next() == [c, d, a] as Set + ordering.hasNext() == true + ordering.next() == [b] as Set + ordering.hasNext() == false + } + + def "test impossible https://en.wikipedia.org/wiki/Dependency_graph example"() { + given: + def a = new TestVertex(value: "a") + def b = new TestVertex(value: "b") + def c = new TestVertex(value: "c") + def d = new TestVertex(value: "d") + def graph = [] as DependencyGraph + graph + .addDependency(a, b) + .addDependency(b, d) + .addDependency(b, c) + .addDependency(c, d) + .addDependency(c, a) + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + ordering.hasNext() + ordering.next() // [d] + ordering.hasNext() + + then: + graphql.AssertException e = thrown() + e.message.contains("couldn't calculate next closure") + } + + def "test illegal next"() { + given: + def a = new TestVertex(value: "a") + def b = new TestVertex(value: "b") + def c = new TestVertex(value: "c") + def d = new TestVertex(value: "d") + def graph = [] as DependencyGraph + graph + .addDependency(a, b) + .addDependency(a, c) + .addDependency(b, d) + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + ordering.hasNext() + ordering.next() + ordering.next() + + then: + java.util.NoSuchElementException e = thrown() + e.message.contains("next closure hasn't been calculated yet") + } + + def "test hasNext idempotency"() { + given: + def a = new TestVertex(value: "a") + def b = new TestVertex(value: "b") + def c = new TestVertex(value: "c") + def d = new TestVertex(value: "d") + def graph = [] as DependencyGraph + graph + .addDependency(a, b) + .addDependency(a, c) + .addDependency(b, d) + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + + then: + ordering.hasNext() == ordering.hasNext() + ordering.next() == [c, d] as Set + ordering.hasNext() == ordering.hasNext() + ordering.next() == [b] as Set + ordering.hasNext() == ordering.hasNext() + ordering.next() == [a] as Set + ordering.hasNext() == ordering.hasNext() + } + + def "test close by value"() { + given: + def a = new TestVertex(value: "a") + def b = new TestVertex(value: "b") + def c = new TestVertex(value: "c") + def d = new TestVertex(value: "d") + def graph = [] as DependencyGraph + graph + .addDependency(a, b) + .addDependency(a, c) + .addDependency(b, d) + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + + then: + ordering.hasNext() == true + ordering.next() == [c, d] as Set + ordering.close([new TestVertex(value: "c"), new TestVertex(value: "d")]) + ordering.hasNext() == true + ordering.next() == [b] as Set + ordering.close([new TestVertex(value: "b")]) + ordering.hasNext() == true + ordering.next() == [new TestVertex(value: "a")] as Set + ordering.close([a]) + ordering.hasNext() == false + } + + def "test close by id"() { + given: + def a = new TestVertex(value: "a") + def b = new TestVertex(value: "b") + def c = new TestVertex(value: "c") + def d = new TestVertex(value: "d") + def graph = [] as DependencyGraph + graph + .addDependency(a, b) + .addDependency(a, c) + .addDependency(b, d) + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + + then: + ordering.hasNext() == true + ordering.next() == [c, d] as Set + ordering.close([new TestVertex(value: "c").id(c.getId()), new TestVertex(value: "d").id(d.getId())]) + ordering.hasNext() == true + ordering.next() == [b] as Set + ordering.close([new TestVertex(value: "b").id(b.getId())]) + ordering.hasNext() == true + ordering.next() == [new TestVertex(value: "a").id(a.getId())] as Set + ordering.close([a]) + ordering.hasNext() == false + } + + def "test close by invalid id"() { + given: + def a = new TestVertex(value: "a") + def b = new TestVertex(value: "b") + def c = new TestVertex(value: "c") + def d = new TestVertex(value: "d") + def graph = [] as DependencyGraph + graph + .addDependency(a, b) + .addDependency(a, c) + .addDependency(b, d) + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + ordering.hasNext() + ordering.next() + ordering.close([new TestVertex(value: "c").id(c.getId()), new TestVertex(value: "d").id(12345)]) + + then: + java.lang.IllegalArgumentException e = thrown() + e.message.contains("node not found") + } + + def "test close by invalid value"() { + given: + def a = new TestVertex(value: "a") + def b = new TestVertex(value: "b") + def c = new TestVertex(value: "c") + def d = new TestVertex(value: "d") + def graph = [] as DependencyGraph + graph + .addDependency(a, b) + .addDependency(a, c) + .addDependency(b, d) + + when: + def ordering = graph.orderDependencies([] as DependencyGraphContext) + ordering.hasNext() + ordering.next() + ordering.close([new TestVertex(value: "c").id(c.getId()), new TestVertex(value: "e")]) + + then: + java.lang.IllegalArgumentException e = thrown() + e.message.contains("node not found") + } + + def "test possible https://en.wikipedia.org/wiki/Dependency_graph example via addEdge"() { + given: + def graph = [] as DependencyGraph + def a = graph.addNode(new TestVertex(value: "a")) + def b = graph.addNode(new TestVertex(value: "b")) + def c = graph.addNode(new TestVertex(value: "c")) + def d = graph.addNode(new TestVertex(value: "d")) + + when: + graph + .addEdge(new Edge<>(b, a)) + .addEdge(new Edge<>(c, a)) + .addEdge(new Edge<>(d, b)) + def ordering = graph.orderDependencies([] as DependencyGraphContext) + + then: + graph.order() == 4 + graph.size() == 3 + ordering.hasNext() == true + ordering.next() == [c, d] as Set + ordering.hasNext() == true + ordering.next() == [b] as Set + ordering.hasNext() == true + ordering.next() == [a] as Set + ordering.hasNext() == false + } +} + diff --git a/src/test/groovy/graphql/util/TraverserTest.groovy b/src/test/groovy/graphql/util/TraverserTest.groovy index d0c8bf28af..7e1f485b80 100644 --- a/src/test/groovy/graphql/util/TraverserTest.groovy +++ b/src/test/groovy/graphql/util/TraverserTest.groovy @@ -363,7 +363,7 @@ class TraverserTest extends Specification { def "test traversal with zero roots"() { - def visitor = [] as TraverserVisitor + def visitor = Mock(TraverserVisitor) def roots = [] when: